diff --git a/src/helpers/esp32/WebFileTransferPage.h b/src/helpers/esp32/WebFileTransferPage.h index 5a6e23d..b2587bd 100644 --- a/src/helpers/esp32/WebFileTransferPage.h +++ b/src/helpers/esp32/WebFileTransferPage.h @@ -21,7 +21,7 @@ R"FILEPAGE( :root{color-scheme:dark;--bg:#0b0d0e;--panel:#15191a;--line:#303738;--text:#edf2f1;--muted:#929b99;--accent:#15b6a6;--danger:#ef6a67} *{box-sizing:border-box}body{margin:0;min-height:100vh;background:linear-gradient(150deg,#0b0d0e,#111817);color:var(--text);font-family:ui-monospace,SFMono-Regular,Menlo,monospace} main{width:min(560px,100%);margin:0 auto;padding:24px 16px 40px}header{display:flex;align-items:center;gap:11px;margin-bottom:24px}header b{font-size:20px;letter-spacing:0}header i{display:block;width:9px;height:9px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px var(--accent)} -section{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:16px;margin:12px 0}label{display:block;color:var(--muted);font-size:12px;margin-bottom:7px}input,button{font:inherit;border-radius:6px;min-height:42px}input[type=password]{width:100%;padding:9px 11px;background:#0d1112;color:var(--text);border:1px solid var(--line);font-size:18px;letter-spacing:3px}button{border:1px solid var(--line);background:#202627;color:var(--text);padding:8px 14px;cursor:pointer}button.primary{background:var(--accent);border-color:var(--accent);color:#061412;font-weight:700}button.danger{color:var(--danger)}button:disabled{opacity:.4;cursor:not-allowed}.row{display:flex;gap:8px;margin-top:10px}.row>*{flex:1}.drop{display:block;border:1px dashed #53605e;border-radius:8px;padding:24px 12px;text-align:center;color:var(--muted);cursor:pointer}.drop.ready{border-color:var(--accent);color:var(--text)}#pick{position:absolute;opacity:0;pointer-events:none}#name{margin-top:10px;overflow-wrap:anywhere}progress{width:100%;height:10px;margin-top:14px;accent-color:var(--accent)}#status{min-height:20px;margin-top:10px;color:var(--muted);font-size:13px}#status.ok{color:var(--accent)}#status.err{color:var(--danger)}small{display:block;color:var(--muted);line-height:1.5;margin-top:12px}.file{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line)}.file:last-child{border-bottom:0}.file span{flex:1;min-width:0;overflow-wrap:anywhere}.file small{margin:2px 0 0}.empty{color:var(--muted);padding:10px 0} +section{border:1px solid var(--line);background:var(--panel);border-radius:8px;padding:16px;margin:12px 0}label{display:block;color:var(--muted);font-size:12px;margin-bottom:7px}input,button,select{font:inherit;border-radius:6px;min-height:42px}input[type=password]{width:100%;padding:9px 11px;background:#0d1112;color:var(--text);border:1px solid var(--line);font-size:18px;letter-spacing:3px}button,select{border:1px solid var(--line);background:#202627;color:var(--text);padding:8px 14px}button{cursor:pointer}button.primary{background:var(--accent);border-color:var(--accent);color:#061412;font-weight:700}button.danger{color:var(--danger)}button:disabled,select:disabled{opacity:.4;cursor:not-allowed}.row{display:flex;gap:8px;margin-top:10px}.row>*{flex:1}.file-tools{align-items:center}.file-tools b{flex:1.4}.file-tools select{min-width:0}.drop{display:block;border:1px dashed #53605e;border-radius:8px;padding:24px 12px;text-align:center;color:var(--muted);cursor:pointer}.drop.ready{border-color:var(--accent);color:var(--text)}#pick{position:absolute;opacity:0;pointer-events:none}#name{margin-top:10px;overflow-wrap:anywhere}progress{width:100%;height:10px;margin-top:14px;accent-color:var(--accent)}#status{min-height:20px;margin-top:10px;color:var(--muted);font-size:13px}#status.ok{color:var(--accent)}#status.err{color:var(--danger)}small{display:block;color:var(--muted);line-height:1.5;margin-top:12px}.file{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid var(--line)}.file:last-child{border-bottom:0}.file span{flex:1;min-width:0;overflow-wrap:anywhere}.file small{margin:2px 0 0}.actions{display:flex;gap:6px}.actions button{padding:7px 10px}.empty{color:var(--muted);padding:10px 0}
@@ -41,8 +41,8 @@ section{border:1px solid var(--line);background:var(--panel);border-radius:8px;p Files are uploaded to the device's SD card in /transfer. The transfer session ends when you leave the File Transfer app on the device.
-
Device files
-
Connect to list screenshots and transferred files.
+
Device files
+
Connect to list screenshots, crash reports, backups, and transferred files.
)FILEPAGE"; diff --git a/src/helpers/esp32/WebFileTransferProtocol.h b/src/helpers/esp32/WebFileTransferProtocol.h index a097340..225469d 100644 --- a/src/helpers/esp32/WebFileTransferProtocol.h +++ b/src/helpers/esp32/WebFileTransferProtocol.h @@ -51,11 +51,22 @@ inline bool fileNameValid(const char* name) { return true; } +inline bool firmwareExportName(const char* name) { + if (!fileNameValid(name)) return false; + if (strcmp(name, "wadamesh-crash.elf") == 0 || + strcmp(name, "wadamesh-crash.elf.txt") == 0) return true; + const size_t len = strlen(name); + return len > 14 && strncmp(name, "meshcore-", 9) == 0 && + strcmp(name + len - 5, ".json") == 0; +} + inline bool readablePath(const char* path) { if (!path) return false; const char* leaf = nullptr; if (strncmp(path, "/screenshots/", 13) == 0) leaf = path + 13; else if (strncmp(path, "/transfer/", 10) == 0) leaf = path + 10; + else if (path[0] == '/' && strchr(path + 1, '/') == nullptr && + firmwareExportName(path + 1)) leaf = path + 1; else return false; return fileNameValid(leaf) && strchr(leaf, '/') == nullptr; } diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index f58ce9d..ce736df 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -25373,19 +25373,20 @@ static void fileTransferListNext() { fs::FS& storage = fileTransferStorage(); while (s_file_transfer_listing) { if (!s_file_transfer_list_dir) { - const char* dir_path = s_file_transfer_list_phase == 0 ? "/screenshots" : "/transfer"; + const char* dir_path = s_file_transfer_list_phase == 0 ? "/screenshots" : + s_file_transfer_list_phase == 1 ? "/transfer" : "/"; s_file_transfer_list_dir = storage.open(dir_path, FILE_READ); markSdIo(); if (!s_file_transfer_list_dir || !s_file_transfer_list_dir.isDirectory()) { if (s_file_transfer_list_dir) s_file_transfer_list_dir.close(); - if (s_file_transfer_list_phase == 1) { + if (s_file_transfer_list_phase == 2) { fileTransferStorageIoFailed(); s_file_transfer_listing = false; fileTransferReply("ERR SD read failed"); return; } s_file_transfer_list_phase++; - if (s_file_transfer_list_phase >= 2) { + if (s_file_transfer_list_phase >= 3) { s_file_transfer_listing = false; fileTransferReply("LIST DONE"); } @@ -25397,7 +25398,7 @@ static void fileTransferListNext() { if (!entry) { s_file_transfer_list_dir.close(); s_file_transfer_list_phase++; - if (s_file_transfer_list_phase >= 2) { + if (s_file_transfer_list_phase >= 3) { s_file_transfer_listing = false; fileTransferReply("LIST DONE"); } @@ -25413,8 +25414,11 @@ static void fileTransferListNext() { if (name_fits) snprintf(safe_leaf, sizeof safe_leaf, "%s", leaf); entry.close(); if (directory || !name_fits || !WebFileTransferProtocol::fileNameValid(safe_leaf)) continue; + if (s_file_transfer_list_phase == 2 && + !WebFileTransferProtocol::firmwareExportName(safe_leaf)) continue; - const char* prefix = s_file_transfer_list_phase == 0 ? "/screenshots/" : "/transfer/"; + const char* prefix = s_file_transfer_list_phase == 0 ? "/screenshots/" : + s_file_transfer_list_phase == 1 ? "/transfer/" : "/"; char reply[WebFileTransfer::MAX_REPLY_BYTES]; snprintf(reply, sizeof reply, "ENTRY %lu %s%s", static_cast(size), prefix, safe_leaf); @@ -25480,6 +25484,44 @@ static void fileTransferDownloadBegin(const char* path) { fileTransferReply(reply); } +static void fileTransferDelete(const char* path) { + if (s_file_transfer_uploading || s_file_transfer_downloading || s_file_transfer_listing) { + fileTransferReply("ERR transfer in progress"); + return; + } + if (!WebFileTransferProtocol::readablePath(path)) { + fileTransferReply("ERR invalid delete path"); + return; + } + if (!fileTransferStorageReady()) { + fileTransferStorageIoFailed(); + fileTransferReply("ERR SD card unavailable"); + return; + } + + fs::FS& storage = fileTransferStorage(); + File target = storage.open(path, FILE_READ); + markSdIo(); + if (!target || target.isDirectory()) { + if (target) target.close(); + fileTransferReply("ERR file unavailable"); + return; + } + target.close(); + if (!storage.remove(path)) { + fileTransferStorageIoFailed(); + fileTransferReply("ERR delete failed"); + return; + } + markSdIo(); + const char* leaf = strrchr(path, '/'); + leaf = leaf ? leaf + 1 : path; + snprintf(s_file_transfer_result, sizeof s_file_transfer_result, "Deleted %s", leaf); + fileTransferResetRead(); + s_file_transfer_listing = true; + fileTransferListNext(); +} + static void fileTransferDownloadRead(const char* command) { unsigned long requested = 0; char extra = 0; @@ -25572,6 +25614,7 @@ static void webFileTransferTick() { else if (strcmp(command, "LIST") == 0) fileTransferListBegin(); else if (strcmp(command, "LIST NEXT") == 0) fileTransferListNext(); else if (strncmp(command, "GET ", 4) == 0) fileTransferDownloadBegin(command + 4); + else if (strncmp(command, "DELETE ", 7) == 0) fileTransferDelete(command + 7); else if (strncmp(command, "READ ", 5) == 0) fileTransferDownloadRead(command); else if (strcmp(command, "CANCEL") == 0) { fileTransferResetUpload(true); @@ -25696,7 +25739,7 @@ static void openFileTransferPage() { const lv_coord_t width = sw - 28; lv_obj_t* intro = lv_label_create(s_file_transfer_root); - lv_label_set_text(intro, TR("Upload files or download screenshots from a browser on the same Wi-Fi.")); + lv_label_set_text(intro, TR("Upload, download, or delete files from a browser on the same Wi-Fi.")); lv_obj_set_style_text_font(intro, &g_font_14, LV_PART_MAIN); lv_obj_set_style_text_color(intro, lv_color_hex(COLOR_TEXT), LV_PART_MAIN); lv_label_set_long_mode(intro, LV_LABEL_LONG_WRAP); diff --git a/test/test_web_file_transfer_protocol.cpp b/test/test_web_file_transfer_protocol.cpp index 25d2a69..a15bbc0 100644 --- a/test/test_web_file_transfer_protocol.cpp +++ b/test/test_web_file_transfer_protocol.cpp @@ -50,9 +50,18 @@ int main() { assert(readablePath("/screenshots/capture.png")); assert(readablePath("/transfer/archive.bin")); + assert(readablePath("/wadamesh-crash.elf")); + assert(readablePath("/wadamesh-crash.elf.txt")); + assert(readablePath("/meshcore-backup.json")); + assert(readablePath("/meshcore-20260907-120000.json")); + assert(readablePath("/meshcore-backup-123.json")); assert(!readablePath("/transfer/.upload.part")); assert(!readablePath("/transfer/../secret.txt")); assert(!readablePath("/transfer/nested/file.txt")); + assert(!readablePath("/contacts3")); + assert(!readablePath("/other.json")); + assert(!readablePath("/meshcore-.json")); + assert(!readablePath("/meshcore-backup.json/extra")); assert(!readablePath("/other/archive.bin")); return 0; } \ No newline at end of file