diff --git a/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino b/tasmota/tasmota_xdrv_driver/xdrv_50_filesystem.ino index d3d0acbd72cc..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 \*********************************************************************************************/ @@ -608,7 +768,14 @@ void UFSRun(void) { #ifdef USE_WEBSERVER 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 = "
" @@ -632,7 +799,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 ""; @@ -648,14 +815,14 @@ 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"; #ifdef GUI_TRASH_FILE const char UFS_FORM_SDC_HREFdel[] PROGMEM = //"🗑"; // 🗑️ - "🔥"; // 🔥 + "🔥"; // 🔥 #endif // GUI_TRASH_FILE #ifdef GUI_EDIT_FILE @@ -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,13 +912,20 @@ 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); } 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); @@ -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,19 @@ 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) { strcat(path, "/"); @@ -830,14 +1033,8 @@ 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); - #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); @@ -857,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; @@ -875,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; } @@ -887,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, ""); @@ -922,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(); @@ -952,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); @@ -987,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 @@ -1065,7 +1264,8 @@ void UfsEditor(void) { } WSContentSend_P(HTTP_EDITOR_FORM_END); - WSContentSend_P(UFS_WEB_DIR, PSTR(D_MANAGE_FILE_SYSTEM)); + folderOnly(fname); + WSContentSend_P(UFS_WEB_DIR, fname, PSTR(D_MANAGE_FILE_SYSTEM)); WSContentStop(); } @@ -1100,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; @@ -1119,7 +1336,11 @@ void UfsEditorUpload(void) { fp.close(); - Webserver->sendHeader(F("Location"),F("/ufsu")); + // 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); } @@ -1160,7 +1381,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;