Remake filesystem listing to protobuf
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user