Update: 2024-02-03

Der Esp32 Filesystemmanger spezifisch sortiert als Arduino Tab.

Inhalte von Ordnern werden nur im Stammverzeichnis behandelt.

LittleFS.ino

// ****************************************************************
// Arduino IDE Tab Esp32 Filesystem Manager spezifisch sortiert Modular
// created: Jens Fleischer, 2023-03-26
// last mod: Jens Fleischer, 2024-02-03
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 2.0.6 - 2.0.14
// Getestet auf: ESP32 NodeMCU-32s
/******************************************************************
  Copyright (c) 2023 Jens Fleischer. All rights reserved.

  This file 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 file 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.
*******************************************************************/
// Diese Version von LittleFS sollte als Tab eingebunden werden.
// #include <LittleFS.h> #include <WebServer.h> müssen im Haupttab aufgerufen werden
// Die Funktionalität des ESP32 Webservers ist erforderlich.
// "server.onNotFound()" darf nicht im Setup des ESP32 Webserver stehen.
// Die Funktion "setupFS();" muss im Setup aufgerufen werden.
/**************************************************************************************/

#include <detail/RequestHandlersImpl.h>
#include <list>
#include <tuple>

const char WARNING[] PROGMEM = R"(<h2>LittleFS konnte nicht initialisiert werden!)";
const char HELPER[] PROGMEM = R"(<form method="POST" action="/upload" enctype="multipart/form-data">
                                <input type="file" name="[]" multiple><button>Upload</button></form>Lade die fs.html hoch.)";

void setupFS() {                                                                           // Funktionsaufruf "setupFS();" muss im Setup eingebunden werden
  LittleFS.begin(true);
  server.on("/format", formatFS);
  server.on("/upload", HTTP_POST, sendResponce, handleUpload);
  server.onNotFound([](String path = server.urlDecode(server.uri())) {
    if (!handleFile(path)) server.send(404, "text/html", "Not Found: " + path);
  });
}

bool handleList() {                                                                        // Senden aller Daten an den Client
  File root = LittleFS.open("/");
  using namespace std;
  using records = tuple<String, String, int>;
  list<records> dirList;
  while (File f = root.openNextFile()) {                                                   // Ordner und Dateien zur Liste hinzufügen
    if (f.isDirectory()) {
      uint8_t ran {0};
      File fold = LittleFS.open(static_cast<String>("/") + f.name());
      while (File f = fold.openNextFile()) {
        ran++;
        dirList.emplace_back(fold.name(), f.name(), f.size());
      }
      if (!ran) dirList.emplace_back(fold.name(), "", 0);
    }
    else {
      dirList.emplace_back("", f.name(), f.size());
    }
  }
  dirList.sort([](const records & f, const records & l) {                                  // Dateien sortieren
    if (server.arg(0) == "1") {
      return get<2>(f) > get<2>(l);
    } else {
      for (uint8_t i = 0; i < 31; i++) {
        if (tolower(get<1>(f)[i]) < tolower(get<1>(l)[i])) return true;
        else if (tolower(get<1>(f)[i]) > tolower(get<1>(l)[i])) return false;
      }
      return false;
    }
  });
  dirList.sort([](const records & f, const records & l) {                                  // Ordner sortieren
    if (get<0>(f)[0] != 0x00 || get<0>(l)[0] != 0x00) {
      for (uint8_t i = 0; i < 31; i++) {
        if (tolower(get<0>(f)[i]) < tolower(get<0>(l)[i])) return true;
        else if (tolower(get<0>(f)[i]) > tolower(get<0>(l)[i])) return false;
      }
    }
    return false;
  });
  String temp = "[";
  for (auto& t : dirList) {
    if (temp != "[") temp += ',';
    temp += "{\"folder\":\"" + get<0>(t) + "\",\"name\":\"" + get<1>(t) + "\",\"size\":\"" + formatBytes(get<2>(t)) + "\"}";
  }
  temp += ",{\"usedBytes\":\"" + formatBytes(LittleFS.usedBytes()) +                       // Berechnet den verwendeten Speicherplatz
          "\",\"totalBytes\":\"" + formatBytes(LittleFS.totalBytes()) +                    // Zeigt die Größe des Speichers
          "\",\"freeBytes\":\"" + (LittleFS.totalBytes() - LittleFS.usedBytes()) + "\"}]"; // Berechnet den freien Speicherplatz
  server.send(200, "application/json", temp);
  return true;
}

void deleteFiles(const String &path) {
  DEBUG_F("delete: %s\n", path.c_str());
  if (!LittleFS.remove("/" + path)) {
    File root = LittleFS.open(path);
    while (String filename = root.getNextFileName()) {
      LittleFS.remove(filename);
      LittleFS.rmdir(path);
      if (filename.length() < 1) break;
    }
  }
}

bool handleFile(String &path) {
  if (server.hasArg("new")) {
    String folderName {server.arg("new")};
    for (auto& c : {34, 37, 38, 47, 58, 59, 92}) for (auto& e : folderName) if (e == c) e = 95;   // Ersetzen der nicht erlaubten Zeichen
    DEBUG_F("Creating Dir: %s\n", folderName.c_str());
    LittleFS.mkdir("/" + folderName);
  }
  if (server.hasArg("sort")) return handleList();
  if (server.hasArg("delete")) {
    deleteFiles(server.arg("delete"));
    sendResponce();
    return true;
  }
  if (!LittleFS.exists("/fs.html")) server.send(200, "text/html", LittleFS.begin(true) ? HELPER : WARNING);   // ermöglicht das hochladen der fs.html
  if (path.endsWith("/")) path += "index.html";
  if (path == "/spiffs.html") sendResponce(); // Vorrübergehend für den Admin Tab
  return LittleFS.exists(path) ? ({File f = LittleFS.open(path, "r"); server.streamFile(f, StaticRequestHandler::getContentType(path)); f.close(); true;}) : false;
}

void handleUpload() {                                                                      // Dateien ins Filesystem schreiben
  static File fsUploadFile;
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    if (upload.filename.length() > 31) {  // Dateinamen kürzen
      upload.filename = upload.filename.substring(upload.filename.length() - 31, upload.filename.length());
    }
    DEBUG_F("handleFileUpload Name: /%s\n", upload.filename.c_str());
    fsUploadFile = LittleFS.open("/" + server.arg(0) + "/" + server.urlDecode(upload.filename), "w");
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    DEBUG_F("handleFileUpload Data: %u\n", upload.currentSize);
    fsUploadFile.write(upload.buf, upload.currentSize);
  } else if (upload.status == UPLOAD_FILE_END) {
    DEBUG_F("handleFileUpload Size: %u\n", upload.totalSize);
    fsUploadFile.close();
  }
}

void formatFS() {                                                                          // Formatiert das Filesystem
  LittleFS.format();
  sendResponce();
}

void sendResponce() {
  server.sendHeader("Location", "fs.html");
  server.send(303, "message/http");
}

const String formatBytes(size_t const & bytes) {                                           // lesbare Anzeige der Speichergrößen
  return bytes < 1024 ? static_cast<String>(bytes) + " Byte" : bytes < 1048576 ? static_cast<String>(bytes / 1024.0) + " KB" : static_cast<String>(bytes / 1048576.0) + " MB";
}

Das Webinterface zum LittleFS Tab.

fs.html

<!DOCTYPE HTML> <!-- For more information visit: https://fipsok.de -->
<html lang="de">
  <head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<link rel="stylesheet" href="style32.css">
	<title>Filesystem Manager</title>
	<script>
	  document.addEventListener('DOMContentLoaded', () => {
		list(JSON.parse(localStorage.getItem('sortBy')));
		btn.addEventListener('click', () => {
		  if (!confirm(`Wirklich formatieren? Alle Daten gehen verloren.\nDu musst anschließend fs.html wieder laden.`)) event.preventDefault();
		});
	  });
	  function list(to){
		let myList = document.querySelector('main'), noted = '';
		fetch(`?sort=${to}`).then( (response) => {
          return response.json();
        }).then((json) => {
		  myList.innerHTML = '<nav><input type="radio" id="/" name="group" checked="checked"><label for="/"> &#128193;</label><span id="cr">+&#128193;</nav></span><span id="si"></span>';
		  document.querySelector('form').setAttribute('action', '/upload?f=');
          for (var i = 0; i < json.length - 1; i++) {
		    let dir = '', f = json[i].folder, n = json[i].name;
		    if (f != noted) {
			  noted = f;
			  dir = `<nav><input type="radio" id="${f}" name="group"><label for="${f}"></label> &#128193; ${f} <a href="?delete=/${f}">&#x1f5d1;&#xfe0f;</a></nav>`;
			}
            if (n != '') dir += `<li><a href="${f}/${n}">${n}</a><small> ${json[i].size}</small><a href="${f}/${n}"download="${n}"> Download</a> or<a href="?delete=${f}/${n}"> Delete</a>`;
            myList.insertAdjacentHTML('beforeend', dir);
          }
          myList.insertAdjacentHTML('beforeend', `<li><b id="so">${to ? '&#9660;' : '&#9650;'} LittleFS</b> belegt ${json[i].usedBytes.replace(".00", "")} von ${json[i].totalBytes.replace(".00", "")}`);
          var free = json[i].freeBytes;
		  cr.addEventListener('click', () => {
			document.getElementById('no').classList.toggle('no');
		  });
		  so.addEventListener('click', () => {
			list(to=++to%2);
			localStorage.setItem('sortBy', JSON.stringify(to));
		  });
		  document.addEventListener('change', (e) => {
		    if (e.target.id == 'fs') {
		  	  for (var bytes = 0, i = 0; i < event.target.files.length; i++) bytes += event.target.files[i].size;
              for (var output = `${bytes} Byte`, i = 0, circa = bytes / 1024; circa > 1; circa /= 1024) output = circa.toFixed(2) + [' KB', ' MB', ' GB'][i++];
              if (bytes > free) {
                si.innerHTML = `<li><b> ${output}</b><strong> Ungenügend Speicher frei</strong></li>`;
                up.setAttribute('disabled', 'disabled');
              } 
              else {
			    si.innerHTML = `<li><b>Dateigröße:</b> ${output}</li>`;
                up.removeAttribute('disabled');
              }
			}
            document.querySelectorAll(`input[type=radio]`).forEach(el => { if (el.checked) document.querySelector('form').setAttribute('action', '/upload?f=' + el.id)});
          });
		  document.querySelectorAll('[href^="?delete=/"]').forEach(node => {
            node.addEventListener('click', () => {
	          if (!confirm('Sicher!')) event.preventDefault();
			});
		  });
	    });
      }
	</script>
  </head>
  <body>
	<h2>ESP32 Filesystem Manager</h2>
	<form method="post" enctype="multipart/form-data">
	  <input id="fs" type="file" name="up[]" multiple>
	  <button id="up" disabled>Upload</button>
	</form>
	<form id="no" class="no" method="POST">
	  <input name="new" placeholder="Ordner Name" pattern="[^\x22/%&\\:;]{1,31}" title="Zeichen &#8220; % & / : ; \  sind nicht erlaubt." required="">
	  <button>Create</button>
	</form>
	<main></main>
	<form action="/format" method="POST">
	  <button id="btn">Format LittleFS</button>
	</form>
  </body>
</html>

Die CSS Datei für den LittleFS Tab.

style32.css

/*For more information visit: https://fipsok.de*/
body {
	font-family: sans-serif;
	background-color: #a9a9a9;
	display: flex;
	flex-flow: column;
	align-items: center;
}
h1,h2 {
	color: #e1e1e1;
	text-shadow: 2px 2px 2px black;
}
li {
	background-color: #7cfc00;
	list-style-type: none;
	margin-bottom: 10px;
	padding-right: 5px;
	border-top: 3px solid #7cfc00;
	border-bottom: 3px solid #7cfc00;
	box-shadow: 5px 5px 5px #000000b3;
}
li a:first-child, li b {
	background-color: #ff4500;
	font-weight: bold;
	color: white;
	text-decoration: none;
	padding: 0 5px;
	border-top: 3.2px solid #ff4500;
	border-bottom: 3.6px solid #ff4500;
	text-shadow: 2px 2px 1px black;
	cursor:pointer;
}
li strong {
	color: red;
}
input {
	height: 35px;
	font-size: 13px;
}
h1+main {
	display: flex;
}
aside {
	display: flex;
	flex-direction: column;
	padding: 0.2em;
}
#left {
	align-items: flex-end;
	text-shadow: 1px 1px 2px #757474;
}
.note {
	background-color: salmon;
	padding: 0.5em;
	margin-top: 1em;
	text-align: center;
	max-width: 320px;
	border-radius: 0.5em;
}
nav {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
}
.no {
	display: none;
}
#cr {
	font-weight: bold;
	cursor:pointer;
	font-size: 1.5em;
}
#up {
	width: auto; 
}
button {
	width: 130px;
	height: 40px;
	font-size: 16px;
	margin-top: 1em;
	cursor: pointer;
	box-shadow: 5px 5px 5px #000000b3;
}
div button {
	background-color: #adff2f;
}
form [title] {
	background-color: silver;
	font-size: 16px;
	width: 125px;
}
form:nth-of-type(2) {
	margin-bottom: 1em;
}
[name="group"] {
	display: none;
}
[name="group"] + label {
	font-size: 1.5em;
	margin-right: 5px;
}
[name="group"] + label::before {
	content: "\002610";
}	
[name="group"]:checked + label::before {
	content: '\002611\0027A5';
}