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 @@