You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
AutoConnect/examples/FSBrowser/FSBrowser.ino

671 lines
18 KiB

5 years ago
/*
FSBrowser - A web-based FileSystem Browser for ESP8266 filesystems
7 years ago
Copyright (c) 2015 Hristo Gochkov. All rights reserved.
This file is part of the ESP8266WebServer library for Arduino environment.
5 years ago
7 years ago
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.
5 years ago
7 years ago
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.
5 years ago
7 years ago
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
5 years ago
See readme.md for more information.
7 years ago
*/
5 years ago
////////////////////////////////
// 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
////////////////////////////////
6 years ago
#if defined(ARDUINO_ARCH_ESP8266)
7 years ago
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
6 years ago
#elif defined(ARDUINO_ARCH_ESP32)
#include <WiFi.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#endif
5 years ago
#include <WiFiClient.h>
#include <SPI.h>
7 years ago
#include <AutoConnect.h>
5 years ago
#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
7 years ago
#define DBG_OUTPUT_PORT Serial
5 years ago
// Exclude unnecessary declarations due to applying AutoConnect
// #ifndef STASSID
// #define STASSID "SHAP-G"
// #define STAPSK "A0309T0312#"
// #endif
// const char* ssid = STASSID;
// const char* password = STAPSK;
const char* host = "fsbrowser";
7 years ago
ESP8266WebServer server(80);
6 years ago
5 years ago
static bool fsOK;
String unsupportedFiles = String();
File uploadFile;
// Additional lines as the below to apply AutoConnect
AutoConnect portal(server);
AutoConnectConfig config;
5 years ago
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 += "\"";
7 years ago
} else {
5 years ago
json += "\"false\"";
7 years ago
}
5 years ago
json += F(",\"unsupportedFiles\":\"");
json += unsupportedFiles;
json += "\"}";
server.send(200, "application/json", json);
7 years ago
}
5 years ago
/*
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();
7 years ago
}
5 years ago
/*
Read the given file from the filesystem and stream it back to the client
*/
bool handleFileRead(String path) {
5 years ago
DBG_OUTPUT_PORT.println(String("handleFileRead: ") + path);
if (!fsOK) {
replyServerError(FPSTR(FS_INIT_ERROR));
return true;
}
if (path.endsWith("/")) {
path += "index.htm";
}
5 years ago
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!");
}
7 years ago
file.close();
return true;
}
5 years ago
7 years ago
return false;
}
5 years ago
/*
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() {
5 years ago
if (!fsOK) {
return replyServerError(FPSTR(FS_INIT_ERROR));
}
if (server.uri() != "/edit") {
return;
}
7 years ago
HTTPUpload& upload = server.upload();
if (upload.status == UPLOAD_FILE_START) {
7 years ago
String filename = upload.filename;
5 years ago
// Make sure paths always start with "/"
if (!filename.startsWith("/")) {
filename = "/" + filename;
}
5 years ago
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) {
5 years ago
if (uploadFile) {
size_t bytesWritten = uploadFile.write(upload.buf, upload.currentSize);
if (bytesWritten != upload.currentSize) {
return replyServerError(F("WRITE FAILED"));
}
}
5 years ago
DBG_OUTPUT_PORT.println(String("Upload: WRITE, Bytes: ") + upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
5 years ago
if (uploadFile) {
uploadFile.close();
}
5 years ago
DBG_OUTPUT_PORT.println(String("Upload: END, Size: ") + upload.totalSize);
7 years ago
}
}
5 years ago
/*
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));
}
5 years ago
String uri = ESP8266WebServer::urlDecode(server.uri()); // required to read paths with blanks
if (handleFileRead(uri)) {
return;
}
5 years ago
// 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';
}
5 years ago
message += "path=";
message += server.arg("path");
message += '\n';
DBG_OUTPUT_PORT.print(message);
return replyNotFound(message);
7 years ago
}
5 years ago
/*
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;
}
7 years ago
5 years ago
#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));
6 years ago
#endif
5 years ago
7 years ago
}
5 years ago
void setup(void) {
////////////////////////////////
// SERIAL INIT
7 years ago
DBG_OUTPUT_PORT.begin(115200);
DBG_OUTPUT_PORT.setDebugOutput(true);
5 years ago
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';
6 years ago
}
5 years ago
}
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);
6 years ago
#endif
5 years ago
// 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
7 years ago
server.on("/list", HTTP_GET, handleFileList);
5 years ago
// 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");
5 years ago
if (portal.begin()) {
DBG_OUTPUT_PORT.print(F("Connected! IP address: "));
DBG_OUTPUT_PORT.println(WiFi.localIP());
}
7 years ago
DBG_OUTPUT_PORT.println("HTTP server started");
5 years ago
// 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);
5 years ago
DBG_OUTPUT_PORT.print(F("Open http://"));
DBG_OUTPUT_PORT.print(host);
5 years ago
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"));
}
7 years ago
}
5 years ago
void loop(void) {
// To make AutoConnect recognize the client handling, replace it with:
// server.handleClient();
7 years ago
portal.handleClient();
#ifdef ARDUINO_ARCH_ESP8266
MDNS.update();
#endif
7 years ago
}