mirror of
https://github.com/esp8266/Arduino.git
synced 2025-04-19 23:22:16 +03:00
* Minimal file with a few ESP8266-specific keywords - github issue #3701 * Renamed "SDWebServer" to the more universal "WebFileManager" * SD was replaced by SDFS, and sketch now works on either SDFS, SPIFFS or LittleFS based on a #define logic (required adding a second param to open() and replacing 'FILE_WRITE' by "w") + Added size information to file list and a /status request handler to return filesystem status * Tree panel width is now proportional to window. Changed icons (lighter and more neutral), including one for files. Show size of files. Fill "filename" box upon clicking on a file. Sort files alphabetically. * Replaced by a lighter version * Return the filesystem time in the status object + Massive cleanup/merge/align with some code from the FSBrowser example and misc refactorings * Fixed folder handling * Replaced the FILESYSTEM #define by a filesystem variable, and introduced FSConfig to prevent FS formating. Fixed recursive deletion. Got rid of specific isDir() for SPIFFS. * Made 8.3 lowercase filenames formating optional (disabled by default). Refresh only part of the tree when possible. Selecting a file for upload defaults to the same folder as the last clicked file. Removed the Mkdir button on SPIFFS. * Added 'wait' cursor during asynchronous operations. Slight refactoring of XMLHttpRequest completion handling * Removed limitation "files must have an extension, folders may not". Case insensivity of the extension for the editor and preview. * Support Filenames without extension, Dirnames with extension. Added Save/Discard/Help buttons to Editor, discard confirmation on leave, and refresh tree/status upon save. Removed redundant Ctrl-Z + Ctrl-Shift-Z shortcut declarations. Small bug fixes. + some refactoring * Fixed tree refresh on delete in all cases by returning the remaining path as response to the delete request. Refactoring * Changed FS status in text by a percentage graph, with numbers as tooltip. Unsupported files on SPIFFS (files at root not sarting with "/", files with double "/", files ending with "/") are now detected and reported in the page. * Small fix + refactoring * Restrict filename support check to SPIFFS. * Implemented Move/Rename. Added "loading" screen during async operations (dim with spinner and status). Fixed "discard" feature that kept prompting even after an image was loaded. Improved refresh of parts of the tree, with recursive listing. Moved the "path" id attribute to the "li" elements for folders (was already the case for files). Refactoring and cleanup. * Fixed broken spinner * Cosmetic improvements. Removed non-functional Upload context menu. Fixed error in response to move requests. Added minified version. * Added specific icons for text and image files. Fixed incompatibilities with SPIFFS. Fixed a race condition between deletion and reinsertion of nodes when multiple folders are refreshed. Fixed missing URL decoding for files with special chars (e.g. space char). Moved info from source code comment to a readme.md file. Added source PNG to git. Cleanup. * Added favicon.ico. * Renamed project * Small changes * Add a note about the ace.js dependency * Minor changes * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Restyled version * (dummy edit to retrigger broken CI) * Using unsigned int for comparison with String.length() * Return an error when upload fails (e.g. filesystem full) * Trying to reorder functions to please CI * Reordered functions to please CI. * Moved file * Renamed "SDWebServer" to the more universal "WebFileManager" * SD was replaced by SDFS, and sketch now works on either SDFS, SPIFFS or LittleFS based on a #define logic (required adding a second param to open() and replacing 'FILE_WRITE' by "w") + Added size information to file list and a /status request handler to return filesystem status * Tree panel width is now proportional to window. Changed icons (lighter and more neutral), including one for files. Show size of files. Fill "filename" box upon clicking on a file. Sort files alphabetically. * Replaced by a lighter version * Return the filesystem time in the status object + Massive cleanup/merge/align with some code from the FSBrowser example and misc refactorings * Fixed folder handling * Replaced the FILESYSTEM #define by a filesystem variable, and introduced FSConfig to prevent FS formating. Fixed recursive deletion. Got rid of specific isDir() for SPIFFS. * Made 8.3 lowercase filenames formating optional (disabled by default). Refresh only part of the tree when possible. Selecting a file for upload defaults to the same folder as the last clicked file. Removed the Mkdir button on SPIFFS. * Added 'wait' cursor during asynchronous operations. Slight refactoring of XMLHttpRequest completion handling * Removed limitation "files must have an extension, folders may not". Case insensivity of the extension for the editor and preview. * Support Filenames without extension, Dirnames with extension. Added Save/Discard/Help buttons to Editor, discard confirmation on leave, and refresh tree/status upon save. Removed redundant Ctrl-Z + Ctrl-Shift-Z shortcut declarations. Small bug fixes. + some refactoring * Fixed tree refresh on delete in all cases by returning the remaining path as response to the delete request. Refactoring * Changed FS status in text by a percentage graph, with numbers as tooltip. Unsupported files on SPIFFS (files at root not sarting with "/", files with double "/", files ending with "/") are now detected and reported in the page. * Small fix + refactoring * Restrict filename support check to SPIFFS. * Implemented Move/Rename. Added "loading" screen during async operations (dim with spinner and status). Fixed "discard" feature that kept prompting even after an image was loaded. Improved refresh of parts of the tree, with recursive listing. Moved the "path" id attribute to the "li" elements for folders (was already the case for files). Refactoring and cleanup. * Fixed broken spinner * Cosmetic improvements. Removed non-functional Upload context menu. Fixed error in response to move requests. Added minified version. * Added specific icons for text and image files. Fixed incompatibilities with SPIFFS. Fixed a race condition between deletion and reinsertion of nodes when multiple folders are refreshed. Fixed missing URL decoding for files with special chars (e.g. space char). Moved info from source code comment to a readme.md file. Added source PNG to git. Cleanup. * Added favicon.ico. * Renamed project * Small changes * Add a note about the ace.js dependency * Minor changes * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Define LittleFS by default. If both uncompressed and gz versions exist, use uncompressed version. Small fixes. * Restyled version * (dummy edit to retrigger broken CI) * Using unsigned int for comparison with String.length() * Return an error when upload fails (e.g. filesystem full) * Trying to reorder functions to please CI * Reordered functions to please CI. * Update to use chunked response API * Removed temp files commited by mistake * Avoid using args() as requested * Use html entity for non-breaking space to avoid losing char when minifying * Script to preprocess index.htm * (reformated code) * (comments) * Preprocessed files * Fixed dump to create an actual include file * Optionally embed index.htm in code. (+ documentation and preprocessing script) * (reformated) * If editor cannot be loaded from the web, try a local version, or default to a text viewer if not present * (removed a TODO item :-)) * (forgot to reprocess files after last commit) * (reprocess should be ok this time) * Return error 500 when upload fails immediately (e.g. filesystem full) * Use standard <meter> tag for filesystem use * (updated following changes to index.htm) * Do not include gzipped version in the data folder by default. Leave it in the extras folder and change readme accordingly (plus some reformatingi of the readme file) * Gzipped index file not included in data/edit by default. It is now left in the extras folder. Readme file was updated accordingly (+ some reformating) * Reduce String clutter by reserving and concatenating elements one by one. * Use clear() to reset String. * Avoid comparisons against empty String. * Use char instead of single-char String where possible. * Prefer direct logic over inverted. * Rename returnBlah to replyBlah. * Renamed h2int to hexDigitToInt * Renamed getFileError() to checkForUnsupportedPath(), to avoid confusion with a getter. * Misc improvements. * Added comments about mandatory rebuilding gz and h files in case of update to index.htm. * Addressed a few comments. * Improve replies: bad requests vs server error * (reformated) * Reduce clutter by reserving String size beforehand. * Moved most Strings of more than 10 chars to flash. * Use lib version of urlDecode() instead of a local one, and only call it when required. * Added a comment about the dangers of recursion on embedded devices. * Added a more explicit warning in the .h header comment. * Added a typical set of required files to load ace editor from the ESP. * (reformated) * More explicit warning at the beginning of the .h version. Co-authored-by: david gauchard <gauchard@laas.fr> Co-authored-by: Earle F. Philhower, III <earlephilhower@yahoo.com> Co-authored-by: Develo <deveyes@gmail.com>
679 lines
18 KiB
C++
679 lines
18 KiB
C++
/*
|
|
FSBrowser - A web-based FileSystem Browser for ESP8266 filesystems
|
|
|
|
Copyright (c) 2015 Hristo Gochkov. All rights reserved.
|
|
This file is part of the ESP8266WebServer library for Arduino environment.
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
This library 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
|
|
Lesser General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Lesser General Public
|
|
License along with this library; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
|
|
See readme.md for more information.
|
|
*/
|
|
|
|
////////////////////////////////
|
|
|
|
// Select the FileSystem by uncommenting one of the lines below
|
|
|
|
#define USE_SPIFFS
|
|
//#define USE_LITTLEFS
|
|
//#define USE_SDFS
|
|
|
|
// Uncomment the following line to embed a version of the web page in the code
|
|
// (program code will be larger, but no file will have to be written to the filesystem).
|
|
// Note: the source file "extras/index_htm.h" must have been generated by "extras/reduce_index.sh"
|
|
|
|
//#define INCLUDE_FALLBACK_INDEX_HTM
|
|
|
|
////////////////////////////////
|
|
|
|
#include <ESP8266WiFi.h>
|
|
#include <WiFiClient.h>
|
|
#include <ESP8266WebServer.h>
|
|
#include <ESP8266mDNS.h>
|
|
#include <SPI.h>
|
|
|
|
#ifdef INCLUDE_FALLBACK_INDEX_HTM
|
|
#include "extras/index_htm.h"
|
|
#endif
|
|
|
|
#if defined USE_SPIFFS
|
|
#include <FS.h>
|
|
const char* fsName = "SPIFFS";
|
|
FS* fileSystem = &SPIFFS;
|
|
SPIFFSConfig fileSystemConfig = SPIFFSConfig();
|
|
#elif defined USE_LITTLEFS
|
|
#include <LittleFS.h>
|
|
const char* fsName = "LittleFS";
|
|
FS* fileSystem = &LittleFS;
|
|
LittleFSConfig fileSystemConfig = LittleFSConfig();
|
|
#elif defined USE_SDFS
|
|
#include <SDFS.h>
|
|
const char* fsName = "SDFS";
|
|
FS* fileSystem = &SDFS;
|
|
SDFSConfig fileSystemConfig = SDFSConfig();
|
|
// fileSystemConfig.setCSPin(chipSelectPin);
|
|
#else
|
|
#error Please select a filesystem first by uncommenting one of the "#define USE_xxx" lines at the beginning of the sketch.
|
|
#endif
|
|
|
|
|
|
#define DBG_OUTPUT_PORT Serial
|
|
|
|
#ifndef STASSID
|
|
#define STASSID "your-ssid"
|
|
#define STAPSK "your-password"
|
|
#endif
|
|
|
|
const char* ssid = STASSID;
|
|
const char* password = STAPSK;
|
|
const char* host = "fsbrowser";
|
|
|
|
ESP8266WebServer server(80);
|
|
|
|
static bool fsOK;
|
|
String unsupportedFiles = String();
|
|
|
|
File uploadFile;
|
|
|
|
static const char TEXT_PLAIN[] PROGMEM = "text/plain";
|
|
static const char FS_INIT_ERROR[] PROGMEM = "FS INIT ERROR";
|
|
static const char FILE_NOT_FOUND[] PROGMEM = "FileNotFound";
|
|
|
|
////////////////////////////////
|
|
// Utils to return HTTP codes, and determine content-type
|
|
|
|
void replyOK() {
|
|
server.send(200, FPSTR(TEXT_PLAIN), "");
|
|
}
|
|
|
|
void replyOKWithMsg(String msg) {
|
|
server.send(200, FPSTR(TEXT_PLAIN), msg);
|
|
}
|
|
|
|
void replyNotFound(String msg) {
|
|
server.send(404, FPSTR(TEXT_PLAIN), msg);
|
|
}
|
|
|
|
void replyBadRequest(String msg) {
|
|
DBG_OUTPUT_PORT.println(msg);
|
|
server.send(400, FPSTR(TEXT_PLAIN), msg + "\r\n");
|
|
}
|
|
|
|
void replyServerError(String msg) {
|
|
DBG_OUTPUT_PORT.println(msg);
|
|
server.send(500, FPSTR(TEXT_PLAIN), msg + "\r\n");
|
|
}
|
|
|
|
String getContentType(String filename) {
|
|
if (filename.endsWith(".htm")) {
|
|
return "text/html";
|
|
}
|
|
if (filename.endsWith(".html")) {
|
|
return "text/html";
|
|
}
|
|
if (filename.endsWith(".css")) {
|
|
return "text/css";
|
|
}
|
|
if (filename.endsWith(".js")) {
|
|
return "application/javascript";
|
|
}
|
|
if (filename.endsWith(".png")) {
|
|
return "image/png";
|
|
}
|
|
if (filename.endsWith(".gif")) {
|
|
return "image/gif";
|
|
}
|
|
if (filename.endsWith(".jpg")) {
|
|
return "image/jpeg";
|
|
}
|
|
if (filename.endsWith(".jpeg")) {
|
|
return "image/jpeg";
|
|
}
|
|
if (filename.endsWith(".ico")) {
|
|
return "image/x-icon";
|
|
}
|
|
if (filename.endsWith(".xml")) {
|
|
return "text/xml";
|
|
}
|
|
if (filename.endsWith(".pdf")) {
|
|
return "application/x-pdf";
|
|
}
|
|
if (filename.endsWith(".zip")) {
|
|
return "application/x-zip";
|
|
}
|
|
if (filename.endsWith(".gz")) {
|
|
return "application/x-gzip";
|
|
}
|
|
return FPSTR(TEXT_PLAIN);
|
|
}
|
|
|
|
#ifdef USE_SPIFFS
|
|
/*
|
|
Checks filename for character combinations that are not supported by FSBrowser (alhtough valid on SPIFFS).
|
|
Returns an empty String if supported, or detail of error(s) if unsupported
|
|
*/
|
|
String checkForUnsupportedPath(String filename) {
|
|
String error = String();
|
|
if (!filename.startsWith("/")) {
|
|
error += F("!NO_LEADING_SLASH! ");
|
|
}
|
|
if (filename.indexOf("//") != -1) {
|
|
error += F("!DOUBLE_SLASH! ");
|
|
}
|
|
if (filename.endsWith("/")) {
|
|
error += F("!TRAILING_SLASH! ");
|
|
}
|
|
return error;
|
|
}
|
|
#endif
|
|
|
|
|
|
////////////////////////////////
|
|
// Request handlers
|
|
|
|
/*
|
|
Return the FS type, status and size info
|
|
*/
|
|
void handleStatus() {
|
|
DBG_OUTPUT_PORT.println("handleStatus");
|
|
FSInfo fs_info;
|
|
String json;
|
|
json.reserve(128);
|
|
|
|
json = "{\"type\":\"";
|
|
json += fsName;
|
|
json += "\", \"isOk\":";
|
|
if (fsOK) {
|
|
fileSystem->info(fs_info);
|
|
json += F("\"true\", \"totalBytes\":\"");
|
|
json += fs_info.totalBytes;
|
|
json += F("\", \"usedBytes\":\"");
|
|
json += fs_info.usedBytes;
|
|
json += "\"";
|
|
} else {
|
|
json += "\"false\"";
|
|
}
|
|
json += F(",\"unsupportedFiles\":\"");
|
|
json += unsupportedFiles;
|
|
json += "\"}";
|
|
|
|
server.send(200, "application/json", json);
|
|
}
|
|
|
|
|
|
/*
|
|
Return the list of files in the directory specified by the "dir" query string parameter.
|
|
Also demonstrates the use of chuncked responses.
|
|
*/
|
|
void handleFileList() {
|
|
if (!fsOK) {
|
|
return replyServerError(FPSTR(FS_INIT_ERROR));
|
|
}
|
|
|
|
if (!server.hasArg("dir")) {
|
|
return replyBadRequest(F("DIR ARG MISSING"));
|
|
}
|
|
|
|
String path = server.arg("dir");
|
|
if (path != "/" && !fileSystem->exists(path)) {
|
|
return replyBadRequest("BAD PATH");
|
|
}
|
|
|
|
DBG_OUTPUT_PORT.println(String("handleFileList: ") + path);
|
|
Dir dir = fileSystem->openDir(path);
|
|
path.clear();
|
|
|
|
// use HTTP/1.1 Chunked response to avoid building a huge temporary string
|
|
if (!server.chunkedResponseModeStart(200, "text/json")) {
|
|
server.send(505, F("text/html"), F("HTTP1.1 required"));
|
|
return;
|
|
}
|
|
|
|
// use the same string for every line
|
|
String output;
|
|
output.reserve(64);
|
|
while (dir.next()) {
|
|
#ifdef USE_SPIFFS
|
|
String error = checkForUnsupportedPath(dir.fileName());
|
|
if (error.length() > 0) {
|
|
DBG_OUTPUT_PORT.println(String("Ignoring ") + error + dir.fileName());
|
|
continue;
|
|
}
|
|
#endif
|
|
if (output.length()) {
|
|
// send string from previous iteration
|
|
// as an HTTP chunk
|
|
server.sendContent(output);
|
|
output = ',';
|
|
} else {
|
|
output = '[';
|
|
}
|
|
|
|
output += "{\"type\":\"";
|
|
if (dir.isDirectory()) {
|
|
output += "dir";
|
|
} else {
|
|
output += F("file\",\"size\":\"");
|
|
output += dir.fileSize();
|
|
}
|
|
|
|
output += F("\",\"name\":\"");
|
|
// Always return names without leading "/"
|
|
if (dir.fileName()[0] == '/') {
|
|
output += &(dir.fileName()[1]);
|
|
} else {
|
|
output += dir.fileName();
|
|
}
|
|
|
|
output += "\"}";
|
|
}
|
|
|
|
// send last string
|
|
output += "]";
|
|
server.sendContent(output);
|
|
server.chunkedResponseFinalize();
|
|
}
|
|
|
|
|
|
/*
|
|
Read the given file from the filesystem and stream it back to the client
|
|
*/
|
|
bool handleFileRead(String path) {
|
|
DBG_OUTPUT_PORT.println(String("handleFileRead: ") + path);
|
|
if (!fsOK) {
|
|
replyServerError(FPSTR(FS_INIT_ERROR));
|
|
return true;
|
|
}
|
|
|
|
if (path.endsWith("/")) {
|
|
path += "index.htm";
|
|
}
|
|
|
|
String contentType;
|
|
if (server.hasArg("download")) {
|
|
contentType = F("application/octet-stream");
|
|
} else {
|
|
contentType = getContentType(path);
|
|
}
|
|
|
|
if (!fileSystem->exists(path)) {
|
|
// File not found, try gzip version
|
|
path = path + ".gz";
|
|
}
|
|
if (fileSystem->exists(path)) {
|
|
File file = fileSystem->open(path, "r");
|
|
if (server.streamFile(file, contentType) != file.size()) {
|
|
DBG_OUTPUT_PORT.println("Sent less data than expected!");
|
|
}
|
|
file.close();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/*
|
|
As some FS (e.g. LittleFS) delete the parent folder when the last child has been removed,
|
|
return the path of the closest parent still existing
|
|
*/
|
|
String lastExistingParent(String path) {
|
|
while (!path.isEmpty() && !fileSystem->exists(path)) {
|
|
if (path.lastIndexOf('/') > 0) {
|
|
path = path.substring(0, path.lastIndexOf('/'));
|
|
} else {
|
|
path = String(); // No slash => the top folder does not exist
|
|
}
|
|
}
|
|
DBG_OUTPUT_PORT.println(String("Last existing parent: ") + path);
|
|
return path;
|
|
}
|
|
|
|
/*
|
|
Handle the creation/rename of a new file
|
|
Operation | req.responseText
|
|
---------------+--------------------------------------------------------------
|
|
Create file | parent of created file
|
|
Create folder | parent of created folder
|
|
Rename file | parent of source file
|
|
Move file | parent of source file, or remaining ancestor
|
|
Rename folder | parent of source folder
|
|
Move folder | parent of source folder, or remaining ancestor
|
|
*/
|
|
void handleFileCreate() {
|
|
if (!fsOK) {
|
|
return replyServerError(FPSTR(FS_INIT_ERROR));
|
|
}
|
|
|
|
String path = server.arg("path");
|
|
if (path.isEmpty()) {
|
|
return replyBadRequest(F("PATH ARG MISSING"));
|
|
}
|
|
|
|
#ifdef USE_SPIFFS
|
|
if (checkForUnsupportedPath(path).length() > 0) {
|
|
return replyServerError(F("INVALID FILENAME"));
|
|
}
|
|
#endif
|
|
|
|
if (path == "/") {
|
|
return replyBadRequest("BAD PATH");
|
|
}
|
|
if (fileSystem->exists(path)) {
|
|
return replyBadRequest(F("PATH FILE EXISTS"));
|
|
}
|
|
|
|
String src = server.arg("src");
|
|
if (src.isEmpty()) {
|
|
// No source specified: creation
|
|
DBG_OUTPUT_PORT.println(String("handleFileCreate: ") + path);
|
|
if (path.endsWith("/")) {
|
|
// Create a folder
|
|
path.remove(path.length() - 1);
|
|
if (!fileSystem->mkdir(path)) {
|
|
return replyServerError(F("MKDIR FAILED"));
|
|
}
|
|
} else {
|
|
// Create a file
|
|
File file = fileSystem->open(path, "w");
|
|
if (file) {
|
|
file.write((const char *)0);
|
|
file.close();
|
|
} else {
|
|
return replyServerError(F("CREATE FAILED"));
|
|
}
|
|
}
|
|
if (path.lastIndexOf('/') > -1) {
|
|
path = path.substring(0, path.lastIndexOf('/'));
|
|
}
|
|
replyOKWithMsg(path);
|
|
} else {
|
|
// Source specified: rename
|
|
if (src == "/") {
|
|
return replyBadRequest("BAD SRC");
|
|
}
|
|
if (!fileSystem->exists(src)) {
|
|
return replyBadRequest(F("SRC FILE NOT FOUND"));
|
|
}
|
|
|
|
DBG_OUTPUT_PORT.println(String("handleFileCreate: ") + path + " from " + src);
|
|
|
|
if (path.endsWith("/")) {
|
|
path.remove(path.length() - 1);
|
|
}
|
|
if (src.endsWith("/")) {
|
|
src.remove(src.length() - 1);
|
|
}
|
|
if (!fileSystem->rename(src, path)) {
|
|
return replyServerError(F("RENAME FAILED"));
|
|
}
|
|
replyOKWithMsg(lastExistingParent(src));
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Delete the file or folder designed by the given path.
|
|
If it's a file, delete it.
|
|
If it's a folder, delete all nested contents first then the folder itself
|
|
|
|
IMPORTANT NOTE: using recursion is generally not recommended on embedded devices and can lead to crashes (stack overflow errors).
|
|
This use is just for demonstration purpose, and FSBrowser might crash in case of deeply nested filesystems.
|
|
Please don't do this on a production system.
|
|
*/
|
|
void deleteRecursive(String path) {
|
|
File file = fileSystem->open(path, "r");
|
|
bool isDir = file.isDirectory();
|
|
file.close();
|
|
|
|
// If it's a plain file, delete it
|
|
if (!isDir) {
|
|
fileSystem->remove(path);
|
|
return;
|
|
}
|
|
|
|
// Otherwise delete its contents first
|
|
Dir dir = fileSystem->openDir(path);
|
|
|
|
while (dir.next()) {
|
|
deleteRecursive(path + '/' + dir.fileName());
|
|
}
|
|
|
|
// Then delete the folder itself
|
|
fileSystem->rmdir(path);
|
|
}
|
|
|
|
|
|
/*
|
|
Handle a file deletion request
|
|
Operation | req.responseText
|
|
---------------+--------------------------------------------------------------
|
|
Delete file | parent of deleted file, or remaining ancestor
|
|
Delete folder | parent of deleted folder, or remaining ancestor
|
|
*/
|
|
void handleFileDelete() {
|
|
if (!fsOK) {
|
|
return replyServerError(FPSTR(FS_INIT_ERROR));
|
|
}
|
|
|
|
String path = server.arg(0);
|
|
if (path.isEmpty() || path == "/") {
|
|
return replyBadRequest("BAD PATH");
|
|
}
|
|
|
|
DBG_OUTPUT_PORT.println(String("handleFileDelete: ") + path);
|
|
if (!fileSystem->exists(path)) {
|
|
return replyNotFound(FPSTR(FILE_NOT_FOUND));
|
|
}
|
|
deleteRecursive(path);
|
|
|
|
replyOKWithMsg(lastExistingParent(path));
|
|
}
|
|
|
|
/*
|
|
Handle a file upload request
|
|
*/
|
|
void handleFileUpload() {
|
|
if (!fsOK) {
|
|
return replyServerError(FPSTR(FS_INIT_ERROR));
|
|
}
|
|
if (server.uri() != "/edit") {
|
|
return;
|
|
}
|
|
HTTPUpload& upload = server.upload();
|
|
if (upload.status == UPLOAD_FILE_START) {
|
|
String filename = upload.filename;
|
|
// Make sure paths always start with "/"
|
|
if (!filename.startsWith("/")) {
|
|
filename = "/" + filename;
|
|
}
|
|
DBG_OUTPUT_PORT.println(String("handleFileUpload Name: ") + filename);
|
|
uploadFile = fileSystem->open(filename, "w");
|
|
if (!uploadFile) {
|
|
return replyServerError(F("CREATE FAILED"));
|
|
}
|
|
DBG_OUTPUT_PORT.println(String("Upload: START, filename: ") + filename);
|
|
} else if (upload.status == UPLOAD_FILE_WRITE) {
|
|
if (uploadFile) {
|
|
size_t bytesWritten = uploadFile.write(upload.buf, upload.currentSize);
|
|
if (bytesWritten != upload.currentSize) {
|
|
return replyServerError(F("WRITE FAILED"));
|
|
}
|
|
}
|
|
DBG_OUTPUT_PORT.println(String("Upload: WRITE, Bytes: ") + upload.currentSize);
|
|
} else if (upload.status == UPLOAD_FILE_END) {
|
|
if (uploadFile) {
|
|
uploadFile.close();
|
|
}
|
|
DBG_OUTPUT_PORT.println(String("Upload: END, Size: ") + upload.totalSize);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
The "Not Found" handler catches all URI not explicitely declared in code
|
|
First try to find and return the requested file from the filesystem,
|
|
and if it fails, return a 404 page with debug information
|
|
*/
|
|
void handleNotFound() {
|
|
if (!fsOK) {
|
|
return replyServerError(FPSTR(FS_INIT_ERROR));
|
|
}
|
|
|
|
String uri = ESP8266WebServer::urlDecode(server.uri()); // required to read paths with blanks
|
|
|
|
if (handleFileRead(uri)) {
|
|
return;
|
|
}
|
|
|
|
// Dump debug data
|
|
String message;
|
|
message.reserve(100);
|
|
message = F("Error: File not found\n\nURI: ");
|
|
message += uri;
|
|
message += F("\nMethod: ");
|
|
message += (server.method() == HTTP_GET) ? "GET" : "POST";
|
|
message += F("\nArguments: ");
|
|
message += server.args();
|
|
message += '\n';
|
|
for (uint8_t i = 0; i < server.args(); i++) {
|
|
message += F(" NAME:");
|
|
message += server.argName(i);
|
|
message += F("\n VALUE:");
|
|
message += server.arg(i);
|
|
message += '\n';
|
|
}
|
|
message += "path=";
|
|
message += server.arg("path");
|
|
message += '\n';
|
|
DBG_OUTPUT_PORT.print(message);
|
|
|
|
return replyNotFound(message);
|
|
}
|
|
|
|
/*
|
|
This specific handler returns the index.htm (or a gzipped version) from the /edit folder.
|
|
If the file is not present but the flag INCLUDE_FALLBACK_INDEX_HTM has been set, falls back to the version
|
|
embedded in the program code.
|
|
Otherwise, fails with a 404 page with debug information
|
|
*/
|
|
void handleGetEdit() {
|
|
if (handleFileRead(F("/edit/index.htm"))) {
|
|
return;
|
|
}
|
|
|
|
#ifdef INCLUDE_FALLBACK_INDEX_HTM
|
|
server.sendHeader(F("Content-Encoding"), "gzip");
|
|
server.send(200, "text/html", index_htm_gz, index_htm_gz_len);
|
|
#else
|
|
replyNotFound(FPSTR(FILE_NOT_FOUND));
|
|
#endif
|
|
|
|
}
|
|
|
|
void setup(void) {
|
|
////////////////////////////////
|
|
// SERIAL INIT
|
|
DBG_OUTPUT_PORT.begin(115200);
|
|
DBG_OUTPUT_PORT.setDebugOutput(true);
|
|
DBG_OUTPUT_PORT.print('\n');
|
|
|
|
////////////////////////////////
|
|
// FILESYSTEM INIT
|
|
|
|
fileSystemConfig.setAutoFormat(false);
|
|
fileSystem->setConfig(fileSystemConfig);
|
|
fsOK = fileSystem->begin();
|
|
DBG_OUTPUT_PORT.println(fsOK ? F("Filesystem initialized.") : F("Filesystem init failed!"));
|
|
|
|
#ifdef USE_SPIFFS
|
|
// Debug: dump on console contents of filessytem with no filter and check filenames validity
|
|
Dir dir = fileSystem->openDir("");
|
|
DBG_OUTPUT_PORT.println(F("List of files at root of filesystem:"));
|
|
while (dir.next()) {
|
|
String error = checkForUnsupportedPath(dir.fileName());
|
|
String fileInfo = dir.fileName() + (dir.isDirectory() ? " [DIR]" : String(" (") + dir.fileSize() + "b)");
|
|
DBG_OUTPUT_PORT.println(error + fileInfo);
|
|
if (error.length() > 0) {
|
|
unsupportedFiles += error + fileInfo + '\n';
|
|
}
|
|
}
|
|
DBG_OUTPUT_PORT.println();
|
|
|
|
// Keep the "unsupportedFiles" variable to show it, but clean it up
|
|
unsupportedFiles.replace("\n", "<br/>");
|
|
unsupportedFiles = unsupportedFiles.substring(0, unsupportedFiles.length() - 5);
|
|
#endif
|
|
|
|
////////////////////////////////
|
|
// WI-FI INIT
|
|
DBG_OUTPUT_PORT.printf("Connecting to %s\n", ssid);
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.begin(ssid, password);
|
|
// Wait for connection
|
|
while (WiFi.status() != WL_CONNECTED) {
|
|
delay(500);
|
|
DBG_OUTPUT_PORT.print(".");
|
|
}
|
|
DBG_OUTPUT_PORT.println("");
|
|
DBG_OUTPUT_PORT.print(F("Connected! IP address: "));
|
|
DBG_OUTPUT_PORT.println(WiFi.localIP());
|
|
|
|
////////////////////////////////
|
|
// MDNS INIT
|
|
if (MDNS.begin(host)) {
|
|
MDNS.addService("http", "tcp", 80);
|
|
DBG_OUTPUT_PORT.print(F("Open http://"));
|
|
DBG_OUTPUT_PORT.print(host);
|
|
DBG_OUTPUT_PORT.println(F(".local/edit to open the FileSystem Browser"));
|
|
}
|
|
|
|
////////////////////////////////
|
|
// WEB SERVER INIT
|
|
|
|
// Filesystem status
|
|
server.on("/status", HTTP_GET, handleStatus);
|
|
|
|
// List directory
|
|
server.on("/list", HTTP_GET, handleFileList);
|
|
|
|
// Load editor
|
|
server.on("/edit", HTTP_GET, handleGetEdit);
|
|
|
|
// Create file
|
|
server.on("/edit", HTTP_PUT, handleFileCreate);
|
|
|
|
// Delete file
|
|
server.on("/edit", HTTP_DELETE, handleFileDelete);
|
|
|
|
// Upload file
|
|
// - first callback is called after the request has ended with all parsed arguments
|
|
// - second callback handles file upload at that location
|
|
server.on("/edit", HTTP_POST, replyOK, handleFileUpload);
|
|
|
|
// Default handler for all URIs not defined above
|
|
// Use it to read files from filesystem
|
|
server.onNotFound(handleNotFound);
|
|
|
|
// Start server
|
|
server.begin();
|
|
DBG_OUTPUT_PORT.println("HTTP server started");
|
|
}
|
|
|
|
|
|
void loop(void) {
|
|
server.handleClient();
|
|
MDNS.update();
|
|
}
|