From 4b9e9c6969ab1dbf311f5a5b57f7acd13235a158 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Mon, 22 Dec 2025 01:25:17 +1100 Subject: [PATCH] Stream CrossPointWebServer::handleFileList response to client --- src/network/CrossPointWebServer.cpp | 213 ++++++++++++++-------------- src/network/CrossPointWebServer.h | 26 ++-- 2 files changed, 121 insertions(+), 118 deletions(-) diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 6291627..7b7d59e 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -5,7 +5,6 @@ #include -#include "config.h" #include "html/FilesPageFooterHtml.generated.h" #include "html/FilesPageHeaderHtml.generated.h" #include "html/HomePageHtml.generated.h" @@ -15,7 +14,7 @@ namespace { // Folders/files to hide from the web interface file browser // Note: Items starting with "." are automatically hidden const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"}; -const size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]); +constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]); // Helper function to escape HTML special characters to prevent XSS String escapeHtml(const String& input) { @@ -23,7 +22,7 @@ String escapeHtml(const String& input) { output.reserve(input.length() * 1.1); // Pre-allocate with some extra space for (size_t i = 0; i < input.length(); i++) { - char c = input.charAt(i); + const char c = input.charAt(i); switch (c) { case '&': output += "&"; @@ -72,7 +71,7 @@ void CrossPointWebServer::begin() { Serial.printf("[%lu] [WEB] [MEM] Free heap before begin: %d bytes\n", millis(), ESP.getFreeHeap()); Serial.printf("[%lu] [WEB] Creating web server on port %d...\n", millis(), port); - server = new WebServer(port); + server.reset(new WebServer(port)); Serial.printf("[%lu] [WEB] [MEM] Free heap after WebServer allocation: %d bytes\n", millis(), ESP.getFreeHeap()); if (!server) { @@ -82,20 +81,20 @@ void CrossPointWebServer::begin() { // Setup routes Serial.printf("[%lu] [WEB] Setting up routes...\n", millis()); - server->on("/", HTTP_GET, [this]() { handleRoot(); }); - server->on("/status", HTTP_GET, [this]() { handleStatus(); }); - server->on("/files", HTTP_GET, [this]() { handleFileList(); }); + server->on("/", HTTP_GET, [this] { handleRoot(); }); + server->on("/status", HTTP_GET, [this] { handleStatus(); }); + server->on("/files", HTTP_GET, [this] { handleFileList(); }); // Upload endpoint with special handling for multipart form data - server->on("/upload", HTTP_POST, [this]() { handleUploadPost(); }, [this]() { handleUpload(); }); + server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); }); // Create folder endpoint - server->on("/mkdir", HTTP_POST, [this]() { handleCreateFolder(); }); + server->on("/mkdir", HTTP_POST, [this] { handleCreateFolder(); }); // Delete file/folder endpoint - server->on("/delete", HTTP_POST, [this]() { handleDelete(); }); + server->on("/delete", HTTP_POST, [this] { handleDelete(); }); - server->onNotFound([this]() { handleNotFound(); }); + server->onNotFound([this] { handleNotFound(); }); Serial.printf("[%lu] [WEB] [MEM] Free heap after route setup: %d bytes\n", millis(), ESP.getFreeHeap()); server->begin(); @@ -108,7 +107,8 @@ void CrossPointWebServer::begin() { void CrossPointWebServer::stop() { if (!running || !server) { - Serial.printf("[%lu] [WEB] stop() called but already stopped (running=%d, server=%p)\n", millis(), running, server); + Serial.printf("[%lu] [WEB] stop() called but already stopped (running=%d, server=%p)\n", millis(), running, + server.get()); return; } @@ -128,9 +128,7 @@ void CrossPointWebServer::stop() { delay(50); Serial.printf("[%lu] [WEB] Waited 50ms before deleting server\n", millis()); - delete server; - server = nullptr; - + server.reset(); Serial.printf("[%lu] [WEB] Web server stopped and deleted\n", millis()); Serial.printf("[%lu] [WEB] [MEM] Free heap after delete server: %d bytes\n", millis(), ESP.getFreeHeap()); @@ -139,7 +137,7 @@ void CrossPointWebServer::stop() { Serial.printf("[%lu] [WEB] [MEM] Free heap final: %d bytes\n", millis(), ESP.getFreeHeap()); } -void CrossPointWebServer::handleClient() { +void CrossPointWebServer::handleClient() const { static unsigned long lastDebugPrint = 0; // Check running flag FIRST before accessing server @@ -162,7 +160,7 @@ void CrossPointWebServer::handleClient() { server->handleClient(); } -void CrossPointWebServer::handleRoot() { +void CrossPointWebServer::handleRoot() const { String html = HomePageHtml; // Replace placeholders with actual values @@ -174,13 +172,13 @@ void CrossPointWebServer::handleRoot() { Serial.printf("[%lu] [WEB] Served root page\n", millis()); } -void CrossPointWebServer::handleNotFound() { +void CrossPointWebServer::handleNotFound() const { String message = "404 Not Found\n\n"; message += "URI: " + server->uri() + "\n"; server->send(404, "text/plain", message); } -void CrossPointWebServer::handleStatus() { +void CrossPointWebServer::handleStatus() const { String json = "{"; json += "\"version\":\"" + String(CROSSPOINT_VERSION) + "\","; json += "\"ip\":\"" + WiFi.localIP().toString() + "\","; @@ -192,7 +190,7 @@ void CrossPointWebServer::handleStatus() { server->send(200, "application/json", json); } -std::vector CrossPointWebServer::scanFiles(const char* path) { +std::vector CrossPointWebServer::scanFiles(const char* path) const { std::vector files; File root = SD.open(path); @@ -211,7 +209,7 @@ std::vector CrossPointWebServer::scanFiles(const char* path) { File file = root.openNextFile(); while (file) { - String fileName = String(file.name()); + auto fileName = String(file.name()); // Skip hidden items (starting with ".") bool shouldHide = fileName.startsWith("."); @@ -251,24 +249,26 @@ std::vector CrossPointWebServer::scanFiles(const char* path) { return files; } -String CrossPointWebServer::formatFileSize(size_t bytes) { +String CrossPointWebServer::formatFileSize(const size_t bytes) const { if (bytes < 1024) { return String(bytes) + " B"; - } else if (bytes < 1024 * 1024) { - return String(bytes / 1024.0, 1) + " KB"; - } else { - return String(bytes / (1024.0 * 1024.0), 1) + " MB"; } + if (bytes < 1024 * 1024) { + return String(bytes / 1024.0, 1) + " KB"; + } + return String(bytes / (1024.0 * 1024.0), 1) + " MB"; } -bool CrossPointWebServer::isEpubFile(const String& filename) { +bool CrossPointWebServer::isEpubFile(const String& filename) const { String lower = filename; lower.toLowerCase(); return lower.endsWith(".epub"); } -void CrossPointWebServer::handleFileList() { - String html = FilesPageHeaderHtml; +void CrossPointWebServer::handleFileList() const { + server->setContentLength(CONTENT_LENGTH_UNKNOWN); + server->send(200, "text/html", ""); + server->sendContent(FilesPageHeaderHtml); // Get current path from query string (default to root) String currentPath = "/"; @@ -288,11 +288,11 @@ void CrossPointWebServer::handleFileList() { if (server->hasArg("msg")) { String msg = escapeHtml(server->arg("msg")); String msgType = server->hasArg("type") ? escapeHtml(server->arg("type")) : "success"; - html += "
" + msg + "
"; + server->sendContent("
" + msg + "
"); } // Hidden input to store current path for JavaScript - html += ""; + server->sendContent(""); // Scan files in current path first (we need counts for the header) std::vector files = scanFiles(currentPath.c_str()); @@ -311,72 +311,73 @@ void CrossPointWebServer::handleFileList() { } // Page header with inline breadcrumb and action buttons - html += "
"; - html += "
"; - html += "

📁 File Manager

"; + server->sendContent( + "
" + "
" + "

📁 File Manager

"); // Inline breadcrumb - html += "
"; - html += "/"; + server->sendContent( + "
" + "/"); if (currentPath == "/") { - html += "🏠"; + server->sendContent("🏠"); } else { - html += "🏠"; + server->sendContent("🏠"); String pathParts = currentPath.substring(1); // Remove leading / String buildPath = ""; int start = 0; int end = pathParts.indexOf('/'); - while (start < (int)pathParts.length()) { + while (start < static_cast(pathParts.length())) { String part; if (end == -1) { part = pathParts.substring(start); - buildPath += "/" + part; - html += "/" + escapeHtml(part) + ""; + server->sendContent("/" + escapeHtml(part) + ""); break; } else { part = pathParts.substring(start, end); buildPath += "/" + part; - html += "/" + escapeHtml(part) + ""; + server->sendContent("/" + + escapeHtml(part) + ""); start = end + 1; end = pathParts.indexOf('/', start); } } } - html += "
"; - html += "
"; + server->sendContent("
"); // Action buttons - html += "
"; - html += ""; - html += ""; - html += "
"; - - html += "
"; // end page-header + server->sendContent( + "
" + "" + "" + "
" + "
"); // end page-header // Contents card with inline summary - html += "
"; - // Contents header with inline stats - html += "
"; - html += "

Contents

"; - html += ""; - html += String(folderCount) + " folder" + (folderCount != 1 ? "s" : "") + ", "; - html += String(files.size() - folderCount) + " file" + ((files.size() - folderCount) != 1 ? "s" : "") + ", "; - html += formatFileSize(totalSize); - html += ""; - html += "
"; + server->sendContent( + "
" + // Contents header with inline stats + "
" + "

Contents

" + ""); + server->sendContent(String(folderCount) + " folder" + (folderCount != 1 ? "s" : "") + ", " + + String(files.size() - folderCount) + " file" + ((files.size() - folderCount) != 1 ? "s" : "") + + ", " + formatFileSize(totalSize) + "
"); if (files.empty()) { - html += "
This folder is empty
"; + server->sendContent("
This folder is empty
"); } else { - html += ""; - html += ""; + server->sendContent( + "
NameTypeSizeActions
" + ""); // Sort files: folders first, then epub files, then other files, alphabetically within each group std::sort(files.begin(), files.end(), [](const FileInfo& a, const FileInfo& b) { @@ -407,20 +408,22 @@ void CrossPointWebServer::handleFileList() { if (!folderPath.endsWith("/")) folderPath += "/"; folderPath += file.name; - html += ""; - html += ""; - html += ""; - html += ""; + server->sendContent(""); + server->sendContent(""); + server->sendContent(""); + server->sendContent(""); // Escape quotes for JavaScript string String escapedName = file.name; escapedName.replace("'", "\\'"); String escapedPath = folderPath; escapedPath.replace("'", "\\'"); - html += ""; - html += ""; + server->sendContent(""); + server->sendContent(""); } else { rowClass = file.isEpub ? "epub-file" : ""; icon = file.isEpub ? "📗" : "📄"; @@ -435,29 +438,29 @@ void CrossPointWebServer::handleFileList() { if (!filePath.endsWith("/")) filePath += "/"; filePath += file.name; - html += ""; - html += ""; - html += ""; - html += ""; + server->sendContent(""); + server->sendContent(""); + server->sendContent(""); + server->sendContent(""); // Escape quotes for JavaScript string String escapedName = file.name; escapedName.replace("'", "\\'"); String escapedPath = filePath; escapedPath.replace("'", "\\'"); - html += ""; - html += ""; + server->sendContent(""); + server->sendContent(""); } } - html += "
NameTypeSizeActions
" + icon + ""; - html += "" + escapeHtml(file.name) + "" + - badge + "" + typeStr + "" + sizeStr + "
" + icon + + "" + "" + escapeHtml(file.name) + "" + badge + + "" + typeStr + "" + sizeStr + "
" + icon + "" + escapeHtml(file.name) + badge + "" + typeStr + "" + sizeStr + "
" + icon + "" + escapeHtml(file.name) + badge + + "" + typeStr + "" + sizeStr + "
"; + server->sendContent(""); } - html += "
"; - - html += FilesPageFooterHtml; - - server->send(200, "text/html", html); + server->sendContent("
"); + server->sendContent(FilesPageFooterHtml); + // Signal end of content + server->sendContent(""); Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str()); } @@ -469,7 +472,7 @@ static size_t uploadSize = 0; static bool uploadSuccess = false; static String uploadError = ""; -void CrossPointWebServer::handleUpload() { +void CrossPointWebServer::handleUpload() const { static unsigned long lastWriteTime = 0; static unsigned long uploadStartTime = 0; static size_t lastLoggedSize = 0; @@ -480,7 +483,7 @@ void CrossPointWebServer::handleUpload() { return; } - HTTPUpload& upload = server->upload(); + const HTTPUpload& upload = server->upload(); if (upload.status == UPLOAD_FILE_START) { uploadFileName = upload.filename; @@ -533,10 +536,10 @@ void CrossPointWebServer::handleUpload() { Serial.printf("[%lu] [WEB] [UPLOAD] File created successfully: %s\n", millis(), filePath.c_str()); } else if (upload.status == UPLOAD_FILE_WRITE) { if (uploadFile && uploadError.isEmpty()) { - unsigned long writeStartTime = millis(); - size_t written = uploadFile.write(upload.buf, upload.currentSize); - unsigned long writeEndTime = millis(); - unsigned long writeDuration = writeEndTime - writeStartTime; + const unsigned long writeStartTime = millis(); + const size_t written = uploadFile.write(upload.buf, upload.currentSize); + const unsigned long writeEndTime = millis(); + const unsigned long writeDuration = writeEndTime - writeStartTime; if (written != upload.currentSize) { uploadError = "Failed to write to SD card - disk may be full"; @@ -548,9 +551,9 @@ void CrossPointWebServer::handleUpload() { // Log progress every 50KB or if write took >100ms if (uploadSize - lastLoggedSize >= 51200 || writeDuration > 100) { - unsigned long timeSinceStart = millis() - uploadStartTime; - unsigned long timeSinceLastWrite = millis() - lastWriteTime; - float kbps = (uploadSize / 1024.0) / (timeSinceStart / 1000.0); + const unsigned long timeSinceStart = millis() - uploadStartTime; + const unsigned long timeSinceLastWrite = millis() - lastWriteTime; + const float kbps = (uploadSize / 1024.0) / (timeSinceStart / 1000.0); Serial.printf( "[%lu] [WEB] [UPLOAD] Progress: %d bytes (%.1f KB), %.1f KB/s, write took %lu ms, gap since last: %lu " @@ -584,23 +587,23 @@ void CrossPointWebServer::handleUpload() { } } -void CrossPointWebServer::handleUploadPost() { +void CrossPointWebServer::handleUploadPost() const { if (uploadSuccess) { server->send(200, "text/plain", "File uploaded successfully: " + uploadFileName); } else { - String error = uploadError.isEmpty() ? "Unknown error during upload" : uploadError; + const String error = uploadError.isEmpty() ? "Unknown error during upload" : uploadError; server->send(400, "text/plain", error); } } -void CrossPointWebServer::handleCreateFolder() { +void CrossPointWebServer::handleCreateFolder() const { // Get folder name from form data if (!server->hasArg("name")) { server->send(400, "text/plain", "Missing folder name"); return; } - String folderName = server->arg("name"); + const String folderName = server->arg("name"); // Validate folder name if (folderName.isEmpty()) { @@ -643,7 +646,7 @@ void CrossPointWebServer::handleCreateFolder() { } } -void CrossPointWebServer::handleDelete() { +void CrossPointWebServer::handleDelete() const { // Get path from form data if (!server->hasArg("path")) { server->send(400, "text/plain", "Missing path"); @@ -651,7 +654,7 @@ void CrossPointWebServer::handleDelete() { } String itemPath = server->arg("path"); - String itemType = server->hasArg("type") ? server->arg("type") : "file"; + const String itemType = server->hasArg("type") ? server->arg("type") : "file"; // Validate path if (itemPath.isEmpty() || itemPath == "/") { @@ -665,7 +668,7 @@ void CrossPointWebServer::handleDelete() { } // Security check: prevent deletion of protected items - String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1); + const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1); // Check if item starts with a dot (hidden/system file) if (itemName.startsWith(".")) { diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index 16983b0..e37eb4e 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -24,7 +24,7 @@ class CrossPointWebServer { void stop(); // Call this periodically to handle client requests - void handleClient(); + void handleClient() const; // Check if server is running bool isRunning() const { return running; } @@ -33,22 +33,22 @@ class CrossPointWebServer { uint16_t getPort() const { return port; } private: - WebServer* server = nullptr; + std::unique_ptr server = nullptr; bool running = false; uint16_t port = 80; // File scanning - std::vector scanFiles(const char* path = "/"); - String formatFileSize(size_t bytes); - bool isEpubFile(const String& filename); + std::vector scanFiles(const char* path = "/") const; + String formatFileSize(size_t bytes) const; + bool isEpubFile(const String& filename) const; // Request handlers - void handleRoot(); - void handleNotFound(); - void handleStatus(); - void handleFileList(); - void handleUpload(); - void handleUploadPost(); - void handleCreateFolder(); - void handleDelete(); + void handleRoot() const; + void handleNotFound() const; + void handleStatus() const; + void handleFileList() const; + void handleUpload() const; + void handleUploadPost() const; + void handleCreateFolder() const; + void handleDelete() const; };