#include "WebServer.hpp" #include "../SdCard.hpp" #include "esp_log.h" #include #include #include #include #include #include static const char* TAG = "WebServer"; // Embedded file manager HTML (from Flash) extern const uint8_t filemanager_html_start[] asm("_binary_filemanager_html_start"); extern const uint8_t filemanager_html_end[] asm("_binary_filemanager_html_end"); // Maximum upload size (2 MB) static constexpr size_t MAX_UPLOAD_SIZE = 2 * 1024 * 1024; // GET /files - Serve embedded file manager HTML esp_err_t WebServer::fileManagerHandler(httpd_req_t* req) { httpd_resp_set_type(req, "text/html; charset=utf-8"); size_t len = filemanager_html_end - filemanager_html_start; httpd_resp_send(req, (const char*)filemanager_html_start, len); return ESP_OK; } // GET /api/files/list?path=/ - List directory contents esp_err_t WebServer::filesListHandler(httpd_req_t* req) { char path[128] = "/"; getQueryParam(req, "path", path, sizeof(path)); char fullPath[256]; snprintf(fullPath, sizeof(fullPath), "%.7s%.127s", SdCard::MOUNT_POINT, path); if (!SdCard::instance().isMounted()) { return sendJsonError(req, "SD card not mounted"); } DIR* dir = opendir(fullPath); if (!dir) { return sendJsonError(req, "Cannot open directory"); } cJSON* json = cJSON_CreateObject(); cJSON_AddBoolToObject(json, "success", true); cJSON_AddStringToObject(json, "path", path); cJSON* files = cJSON_AddArrayToObject(json, "files"); struct dirent* entry; while ((entry = readdir(dir)) != nullptr) { if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) continue; char entryPath[512]; size_t fullPathLen = strlen(fullPath); const char* sep = (fullPathLen > 0 && fullPath[fullPathLen-1] == '/') ? "" : "/"; snprintf(entryPath, sizeof(entryPath), "%.255s%s%.255s", fullPath, sep, entry->d_name); struct stat st; bool isDir = false; size_t fileSize = 0; if (stat(entryPath, &st) == 0) { isDir = S_ISDIR(st.st_mode); fileSize = st.st_size; } else { isDir = (entry->d_type == DT_DIR); } cJSON* fileObj = cJSON_CreateObject(); cJSON_AddStringToObject(fileObj, "name", entry->d_name); cJSON_AddBoolToObject(fileObj, "isDir", isDir); cJSON_AddNumberToObject(fileObj, "size", (double)fileSize); cJSON_AddItemToArray(files, fileObj); } closedir(dir); return sendJsonObject(req, json); } // POST /api/files/upload?path=/ - Upload file (multipart/form-data) esp_err_t WebServer::filesUploadHandler(httpd_req_t* req) { char path[128] = "/"; getQueryParam(req, "path", path, sizeof(path)); if (!SdCard::instance().isMounted()) { return sendJsonError(req, "SD card not mounted"); } if (req->content_len > MAX_UPLOAD_SIZE) { return sendJsonError(req, "File too large (max 2MB)"); } char contentType[128] = ""; if (httpd_req_get_hdr_value_str(req, "Content-Type", contentType, sizeof(contentType)) != ESP_OK) { return sendJsonError(req, "Missing Content-Type"); } char* boundaryStart = strstr(contentType, "boundary="); if (!boundaryStart) { return sendJsonError(req, "Invalid multipart boundary"); } boundaryStart += 9; char boundary[72]; snprintf(boundary, sizeof(boundary), "--%.68s", boundaryStart); size_t contentLen = req->content_len; char* fullData = new char[contentLen + 1]; if (!fullData) { return sendJsonError(req, "Out of memory"); } size_t totalReceived = 0; while (totalReceived < contentLen) { int ret = httpd_req_recv(req, fullData + totalReceived, contentLen - totalReceived); if (ret <= 0) { delete[] fullData; return sendJsonError(req, "Receive failed"); } totalReceived += ret; } fullData[totalReceived] = '\0'; char* filenameStart = strstr(fullData, "filename=\""); if (!filenameStart) { delete[] fullData; return sendJsonError(req, "No filename found"); } filenameStart += 10; char* filenameEnd = strchr(filenameStart, '"'); if (!filenameEnd) { delete[] fullData; return sendJsonError(req, "Invalid filename"); } size_t filenameLen = filenameEnd - filenameStart; char filename[128]; if (filenameLen >= sizeof(filename)) filenameLen = sizeof(filename) - 1; memcpy(filename, filenameStart, filenameLen); filename[filenameLen] = '\0'; char* dataStart = strstr(filenameEnd, "\r\n\r\n"); if (!dataStart) { delete[] fullData; return sendJsonError(req, "Invalid multipart format"); } dataStart += 4; char endBoundary[80]; snprintf(endBoundary, sizeof(endBoundary), "\r\n%.72s", boundary); char* dataEnd = strstr(dataStart, endBoundary); if (!dataEnd) { dataEnd = strstr(dataStart, boundary); } if (!dataEnd) { delete[] fullData; return sendJsonError(req, "End boundary not found"); } size_t fileSize = dataEnd - dataStart; char resolvedDir[384]; char targetDirPath[256]; snprintf(targetDirPath, sizeof(targetDirPath), "%.7s%.127s", SdCard::MOUNT_POINT, path); const char* actualDir = targetDirPath; if (resolveCaseInsensitivePath(targetDirPath, resolvedDir, sizeof(resolvedDir))) { actualDir = resolvedDir; } char fullPath[384]; snprintf(fullPath, sizeof(fullPath), "%.250s/%.120s", actualDir, filename); ESP_LOGI(TAG, "Uploading: %s (%zu bytes)", fullPath, fileSize); struct stat dirStat; if (stat(actualDir, &dirStat) != 0) { if (mkdir(actualDir, 0755) != 0 && errno != EEXIST) { delete[] fullData; return sendJsonError(req, "Cannot create directory"); } } FILE* f = fopen(fullPath, "wb"); if (!f) { ESP_LOGE(TAG, "Cannot create file: %s (errno=%d)", fullPath, errno); delete[] fullData; return sendJsonError(req, "Cannot create file"); } size_t written = fwrite(dataStart, 1, fileSize, f); fclose(f); delete[] fullData; if (written != fileSize) { return sendJsonError(req, "Write failed"); } ESP_LOGI(TAG, "Upload complete: %s", fullPath); cJSON* json = cJSON_CreateObject(); cJSON_AddBoolToObject(json, "success", true); cJSON_AddStringToObject(json, "filename", filename); cJSON_AddNumberToObject(json, "size", (double)fileSize); return sendJsonObject(req, json); } // GET /api/files/download?file=/path/to/file - Download file esp_err_t WebServer::filesDownloadHandler(httpd_req_t* req) { char file[256] = ""; if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Missing file parameter"); return ESP_FAIL; } char fullPath[384]; snprintf(fullPath, sizeof(fullPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); const char* basename = strrchr(file, '/'); basename = basename ? basename + 1 : file; char disposition[256]; snprintf(disposition, sizeof(disposition), "attachment; filename=\"%.200s\"", basename); httpd_resp_set_hdr(req, "Content-Disposition", disposition); return sendFile(req, fullPath); } // DELETE /api/files/delete?file=/path/to/file - Delete file or empty directory esp_err_t WebServer::filesDeleteHandler(httpd_req_t* req) { char file[256] = ""; if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { return sendJsonError(req, "Missing file parameter"); } if (!SdCard::instance().isMounted()) { return sendJsonError(req, "SD card not mounted"); } char fullPath[384]; snprintf(fullPath, sizeof(fullPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); ESP_LOGI(TAG, "Deleting: %s", fullPath); struct stat st; if (stat(fullPath, &st) != 0) { return sendJsonError(req, "File not found"); } int result; if (S_ISDIR(st.st_mode)) { result = rmdir(fullPath); } else { result = unlink(fullPath); } if (result == 0) { return sendJsonSuccess(req); } return sendJsonError(req, "Delete failed (directory not empty?)"); } // POST /api/files/mkdir?path=/path/to/new/dir - Create directory esp_err_t WebServer::filesMkdirHandler(httpd_req_t* req) { char path[256] = ""; if (!getQueryParam(req, "path", path, sizeof(path)) || strlen(path) == 0) { return sendJsonError(req, "Missing path parameter"); } if (!SdCard::instance().isMounted()) { return sendJsonError(req, "SD card not mounted"); } char fullPath[384]; snprintf(fullPath, sizeof(fullPath), "%.7s%.255s", SdCard::MOUNT_POINT, path); ESP_LOGI(TAG, "Creating directory: %s", fullPath); if (mkdir(fullPath, 0755) == 0) { return sendJsonSuccess(req); } return sendJsonError(req, "Cannot create directory"); } // GET /api/files/read?file=/path/to/file - Read text file content for editor esp_err_t WebServer::filesReadHandler(httpd_req_t* req) { char file[256] = ""; if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { return sendJsonError(req, "Missing file parameter"); } if (!SdCard::instance().isMounted()) { return sendJsonError(req, "SD card not mounted"); } char targetPath[384]; snprintf(targetPath, sizeof(targetPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); char resolvedPath[384]; const char* actualPath = targetPath; if (resolveCaseInsensitivePath(targetPath, resolvedPath, sizeof(resolvedPath))) { actualPath = resolvedPath; } struct stat st; if (stat(actualPath, &st) != 0) { return sendJsonError(req, "File not found"); } if (st.st_size > 65536) { return sendJsonError(req, "File too large for editor (max 64KB)"); } FILE* f = fopen(actualPath, "r"); if (!f) { return sendJsonError(req, "Cannot open file"); } char* content = new char[st.st_size + 1]; if (!content) { fclose(f); return sendJsonError(req, "Out of memory"); } size_t bytesRead = fread(content, 1, st.st_size, f); fclose(f); content[bytesRead] = '\0'; httpd_resp_set_type(req, "text/plain; charset=utf-8"); httpd_resp_send(req, content, bytesRead); delete[] content; return ESP_OK; } // POST /api/files/write?file=/path/to/file - Write text file content from editor esp_err_t WebServer::filesWriteHandler(httpd_req_t* req) { char file[256] = ""; if (!getQueryParam(req, "file", file, sizeof(file)) || strlen(file) == 0) { return sendJsonError(req, "Missing file parameter"); } if (!SdCard::instance().isMounted()) { return sendJsonError(req, "SD card not mounted"); } if (req->content_len > 65536) { return sendJsonError(req, "Content too large (max 64KB)"); } char targetPath[384]; snprintf(targetPath, sizeof(targetPath), "%.7s%.255s", SdCard::MOUNT_POINT, file); char resolvedPath[384]; const char* actualPath = targetPath; if (resolveCaseInsensitivePath(targetPath, resolvedPath, sizeof(resolvedPath))) { actualPath = resolvedPath; } char* content = new char[req->content_len + 1]; if (!content) { return sendJsonError(req, "Out of memory"); } size_t received = 0; while (received < req->content_len) { int ret = httpd_req_recv(req, content + received, req->content_len - received); if (ret <= 0) { delete[] content; return sendJsonError(req, "Receive failed"); } received += ret; } content[received] = '\0'; ESP_LOGI(TAG, "Writing file: %s (%zu bytes)", actualPath, received); FILE* f = fopen(actualPath, "w"); if (!f) { delete[] content; return sendJsonError(req, "Cannot create file"); } size_t written = fwrite(content, 1, received, f); fclose(f); delete[] content; if (written != received) { return sendJsonError(req, "Write incomplete"); } return sendJsonSuccess(req); }