diff --git a/app/src/routes/system/filesystem/FileSystem.svelte b/app/src/routes/system/filesystem/FileSystem.svelte index 32e51da..f17164d 100644 --- a/app/src/routes/system/filesystem/FileSystem.svelte +++ b/app/src/routes/system/filesystem/FileSystem.svelte @@ -6,8 +6,11 @@ import { modals } from 'svelte-modals' import NewFolderDialog from './NewFolderDialog.svelte' import NewFileDialog from './NewFileDialog.svelte' + import { api } from '$lib/api' + import type { Response, FileEntry } from '$lib/platform_shared/api' let currentPath = $state('/') + let fileTree = $state(null) let files = $state>([]) let directories = $state>([]) let loading = $state(false) @@ -22,17 +25,47 @@ let downloadProgress = $state(null) let uploadInputRef: HTMLInputElement - async function loadDirectory(path: string = currentPath) { + function getNodeAtPath(root: FileEntry, path: string): FileEntry | null { + if (path === '/') return root + const parts = path.split('/').filter(Boolean) + let current = root + for (const part of parts) { + const child = current.children?.find((c) => c.name === part) + if (!child) return null + current = child + } + return current + } + + function updateFilesAndDirectories(node: FileEntry | null) { + if (!node || !node.children) { + files = [] + directories = [] + return + } + files = node.children + .filter((c) => !c.isDirectory) + .map((c) => ({ name: c.name, size: c.size })) + directories = node.children + .filter((c) => c.isDirectory) + .map((c) => ({ name: c.name })) + } + + async function loadFileTree() { loading = true error = '' try { - const result = await fileSystemClient.listDirectory(path) - if (result.success) { - files = result.files - directories = result.directories - currentPath = path + const result = await api.get('/api/files') + if (result.isOk()) { + const response = result.inner + if (response.fileList && response.fileList.entries.length > 0) { + fileTree = response.fileList.entries[0] + updateFilesAndDirectories(getNodeAtPath(fileTree, currentPath)) + } else { + error = response.errorMessage || 'Failed to load file tree' + } } else { - error = result.error || 'Failed to load directory' + error = 'Failed to fetch file list' } } catch (e) { error = e instanceof Error ? e.message : 'Unknown error' @@ -41,6 +74,15 @@ } } + async function loadDirectory(path: string = currentPath) { + currentPath = path + if (fileTree) { + updateFilesAndDirectories(getNodeAtPath(fileTree, path)) + } else { + await loadFileTree() + } + } + async function navigateTo(dirName: string) { const newPath = currentPath === '/' ? `/${dirName}` : `${currentPath}/${dirName}` await loadDirectory(newPath) @@ -94,7 +136,7 @@ if (result.success) { isEditing = false - await loadDirectory() // Refresh to update file sizes + await loadFileTree() // Refresh to update file sizes } else { error = result.error || 'Failed to save file' } @@ -125,7 +167,7 @@ ) if (result.success) { - await loadDirectory() + await loadFileTree() } else { error = result.error || 'Upload failed' } @@ -173,7 +215,7 @@ selectedFile = '' fileContent = '' } - await loadDirectory() + await loadFileTree() } else { error = result.error || 'Delete failed' } @@ -191,7 +233,7 @@ try { const result = await fileSystemClient.createDirectory(path) if (result.success) { - await loadDirectory() + await loadFileTree() } else { error = result.error || 'Failed to create directory' } @@ -212,7 +254,7 @@ const result = await fileSystemClient.uploadFile(path, data) if (result.success) { - await loadDirectory() + await loadFileTree() await loadFileContent(fileName) } else { error = result.error || 'Failed to create file' @@ -242,9 +284,9 @@ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } - // Load initial directory + // Load initial file tree $effect(() => { - loadDirectory('/') + loadFileTree() }) diff --git a/esp32/include/filesystem.h b/esp32/include/filesystem.h index 7f9eed4..688ad3f 100644 --- a/esp32/include/filesystem.h +++ b/esp32/include/filesystem.h @@ -4,6 +4,7 @@ #include #include #include +#include #define ESP_FS LittleFS @@ -17,10 +18,12 @@ namespace FileSystem { +void listFilesProto(const std::string &directory, api_FileEntry *entry); std::string listFiles(const std::string &directory, bool isRoot = true); bool deleteFile(const char *filename); bool editFile(const char *filename, const char *content); +esp_err_t getFilesProto(httpd_req_t *request); esp_err_t getFiles(httpd_req_t *request); esp_err_t getConfigFile(httpd_req_t *request); esp_err_t handleDelete(httpd_req_t *request, JsonVariant &json); diff --git a/esp32/src/filesystem.cpp b/esp32/src/filesystem.cpp index f331752..15a6fdf 100644 --- a/esp32/src/filesystem.cpp +++ b/esp32/src/filesystem.cpp @@ -1,10 +1,94 @@ #include #include +#include +#include static const char *TAG = "FileService"; namespace FileSystem { +// Storage for dynamically allocated FileEntry arrays +static std::vector allocatedEntries; + +static void freeAllocatedEntries() { + for (auto ptr : allocatedEntries) { + delete[] ptr; + } + allocatedEntries.clear(); +} + +void listFilesProto(const std::string &directory, api_FileEntry *entry) { + File root = ESP_FS.open(directory.find("/") == 0 ? directory.c_str() : ("/" + directory).c_str()); + if (!root.isDirectory()) { + entry->children_count = 0; + entry->children = nullptr; + return; + } + + // First pass: count children + std::vector files; + File file = root.openNextFile(); + while (file) { + files.push_back(file); + file = root.openNextFile(); + } + + if (files.empty()) { + entry->children_count = 0; + entry->children = nullptr; + return; + } + + // Allocate children array + entry->children_count = files.size(); + entry->children = new api_FileEntry[files.size()]; + allocatedEntries.push_back(entry->children); + + // Fill children + for (size_t i = 0; i < files.size(); i++) { + api_FileEntry &child = entry->children[i]; + memset(&child, 0, sizeof(child)); + + std::string name = std::string(files[i].name()); + strncpy(child.name, name.c_str(), sizeof(child.name) - 1); + child.name[sizeof(child.name) - 1] = '\0'; + + child.is_directory = files[i].isDirectory(); + if (child.is_directory) { + listFilesProto(name, &child); + } else { + child.size = files[i].size(); + child.children_count = 0; + child.children = nullptr; + } + } +} + +esp_err_t getFilesProto(httpd_req_t *request) { + freeAllocatedEntries(); // Clean up any previous allocations + + api_Response res = api_Response_init_zero; + res.status_code = 200; + res.which_payload = api_Response_file_list_tag; + + // Create root entry + api_FileEntry rootEntry = api_FileEntry_init_zero; + strncpy(rootEntry.name, "root", sizeof(rootEntry.name) - 1); + rootEntry.is_directory = true; + listFilesProto("/", &rootEntry); + + // Allocate entries array for FileList + res.payload.file_list.entries_count = 1; + res.payload.file_list.entries = new api_FileEntry[1]; + allocatedEntries.push_back(res.payload.file_list.entries); + res.payload.file_list.entries[0] = rootEntry; + + esp_err_t result = WebServer::sendProto(request, 200, res, api_Response_fields); + + freeAllocatedEntries(); // Clean up after sending + return result; +} + esp_err_t getFiles(httpd_req_t *request) { std::string files = listFiles("/"); httpd_resp_set_type(request, "application/json"); diff --git a/esp32/src/main.cpp b/esp32/src/main.cpp index c66e328..76a774a 100644 --- a/esp32/src/main.cpp +++ b/esp32/src/main.cpp @@ -42,6 +42,7 @@ void setupServer() { server.config(50 + WWW_ASSETS_COUNT, 32768); server.listen(80); + // TODO: REMAKE TO PROTO server.on("/api/system/reset", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) { return system_service::handleReset(request); }); server.on("/api/system/restart", HTTP_POST, @@ -49,6 +50,7 @@ void setupServer() { server.on("/api/system/sleep", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) { return system_service::handleSleep(request); }); #if USE_CAMERA + // TODO: REMAKE TO PROTO server.on("/api/camera/still", HTTP_GET, [&](httpd_req_t *request) { return cameraService.cameraStill(request); }); server.on("/api/camera/stream", HTTP_GET, [&](httpd_req_t *request) { return cameraService.cameraStream(request); }); @@ -64,6 +66,7 @@ void setupServer() { return servoController.protoEndpoint.handleStateUpdate(request, protoReq); }); + // TODO: REMAKE TO PROTO server.on("/api/wifi/sta/settings", HTTP_GET, [&](httpd_req_t *request) { return wifiService.endpoint.getState(request); }); server.on("/api/wifi/sta/settings", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) { @@ -81,7 +84,8 @@ void setupServer() { [&](httpd_req_t *request, api_Request *protoReq) { return apService.protoEndpoint.handleStateUpdate(request, protoReq); }); - + + // TODO: REMAKE TO PROTO server.on("/api/peripherals", HTTP_GET, [&](httpd_req_t *request) { return peripherals.endpoint.getState(request); }); server.on("/api/peripherals", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) { @@ -89,6 +93,7 @@ void setupServer() { }); #if FT_ENABLED(USE_MDNS) + // TODO: REMAKE TO PROTO server.on("/api/mdns", HTTP_GET, [&](httpd_req_t *request) { return mdnsService.endpoint.getState(request); }); server.on("/api/mdns", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) { return mdnsService.endpoint.handleStateUpdate(request, json); @@ -97,9 +102,10 @@ void setupServer() { server.on("/api/mdns/query", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) { return mdnsService.queryServices(request, json); }); #endif - + + // TODO: REMAKE TO PROTO server.on("/api/config/*", HTTP_GET, [](httpd_req_t *request) { return FileSystem::getConfigFile(request); }); - server.on("/api/files", HTTP_GET, [&](httpd_req_t *request) { return FileSystem::getFiles(request); }); + server.on("/api/files", HTTP_GET, [&](httpd_req_t *request) { return FileSystem::getFilesProto(request); }); server.on("/api/files/delete", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) { return FileSystem::handleDelete(request, json); }); server.on("/api/files/edit", HTTP_POST, diff --git a/platform_shared/api.options b/platform_shared/api.options index 6b315a5..539ae00 100644 --- a/platform_shared/api.options +++ b/platform_shared/api.options @@ -6,4 +6,8 @@ api.APStatus.mac_address max_size:18 api.Servo.name max_size:16 api.ServoSettings.servos max_count:12 +api.FileEntry.name max_size:64 +api.FileEntry.children type:FT_POINTER +api.FileList.entries type:FT_POINTER + api.Response.error_message type:FT_POINTER diff --git a/platform_shared/api.proto b/platform_shared/api.proto index 51bda3c..6da5296 100644 --- a/platform_shared/api.proto +++ b/platform_shared/api.proto @@ -59,6 +59,23 @@ message ServoSettings { message ServoSettingsRequest {} +// ============================================================================= +// File System - shared data types +// ============================================================================= + +message FileEntry { + string name = 1; + bool is_directory = 2; + uint32 size = 3; // Only for files + repeated FileEntry children = 4; // Only for directories +} + +message FileList { + repeated FileEntry entries = 1; +} + +message FileListRequest {} + // ============================================================================= // REST API wrappers - used by HTTP endpoints // ============================================================================= @@ -71,6 +88,7 @@ message Request { APStatusRequest ap_status_request = 12; ServoSettings servo_settings = 20; ServoSettingsRequest servo_settings_request = 21; + FileListRequest file_list_request = 30; } } @@ -83,5 +101,6 @@ message Response { APSettings ap_settings = 10; APStatus ap_status = 11; ServoSettings servo_settings = 20; + FileList file_list = 30; } }