Remake filesystem listing to protobuf

This commit is contained in:
Niklas Jensen
2026-01-25 00:49:39 +01:00
committed by nikguin04
parent 6e7f7bb657
commit 9666baf858
6 changed files with 175 additions and 17 deletions
@@ -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<FileEntry | null>(null)
let files = $state<Array<{ name: string; size: number }>>([])
let directories = $state<Array<{ name: string }>>([])
let loading = $state(false)
@@ -22,17 +25,47 @@
let downloadProgress = $state<TransferProgress | null>(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<Response>('/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()
})
</script>
+3
View File
@@ -4,6 +4,7 @@
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <string>
#include <platform_shared/api.pb.h>
#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);
+84
View File
@@ -1,10 +1,94 @@
#include <filesystem.h>
#include <communication/webserver.h>
#include <vector>
#include <cstring>
static const char *TAG = "FileService";
namespace FileSystem {
// Storage for dynamically allocated FileEntry arrays
static std::vector<api_FileEntry*> 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<File> 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");
+7 -1
View File
@@ -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) {
@@ -82,6 +85,7 @@ void setupServer() {
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);
@@ -98,8 +103,9 @@ void setupServer() {
[&](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,
+4
View File
@@ -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
+19
View File
@@ -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;
}
}