Remake filesystem listing to protobuf
This commit is contained in:
@@ -6,8 +6,11 @@
|
|||||||
import { modals } from 'svelte-modals'
|
import { modals } from 'svelte-modals'
|
||||||
import NewFolderDialog from './NewFolderDialog.svelte'
|
import NewFolderDialog from './NewFolderDialog.svelte'
|
||||||
import NewFileDialog from './NewFileDialog.svelte'
|
import NewFileDialog from './NewFileDialog.svelte'
|
||||||
|
import { api } from '$lib/api'
|
||||||
|
import type { Response, FileEntry } from '$lib/platform_shared/api'
|
||||||
|
|
||||||
let currentPath = $state('/')
|
let currentPath = $state('/')
|
||||||
|
let fileTree = $state<FileEntry | null>(null)
|
||||||
let files = $state<Array<{ name: string; size: number }>>([])
|
let files = $state<Array<{ name: string; size: number }>>([])
|
||||||
let directories = $state<Array<{ name: string }>>([])
|
let directories = $state<Array<{ name: string }>>([])
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
@@ -22,17 +25,47 @@
|
|||||||
let downloadProgress = $state<TransferProgress | null>(null)
|
let downloadProgress = $state<TransferProgress | null>(null)
|
||||||
let uploadInputRef: HTMLInputElement
|
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
|
loading = true
|
||||||
error = ''
|
error = ''
|
||||||
try {
|
try {
|
||||||
const result = await fileSystemClient.listDirectory(path)
|
const result = await api.get<Response>('/api/files')
|
||||||
if (result.success) {
|
if (result.isOk()) {
|
||||||
files = result.files
|
const response = result.inner
|
||||||
directories = result.directories
|
if (response.fileList && response.fileList.entries.length > 0) {
|
||||||
currentPath = path
|
fileTree = response.fileList.entries[0]
|
||||||
|
updateFilesAndDirectories(getNodeAtPath(fileTree, currentPath))
|
||||||
|
} else {
|
||||||
|
error = response.errorMessage || 'Failed to load file tree'
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
error = result.error || 'Failed to load directory'
|
error = 'Failed to fetch file list'
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Unknown error'
|
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) {
|
async function navigateTo(dirName: string) {
|
||||||
const newPath = currentPath === '/' ? `/${dirName}` : `${currentPath}/${dirName}`
|
const newPath = currentPath === '/' ? `/${dirName}` : `${currentPath}/${dirName}`
|
||||||
await loadDirectory(newPath)
|
await loadDirectory(newPath)
|
||||||
@@ -94,7 +136,7 @@
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
isEditing = false
|
isEditing = false
|
||||||
await loadDirectory() // Refresh to update file sizes
|
await loadFileTree() // Refresh to update file sizes
|
||||||
} else {
|
} else {
|
||||||
error = result.error || 'Failed to save file'
|
error = result.error || 'Failed to save file'
|
||||||
}
|
}
|
||||||
@@ -125,7 +167,7 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadDirectory()
|
await loadFileTree()
|
||||||
} else {
|
} else {
|
||||||
error = result.error || 'Upload failed'
|
error = result.error || 'Upload failed'
|
||||||
}
|
}
|
||||||
@@ -173,7 +215,7 @@
|
|||||||
selectedFile = ''
|
selectedFile = ''
|
||||||
fileContent = ''
|
fileContent = ''
|
||||||
}
|
}
|
||||||
await loadDirectory()
|
await loadFileTree()
|
||||||
} else {
|
} else {
|
||||||
error = result.error || 'Delete failed'
|
error = result.error || 'Delete failed'
|
||||||
}
|
}
|
||||||
@@ -191,7 +233,7 @@
|
|||||||
try {
|
try {
|
||||||
const result = await fileSystemClient.createDirectory(path)
|
const result = await fileSystemClient.createDirectory(path)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadDirectory()
|
await loadFileTree()
|
||||||
} else {
|
} else {
|
||||||
error = result.error || 'Failed to create directory'
|
error = result.error || 'Failed to create directory'
|
||||||
}
|
}
|
||||||
@@ -212,7 +254,7 @@
|
|||||||
|
|
||||||
const result = await fileSystemClient.uploadFile(path, data)
|
const result = await fileSystemClient.uploadFile(path, data)
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
await loadDirectory()
|
await loadFileTree()
|
||||||
await loadFileContent(fileName)
|
await loadFileContent(fileName)
|
||||||
} else {
|
} else {
|
||||||
error = result.error || 'Failed to create file'
|
error = result.error || 'Failed to create file'
|
||||||
@@ -242,9 +284,9 @@
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initial directory
|
// Load initial file tree
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
loadDirectory('/')
|
loadFileTree()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <LittleFS.h>
|
#include <LittleFS.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <platform_shared/api.pb.h>
|
||||||
|
|
||||||
#define ESP_FS LittleFS
|
#define ESP_FS LittleFS
|
||||||
|
|
||||||
@@ -17,10 +18,12 @@
|
|||||||
|
|
||||||
namespace FileSystem {
|
namespace FileSystem {
|
||||||
|
|
||||||
|
void listFilesProto(const std::string &directory, api_FileEntry *entry);
|
||||||
std::string listFiles(const std::string &directory, bool isRoot = true);
|
std::string listFiles(const std::string &directory, bool isRoot = true);
|
||||||
bool deleteFile(const char *filename);
|
bool deleteFile(const char *filename);
|
||||||
bool editFile(const char *filename, const char *content);
|
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 getFiles(httpd_req_t *request);
|
||||||
esp_err_t getConfigFile(httpd_req_t *request);
|
esp_err_t getConfigFile(httpd_req_t *request);
|
||||||
esp_err_t handleDelete(httpd_req_t *request, JsonVariant &json);
|
esp_err_t handleDelete(httpd_req_t *request, JsonVariant &json);
|
||||||
|
|||||||
@@ -1,10 +1,94 @@
|
|||||||
#include <filesystem.h>
|
#include <filesystem.h>
|
||||||
#include <communication/webserver.h>
|
#include <communication/webserver.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
static const char *TAG = "FileService";
|
static const char *TAG = "FileService";
|
||||||
|
|
||||||
namespace FileSystem {
|
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) {
|
esp_err_t getFiles(httpd_req_t *request) {
|
||||||
std::string files = listFiles("/");
|
std::string files = listFiles("/");
|
||||||
httpd_resp_set_type(request, "application/json");
|
httpd_resp_set_type(request, "application/json");
|
||||||
|
|||||||
+9
-3
@@ -42,6 +42,7 @@ void setupServer() {
|
|||||||
server.config(50 + WWW_ASSETS_COUNT, 32768);
|
server.config(50 + WWW_ASSETS_COUNT, 32768);
|
||||||
server.listen(80);
|
server.listen(80);
|
||||||
|
|
||||||
|
// TODO: REMAKE TO PROTO
|
||||||
server.on("/api/system/reset", HTTP_POST,
|
server.on("/api/system/reset", HTTP_POST,
|
||||||
[&](httpd_req_t *request, JsonVariant &json) { return system_service::handleReset(request); });
|
[&](httpd_req_t *request, JsonVariant &json) { return system_service::handleReset(request); });
|
||||||
server.on("/api/system/restart", HTTP_POST,
|
server.on("/api/system/restart", HTTP_POST,
|
||||||
@@ -49,6 +50,7 @@ void setupServer() {
|
|||||||
server.on("/api/system/sleep", HTTP_POST,
|
server.on("/api/system/sleep", HTTP_POST,
|
||||||
[&](httpd_req_t *request, JsonVariant &json) { return system_service::handleSleep(request); });
|
[&](httpd_req_t *request, JsonVariant &json) { return system_service::handleSleep(request); });
|
||||||
#if USE_CAMERA
|
#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/still", HTTP_GET, [&](httpd_req_t *request) { return cameraService.cameraStill(request); });
|
||||||
server.on("/api/camera/stream", HTTP_GET,
|
server.on("/api/camera/stream", HTTP_GET,
|
||||||
[&](httpd_req_t *request) { return cameraService.cameraStream(request); });
|
[&](httpd_req_t *request) { return cameraService.cameraStream(request); });
|
||||||
@@ -64,6 +66,7 @@ void setupServer() {
|
|||||||
return servoController.protoEndpoint.handleStateUpdate(request, protoReq);
|
return servoController.protoEndpoint.handleStateUpdate(request, protoReq);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: REMAKE TO PROTO
|
||||||
server.on("/api/wifi/sta/settings", HTTP_GET,
|
server.on("/api/wifi/sta/settings", HTTP_GET,
|
||||||
[&](httpd_req_t *request) { return wifiService.endpoint.getState(request); });
|
[&](httpd_req_t *request) { return wifiService.endpoint.getState(request); });
|
||||||
server.on("/api/wifi/sta/settings", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) {
|
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) {
|
[&](httpd_req_t *request, api_Request *protoReq) {
|
||||||
return apService.protoEndpoint.handleStateUpdate(request, protoReq);
|
return apService.protoEndpoint.handleStateUpdate(request, protoReq);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: REMAKE TO PROTO
|
||||||
server.on("/api/peripherals", HTTP_GET,
|
server.on("/api/peripherals", HTTP_GET,
|
||||||
[&](httpd_req_t *request) { return peripherals.endpoint.getState(request); });
|
[&](httpd_req_t *request) { return peripherals.endpoint.getState(request); });
|
||||||
server.on("/api/peripherals", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) {
|
server.on("/api/peripherals", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) {
|
||||||
@@ -89,6 +93,7 @@ void setupServer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
#if FT_ENABLED(USE_MDNS)
|
#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_GET, [&](httpd_req_t *request) { return mdnsService.endpoint.getState(request); });
|
||||||
server.on("/api/mdns", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) {
|
server.on("/api/mdns", HTTP_POST, [&](httpd_req_t *request, JsonVariant &json) {
|
||||||
return mdnsService.endpoint.handleStateUpdate(request, json);
|
return mdnsService.endpoint.handleStateUpdate(request, json);
|
||||||
@@ -97,9 +102,10 @@ void setupServer() {
|
|||||||
server.on("/api/mdns/query", HTTP_POST,
|
server.on("/api/mdns/query", HTTP_POST,
|
||||||
[&](httpd_req_t *request, JsonVariant &json) { return mdnsService.queryServices(request, json); });
|
[&](httpd_req_t *request, JsonVariant &json) { return mdnsService.queryServices(request, json); });
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// TODO: REMAKE TO PROTO
|
||||||
server.on("/api/config/*", HTTP_GET, [](httpd_req_t *request) { return FileSystem::getConfigFile(request); });
|
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,
|
server.on("/api/files/delete", HTTP_POST,
|
||||||
[&](httpd_req_t *request, JsonVariant &json) { return FileSystem::handleDelete(request, json); });
|
[&](httpd_req_t *request, JsonVariant &json) { return FileSystem::handleDelete(request, json); });
|
||||||
server.on("/api/files/edit", HTTP_POST,
|
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.Servo.name max_size:16
|
||||||
api.ServoSettings.servos max_count:12
|
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
|
api.Response.error_message type:FT_POINTER
|
||||||
|
|||||||
@@ -59,6 +59,23 @@ message ServoSettings {
|
|||||||
|
|
||||||
message ServoSettingsRequest {}
|
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
|
// REST API wrappers - used by HTTP endpoints
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -71,6 +88,7 @@ message Request {
|
|||||||
APStatusRequest ap_status_request = 12;
|
APStatusRequest ap_status_request = 12;
|
||||||
ServoSettings servo_settings = 20;
|
ServoSettings servo_settings = 20;
|
||||||
ServoSettingsRequest servo_settings_request = 21;
|
ServoSettingsRequest servo_settings_request = 21;
|
||||||
|
FileListRequest file_list_request = 30;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,5 +101,6 @@ message Response {
|
|||||||
APSettings ap_settings = 10;
|
APSettings ap_settings = 10;
|
||||||
APStatus ap_status = 11;
|
APStatus ap_status = 11;
|
||||||
ServoSettings servo_settings = 20;
|
ServoSettings servo_settings = 20;
|
||||||
|
FileList file_list = 30;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user