393 lines
12 KiB
C++
393 lines
12 KiB
C++
#include "WebServer.hpp"
|
|
#include "../SdCard.hpp"
|
|
#include "esp_log.h"
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <sys/stat.h>
|
|
#include <dirent.h>
|
|
#include <unistd.h>
|
|
#include <errno.h>
|
|
|
|
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);
|
|
}
|