FSBrowser.ino 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. /*
  2. FSBrowser - A web-based FileSystem Browser for ESP8266 filesystems
  3. Copyright (c) 2015 Hristo Gochkov. All rights reserved.
  4. This file is part of the ESP8266WebServer library for Arduino environment.
  5. This library is free software; you can redistribute it and/or
  6. modify it under the terms of the GNU Lesser General Public
  7. License as published by the Free Software Foundation; either
  8. version 2.1 of the License, or (at your option) any later version.
  9. This library is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. Lesser General Public License for more details.
  13. You should have received a copy of the GNU Lesser General Public
  14. License along with this library; if not, write to the Free Software
  15. Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  16. See readme.md for more information.
  17. */
  18. ////////////////////////////////
  19. // Select the FileSystem by uncommenting one of the lines below
  20. //#define USE_SPIFFS
  21. #define USE_LITTLEFS
  22. //#define USE_SDFS
  23. // Uncomment the following line to embed a version of the web page in the code
  24. // (program code will be larger, but no file will have to be written to the filesystem).
  25. // Note: the source file "extras/index_htm.h" must have been generated by "extras/reduce_index.sh"
  26. //#define INCLUDE_FALLBACK_INDEX_HTM
  27. ////////////////////////////////
  28. #include <ESP8266WiFi.h>
  29. #include <WiFiClient.h>
  30. #include <ESP8266WebServer.h>
  31. #include <ESP8266mDNS.h>
  32. #include <SPI.h>
  33. #include <AutoConnect.h>
  34. #ifdef INCLUDE_FALLBACK_INDEX_HTM
  35. #include "extras/index_htm.h"
  36. #endif
  37. #if defined USE_SPIFFS
  38. #include <FS.h>
  39. const char* fsName = "SPIFFS";
  40. FS* fileSystem = &SPIFFS;
  41. SPIFFSConfig fileSystemConfig = SPIFFSConfig();
  42. #elif defined USE_LITTLEFS
  43. #include <LittleFS.h>
  44. const char* fsName = "LittleFS";
  45. FS* fileSystem = &LittleFS;
  46. LittleFSConfig fileSystemConfig = LittleFSConfig();
  47. #elif defined USE_SDFS
  48. #include <SDFS.h>
  49. const char* fsName = "SDFS";
  50. FS* fileSystem = &SDFS;
  51. SDFSConfig fileSystemConfig = SDFSConfig();
  52. // fileSystemConfig.setCSPin(chipSelectPin);
  53. #else
  54. #error Please select a filesystem first by uncommenting one of the "#define USE_xxx" lines at the beginning of the sketch.
  55. #endif
  56. #define DBG_OUTPUT_PORT Serial
  57. // Exclude unnecessary declarations due to applying AutoConnect
  58. // #ifndef STASSID
  59. // #define STASSID "your-ssid"
  60. // #define STAPSK "your-password"
  61. // #endif
  62. // const char* ssid = STASSID;
  63. // const char* password = STAPSK;
  64. const char* host = "fsbrowser";
  65. ESP8266WebServer server(80);
  66. static bool fsOK;
  67. String unsupportedFiles = String();
  68. File uploadFile;
  69. // Additional lines as the below to apply AutoConnect
  70. AutoConnect portal(server);
  71. AutoConnectConfig config;
  72. static const char TEXT_PLAIN[] PROGMEM = "text/plain";
  73. static const char FS_INIT_ERROR[] PROGMEM = "FS INIT ERROR";
  74. static const char FILE_NOT_FOUND[] PROGMEM = "FileNotFound";
  75. ////////////////////////////////
  76. // Utils to return HTTP codes, and determine content-type
  77. void replyOK() {
  78. server.send(200, FPSTR(TEXT_PLAIN), "");
  79. }
  80. void replyOKWithMsg(String msg) {
  81. server.send(200, FPSTR(TEXT_PLAIN), msg);
  82. }
  83. void replyNotFound(String msg) {
  84. server.send(404, FPSTR(TEXT_PLAIN), msg);
  85. }
  86. void replyBadRequest(String msg) {
  87. DBG_OUTPUT_PORT.println(msg);
  88. server.send(400, FPSTR(TEXT_PLAIN), msg + "\r\n");
  89. }
  90. void replyServerError(String msg) {
  91. DBG_OUTPUT_PORT.println(msg);
  92. server.send(500, FPSTR(TEXT_PLAIN), msg + "\r\n");
  93. }
  94. #ifdef USE_SPIFFS
  95. /*
  96. Checks filename for character combinations that are not supported by FSBrowser (alhtough valid on SPIFFS).
  97. Returns an empty String if supported, or detail of error(s) if unsupported
  98. */
  99. String checkForUnsupportedPath(String filename) {
  100. String error = String();
  101. if (!filename.startsWith("/")) {
  102. error += F("!NO_LEADING_SLASH! ");
  103. }
  104. if (filename.indexOf("//") != -1) {
  105. error += F("!DOUBLE_SLASH! ");
  106. }
  107. if (filename.endsWith("/")) {
  108. error += F("!TRAILING_SLASH! ");
  109. }
  110. return error;
  111. }
  112. #endif
  113. ////////////////////////////////
  114. // Request handlers
  115. /*
  116. Return the FS type, status and size info
  117. */
  118. void handleStatus() {
  119. DBG_OUTPUT_PORT.println("handleStatus");
  120. FSInfo fs_info;
  121. String json;
  122. json.reserve(128);
  123. json = "{\"type\":\"";
  124. json += fsName;
  125. json += "\", \"isOk\":";
  126. if (fsOK) {
  127. fileSystem->info(fs_info);
  128. json += F("\"true\", \"totalBytes\":\"");
  129. json += fs_info.totalBytes;
  130. json += F("\", \"usedBytes\":\"");
  131. json += fs_info.usedBytes;
  132. json += "\"";
  133. } else {
  134. json += "\"false\"";
  135. }
  136. json += F(",\"unsupportedFiles\":\"");
  137. json += unsupportedFiles;
  138. json += "\"}";
  139. server.send(200, "application/json", json);
  140. }
  141. /*
  142. Return the list of files in the directory specified by the "dir" query string parameter.
  143. Also demonstrates the use of chuncked responses.
  144. */
  145. void handleFileList() {
  146. if (!fsOK) {
  147. return replyServerError(FPSTR(FS_INIT_ERROR));
  148. }
  149. if (!server.hasArg("dir")) {
  150. return replyBadRequest(F("DIR ARG MISSING"));
  151. }
  152. String path = server.arg("dir");
  153. if (path != "/" && !fileSystem->exists(path)) {
  154. return replyBadRequest("BAD PATH");
  155. }
  156. DBG_OUTPUT_PORT.println(String("handleFileList: ") + path);
  157. Dir dir = fileSystem->openDir(path);
  158. path.clear();
  159. // use HTTP/1.1 Chunked response to avoid building a huge temporary string
  160. if (!server.chunkedResponseModeStart(200, "text/json")) {
  161. server.send(505, F("text/html"), F("HTTP1.1 required"));
  162. return;
  163. }
  164. // use the same string for every line
  165. String output;
  166. output.reserve(64);
  167. while (dir.next()) {
  168. #ifdef USE_SPIFFS
  169. String error = checkForUnsupportedPath(dir.fileName());
  170. if (error.length() > 0) {
  171. DBG_OUTPUT_PORT.println(String("Ignoring ") + error + dir.fileName());
  172. continue;
  173. }
  174. #endif
  175. if (output.length()) {
  176. // send string from previous iteration
  177. // as an HTTP chunk
  178. server.sendContent(output);
  179. output = ',';
  180. } else {
  181. output = '[';
  182. }
  183. output += "{\"type\":\"";
  184. if (dir.isDirectory()) {
  185. output += "dir";
  186. } else {
  187. output += F("file\",\"size\":\"");
  188. output += dir.fileSize();
  189. }
  190. output += F("\",\"name\":\"");
  191. // Always return names without leading "/"
  192. if (dir.fileName()[0] == '/') {
  193. output += &(dir.fileName()[1]);
  194. } else {
  195. output += dir.fileName();
  196. }
  197. output += "\"}";
  198. }
  199. // send last string
  200. output += "]";
  201. server.sendContent(output);
  202. server.chunkedResponseFinalize();
  203. }
  204. /*
  205. Read the given file from the filesystem and stream it back to the client
  206. */
  207. bool handleFileRead(String path) {
  208. DBG_OUTPUT_PORT.println(String("handleFileRead: ") + path);
  209. if (!fsOK) {
  210. replyServerError(FPSTR(FS_INIT_ERROR));
  211. return true;
  212. }
  213. if (path.endsWith("/")) {
  214. path += "index.htm";
  215. }
  216. String contentType;
  217. if (server.hasArg("download")) {
  218. contentType = F("application/octet-stream");
  219. } else {
  220. contentType = mime::getContentType(path);
  221. }
  222. if (!fileSystem->exists(path)) {
  223. // File not found, try gzip version
  224. path = path + ".gz";
  225. }
  226. if (fileSystem->exists(path)) {
  227. File file = fileSystem->open(path, "r");
  228. if (server.streamFile(file, contentType) != file.size()) {
  229. DBG_OUTPUT_PORT.println("Sent less data than expected!");
  230. }
  231. file.close();
  232. return true;
  233. }
  234. return false;
  235. }
  236. /*
  237. As some FS (e.g. LittleFS) delete the parent folder when the last child has been removed,
  238. return the path of the closest parent still existing
  239. */
  240. String lastExistingParent(String path) {
  241. while (!path.isEmpty() && !fileSystem->exists(path)) {
  242. if (path.lastIndexOf('/') > 0) {
  243. path = path.substring(0, path.lastIndexOf('/'));
  244. } else {
  245. path = String(); // No slash => the top folder does not exist
  246. }
  247. }
  248. DBG_OUTPUT_PORT.println(String("Last existing parent: ") + path);
  249. return path;
  250. }
  251. /*
  252. Handle the creation/rename of a new file
  253. Operation | req.responseText
  254. ---------------+--------------------------------------------------------------
  255. Create file | parent of created file
  256. Create folder | parent of created folder
  257. Rename file | parent of source file
  258. Move file | parent of source file, or remaining ancestor
  259. Rename folder | parent of source folder
  260. Move folder | parent of source folder, or remaining ancestor
  261. */
  262. void handleFileCreate() {
  263. if (!fsOK) {
  264. return replyServerError(FPSTR(FS_INIT_ERROR));
  265. }
  266. String path = server.arg("path");
  267. if (path.isEmpty()) {
  268. return replyBadRequest(F("PATH ARG MISSING"));
  269. }
  270. #ifdef USE_SPIFFS
  271. if (checkForUnsupportedPath(path).length() > 0) {
  272. return replyServerError(F("INVALID FILENAME"));
  273. }
  274. #endif
  275. if (path == "/") {
  276. return replyBadRequest("BAD PATH");
  277. }
  278. if (fileSystem->exists(path)) {
  279. return replyBadRequest(F("PATH FILE EXISTS"));
  280. }
  281. String src = server.arg("src");
  282. if (src.isEmpty()) {
  283. // No source specified: creation
  284. DBG_OUTPUT_PORT.println(String("handleFileCreate: ") + path);
  285. if (path.endsWith("/")) {
  286. // Create a folder
  287. path.remove(path.length() - 1);
  288. if (!fileSystem->mkdir(path)) {
  289. return replyServerError(F("MKDIR FAILED"));
  290. }
  291. } else {
  292. // Create a file
  293. File file = fileSystem->open(path, "w");
  294. if (file) {
  295. file.write((const char *)0);
  296. file.close();
  297. } else {
  298. return replyServerError(F("CREATE FAILED"));
  299. }
  300. }
  301. if (path.lastIndexOf('/') > -1) {
  302. path = path.substring(0, path.lastIndexOf('/'));
  303. }
  304. replyOKWithMsg(path);
  305. } else {
  306. // Source specified: rename
  307. if (src == "/") {
  308. return replyBadRequest("BAD SRC");
  309. }
  310. if (!fileSystem->exists(src)) {
  311. return replyBadRequest(F("SRC FILE NOT FOUND"));
  312. }
  313. DBG_OUTPUT_PORT.println(String("handleFileCreate: ") + path + " from " + src);
  314. if (path.endsWith("/")) {
  315. path.remove(path.length() - 1);
  316. }
  317. if (src.endsWith("/")) {
  318. src.remove(src.length() - 1);
  319. }
  320. if (!fileSystem->rename(src, path)) {
  321. return replyServerError(F("RENAME FAILED"));
  322. }
  323. replyOKWithMsg(lastExistingParent(src));
  324. }
  325. }
  326. /*
  327. Delete the file or folder designed by the given path.
  328. If it's a file, delete it.
  329. If it's a folder, delete all nested contents first then the folder itself
  330. IMPORTANT NOTE: using recursion is generally not recommended on embedded devices and can lead to crashes (stack overflow errors).
  331. This use is just for demonstration purpose, and FSBrowser might crash in case of deeply nested filesystems.
  332. Please don't do this on a production system.
  333. */
  334. void deleteRecursive(String path) {
  335. File file = fileSystem->open(path, "r");
  336. bool isDir = file.isDirectory();
  337. file.close();
  338. // If it's a plain file, delete it
  339. if (!isDir) {
  340. fileSystem->remove(path);
  341. return;
  342. }
  343. // Otherwise delete its contents first
  344. Dir dir = fileSystem->openDir(path);
  345. while (dir.next()) {
  346. deleteRecursive(path + '/' + dir.fileName());
  347. }
  348. // Then delete the folder itself
  349. fileSystem->rmdir(path);
  350. }
  351. /*
  352. Handle a file deletion request
  353. Operation | req.responseText
  354. ---------------+--------------------------------------------------------------
  355. Delete file | parent of deleted file, or remaining ancestor
  356. Delete folder | parent of deleted folder, or remaining ancestor
  357. */
  358. void handleFileDelete() {
  359. if (!fsOK) {
  360. return replyServerError(FPSTR(FS_INIT_ERROR));
  361. }
  362. String path = server.arg(0);
  363. if (path.isEmpty() || path == "/") {
  364. return replyBadRequest("BAD PATH");
  365. }
  366. DBG_OUTPUT_PORT.println(String("handleFileDelete: ") + path);
  367. if (!fileSystem->exists(path)) {
  368. return replyNotFound(FPSTR(FILE_NOT_FOUND));
  369. }
  370. deleteRecursive(path);
  371. replyOKWithMsg(lastExistingParent(path));
  372. }
  373. /*
  374. Handle a file upload request
  375. */
  376. void handleFileUpload() {
  377. if (!fsOK) {
  378. return replyServerError(FPSTR(FS_INIT_ERROR));
  379. }
  380. if (server.uri() != "/edit") {
  381. return;
  382. }
  383. HTTPUpload& upload = server.upload();
  384. if (upload.status == UPLOAD_FILE_START) {
  385. String filename = upload.filename;
  386. // Make sure paths always start with "/"
  387. if (!filename.startsWith("/")) {
  388. filename = "/" + filename;
  389. }
  390. DBG_OUTPUT_PORT.println(String("handleFileUpload Name: ") + filename);
  391. uploadFile = fileSystem->open(filename, "w");
  392. if (!uploadFile) {
  393. return replyServerError(F("CREATE FAILED"));
  394. }
  395. DBG_OUTPUT_PORT.println(String("Upload: START, filename: ") + filename);
  396. } else if (upload.status == UPLOAD_FILE_WRITE) {
  397. if (uploadFile) {
  398. size_t bytesWritten = uploadFile.write(upload.buf, upload.currentSize);
  399. if (bytesWritten != upload.currentSize) {
  400. return replyServerError(F("WRITE FAILED"));
  401. }
  402. }
  403. DBG_OUTPUT_PORT.println(String("Upload: WRITE, Bytes: ") + upload.currentSize);
  404. } else if (upload.status == UPLOAD_FILE_END) {
  405. if (uploadFile) {
  406. uploadFile.close();
  407. }
  408. DBG_OUTPUT_PORT.println(String("Upload: END, Size: ") + upload.totalSize);
  409. }
  410. }
  411. /*
  412. The "Not Found" handler catches all URI not explicitely declared in code
  413. First try to find and return the requested file from the filesystem,
  414. and if it fails, return a 404 page with debug information
  415. */
  416. void handleNotFound() {
  417. if (!fsOK) {
  418. return replyServerError(FPSTR(FS_INIT_ERROR));
  419. }
  420. String uri = ESP8266WebServer::urlDecode(server.uri()); // required to read paths with blanks
  421. if (handleFileRead(uri)) {
  422. return;
  423. }
  424. // Dump debug data
  425. String message;
  426. message.reserve(100);
  427. message = F("Error: File not found\n\nURI: ");
  428. message += uri;
  429. message += F("\nMethod: ");
  430. message += (server.method() == HTTP_GET) ? "GET" : "POST";
  431. message += F("\nArguments: ");
  432. message += server.args();
  433. message += '\n';
  434. for (uint8_t i = 0; i < server.args(); i++) {
  435. message += F(" NAME:");
  436. message += server.argName(i);
  437. message += F("\n VALUE:");
  438. message += server.arg(i);
  439. message += '\n';
  440. }
  441. message += "path=";
  442. message += server.arg("path");
  443. message += '\n';
  444. DBG_OUTPUT_PORT.print(message);
  445. return replyNotFound(message);
  446. }
  447. /*
  448. This specific handler returns the index.htm (or a gzipped version) from the /edit folder.
  449. If the file is not present but the flag INCLUDE_FALLBACK_INDEX_HTM has been set, falls back to the version
  450. embedded in the program code.
  451. Otherwise, fails with a 404 page with debug information
  452. */
  453. void handleGetEdit() {
  454. if (handleFileRead(F("/edit/index.htm"))) {
  455. return;
  456. }
  457. #ifdef INCLUDE_FALLBACK_INDEX_HTM
  458. server.sendHeader(F("Content-Encoding"), "gzip");
  459. server.send(200, "text/html", index_htm_gz, index_htm_gz_len);
  460. #else
  461. replyNotFound(FPSTR(FILE_NOT_FOUND));
  462. #endif
  463. }
  464. void setup(void) {
  465. ////////////////////////////////
  466. // SERIAL INIT
  467. DBG_OUTPUT_PORT.begin(115200);
  468. DBG_OUTPUT_PORT.setDebugOutput(true);
  469. DBG_OUTPUT_PORT.print('\n');
  470. ////////////////////////////////
  471. // FILESYSTEM INIT
  472. fileSystemConfig.setAutoFormat(false);
  473. fileSystem->setConfig(fileSystemConfig);
  474. fsOK = fileSystem->begin();
  475. DBG_OUTPUT_PORT.println(fsOK ? F("Filesystem initialized.") : F("Filesystem init failed!"));
  476. #ifdef USE_SPIFFS
  477. // Debug: dump on console contents of filessytem with no filter and check filenames validity
  478. Dir dir = fileSystem->openDir("");
  479. DBG_OUTPUT_PORT.println(F("List of files at root of filesystem:"));
  480. while (dir.next()) {
  481. String error = checkForUnsupportedPath(dir.fileName());
  482. String fileInfo = dir.fileName() + (dir.isDirectory() ? " [DIR]" : String(" (") + dir.fileSize() + "b)");
  483. DBG_OUTPUT_PORT.println(error + fileInfo);
  484. if (error.length() > 0) {
  485. unsupportedFiles += error + fileInfo + '\n';
  486. }
  487. }
  488. DBG_OUTPUT_PORT.println();
  489. // Keep the "unsupportedFiles" variable to show it, but clean it up
  490. unsupportedFiles.replace("\n", "<br/>");
  491. unsupportedFiles = unsupportedFiles.substring(0, unsupportedFiles.length() - 5);
  492. #endif
  493. // With applying AutoConnect, making WiFi connection is not necessary.
  494. // WI-FI INIT
  495. // DBG_OUTPUT_PORT.printf("Connecting to %s\n", ssid);
  496. // WiFi.mode(WIFI_STA);
  497. // WiFi.begin(ssid, password);
  498. // // Wait for connection
  499. // while (WiFi.status() != WL_CONNECTED) {
  500. // delay(500);
  501. // DBG_OUTPUT_PORT.print(".");
  502. // }
  503. // DBG_OUTPUT_PORT.println("");
  504. // DBG_OUTPUT_PORT.print(F("Connected! IP address: "));
  505. // DBG_OUTPUT_PORT.println(WiFi.localIP());
  506. ////////////////////////////////
  507. // WEB SERVER INIT
  508. // Filesystem status
  509. server.on("/status", HTTP_GET, handleStatus);
  510. // List directory
  511. server.on("/list", HTTP_GET, handleFileList);
  512. // Load editor
  513. server.on("/edit", HTTP_GET, handleGetEdit);
  514. // Create file
  515. server.on("/edit", HTTP_PUT, handleFileCreate);
  516. // Delete file
  517. server.on("/edit", HTTP_DELETE, handleFileDelete);
  518. // Upload file
  519. // - first callback is called after the request has ended with all parsed arguments
  520. // - second callback handles file upload at that location
  521. server.on("/edit", HTTP_POST, replyOK, handleFileUpload);
  522. // Default handler for all URIs not defined above
  523. // Use it to read files from filesystem
  524. // To make AutoConnect recognize the 404 handler, replace it with:
  525. //server.onNotFound(handleNotFound);
  526. portal.onNotFound(handleNotFound);
  527. // Using AutoConnect does not require the HTTP server to be started
  528. // intentionally. It is launched inside AutoConnect.begin.
  529. // Start server
  530. // server.begin();
  531. // DBG_OUTPUT_PORT.println("HTTP server started");
  532. // Start AutoConnect
  533. config.title = "FSBrowser";
  534. portal.config(config);
  535. portal.append("/edit", "Edit");
  536. portal.append("/list?dir=\"/\"", "List");
  537. if (portal.begin()) {
  538. DBG_OUTPUT_PORT.print(F("Connected! IP address: "));
  539. DBG_OUTPUT_PORT.println(WiFi.localIP());
  540. }
  541. DBG_OUTPUT_PORT.println("HTTP server started");
  542. // With applying AutoConnect, the MDNS service must be started after
  543. // establishing a WiFi connection.
  544. // MDNS INIT
  545. if (MDNS.begin(host)) {
  546. MDNS.addService("http", "tcp", 80);
  547. DBG_OUTPUT_PORT.print(F("Open http://"));
  548. DBG_OUTPUT_PORT.print(host);
  549. DBG_OUTPUT_PORT.println(F(".local/edit to open the FileSystem Browser"));
  550. DBG_OUTPUT_PORT.print(F("Open http://"));
  551. DBG_OUTPUT_PORT.print(host);
  552. DBG_OUTPUT_PORT.println(F(".local/_ac to AutoConnect statistics"));
  553. }
  554. }
  555. void loop(void) {
  556. // To make AutoConnect recognize the client handling, replace it with:
  557. // server.handleClient();
  558. portal.handleClient();
  559. MDNS.update();
  560. }