From ec82ff209d69ad09ceffc4ac2eb7ddd4d43f2c35 Mon Sep 17 00:00:00 2001 From: gebeto Date: Tue, 23 Dec 2025 14:34:40 +0200 Subject: [PATCH] improve file manager --- scripts/build_html.py | 80 +++++++++++++++---- src/network/CrossPointWebServer.cpp | 89 +++++++++++++++++++-- src/network/CrossPointWebServer.h | 3 +- src/network/html/FilesPage.html | 117 +++++++++++++++++++--------- 4 files changed, 228 insertions(+), 61 deletions(-) diff --git a/scripts/build_html.py b/scripts/build_html.py index 248aba84..42985b9c 100644 --- a/scripts/build_html.py +++ b/scripts/build_html.py @@ -3,6 +3,38 @@ import re SRC_DIR = "src" + +def bytes_to_cpp_byte_array(input_file_bytes: bytes, input_file_path: str, output_header_file: str, variable_name: str): + # Format bytes into C++ byte array initialiser format (hex values) + hex_values = [f"0x{b:02x}" for b in input_file_bytes] + bytes_array_declaration = ", ".join(hex_values) + data_length = len(input_file_bytes) + + # Generate the C++ header file content + header_content = f""" +#ifndef {variable_name.upper()}_H +#define {variable_name.upper()}_H + +#include +#include + +// Embedded file: {os.path.basename(input_file_path)} +constexpr uint8_t {variable_name}_data[] = {{ + {bytes_array_declaration} +}}; +constexpr size_t {variable_name}_size = {data_length}; + +#endif // {variable_name.upper()}_H +""" + + try: + with open(output_header_file, 'w') as f: + f.write(header_content) + print(f"Successfully generated C++ header file: {output_header_file}") + except IOError as e: + print(f"Error writing header file: {e}") + exit(1) + def minify_html(html: str) -> str: # Tags where whitespace should be preserved preserve_tags = ['pre', 'code', 'textarea', 'script', 'style'] @@ -31,21 +63,37 @@ def minify_html(html: str) -> str: return html.strip() +def read_src_file(root: str, file: str) -> str: + file_path = os.path.join(root, file) + with open(file_path, "r", encoding="utf-8") as f: + return f.read() + +def build_static(src_path: str, dest_dir: str): + filename = os.path.basename(src_path) + base_name, extension = os.path.splitext(filename) + postfix = extension[1:].capitalize() + base_name = f"{os.path.splitext(filename)[0]}{postfix}" + output_cpp_header = os.path.join(dest_dir, f"{base_name}.generated.h") + cpp_variable_name = base_name + if not os.path.exists(dest_dir): + os.makedirs(dest_dir) + + if extension == ".html": + # minify HTML content + src_string = open(src_path, "r").read() + minified_str = minify_html(src_string) + src_bytes = minified_str.encode("utf-8") + else: + src_bytes = open(src_path, "rb").read() + + bytes_to_cpp_byte_array( + input_file_bytes=src_bytes, + input_file_path=src_path, + output_header_file=output_cpp_header, + variable_name=cpp_variable_name + ) + for root, _, files in os.walk(SRC_DIR): for file in files: - if file.endswith(".html"): - html_path = os.path.join(root, file) - with open(html_path, "r", encoding="utf-8") as f: - html_content = f.read() - - # minified = regex.sub("\g<1>", html_content) - minified = minify_html(html_content) - base_name = f"{os.path.splitext(file)[0]}Html" - header_path = os.path.join(root, f"{base_name}.generated.h") - - with open(header_path, "w", encoding="utf-8") as h: - h.write(f"// THIS FILE IS AUTOGENERATED, DO NOT EDIT MANUALLY\n\n") - h.write(f"#pragma once\n") - h.write(f'constexpr char {base_name}[] PROGMEM = R"rawliteral({minified})rawliteral";\n') - - print(f"Generated: {header_path}") + if file.endswith((".html", ".css", ".js")): + build_static(os.path.join(root, file), root) diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 3a26a736..1c726bb0 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -67,20 +67,23 @@ void CrossPointWebServer::begin() { // Setup routes Serial.printf("[%lu] [WEB] Setting up routes...\n", millis()); - server->on("/", HTTP_GET, [this] { handleRoot(); }); + server->on("/", HTTP_GET, [this] { handleStatusPage(); }); server->on("/files", HTTP_GET, [this] { handleFileList(); }); server->on("/api/status", HTTP_GET, [this] { handleStatus(); }); server->on("/api/files", HTTP_GET, [this] { handleFileListData(); }); // Upload endpoint with special handling for multipart form data - server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); }); + server->on("/api/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); }); // Create folder endpoint - server->on("/mkdir", HTTP_POST, [this] { handleCreateFolder(); }); + server->on("/api/mkdir", HTTP_POST, [this] { handleCreateFolder(); }); + + // Move file/folder endpoint + server->on("/api/move", HTTP_POST, [this] { handleMove(); }); // Delete file/folder endpoint - server->on("/delete", HTTP_POST, [this] { handleDelete(); }); + server->on("/api/delete", HTTP_POST, [this] { handleDelete(); }); server->onNotFound([this] { handleNotFound(); }); Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap()); @@ -150,8 +153,10 @@ void CrossPointWebServer::handleClient() const { server->handleClient(); } -void CrossPointWebServer::handleRoot() const { - server->send(200, "text/html", HomePageHtml); +void CrossPointWebServer::handleStatusPage() const { + server->setContentLength(HomePageHtml_size); + server->send(200, "text/html", ""); + server->sendContent((char*)HomePageHtml_data, HomePageHtml_size); Serial.printf("[%lu] [WEB] Served root page\n", millis()); } @@ -241,7 +246,11 @@ bool CrossPointWebServer::isEpubFile(const String& filename) const { return lower.endsWith(".epub"); } -void CrossPointWebServer::handleFileList() const { server->send(200, "text/html", FilesPageHtml); } +void CrossPointWebServer::handleFileList() const { + server->setContentLength(FilesPageHtml_size); + server->send(200, "text/html", ""); + server->sendContent((char*)FilesPageHtml_data, FilesPageHtml_size); +} void CrossPointWebServer::handleFileListData() const { // Get current path from query string (default to root) @@ -555,3 +564,69 @@ void CrossPointWebServer::handleDelete() const { server->send(500, "text/plain", "Failed to delete item"); } } + +void CrossPointWebServer::handleMove() const { + // Get path from form data + if (!server->hasArg("path")) { + server->send(400, "text/plain", "Missing path"); + return; + } + + if (!server->hasArg("new_path")) { + server->send(400, "text/plain", "Missing new path"); + return; + } + + String itemPath = server->arg("path"); + String newItemPath = server->arg("new_path"); + const String itemType = server->hasArg("type") ? server->arg("type") : "file"; + + // Validate path + if (itemPath.isEmpty() || itemPath == "/") { + server->send(400, "text/plain", "Cannot move root directory"); + return; + } + + // Ensure path starts with / + if (!itemPath.startsWith("/")) { + itemPath = "/" + itemPath; + } + + // Security check: prevent renaming of protected items + const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1); + + // Check if item starts with a dot (hidden/system file) + if (itemName.startsWith(".")) { + Serial.printf("[%lu] [WEB] Move rejected - hidden/system item: %s\n", millis(), itemPath.c_str()); + server->send(403, "text/plain", "Cannot move system files"); + return; + } + + // Check against explicitly protected items + for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) { + if (itemName.equals(HIDDEN_ITEMS[i])) { + Serial.printf("[%lu] [WEB] Move rejected - protected item: %s\n", millis(), itemPath.c_str()); + server->send(403, "text/plain", "Cannot move protected items"); + return; + } + } + + // Check if item exists + if (!SdMan.exists(itemPath.c_str())) { + Serial.printf("[%lu] [WEB] Move failed - item not found: %s\n", millis(), itemPath.c_str()); + server->send(404, "text/plain", "Item not found"); + return; + } + + Serial.printf("[%lu] [WEB] Attempting to move %s: %s\n", millis(), itemType.c_str(), itemPath.c_str()); + + bool success = SdMan.rename(itemPath.c_str(), newItemPath.c_str()); + + if (success) { + Serial.printf("[%lu] [WEB] Successfully moved: %s\n", millis(), itemPath.c_str()); + server->send(200, "text/plain", "Moved successfully"); + } else { + Serial.printf("[%lu] [WEB] Failed to move: %s\n", millis(), itemPath.c_str()); + server->send(500, "text/plain", "Failed to move item"); + } +} diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index 1be07b4a..ea69abcb 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -44,7 +44,7 @@ class CrossPointWebServer { bool isEpubFile(const String& filename) const; // Request handlers - void handleRoot() const; + void handleStatusPage() const; void handleNotFound() const; void handleStatus() const; void handleFileList() const; @@ -53,4 +53,5 @@ class CrossPointWebServer { void handleUploadPost() const; void handleCreateFolder() const; void handleDelete() const; + void handleMove() const; }; diff --git a/src/network/html/FilesPage.html b/src/network/html/FilesPage.html index 08c0a0be..be93ced7 100644 --- a/src/network/html/FilesPage.html +++ b/src/network/html/FilesPage.html @@ -52,6 +52,7 @@ .breadcrumb-inline a { color: #3498db; text-decoration: none; + cursor: pointer; } .breadcrumb-inline a:hover { text-decoration: underline; @@ -667,7 +668,7 @@