From bfded9bbccec51b9ecfc923b0007661fe0a6149a Mon Sep 17 00:00:00 2001 From: btsimonh Date: Sun, 2 Jul 2023 19:11:49 +0100 Subject: [PATCH 1/3] Remove recursion into folders on Manage Files. May be enabled with UFILESYS_RECURSEFOLDERS_GUI. On Edit of a file, Save and Magane btuttons return to the folder containgint the file being edited. On delete file, UI returns to the folder that the deleted file was in. --- .../xdrv_50_filesystem.ino | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino index d3d0acbd72cc..775fa533e5bf 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino @@ -608,7 +608,7 @@ void UFSRun(void) { #ifdef USE_WEBSERVER const char UFS_WEB_DIR[] PROGMEM = - "

"; + "

"; const char UFS_FORM_FILE_UPLOAD[] PROGMEM = "
" @@ -655,7 +655,7 @@ const char UFS_FORM_SDC_HREF[] PROGMEM = #ifdef GUI_TRASH_FILE const char UFS_FORM_SDC_HREFdel[] PROGMEM = //"🗑"; // 🗑️ - "🔥"; // 🔥 + "🔥"; // 🔥 #endif // GUI_TRASH_FILE #ifdef GUI_EDIT_FILE @@ -823,6 +823,7 @@ void UfsListDir(char *path, uint8_t depth) { if (entry.isDirectory()) { ext_snprintf_P(npath, sizeof(npath), UFS_FORM_SDC_HREF, ppe, epe); WSContentSend_P(UFS_FORM_SDC_DIRd, npath, ep, name); +#ifdef UFILESYS_RECURSEFOLDERS_GUI uint8_t plen = strlen(path); if (plen > 1) { strcat(path, "/"); @@ -830,10 +831,11 @@ void UfsListDir(char *path, uint8_t depth) { strcat(path, ep); UfsListDir(path, depth + 4); path[plen] = 0; +#endif } else { #ifdef GUI_TRASH_FILE - char delpath[128]; - ext_snprintf_P(delpath, sizeof(delpath), UFS_FORM_SDC_HREFdel, ppe, epe); + char delpath[128+UFS_FILENAME_SIZE]; + ext_snprintf_P(delpath, sizeof(delpath), UFS_FORM_SDC_HREFdel, ppe, epe, ppe); #else char delpath[2]; delpath[0]=0; @@ -1065,7 +1067,14 @@ void UfsEditor(void) { } WSContentSend_P(HTTP_EDITOR_FORM_END); - WSContentSend_P(UFS_WEB_DIR, PSTR(D_MANAGE_FILE_SYSTEM)); + for (int i = strlen(fname)-1; i >= 0; i--){ + if (fname[i] == '/'){ + fname[i] = 0; + break; + } + } + + WSContentSend_P(UFS_WEB_DIR, fname, PSTR(D_MANAGE_FILE_SYSTEM)); WSContentStop(); } @@ -1119,7 +1128,17 @@ void UfsEditorUpload(void) { fp.close(); - Webserver->sendHeader(F("Location"),F("/ufsu")); + for (int i = strlen(fname)-1; i >= 0; i--){ + if (fname[i] == '/'){ + fname[i] = 0; + break; + } + } + + char t[20+UFS_FILENAME_SIZE] = "/ufsu?download="; + strcat(t, fname); + Webserver->sendHeader(F("Location"), t); + Webserver->send(303); } @@ -1160,7 +1179,7 @@ bool Xdrv50(uint32_t function) { if (XdrvMailbox.index) { XdrvMailbox.index++; } else { - WSContentSend_PD(UFS_WEB_DIR, PSTR(D_MANAGE_FILE_SYSTEM)); + WSContentSend_PD(UFS_WEB_DIR, "/", PSTR(D_MANAGE_FILE_SYSTEM)); } } break; From 7986deb17b71a9bf214d5e3019138fe7161be4a9 Mon Sep 17 00:00:00 2001 From: btsimonh Date: Mon, 3 Jul 2023 08:22:15 +0100 Subject: [PATCH 2/3] Make newfile put in in the current folder, and return to current folder on save of manage button. --- tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino index 775fa533e5bf..62f03ab45883 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino @@ -632,7 +632,7 @@ const char UFS_FORM_SDC_DIRa[] PROGMEM = const char UFS_FORM_SDC_DIRc[] PROGMEM = "
"; const char UFS_FORM_FILE_UPGb[] PROGMEM = - "
" + "" "
"; const char UFS_FORM_FILE_UPGb1[] PROGMEM = "" D_SHOW_HIDDEN_FILES ""; @@ -739,7 +739,7 @@ void UfsDirectory(void) { } WSContentSend_P(UFS_FORM_SDC_DIRc); #ifdef GUI_EDIT_FILE - WSContentSend_P(UFS_FORM_FILE_UPGb); + WSContentSend_P(UFS_FORM_FILE_UPGb, ufs_path); #endif if (!isSDC()) { WSContentSend_P(UFS_FORM_FILE_UPGb1); @@ -1072,6 +1072,11 @@ void UfsEditor(void) { fname[i] = 0; break; } + fname[i] = 0; + } + if (!fname[0]){ + fname[0] = '/'; + fname[1] = 0; } WSContentSend_P(UFS_WEB_DIR, fname, PSTR(D_MANAGE_FILE_SYSTEM)); @@ -1133,6 +1138,11 @@ void UfsEditorUpload(void) { fname[i] = 0; break; } + fname[i] = 0; + } + if (!fname[0]){ + fname[0] = '/'; + fname[1] = 0; } char t[20+UFS_FILENAME_SIZE] = "/ufsu?download="; From 9bba0af64b25d58b92cb4862999daa7719a43981 Mon Sep 17 00:00:00 2001 From: btsimonh Date: Sun, 9 Jul 2023 12:58:14 +0100 Subject: [PATCH 3/3] Add folderOnly and FileOnly functions to reduce code duplication. Enable folder delete. Enable folder listing to be aborted (x in browser) Disbale ESP32 Download Task. Needs attention. Allow folder create from newfile name. --- .../xdrv_50_filesystem.ino | 330 ++++++++++++++---- 1 file changed, 261 insertions(+), 69 deletions(-) diff --git a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino index 62f03ab45883..ee99a4f801ff 100644 --- a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino +++ b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino @@ -18,6 +18,17 @@ */ #ifdef USE_UFILESYS + +// saves 80 bytes of flash, makes the UI cleaner for folders containing lots of files. +// disables recursive folder listing in file UI +//#define UFILESYS_NO_RECURSE_GUI + +// Enables serving of static files on /fs/ +// costs 1844 bytes of flash and 40 bytes of RAM +// probably not useful on esp8266, but useful on esp32 +// You could serve a whole webapp from Tas itself. +//#define UFILESYS_STATIC_SERVING + /*********************************************************************************************\ This driver adds universal file system support for - ESP8266 (sd card or littlefs on > 1 M devices with special linker file e.g. eagle.flash.4m2m.ld) @@ -135,6 +146,36 @@ void UfsInit(void) { } } +// simple put a zero at last '/' +// modifies input string +char *folderOnly(char *fname){ + for (int i = strlen(fname)-1; i >= 0; i--){ + if (fname[i] == '/'){ + fname[i] = 0; + break; + } + fname[i] = 0; + } + if (!fname[0]){ + fname[0] = '/'; + fname[1] = 0; + } + return fname; +} + +// returns everything after last '/' of whiole input if no '/' +char *fileOnly(char *fname){ + char *cp = fname; + for (uint32_t cnt = strlen(fname); cnt >= 0; cnt--) { + if (fname[cnt] == '/') { + cp = &fname[cnt + 1]; + break; + } + } + return cp; +} + + #ifdef USE_SDCARD void UfsCheckSDCardInit(void) { // Try SPI mode first @@ -503,10 +544,18 @@ char* UfsFilename(char* fname, char* fname_in) { } const char kUFSCommands[] PROGMEM = "Ufs|" // Prefix - "|Type|Size|Free|Delete|Rename|Run"; + "|Type|Size|Free|Delete|Rename|Run" +#ifdef UFILESYS_STATIC_SERVING + "|Serve" +#endif + ; void (* const kUFSCommand[])(void) PROGMEM = { - &UFSInfo, &UFSType, &UFSSize, &UFSFree, &UFSDelete, &UFSRename, &UFSRun}; + &UFSInfo, &UFSType, &UFSSize, &UFSFree, &UFSDelete, &UFSRename, &UFSRun +#ifdef UFILESYS_STATIC_SERVING + ,&UFSServe +#endif + }; void UFSInfo(void) { Response_P(PSTR("{\"Ufs\":{\"Type\":%d,\"Size\":%d,\"Free\":%d}"), ufs_type, UfsInfo(0, 0), UfsInfo(1, 0)); @@ -586,6 +635,115 @@ void UFSRename(void) { } } +#ifdef UFILESYS_STATIC_SERVING +/* +* Serves a filesystem folder at a web url. +* NOTE - this is expensive on flash -> +2.5kbytes. +* like "UFSServe ,[,]" +* e.g. "UFSServe /sd/,/mysdcard/,1" - will serve the /sd/ fs folder as https:///mysdcard/ with no auth required +* e.g. "UFSServe /www/,/" - will serve the /www/ fs folder as https:/// with auth required if TAS has a password setup +* defaults to 0 - i.e. the default is to require auth if configured +* it WILL serve on / - so conflicting urls could occur. I beleive native TAS urls will have priority. +* you can serve multiple folders, and they can each be auth or noauth +* +* by default, it also enables cors on the webserver - this allows you to have +* a website external to TAS which can access the files, else the browser refuses. +*/ +#include "detail/RequestHandlersImpl.h" + +// class to allow us to request auth when required. +// StaticRequestHandler is in the above header +class StaticRequestHandlerAuth : public StaticRequestHandler { +public: + StaticRequestHandlerAuth(FS& fs, const char* path, const char* uri, const char* cache_header): + StaticRequestHandler(fs, path, uri, cache_header) + { + } + + // we replace the handle method, + // and look for authentication only if we would serve the file. + // if we check earlier, then we will reject as unauth even though a later + // handler may be public, and so fail to serve public files. + bool handle(WebServer& server, HTTPMethod requestMethod, String requestUri) override { + if (!canHandle(requestMethod, requestUri)) + return false; + + log_v("StaticRequestHandler::handle: request=%s _uri=%s\r\n", requestUri.c_str(), _uri.c_str()); + + String path(_path); + + if (!_isFile) { + // Base URI doesn't point to a file. + // If a directory is requested, look for index file. + if (requestUri.endsWith("/")) + requestUri += "index.htm"; + + // Append whatever follows this URI in request to get the file path. + path += requestUri.substring(_baseUriLength); + } + log_v("StaticRequestHandler::handle: path=%s, isFile=%d\r\n", path.c_str(), _isFile); + + String contentType = getContentType(path); + + // look for gz file, only if the original specified path is not a gz. So part only works to send gzip via content encoding when a non compressed is asked for + // if you point the the path to gzip you will serve the gzip as content type "application/x-gzip", not text or javascript etc... + if (!path.endsWith(FPSTR(mimeTable[gz].endsWith)) && !_fs.exists(path)) { + String pathWithGz = path + FPSTR(mimeTable[gz].endsWith); + if(_fs.exists(pathWithGz)) + path += FPSTR(mimeTable[gz].endsWith); + } + + File f = _fs.open(path, "r"); + if (!f || !f.available()) + return false; + + if (!WebAuthenticate()) { + AddLog(LOG_LEVEL_ERROR, PSTR("UFS: serv of %s denied"), requestUri.c_str()); + server.requestAuthentication(); + return true; + } + + if (_cache_header.length() != 0) + server.sendHeader("Cache-Control", _cache_header); + + server.streamFile(f, contentType); + return true; + } +}; + +void UFSServe(void) { + if (XdrvMailbox.data_len > 0) { + bool result = false; + char *fpath = strtok(XdrvMailbox.data, ","); + char *url = strtok(nullptr, ","); + char *noauth = strtok(nullptr, ","); + if (fpath && url) { + char t[] = ""; + StaticRequestHandlerAuth *staticHandler; + if (noauth && *noauth == '1'){ + staticHandler = (StaticRequestHandlerAuth *) new StaticRequestHandler(*ffsp, fpath, url, (char *)nullptr); + } else { + staticHandler = new StaticRequestHandlerAuth(*ffsp, fpath, url, (char *)nullptr); + } + if (staticHandler) { + //Webserver->serveStatic(url, *ffsp, fpath); + Webserver->addHandler(staticHandler); + Webserver->enableCORS(true); + result = true; + } else { + // could this happen? only lack of memory. + result = false; + } + } + if (!result) { + ResponseCmndFailed(); + } else { + ResponseCmndDone(); + } + } +} +#endif // UFILESYS_STATIC_SERVING + void UFSRun(void) { if (XdrvMailbox.data_len > 0) { char fname[UFS_FILENAME_SIZE]; @@ -601,6 +759,8 @@ void UFSRun(void) { } } + + /*********************************************************************************************\ * Web support \*********************************************************************************************/ @@ -610,6 +770,13 @@ void UFSRun(void) { const char UFS_WEB_DIR[] PROGMEM = "

"; +const char UFS_CURRDIR[] PROGMEM = + "

%s: %s

"; + +#ifndef D_CURR_DIR + #define D_CURR_DIR "Folder" +#endif + const char UFS_FORM_FILE_UPLOAD[] PROGMEM = "
" "
 " D_MANAGE_FILE_SYSTEM " "; @@ -648,7 +815,7 @@ const char UFS_FORM_SDC_DIR_HIDDABLE[] PROGMEM = const char UFS_FORM_SDC_DIRd[] PROGMEM = "
%s
"; const char UFS_FORM_SDC_DIRb[] PROGMEM = - "%s %s %8d %s %s"; + "%s %19s %8d %s %s"; const char UFS_FORM_SDC_HREF[] PROGMEM = "ufsd?download=%s/%s"; @@ -684,36 +851,48 @@ void UfsDirectory(void) { AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_MANAGE_FILE_SYSTEM)); uint8_t depth = 0; + uint8_t isdir = 0; strcpy(ufs_path, "/"); - if (Webserver->hasArg(F("download"))) { - String stmp = Webserver->arg(F("download")); - char *cp = (char*)stmp.c_str(); - if (UfsDownloadFile(cp)) { - // is directory - strcpy(ufs_path, cp); - } else { - return; - } - } - if (Webserver->hasArg(F("dir"))) { String stmp = Webserver->arg(F("dir")); ufs_dir = atoi(stmp.c_str()); - if (ufs_dir == 1) { - dfsp = ufsp; - } else { - if (ffsp) { - dfsp = ffsp; - } + } + + if (ufs_dir == 1) { + dfsp = ufsp; + } else { + if (ffsp) { + dfsp = ffsp; } } if (Webserver->hasArg(F("delete"))) { String stmp = Webserver->arg(F("delete")); char *cp = (char*)stmp.c_str(); - dfsp->remove(cp); + File download_file = dfsp->open(cp, UFS_FILE_READ); + if (download_file) { + if (download_file.isDirectory()) { + download_file.close(); + dfsp->rmdir(cp); + } else { + download_file.close(); + dfsp->remove(cp); + } + } + } + + if (Webserver->hasArg(F("download"))) { + String stmp = Webserver->arg(F("download")); + char *cp = (char*)stmp.c_str(); + if (UfsDownloadFile(cp)) { + // is directory + strcpy(ufs_path, cp); + isdir = 1; + } else { + return; + } } WSContentStart_P(PSTR(D_MANAGE_FILE_SYSTEM)); @@ -733,6 +912,13 @@ void UfsDirectory(void) { WSContentSend_P(UFS_FORM_FILE_UPG, PSTR(D_SCRIPT_UPLOAD)); + if (isdir){ + // if a folder, show 'folder: xxx' if not '/' + if (ufs_path[0] != '/' || strlen(ufs_path) != 1){ + WSContentSend_P(UFS_CURRDIR, PSTR(D_CURR_DIR), ufs_path); + } + } + WSContentSend_P(UFS_FORM_SDC_DIRa); if (ufs_type) { UfsListDir(ufs_path, depth); @@ -788,6 +974,13 @@ void UfsListDir(char *path, uint8_t depth) { } char *ep; while (true) { + WiFiClient client = Webserver->client(); + // abort if the client disconnected + // if there is a huge folder, then this gives a way out by refresh of browser + if (!client.connected()){ + break; + } + File entry = dir.openNextFile(); if (!entry) { break; @@ -820,9 +1013,18 @@ void UfsListDir(char *path, uint8_t depth) { const char* ppe = pp_escaped_string.c_str(); // this can't be merged on a single line otherwise the String object can be freed const char* epe = ep_escaped_string.c_str(); sprintf(cp, format, ep); +#ifdef GUI_TRASH_FILE + char delpath[128+UFS_FILENAME_SIZE]; + ext_snprintf_P(delpath, sizeof(delpath), UFS_FORM_SDC_HREFdel, ppe, epe, ppe[0]?ppe:"/"); +#else + char delpath[2] = " "; +#endif // GUI_TRASH_FILE if (entry.isDirectory()) { ext_snprintf_P(npath, sizeof(npath), UFS_FORM_SDC_HREF, ppe, epe); - WSContentSend_P(UFS_FORM_SDC_DIRd, npath, ep, name); + + WSContentSend_P(UFS_FORM_SDC_DIRb, hiddable ? UFS_FORM_SDC_DIR_HIDDABLE : UFS_FORM_SDC_DIR_NORMAL, npath, epe, + HtmlEscape(name).c_str(), "", 0, delpath, " "); + //WSContentSend_P(UFS_FORM_SDC_DIRd, npath, ep, name); #ifdef UFILESYS_RECURSEFOLDERS_GUI uint8_t plen = strlen(path); if (plen > 1) { @@ -833,13 +1035,6 @@ void UfsListDir(char *path, uint8_t depth) { path[plen] = 0; #endif } else { - #ifdef GUI_TRASH_FILE - char delpath[128+UFS_FILENAME_SIZE]; - ext_snprintf_P(delpath, sizeof(delpath), UFS_FORM_SDC_HREFdel, ppe, epe, ppe); - #else - char delpath[2]; - delpath[0]=0; - #endif // GUI_TRASH_FILE #ifdef GUI_EDIT_FILE char editpath[128]; ext_snprintf_P(editpath, sizeof(editpath), UFS_FORM_SDC_HREFedit, ppe, epe); @@ -859,12 +1054,16 @@ void UfsListDir(char *path, uint8_t depth) { } #ifdef ESP32 -#define ESP32_DOWNLOAD_TASK +// this actually does not work reliably, as the +// webserver can close the connection before the download task completes +//#define ESP32_DOWNLOAD_TASK #endif // ESP32 uint8_t UfsDownloadFile(char *file) { File download_file; + AddLog(LOG_LEVEL_INFO, PSTR("UFS: File '%s' download"), file); + if (!dfsp->exists(file)) { AddLog(LOG_LEVEL_INFO, PSTR("UFS: File '%s' not found"), file); return 0; @@ -877,6 +1076,7 @@ uint8_t UfsDownloadFile(char *file) { } if (download_file.isDirectory()) { + AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: File '%s' to download is directory"), file); download_file.close(); return 1; } @@ -889,13 +1089,7 @@ uint8_t UfsDownloadFile(char *file) { Webserver->setContentLength(flen); char attachment[100]; - char *cp; - for (uint32_t cnt = strlen(file); cnt >= 0; cnt--) { - if (file[cnt] == '/') { - cp = &file[cnt + 1]; - break; - } - } + char *cp = fileOnly(file); snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=%s"), cp); Webserver->sendHeader(F("Content-Disposition"), attachment); WSSend(200, CT_APP_STREAM, ""); @@ -924,6 +1118,10 @@ uint8_t UfsDownloadFile(char *file) { #endif // ESP32_DOWNLOAD_TASK + +// to make this work I thing you wouold need to duplicate the client +// BEFORE starting the task, so that the webserver does not close it's +// copy of the client. #ifdef ESP32_DOWNLOAD_TASK download_file.close(); @@ -954,20 +1152,18 @@ void download_task(void *path) { WiFiClient download_Client; char *file = (char*) path; + AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: ESP32 File '%s' to download"), file); + download_file = dfsp->open(file, UFS_FILE_READ); uint32_t flen = download_file.size(); + AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: len %d to download"), flen); + download_Client = Webserver->client(); Webserver->setContentLength(flen); char attachment[100]; - char *cp; - for (uint32_t cnt = strlen(file); cnt >= 0; cnt--) { - if (file[cnt] == '/') { - cp = &file[cnt + 1]; - break; - } - } + char *cp = fileOnly(file); //snprintf_P(attachment, sizeof(attachment), PSTR("download file '%s' as '%s'"), file, cp); //Webserver->sendHeader(F("X-Tasmota-Debug"), attachment); snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=%s"), cp); @@ -989,6 +1185,7 @@ void download_task(void *path) { UfsData.download_busy = false; vTaskDelete( NULL ); free(path); + AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: esp32 sent file")); } #endif // ESP32_DOWNLOAD_TASK @@ -1067,18 +1264,7 @@ void UfsEditor(void) { } WSContentSend_P(HTTP_EDITOR_FORM_END); - for (int i = strlen(fname)-1; i >= 0; i--){ - if (fname[i] == '/'){ - fname[i] = 0; - break; - } - fname[i] = 0; - } - if (!fname[0]){ - fname[0] = '/'; - fname[1] = 0; - } - + folderOnly(fname); WSContentSend_P(UFS_WEB_DIR, fname, PSTR(D_MANAGE_FILE_SYSTEM)); WSContentStop(); } @@ -1114,6 +1300,23 @@ void UfsEditorUpload(void) { return; } + // recursively create folder(s) + char tmp[UFS_FILENAME_SIZE]; + char folder[UFS_FILENAME_SIZE] = ""; + strcpy(tmp, fname); + // zap file name off the end + folderOnly(tmp); + char *tf = strtok(tmp, "/"); + while(tf){ + if (*tf){ + strcat(folder, "/"); + strcat(folder, tf); + } + // we don;t care if it fails - it may already exist. + dfsp->mkdir(folder); + tf = strtok(nullptr, "/"); + } + File fp = dfsp->open(fname, "w"); if (!fp) { Web.upload_error = 1; @@ -1133,22 +1336,11 @@ void UfsEditorUpload(void) { fp.close(); - for (int i = strlen(fname)-1; i >= 0; i--){ - if (fname[i] == '/'){ - fname[i] = 0; - break; - } - fname[i] = 0; - } - if (!fname[0]){ - fname[0] = '/'; - fname[1] = 0; - } - + // zap file name off the end + folderOnly(fname); char t[20+UFS_FILENAME_SIZE] = "/ufsu?download="; strcat(t, fname); Webserver->sendHeader(F("Location"), t); - Webserver->send(303); }