Der Esp32 Filesystemmanger spezifisch sortiert als Arduino Tab.
// ****************************************************************
// Arduino IDE Tab Esp32 Filesystem Manager spezifisch sortiert Modular
// created: Jens Fleischer, 2023-03-26
// last mod: Jens Fleischer, 2024-06-03
// For more information visit: https://fipsok.de
// ****************************************************************
// Hardware: Esp32
// Software: Esp32 Arduino Core 2.0.6 - 3.0.0
// 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", ("Page Not Found: ") + path);
});
const char * headerkeys[] = {"If-None-Match"}; // "If-None-Match" HTTP-Anfrage-Header einfügen
server.collectHeaders(headerkeys, static_cast<size_t>(1)); // für ETag Unterstüzung: vor Core Version 3.x.x.
}
bool handleList() { // Senden aller Daten an den Client
File root = LittleFS.open("/");
using namespace std;
using records = tuple<String, String, size_t, time_t>;
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(), f.getLastWrite());
}
if (!ran) dirList.emplace_back(fold.name(), "", 0, 0);
}
else {
dirList.emplace_back("", f.name(), f.size(), f.getLastWrite());
}
}
dirList.sort([](const records & f, const records & l) { // Dateien sortieren
if (server.arg(0) == "1") { // nach Größe
return get<2>(f) > get<2>(l);
} else if (server.arg(0) == "2") { // nach Zeit
return get<3>(f) > get<3>(l);
} else { // nach Name
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)) + "\",\"time\":\"" + get<3>(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(); // Umleitung für den Admin Tab
File f = LittleFS.open(path, "r");
String eTag = String(f.getLastWrite(), HEX); // Verwendet den Zeitstempel der Dateiänderung, um den ETag zu erstellen.
if (server.header("If-None-Match") == eTag) {
server.send(304);
return true;
}
server.sendHeader("ETag", eTag);
return LittleFS.exists(path) ? server.streamFile(f, StaticRequestHandler::getContentType(path)) : 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();
});
});
async function list(to){
let resp = await fetch(`?sort=${to}`);
let json = await resp.json();
let myList = document.querySelector('main'), noted = '';
myList.innerHTML = '<nav><input type="radio" id="/" name="group" checked="checked"><label for="/"> 📁</label><span id="cr">+📁</nav></span><span id="si"></span>';
for (var i = 0; i < json.length - 1; i++) {
let dir = '', f = json[i].folder, n = json[i].name, t = new Date(json[i].time*1000).toLocaleString();
if (f != noted) {
noted = f;
dir = `<nav><input type="radio" id="${f}" name="group"><label for="${f}"></label> 📁 ${f} <a href="?delete=/${f}">🗑️</a></nav>`;
}
if (n != '') dir += `<li><a title="Geändert: ${t}" 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 ? to > 1 ? 'Time' : 'Size' : 'A - Z'}</b> LittleFS 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%3);
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 “ % & / : ; \ 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';
}