123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663 |
- /*
- 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>
- #include <AutoConnect.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
- // Exclude unnecessary declarations due to applying AutoConnect
- // #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;
- // Additional lines as the below to apply AutoConnect
- AutoConnect portal(server);
- AutoConnectConfig config;
- 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");
- }
- #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 = mime::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
- // With applying AutoConnect, making WiFi connection is not necessary.
- // 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());
- ////////////////////////////////
- // 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
- // To make AutoConnect recognize the 404 handler, replace it with:
- //server.onNotFound(handleNotFound);
- portal.onNotFound(handleNotFound);
- // Using AutoConnect does not require the HTTP server to be started
- // intentionally. It is launched inside AutoConnect.begin.
- // Start server
- // server.begin();
- // DBG_OUTPUT_PORT.println("HTTP server started");
- // Start AutoConnect
- config.title = "FSBrowser";
- portal.config(config);
- portal.append("/edit", "Edit");
- portal.append("/list?dir=\"/\"", "List");
- if (portal.begin()) {
- DBG_OUTPUT_PORT.print(F("Connected! IP address: "));
- DBG_OUTPUT_PORT.println(WiFi.localIP());
- }
- DBG_OUTPUT_PORT.println("HTTP server started");
- // With applying AutoConnect, the MDNS service must be started after
- // establishing a WiFi connection.
- // 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"));
- DBG_OUTPUT_PORT.print(F("Open http://"));
- DBG_OUTPUT_PORT.print(host);
- DBG_OUTPUT_PORT.println(F(".local/_ac to AutoConnect statistics"));
- }
- }
- void loop(void) {
- // To make AutoConnect recognize the client handling, replace it with:
- // server.handleClient();
- portal.handleClient();
- MDNS.update();
- }
|