Skip to content

Commit 4e68541

Browse files
fhirschmannclaude
andcommitted
feat: add SD card cleanup for macOS metadata junk
New /sdclean endpoint recursively removes .DS_Store, ._* AppleDouble files and Spotlight/Trashes/fsevents folders from the SD card. The files tab gets a broom button with a confirmation dialog; the tree and the capacity gauge refresh afterwards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 5183e78 commit 4e68541

5 files changed

Lines changed: 111 additions & 0 deletions

File tree

‎html/locales/de.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,14 @@
157157
"files": {
158158
"title": "Dateien",
159159
"loading": "Wird geladen...",
160+
"clean": {
161+
"title": "SD-Karte bereinigen",
162+
"prompt": "Entfernt macOS-Metadaten von der SD-Karte (.DS_Store, ._*-Dateien, Spotlight- und Papierkorb-Ordner). Musikdateien bleiben unangetastet. Fortfahren?",
163+
"button": "SD-Karte bereinigen (macOS-Metadaten löschen)",
164+
"confirm": "Bereinigen",
165+
"running": "Bereinige SD-Karte…",
166+
"done": "Bereinigung abgeschlossen: {{count}} Einträge entfernt"
167+
},
160168
"storage": {
161169
"title": "Speicherplatz",
162170
"info": "{{free}} GB frei von {{total}} GB ({{percent}} % belegt)"

‎html/locales/en.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@
158158
"files": {
159159
"title": "Files",
160160
"loading": "Please wait...",
161+
"clean": {
162+
"title": "Clean SD card",
163+
"prompt": "Removes macOS metadata from the SD card (.DS_Store, ._* files, Spotlight and trash folders). Music files are left untouched. Continue?",
164+
"button": "Clean SD card (remove macOS metadata)",
165+
"confirm": "Clean",
166+
"running": "Cleaning SD card…",
167+
"done": "Cleanup finished: removed {{count}} entries"
168+
},
161169
"storage": {
162170
"title": "Storage",
163171
"info": "{{free}} GB free of {{total}} GB ({{percent}} % used)"

‎html/locales/fr.json‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@
150150
"files": {
151151
"title": "Fichiers",
152152
"loading": "Veuillez patienter...",
153+
"clean": {
154+
"title": "Nettoyer la carte SD",
155+
"prompt": "Supprime les métadonnées macOS de la carte SD (.DS_Store, fichiers ._*, dossiers Spotlight et corbeille). Les fichiers musicaux ne sont pas touchés. Continuer ?",
156+
"button": "Nettoyer la carte SD (supprimer les métadonnées macOS)",
157+
"confirm": "Nettoyer",
158+
"running": "Nettoyage de la carte SD…",
159+
"done": "Nettoyage terminé : {{count}} entrées supprimées"
160+
},
153161
"storage": {
154162
"title": "Stockage",
155163
"info": "{{free}} Go libres sur {{total}} Go ({{percent}} % utilisés)"

‎html/management.html‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,6 +1228,24 @@ <h5 class="modal-title" data-i18n="tools.nvs.delete.title"></h5>
12281228
</div>
12291229
</div>
12301230
</div>
1231+
<!-- Modal dialog for SD card cleanup -->
1232+
<div class="modal fade" data-bs-backdrop="static" id="cleanSdModal" role="dialog">
1233+
<div class="modal-dialog" role="document">
1234+
<div class="modal-content">
1235+
<div class="modal-header">
1236+
<h5 class="modal-title" data-i18n="files.clean.title"></h5>
1237+
</div>
1238+
<div class="modal-body">
1239+
<label data-i18n="files.clean.prompt"></label>
1240+
</div>
1241+
<div class="modal-footer">
1242+
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" data-i18n="cancel"></button>
1243+
<a class="btn btn-warning btn-ok" data-i18n="files.clean.confirm"
1244+
href="javascript:cleanSd();"></a>
1245+
</div>
1246+
</div>
1247+
</div>
1248+
</div>
12311249
<!-- Modal dialog for delete NVS RFID assignments -->
12321250
<div class="modal fade" data-bs-backdrop="static" id="deleteNVSRFIDModal" role="dialog">
12331251
<div class="modal-dialog" role="document">
@@ -1461,6 +1479,9 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
14611479
style="height:38px; width: 200px; border-bottom-left-radius: 0; border-top-left-radius: 0;"
14621480
id="filetreesearch" data-i18n="[placeholder]files.search.placeholder"
14631481
onsearch="searchFiles($(this).val())" onkeyup="searchFiles($(this).val())">
1482+
<button id="cleansd" class="btn btn-warning float-end" data-bs-toggle="modal"
1483+
data-bs-target="#cleanSdModal" data-i18n="[title]files.clean.button"><span
1484+
class="fas fa-broom"></span></button>
14641485
<button id="expandfilelist" class="btn btn-primary float-end"
14651486
onclick="resizeFileList(true)"><span class="fas fa-expand-alt"></span></button>
14661487
<button id="shrinkfilelist" class="btn btn-primary float-end"
@@ -4333,6 +4354,24 @@ <h5 class="modal-title" data-i18n="tools.nvs.erase.title"></h5>
43334354
}
43344355
return dDisplay + hDisplay + mDisplay;
43354356
}
4357+
async function cleanSd() {
4358+
$('#cleanSdModal').modal('hide');
4359+
toaster.info(i18next.t("files.clean.running"));
4360+
const response = await fetch("http://" + host + "/sdclean", {
4361+
method: "POST"
4362+
});
4363+
if (response.ok) {
4364+
const result = await response.json();
4365+
toaster.success(i18next.t("files.clean.done", {
4366+
count: result.deleted
4367+
}));
4368+
buildFileSystemTree("/");
4369+
refreshSdCapacity();
4370+
} else {
4371+
toaster.error(response.statusText);
4372+
}
4373+
}
4374+
43364375
async function refreshSecurityStatus() {
43374376
try {
43384377
const status = await (await fetch("http://" + host + "/security")).json();

‎src/Web.cpp‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ static void explorerHandleFileStorageTask(void *parameter);
7171
static void explorerHandleListRequest(AsyncWebServerRequest *request);
7272
static void explorerHandleDownloadRequest(AsyncWebServerRequest *request);
7373
static void explorerHandleDeleteRequest(AsyncWebServerRequest *request);
74+
static void handleCleanSdRequest(AsyncWebServerRequest *request);
7475
static void explorerHandleCreateRequest(AsyncWebServerRequest *request);
7576
static void explorerHandleRenameRequest(AsyncWebServerRequest *request);
7677
static void explorerHandleAudioRequest(AsyncWebServerRequest *request);
@@ -753,6 +754,8 @@ void webserverStart(void) {
753754

754755
wServer.on("/explorer", HTTP_PATCH, explorerHandleRenameRequest);
755756

757+
wServer.on("/sdclean", HTTP_POST, handleCleanSdRequest);
758+
756759
wServer.on("/exploreraudio", HTTP_POST, explorerHandleAudioRequest);
757760

758761
wServer.on("/trackprogress", HTTP_GET, handleTrackProgressRequest);
@@ -2194,6 +2197,51 @@ bool explorerDeleteDirectory(File dir) {
21942197
return gFSystem.rmdir(dir);
21952198
}
21962199

2200+
// macOS metadata junk: Finder/Spotlight droppings that are useless on a music box
2201+
static bool Web_IsMacJunkName(const char *name) {
2202+
return (strcmp(name, ".DS_Store") == 0) || (strncmp(name, "._", 2) == 0) || (strcmp(name, ".Spotlight-V100") == 0) || (strcmp(name, ".fseventsd") == 0) || (strcmp(name, ".Trashes") == 0) || (strcmp(name, ".TemporaryItems") == 0);
2203+
}
2204+
2205+
static void Web_CleanDirectory(File dir, uint32_t &deletedCount) {
2206+
File file = dir.openNextFile();
2207+
while (file) {
2208+
const String path = file.path();
2209+
const bool isDir = file.isDirectory();
2210+
if (Web_IsMacJunkName(file.name())) {
2211+
file.close();
2212+
if (isDir) {
2213+
File junkDir = gFSystem.open(path);
2214+
if (junkDir && explorerDeleteDirectory(junkDir)) {
2215+
deletedCount++;
2216+
}
2217+
} else if (gFSystem.remove(path)) {
2218+
deletedCount++;
2219+
}
2220+
} else if (isDir) {
2221+
Web_CleanDirectory(file, deletedCount);
2222+
file.close();
2223+
} else {
2224+
file.close();
2225+
}
2226+
file = dir.openNextFile();
2227+
esp_task_wdt_reset();
2228+
}
2229+
}
2230+
2231+
// Handles the request to remove macOS metadata junk from the SD card
2232+
static void handleCleanSdRequest(AsyncWebServerRequest *request) {
2233+
uint32_t deletedCount = 0;
2234+
File root = gFSystem.open("/");
2235+
if (root && root.isDirectory()) {
2236+
Web_CleanDirectory(root, deletedCount);
2237+
}
2238+
Log_Printf(LOGLEVEL_NOTICE, "SD cleanup: removed %lu entries", (unsigned long) deletedCount);
2239+
AsyncJsonResponse *response = new AsyncJsonResponse(false);
2240+
response->getRoot()["deleted"] = deletedCount;
2241+
response->setLength();
2242+
request->send(response);
2243+
}
2244+
21972245
// Handles download request of a file
21982246
// requires a GET parameter path to the file
21992247
void explorerHandleDownloadRequest(AsyncWebServerRequest *request) {

0 commit comments

Comments
 (0)