/* xdrv_50_filesystem.ino - unified file system for Tasmota Copyright (C) 2020 Gerhard Mutz and Theo Arends This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_UFILESYS /*********************************************************************************************\ 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) (makes no sense on 1M devices without sd card) - ESP32 (sd card or littlefs or sfatfile system). The sd card chip select is the standard SDCARD_CS or when not found SDCARD_CS_PIN and initializes the FS System Pointer ufsp which can be used by all standard file system calls. The only specific call is UfsInfo() which gets the total size (0) and free size (1). A button is created in the setup section to show up the file directory to download and upload files subdirectories are supported. Supported commands: ufs fs info ufstype get filesytem type 0=none 1=SD 2=Flashfile ufssize total size in kB ufsfree free size in kB \*********************************************************************************************/ #define XDRV_50 50 #define UFS_TNONE 0 #define UFS_TSDC 1 #define UFS_TFAT 2 #define UFS_TLFS 3 /* // In tasmota.ino #ifdef ESP8266 #include #include #ifdef USE_SDCARD #include #include #endif // USE_SDCARD #endif // ESP8266 #ifdef ESP32 #include #ifdef USE_SDCARD #include #endif // USE_SDCARD #include "FFat.h" #include "FS.h" #endif // ESP32 */ // Global file system pointer FS *ufsp; // Flash file system pointer FS *ffsp; // Local pointer for file managment FS *dfsp; char ufs_path[48]; File ufs_upload_file; uint8_t ufs_dir; // 0 = None, 1 = SD, 2 = ffat, 3 = littlefs uint8_t ufs_type; uint8_t ffs_type; struct { char run_file[48]; int run_file_pos = -1; bool run_file_mutex = 0; bool download_busy; } UfsData; /*********************************************************************************************/ // Init flash file system void UfsInitOnce(void) { ufs_type = 0; ffsp = 0; ufs_dir = 0; #ifdef ESP8266 ffsp = &LittleFS; if (!LittleFS.begin()) { ffsp = nullptr; return; } #endif // ESP8266 #ifdef ESP32 // try lfs first ffsp = &LittleFS; if (!LittleFS.begin(true, "") && !LittleFS.begin(true, "", 5, "fs_1")) { // force empty mount point to make it the fallback FS // ffat is second ffsp = &FFat; if (!FFat.begin(true, "")) { ffsp = nullptr; return; } ffs_type = UFS_TFAT; ufs_type = ffs_type; ufsp = ffsp; dfsp = ffsp; return; } #endif // ESP32 ffs_type = UFS_TLFS; ufs_type = ffs_type; ufsp = ffsp; dfsp = ffsp; } // Called from tasmota.ino at restart. This inits flash file only void UfsInit(void) { UfsData.run_file_pos = -1; UfsInitOnce(); if (ufs_type) { AddLog(LOG_LEVEL_INFO, PSTR("UFS: FlashFS mounted with %d kB free"), UfsInfo(1, 0)); } } #ifdef USE_SDCARD void UfsCheckSDCardInit(void) { // Try SPI mode first // SPI mode requires SDCARD_CS to be configured if (TasmotaGlobal.spi_enabled && PinUsed(GPIO_SDCARD_CS)) { int8_t cs = Pin(GPIO_SDCARD_CS); #ifdef EPS8266 SPI.begin(); #endif // EPS8266 #ifdef ESP32 SPI.begin(Pin(GPIO_SPI_CLK), Pin(GPIO_SPI_MISO), Pin(GPIO_SPI_MOSI), -1); #endif // ESP32 if (SD.begin(cs)) { #ifdef ESP8266 ufsp = &SDFS; #endif // ESP8266 #ifdef ESP32 ufsp = &SD; #endif // ESP32 ufs_type = UFS_TSDC; dfsp = ufsp; if (ffsp) {ufs_dir = 1;} // make sd card the global filesystem #ifdef ESP8266 // on esp8266 sdcard info takes several seconds !!!, so we ommit it here AddLog(LOG_LEVEL_INFO, PSTR("UFS: SDCard mounted")); #endif // ESP8266 #ifdef ESP32 AddLog(LOG_LEVEL_INFO, PSTR("UFS: SDCard mounted (SPI mode) with %d kB free"), UfsInfo(1, 0)); #endif // ESP32 } } #if defined(ESP32) && defined(SOC_SDMMC_HOST_SUPPORTED) // ESP32 and SDMMC supported (not Esp32C3) // check if SDIO is configured else if (PinUsed(GPIO_SDIO_CLK) && PinUsed(GPIO_SDIO_CMD) && PinUsed(GPIO_SDIO_D0)) { int32_t sdio_cmd = Pin(GPIO_SDIO_CMD); int32_t sdio_clk = Pin(GPIO_SDIO_CLK); int32_t sdio_d0 = Pin(GPIO_SDIO_D0); int32_t sdio_d1 = Pin(GPIO_SDIO_D1); int32_t sdio_d2 = Pin(GPIO_SDIO_D2); int32_t sdio_d3 = Pin(GPIO_SDIO_D3); bool bit_4_mode = (sdio_d1 >= 0) && (sdio_d2 >= 0) && (sdio_d3 >= 0); // enable 4-bit mode if possible if (bit_4_mode) { // AddLog(LOG_LEVEL_DEBUG, "UFS: trying SDIO 4-bit clk=%i cmd=%i d0=%i d1=%i d2=%i d3=%i", sdio_clk, sdio_cmd, sdio_d0, sdio_d1, sdio_d2, sdio_d3); SD_MMC.setPins(sdio_clk, sdio_cmd, sdio_d0, sdio_d1, sdio_d2, sdio_d3); } else { // AddLog(LOG_LEVEL_DEBUG, "UFS: trying SDIO 1-bit clk=%i cmd=%i d0=%i", sdio_clk, sdio_cmd, sdio_d0); SD_MMC.setPins(sdio_clk, sdio_cmd, sdio_d0); } if (SD_MMC.begin("/sd", !bit_4_mode /*mode 1 bit*/, false /*format_if_failed*/)) { // mount under "/sd" to be consistent with SD SPI ufsp = &SD_MMC; ufs_type = UFS_TSDC; dfsp = ufsp; if (ffsp) {ufs_dir = 1;} // make sd card the global filesystem AddLog(LOG_LEVEL_INFO, PSTR("UFS: SDCard mounted (SDIO %i-bit) with %d kB free"), bit_4_mode ? 4 : 1, UfsInfo(1, 0)); } } #endif } #endif // USE_SDCARD uint32_t UfsInfo(uint32_t sel, uint32_t type) { uint64_t result = 0; FS *ifsp = ufsp; uint8_t itype = ufs_type; if (type) { ifsp = ffsp; itype = ffs_type; } #ifdef ESP8266 FSInfo64 fsinfo; #endif // ESP8266 switch (itype) { case UFS_TSDC: #ifdef USE_SDCARD #ifdef ESP8266 ifsp->info64(fsinfo); if (sel == 0) { result = fsinfo.totalBytes; } else { result = (fsinfo.totalBytes - fsinfo.usedBytes); } #endif // ESP8266 #ifdef ESP32 if (sel == 0) { result = SD.totalBytes(); } else { result = (SD.totalBytes() - SD.usedBytes()); } #endif // ESP32 #endif // USE_SDCARD break; case UFS_TLFS: #ifdef ESP8266 ifsp->info64(fsinfo); if (sel == 0) { result = fsinfo.totalBytes; } else { result = (fsinfo.totalBytes - fsinfo.usedBytes); } #endif // ESP8266 #ifdef ESP32 if (sel == 0) { result = LittleFS.totalBytes(); } else { result = LittleFS.totalBytes() - LittleFS.usedBytes(); } #endif // ESP32 break; case UFS_TFAT: #ifdef ESP32 if (sel == 0) { result = FFat.totalBytes(); } else { result = FFat.freeBytes(); } #endif // ESP32 break; } return result / 1024; } uint32_t UfsSize(void) { return UfsInfo(0, ufs_dir == 2 ? 1:0); } uint32_t UfsFree(void) { return UfsInfo(1, ufs_dir == 2 ? 1:0); } #if USE_LONG_FILE_NAMES>0 #undef REJCMPL #define REJCMPL 6 #else #undef REJCMPL #define REJCMPL 8 #endif uint8_t UfsReject(char *name) { char *lcp = strrchr(name,'/'); if (lcp) { name = lcp + 1; } while (*name=='/') { name++; } if (*name=='.') { return 1; } if (!strncasecmp(name, "SPOTLI~1", REJCMPL)) { return 1; } if (!strncasecmp(name, "TRASHE~1", REJCMPL)) { return 1; } if (!strncasecmp(name, "FSEVEN~1", REJCMPL)) { return 1; } if (!strncasecmp(name, "SYSTEM~1", REJCMPL)) { return 1; } if (!strncasecmp(name, "System Volume", 13)) { return 1; } return 0; } /*********************************************************************************************\ * Tfs low level functions \*********************************************************************************************/ bool TfsFileExists(const char *fname){ if (!ffs_type) { return false; } bool yes = ffsp->exists(fname); if (!yes) { AddLog(LOG_LEVEL_DEBUG, PSTR("TFS: File '%s' not found"), fname +1); // Skip leading slash } return yes; } size_t TfsFileSize(const char *fname){ if (!ffs_type) { return 0; } File file = ffsp->open(fname, "r"); if (!file) { return 0; } size_t flen = file.size(); file.close(); return flen; } bool TfsSaveFile(const char *fname, const uint8_t *buf, uint32_t len) { if (!ffs_type) { return false; } #ifdef USE_WEBCAM WcInterrupt(0); // Stop stream if active to fix TG1WDT_SYS_RESET #endif bool result = false; File file = ffsp->open(fname, "w"); if (!file) { AddLog(LOG_LEVEL_INFO, PSTR("TFS: Save failed")); } else { // This will timeout on ESP32-webcam // But now solved with WcInterrupt(0) in support_esp.ino file.write(buf, len); /* // This will still timeout on ESP32-webcam when wcresolution 10 uint32_t count = len / 512; uint32_t chunk = len / count; for (uint32_t i = 0; i < count; i++) { file.write(buf + (i * chunk), chunk); // do actually wait a little to allow ESP32 tasks to tick // fixes task timeout in ESP32Solo1 style unicore code and webcam. delay(10); OsWatchLoop(); } uint32_t left = len % count; if (left) { file.write(buf + (count * chunk), left); } */ file.close(); result = true; } #ifdef USE_WEBCAM WcInterrupt(1); #endif return result; } bool TfsInitFile(const char *fname, uint32_t len, uint8_t init_value) { if (!ffs_type) { return false; } File file = ffsp->open(fname, "w"); if (!file) { AddLog(LOG_LEVEL_INFO, PSTR("TFS: Erase failed")); return false; } for (uint32_t i = 0; i < len; i++) { file.write(&init_value, 1); } file.close(); return true; } bool TfsLoadFile(const char *fname, uint8_t *buf, uint32_t len) { if (!ffs_type) { return false; } File file = ffsp->open(fname, "r"); if (!file) { AddLog(LOG_LEVEL_INFO, PSTR("TFS: File '%s' not found"), fname +1); // Skip leading slash return false; } size_t flen = file.size(); if (len > flen) { len = flen; } // Adjust requested length to smaller file length file.read(buf, len); file.close(); return true; } String TfsLoadString(const char *fname) { // Use a reasonable amount of stack space considering 4k/8k available on ESP8266/ESP32 and manageable string length char buf[2048] = { 0 }; // Prepare empty string of max 2047 characters on stack TfsLoadFile(fname, (uint8_t*)buf, 2047); // Leave last position as end of string ('\0') return String(buf); // Received string or empty on error } bool TfsDeleteFile(const char *fname) { if (!ffs_type) { return false; } if (!ffsp->remove(fname)) { AddLog(LOG_LEVEL_INFO, PSTR("TFS: Delete failed")); return false; } return true; } bool TfsRenameFile(const char *fname1, const char *fname2) { if (!ffs_type) { return false; } if (!ffsp->rename(fname1, fname2)) { AddLog(LOG_LEVEL_INFO, PSTR("TFS: Rename failed")); return false; } return true; } /*********************************************************************************************\ * File command execute support \*********************************************************************************************/ bool UfsExecuteCommandFileReady(void) { return (UfsData.run_file_pos < 0); // Check file ready to disable concurrency } void UfsExecuteCommandFileLoop(void) { if (UfsExecuteCommandFileReady() || !ffs_type) { return; } if (BACKLOG_EMPTY && strlen(UfsData.run_file) && !UfsData.run_file_mutex) { File file = ffsp->open(UfsData.run_file, "r"); if (!file || !file.seek(UfsData.run_file_pos)) { UfsData.run_file_pos = -1; // Signal file ready return; } UfsData.run_file_mutex = true; char cmd_line[512]; cmd_line[0] = '\0'; // Clear in case of re-entry while (file.available()) { uint16_t index = 0; bool comment = false; while (file.available()) { uint8_t buf[1]; file.read(buf, 1); if ((buf[0] == '\n') || (buf[0] == '\r')) { break; // End of command with linefeed or carriage return } // else if (index && !comment && (buf[0] == ';')) { // break; // End of command on multi command line // } else if ((0 == index) && isspace(buf[0])) { // Skip leading spaces (' ','\t','\n','\v','\f','\r') } else if ((0 == index) && (buf[0] == ';')) { comment = true; // Ignore comment lines until linefeed or carriage return } else if (!comment && (index < sizeof(cmd_line) - 2)) { cmd_line[index++] = buf[0]; // Build command } } if ((index > 0) && (index < sizeof(cmd_line) - 1)) { cmd_line[index] = '\0'; // Valid command received break; } } UfsData.run_file_pos = (file.available()) ? file.position() : -1; file.close(); if (strlen(cmd_line)) { ExecuteCommand(cmd_line, SRC_FILE); } UfsData.run_file_mutex = false; } } bool UfsExecuteCommandFile(const char *fname) { // Check for non-concurrency and file existance if (UfsExecuteCommandFileReady() && TfsFileExists(fname)) { snprintf(UfsData.run_file, sizeof(UfsData.run_file), fname); UfsData.run_file_pos = 0; // Signal start of file return true; } return false; } /*********************************************************************************************\ * Commands \*********************************************************************************************/ const int UFS_FILENAME_SIZE = 48; char* UfsFilename(char* fname, char* fname_in) { fname_in = Trim(fname_in); // Remove possible leading spaces snprintf_P(fname, UFS_FILENAME_SIZE, PSTR("%s%s"), ('/' == fname_in[0]) ? "" : "/", fname_in); return fname; } const char kUFSCommands[] PROGMEM = "Ufs|" // Prefix "|Type|Size|Free|Delete|Rename|Run"; void (* const kUFSCommand[])(void) PROGMEM = { &UFSInfo, &UFSType, &UFSSize, &UFSFree, &UFSDelete, &UFSRename, &UFSRun}; void UFSInfo(void) { Response_P(PSTR("{\"Ufs\":{\"Type\":%d,\"Size\":%d,\"Free\":%d}"), ufs_type, UfsInfo(0, 0), UfsInfo(1, 0)); if (ffs_type && (ffs_type != ufs_type)) { ResponseAppend_P(PSTR(",{\"Type\":%d,\"Size\":%d,\"Free\":%d}"), ffs_type, UfsInfo(0, 1), UfsInfo(1, 1)); } ResponseJsonEnd(); } void UFSType(void) { if (ffs_type && (ffs_type != ufs_type)) { Response_P(PSTR("{\"%s\":[%d,%d]}"), XdrvMailbox.command, ufs_type, ffs_type); } else { ResponseCmndNumber(ufs_type); } } void UFSSize(void) { if (ffs_type && (ffs_type != ufs_type)) { Response_P(PSTR("{\"%s\":[%d,%d]}"), XdrvMailbox.command, UfsInfo(0, 0), UfsInfo(0, 1)); } else { ResponseCmndNumber(UfsInfo(0, 0)); } } void UFSFree(void) { if (ffs_type && (ffs_type != ufs_type)) { Response_P(PSTR("{\"%s\":[%d,%d]}"), XdrvMailbox.command, UfsInfo(1, 0), UfsInfo(1, 1)); } else { ResponseCmndNumber(UfsInfo(1, 0)); } } void UFSDelete(void) { // UfsDelete sdcard or flashfs file if only one of them available // UfsDelete2 flashfs file if available if (XdrvMailbox.data_len > 0) { char fname[UFS_FILENAME_SIZE]; UfsFilename(fname, XdrvMailbox.data); bool result = false; if (ffs_type && (ffs_type != ufs_type) && (2 == XdrvMailbox.index)) { result = TfsDeleteFile(fname); } else { result = (ufs_type && ufsp->remove(fname)); } if (!result) { ResponseCmndFailed(); } else { ResponseCmndDone(); } } } void UFSRename(void) { // UfsRename sdcard or flashfs file if only one of them available // UfsRename2 flashfs file if available if (XdrvMailbox.data_len > 0) { bool result = false; char *fname1 = strtok(XdrvMailbox.data, ","); char *fname2 = strtok(nullptr, ","); if (fname1 && fname2) { char fname_old[UFS_FILENAME_SIZE]; UfsFilename(fname_old, fname1); char fname_new[UFS_FILENAME_SIZE]; UfsFilename(fname_new, fname2); if (ffs_type && (ffs_type != ufs_type) && (2 == XdrvMailbox.index)) { result = TfsRenameFile(fname_old, fname_new); } else { result = (ufs_type && ufsp->rename(fname_old, fname_new)); } } if (!result) { ResponseCmndFailed(); } else { ResponseCmndDone(); } } } void UFSRun(void) { if (XdrvMailbox.data_len > 0) { char fname[UFS_FILENAME_SIZE]; if (UfsExecuteCommandFile(UfsFilename(fname, XdrvMailbox.data))) { ResponseClear(); } else { ResponseCmndFailed(); } } else { bool not_active = UfsExecuteCommandFileReady(); UfsData.run_file_pos = -1; ResponseCmndChar(not_active ? PSTR(D_JSON_DONE) : PSTR(D_JSON_ABORTED)); } } /*********************************************************************************************\ * Web support \*********************************************************************************************/ #ifdef USE_WEBSERVER const char UFS_WEB_DIR[] PROGMEM = "

"; const char UFS_FORM_FILE_UPLOAD[] PROGMEM = "
" "
 " D_MANAGE_FILE_SYSTEM " "; const char UFS_FORM_FILE_UPGc[] PROGMEM = "
" D_FS_SIZE " %s MB - " D_FS_FREE " %s MB"; const char UFS_FORM_FILE_UPGc1[] PROGMEM = "   %s"; const char UFS_FORM_FILE_UPGc2[] PROGMEM = "
"; const char UFS_FORM_FILE_UPG[] PROGMEM = "
" "

" "
" "
"; 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 ""; const char UFS_FORM_FILE_UPGb2[] PROGMEM = "
" "
" ""; const char UFS_FORM_SDC_DIR_NORMAL[] PROGMEM = ""; const char UFS_FORM_SDC_DIR_HIDDABLE[] PROGMEM = " class='hf'"; const char UFS_FORM_SDC_DIRd[] PROGMEM = "
%s
"; const char UFS_FORM_SDC_DIRb[] PROGMEM = "%s %s %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 #define FILE_BUFFER_SIZE 1024 const char UFS_FORM_SDC_HREFedit[] PROGMEM = "📝"; // 📝 const char HTTP_EDITOR_FORM_START[] PROGMEM = "
 " D_EDIT_FILE " " "
" "

" "" "" "
"; #endif // #ifdef GUI_EDIT_FILE void UfsDirectory(void) { if (!HttpCheckPriviledgedAccess()) { return; } AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_MANAGE_FILE_SYSTEM)); uint8_t depth = 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 (Webserver->hasArg(F("delete"))) { String stmp = Webserver->arg(F("delete")); char *cp = (char*)stmp.c_str(); dfsp->remove(cp); } WSContentStart_P(PSTR(D_MANAGE_FILE_SYSTEM)); WSContentSendStyle(); WSContentSend_P(UFS_FORM_FILE_UPLOAD); char ts[FLOATSZ]; dtostrfd((float)UfsInfo(0, ufs_dir == 2 ? 1:0) / 1000, 3, ts); char fs[FLOATSZ]; dtostrfd((float)UfsInfo(1, ufs_dir == 2 ? 1:0) / 1000, 3, fs); WSContentSend_PD(UFS_FORM_FILE_UPGc, WebColor(COL_TEXT), ts, fs); if (ufs_dir) { WSContentSend_P(UFS_FORM_FILE_UPGc1, (ufs_dir == 1)?2:1, (ufs_dir == 1)?PSTR("SDCard"):PSTR("FlashFS")); } WSContentSend_P(UFS_FORM_FILE_UPGc2); WSContentSend_P(UFS_FORM_FILE_UPG, PSTR(D_SCRIPT_UPLOAD)); 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); #endif if (!isSDC()) { WSContentSend_P(UFS_FORM_FILE_UPGb1); } WSContentSend_P(UFS_FORM_FILE_UPGb2); WSContentSpaceButton(BUTTON_MANAGEMENT); WSContentStop(); Web.upload_file_type = UPL_UFSFILE; } // return true if SDC bool isSDC(void) { #ifndef SDC_HIDE_INVISIBLES return false; #else if (((uint32_t)ufsp != (uint32_t)ffsp) && ((uint32_t)ffsp == (uint32_t)dfsp)) return false; if (((uint32_t)ufsp == (uint32_t)ffsp) && (ufs_type != UFS_TSDC)) return false; return true; #endif } void UfsListDir(char *path, uint8_t depth) { char name[48]; char npath[128]; char format[12]; sprintf(format, PSTR("%%-%ds"), 24 - depth); File dir = dfsp->open(path, UFS_FILE_READ); if (dir) { dir.rewindDirectory(); if (strlen(path)>1) { ext_snprintf_P(npath, sizeof(npath), PSTR("ufsd?download=%s"), path); for (uint32_t cnt = strlen(npath) - 1; cnt > 0; cnt--) { if (npath[cnt] == '/') { if (npath[cnt - 1] == '=') { npath[cnt + 1] = 0; } else { npath[cnt] = 0; } break; } } WSContentSend_P(UFS_FORM_SDC_DIRd, npath, path, PSTR("..")); } char *ep; while (true) { File entry = dir.openNextFile(); if (!entry) { break; } // esp32 returns path here, shorten to filename ep = (char*)entry.name(); if (*ep == '/') { ep++; } char *lcp = strrchr(ep,'/'); if (lcp) { ep = lcp + 1; } uint32_t tm = entry.getLastWrite(); String tstr = GetDT(tm); char *pp = path; if (!*(pp + 1)) { pp++; } char *cp = name; // osx formatted disks contain a lot of stuff we dont want bool hiddable = UfsReject((char*)ep); if (!hiddable || !isSDC() ) { for (uint8_t cnt = 0; cnt 1) { strcat(path, "/"); } strcat(path, ep); UfsListDir(path, depth + 4); path[plen] = 0; } 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); #else char editpath[2]; editpath[0]=0; #endif // GUI_TRASH_FILE ext_snprintf_P(npath, sizeof(npath), UFS_FORM_SDC_HREF, ppe, epe); WSContentSend_P(UFS_FORM_SDC_DIRb, hiddable ? UFS_FORM_SDC_DIR_HIDDABLE : UFS_FORM_SDC_DIR_NORMAL, npath, epe, HtmlEscape(name).c_str(), tstr.c_str(), entry.size(), delpath, editpath); } entry.close(); } } dir.close(); } } #ifdef ESP32 #define ESP32_DOWNLOAD_TASK #endif // ESP32 uint8_t UfsDownloadFile(char *file) { File download_file; if (!dfsp->exists(file)) { AddLog(LOG_LEVEL_INFO, PSTR("UFS: File '%s' not found"), file); return 0; } download_file = dfsp->open(file, UFS_FILE_READ); if (!download_file) { AddLog(LOG_LEVEL_INFO, PSTR("UFS: Could not open file '%s'"), file); return 0; } if (download_file.isDirectory()) { download_file.close(); return 1; } #ifndef ESP32_DOWNLOAD_TASK WiFiClient download_Client; uint32_t flen = download_file.size(); 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; } } snprintf_P(attachment, sizeof(attachment), PSTR("attachment; filename=%s"), cp); Webserver->sendHeader(F("Content-Disposition"), attachment); WSSend(200, CT_APP_STREAM, ""); uint8_t buff[512]; uint32_t bread; // transfer is about 150kb/s uint32_t cnt = 0; while (download_file.available()) { bread = download_file.read(buff, sizeof(buff)); uint32_t bw = download_Client.write((const char*)buff, bread); if (!bw) { break; } cnt++; if (cnt > 7) { cnt = 0; //if (glob_script_mem.script_loglevel & 0x80) { // this indeed multitasks, but is slower 50 kB/s // loop(); //} } delay(0); OsWatchLoop(); } download_file.close(); download_Client.stop(); #endif // ESP32_DOWNLOAD_TASK #ifdef ESP32_DOWNLOAD_TASK download_file.close(); if (UfsData.download_busy == true) { AddLog(LOG_LEVEL_INFO, PSTR("UFS: Download is busy")); return 0; } UfsData.download_busy = true; char *path = (char*)malloc(128); strcpy(path,file); BaseType_t ret = xTaskCreatePinnedToCore(download_task, "DT", 6000, (void*)path, 3, nullptr, 1); if (ret != pdPASS) AddLog(LOG_LEVEL_INFO, PSTR("UFS: Download task failed with %d"), ret); yield(); #endif // ESP32_DOWNLOAD_TASK return 0; } #ifdef ESP32_DOWNLOAD_TASK #ifndef DOWNLOAD_SIZE #define DOWNLOAD_SIZE 4096 #endif // DOWNLOAD_SIZE void download_task(void *path) { File download_file; WiFiClient download_Client; char *file = (char*) path; download_file = dfsp->open(file, UFS_FILE_READ); uint32_t flen = download_file.size(); 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; } } //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); Webserver->sendHeader(F("Content-Disposition"), attachment); WSSend(200, CT_APP_STREAM, ""); uint8_t *buff = (uint8_t*)malloc(DOWNLOAD_SIZE); if (buff) { uint32_t bread; while (download_file.available()) { bread = download_file.read(buff, DOWNLOAD_SIZE); uint32_t bw = download_Client.write((const char*)buff, bread); if (!bw) { break; } } free(buff); } download_file.close(); download_Client.stop(); UfsData.download_busy = false; vTaskDelete( NULL ); free(path); } #endif // ESP32_DOWNLOAD_TASK bool UfsUploadFileOpen(const char* upload_filename) { char npath[48]; snprintf_P(npath, sizeof(npath), PSTR("%s/%s"), ufs_path, upload_filename); dfsp->remove(npath); ufs_upload_file = dfsp->open(npath, UFS_FILE_WRITE); return (ufs_upload_file); } bool UfsUploadFileWrite(uint8_t *upload_buf, size_t current_size) { if (ufs_upload_file) { ufs_upload_file.write(upload_buf, current_size); } else { return false; } return true; } void UfsUploadFileClose(void) { ufs_upload_file.close(); } //****************************************************************************************** // File Editor //****************************************************************************************** #ifdef GUI_EDIT_FILE void UfsEditor(void) { if (!HttpCheckPriviledgedAccess()) { return; } AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor GET")); char fname_input[UFS_FILENAME_SIZE]; if (Webserver->hasArg(F("file"))) { WebGetArg(PSTR("file"), fname_input, sizeof(fname_input)); } else { snprintf_P(fname_input, sizeof(fname_input), PSTR(D_NEW_FILE)); } char fname[UFS_FILENAME_SIZE]; UfsFilename(fname, fname_input); // Trim spaces and add slash AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file=%s, ffs_type=%d, TfsFileExist=%d"), fname, ffs_type, dfsp->exists(fname)); WSContentStart_P(PSTR(D_EDIT_FILE)); WSContentSendStyle(); char *bfname = fname +1; WSContentSend_P(HTTP_EDITOR_FORM_START, bfname); // Skip leading slash if (ffs_type && dfsp->exists(fname)) { File fp = dfsp->open(fname, "r"); if (!fp) { AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file open failed")); WSContentSend_P(D_NEW_FILE); } else { uint8_t *buf = (uint8_t*)malloc(FILE_BUFFER_SIZE+1); size_t filelen = fp.size(); AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file len=%d"), filelen); while (filelen > 0) { size_t l = fp.read(buf, FILE_BUFFER_SIZE); AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("UFS: UfsEditor: read=%d"), l); if (l < 0) { break; } buf[l] = '\0'; WSContentSend_P(PSTR("%s"), buf); filelen -= l; } fp.close(); free(buf); AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: read done")); } } else { WSContentSend_P(D_NEW_FILE); } WSContentSend_P(HTTP_EDITOR_FORM_END); WSContentSend_P(UFS_WEB_DIR, PSTR(D_MANAGE_FILE_SYSTEM)); WSContentStop(); } void UfsEditorUpload(void) { AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file upload")); if (!HttpCheckPriviledgedAccess()) { return; } if (!Webserver->hasArg("name")) { AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: file upload - no filename")); WSSend(400, CT_PLAIN, F("400: Bad request - no filename")); return; } char fname_input[UFS_FILENAME_SIZE]; WebGetArg(PSTR("name"), fname_input, sizeof(fname_input)); char fname[UFS_FILENAME_SIZE]; UfsFilename(fname, fname_input); // Trim spaces and add slash AddLog(LOG_LEVEL_DEBUG, PSTR("UFS: UfsEditor: file '%s'"), fname); if (!Webserver->hasArg("content")) { AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: file upload - no content")); WSSend(400, CT_PLAIN, F("400: Bad request - no content")); return; } String content = Webserver->arg("content"); if (!dfsp) { Web.upload_error = 1; AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: 507: no storage available")); WSSend(507, CT_PLAIN, F("507: no storage available")); return; } File fp = dfsp->open(fname, "w"); if (!fp) { Web.upload_error = 1; AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: 400: invalid file name '%s'"), fname); WSSend(400, CT_PLAIN, F("400: bad request - invalid filename")); return; } if (*content.c_str()) { content.replace("\r\n", "\n"); content.replace("\r", "\n"); } if (!fp.print(content)) { AddLog(LOG_LEVEL_ERROR, PSTR("UFS: UfsEditor: write error on '%s'"), fname); } fp.close(); Webserver->sendHeader(F("Location"),F("/ufsu")); Webserver->send(303); } #endif // GUI_EDIT_FILE #endif // USE_WEBSERVER /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv50(uint32_t function) { bool result = false; switch (function) { case FUNC_LOOP: UfsExecuteCommandFileLoop(); break; #ifdef USE_SDCARD case FUNC_PRE_INIT: UfsCheckSDCardInit(); break; #endif // USE_SDCARD case FUNC_MQTT_INIT: if (!TasmotaGlobal.no_autoexec) { UfsExecuteCommandFile(TASM_FILE_AUTOEXEC); } break; case FUNC_COMMAND: result = DecodeCommand(kUFSCommands, kUFSCommand); break; #ifdef USE_WEBSERVER case FUNC_WEB_ADD_MANAGEMENT_BUTTON: if (ufs_type) { if (XdrvMailbox.index) { XdrvMailbox.index++; } else { WSContentSend_PD(UFS_WEB_DIR, PSTR(D_MANAGE_FILE_SYSTEM)); } } break; case FUNC_WEB_ADD_HANDLER: // Webserver->on(F("/ufsd"), UfsDirectory); // Webserver->on(F("/ufsu"), HTTP_GET, UfsDirectory); // Webserver->on(F("/ufsu"), HTTP_POST,[](){Webserver->sendHeader(F("Location"),F("/ufsu"));Webserver->send(303);}, HandleUploadLoop); Webserver->on("/ufsd", UfsDirectory); Webserver->on("/ufsu", HTTP_GET, UfsDirectory); Webserver->on("/ufsu", HTTP_POST,[](){Webserver->sendHeader(F("Location"),F("/ufsu"));Webserver->send(303);}, HandleUploadLoop); #ifdef GUI_EDIT_FILE Webserver->on("/ufse", HTTP_GET, UfsEditor); Webserver->on("/ufse", HTTP_POST, UfsEditorUpload); #endif break; #endif // USE_WEBSERVER } return result; } #endif // USE_UFILESYS