diff --git a/.gitignore b/.gitignore index 51d321d922..9734686ede 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ wled-update.sh /wled00/Release /wled00/wled00.ino.cpp /wled00/html_*.h +compile_commands.json diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index 15104baa53..f6019b8e6f 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -17,6 +17,73 @@ function setBckFilename(x) { x.setAttribute("download","wled_" + x.getAttribute("download") + (sd=="WLED"?"":("_" +sd))); } + function userBackup(type) { + if (!confirm("Create internal backup for " + type + "? This will overwrite any existing backup.")) return; + var btn = gId("ubk" + type.charAt(0).toUpperCase() + type.slice(1)); + btn.disabled = true; + btn.innerHTML = "Creating..."; + var xhr = new XMLHttpRequest(); + xhr.open('POST', getURL('/backup/' + type), true); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + btn.disabled = false; + btn.innerHTML = "Create " + type.charAt(0).toUpperCase() + type.slice(1) + " Backup"; + if (xhr.status === 200) { + showToast(xhr.responseText, false); + updateBackupButtons(); + } else { + showToast("Backup failed: " + xhr.responseText, true); + } + } + }; + xhr.send(); + } + function userRestore(type) { + var message = "Restore " + type + " from internal backup? This will overwrite current " + type + "."; + if (type === 'config') message += " Device will reboot after restore."; + if (!confirm(message)) return; + var btn = gId("ures" + type.charAt(0).toUpperCase() + type.slice(1)); + btn.disabled = true; + btn.innerHTML = "Restoring..."; + var xhr = new XMLHttpRequest(); + xhr.open('POST', getURL('/restore/' + type), true); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + showToast(xhr.responseText, false); + if (type === 'config') { + setTimeout(function() { window.location.href = "/"; }, 3000); + } else { + btn.disabled = false; + btn.innerHTML = "Restore " + type.charAt(0).toUpperCase() + type.slice(1); + } + } else { + btn.disabled = false; + btn.innerHTML = "Restore " + type.charAt(0).toUpperCase() + type.slice(1); + showToast("Restore failed: " + xhr.responseText, true); + } + } + }; + xhr.send(); + } + function updateBackupButtons() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', getURL('/backup/status'), true); + xhr.onreadystatechange = function() { + if (xhr.readyState === 4 && xhr.status === 200) { + try { + var status = JSON.parse(xhr.responseText); + gId("uresCfg").style.display = status.config ? "inline-block" : "none"; + gId("uresPresets").style.display = status.presets ? "inline-block" : "none"; + gId("uresPalettes").style.display = status.palettes ? "inline-block" : "none"; + gId("uresMappings").style.display = status.mappings ? "inline-block" : "none"; + } catch(e) { + console.error("Failed to parse backup status:", e); + } + } + }; + xhr.send(); + } function S() { getLoc(); if (loc) { @@ -26,6 +93,7 @@ loadJS(getURL('/settings/s.js?p=6'), false, undefined, ()=>{ setBckFilename(gId("bckcfg")); setBckFilename(gId("bckpresets")); + updateBackupButtons(); }); // If we set async false, file is loaded and executed, then next statement is processed if (loc) d.Sf.action = getURL('/settings/sec'); } @@ -70,6 +138,22 @@

Backup & Restore

Backup configuration
Restore configuration


+

Internal User Backup

+
⚠ Internal backups are stored on the device filesystem and will be lost if the device is reset or reflashed.
+ User backups will OVERWRITE existing backups of the same type.
+

Configuration

+ +

+

Presets

+ +

+

Custom Palettes

+ +

+

Custom Mappings

+ +

+

About

WLED version ##VERSION##

Contributors, dependencies and special thanks
diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 1d81655d6d..be8eeae5e3 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -103,6 +103,21 @@ inline bool readObjectFromFile(const String &file, const char* key, JsonDocument bool copyFile(const char* src_path, const char* dst_path); bool backupFile(const char* filename); bool restoreFile(const char* filename); +bool userBackupFile(const char* filename); +bool userRestoreFile(const char* filename); +bool userBackupExists(const char* filename); +bool userBackupConfig(); +bool userRestoreConfig(); +bool userBackupConfigExists(); +bool userBackupPresets(); +bool userRestorePresets(); +bool userBackupPresetsExists(); +int userBackupPalettes(); +int userRestorePalettes(); +bool userBackupPalettesExist(); +int userBackupMappings(); +int userRestoreMappings(); +bool userBackupMappingsExist(); bool validateJsonFile(const char* filename); void dumpFilesToSerial(); diff --git a/wled00/file.cpp b/wled00/file.cpp index 9f1dd62256..ebe2530afd 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -516,6 +516,7 @@ bool compareFiles(const char* path1, const char* path2) { } static const char s_backup_fmt[] PROGMEM = "/bkp.%s"; +static const char s_user_backup_fmt[] PROGMEM = "/bku.%s"; bool backupFile(const char* filename) { DEBUG_PRINTF("backup %s \n", filename); @@ -572,6 +573,150 @@ bool validateJsonFile(const char* filename) { return result; } +bool userBackupFile(const char* filename) { + DEBUG_PRINTF("user backup %s \n", filename); + if (!validateJsonFile(filename)) { + DEBUG_PRINTLN(F("broken file")); + return false; + } + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), s_user_backup_fmt, filename + 1); // skip leading '/' in filename + + if (copyFile(filename, backupname)) { + DEBUG_PRINTLN(F("user backup ok")); + return true; + } + DEBUG_PRINTLN(F("user backup failed")); + return false; +} + +bool userRestoreFile(const char* filename) { + DEBUG_PRINTF("user restore %s \n", filename); + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), s_user_backup_fmt, filename + 1); // skip leading '/' in filename + + if (!WLED_FS.exists(backupname)) { + DEBUG_PRINTLN(F("no user backup found")); + return false; + } + + if (!validateJsonFile(backupname)) { + DEBUG_PRINTLN(F("broken user backup")); + return false; + } + + if (copyFile(backupname, filename)) { + DEBUG_PRINTLN(F("user restore ok")); + return true; + } + DEBUG_PRINTLN(F("user restore failed")); + return false; +} + +bool userBackupExists(const char* filename) { + char backupname[32]; + snprintf_P(backupname, sizeof(backupname), s_user_backup_fmt, filename + 1); // skip leading '/' in filename + return WLED_FS.exists(backupname); +} + +// User backup functions for different file types +bool userBackupConfig() { + return userBackupFile("/cfg.json"); +} + +bool userRestoreConfig() { + return userRestoreFile("/cfg.json"); +} + +bool userBackupConfigExists() { + return userBackupExists("/cfg.json"); +} + +bool userBackupPresets() { + return userBackupFile("/presets.json"); +} + +bool userRestorePresets() { + return userRestoreFile("/presets.json"); +} + +bool userBackupPresetsExists() { + return userBackupExists("/presets.json"); +} + +int userBackupPalettes() { + int count = 0; + for (int i = 0; i < 10; i++) { + char filename[32]; + sprintf_P(filename, PSTR("/palette%d.json"), i); + if (WLED_FS.exists(filename)) { + if (userBackupFile(filename)) count++; + } + } + return count; +} + +int userRestorePalettes() { + int count = 0; + for (int i = 0; i < 10; i++) { + char filename[32]; + sprintf_P(filename, PSTR("/palette%d.json"), i); + if (userRestoreFile(filename)) count++; + } + return count; +} + +bool userBackupPalettesExist() { + for (int i = 0; i < 10; i++) { + char filename[32]; + sprintf_P(filename, PSTR("/palette%d.json"), i); + if (userBackupExists(filename)) return true; + } + return false; +} + +int userBackupMappings() { + int count = 0; + // Backup ledmap files + for (int i = 1; i < WLED_MAX_LEDMAPS; i++) { + char filename[32]; + sprintf_P(filename, PSTR("/ledmap%d.json"), i); + if (WLED_FS.exists(filename)) { + if (userBackupFile(filename)) count++; + } + } + // Backup 2D gaps file if it exists + if (WLED_FS.exists("/2d-gaps.json")) { + if (userBackupFile("/2d-gaps.json")) count++; + } + return count; +} + +int userRestoreMappings() { + int count = 0; + // Restore ledmap files + for (int i = 1; i < WLED_MAX_LEDMAPS; i++) { + char filename[32]; + sprintf_P(filename, PSTR("/ledmap%d.json"), i); + if (userRestoreFile(filename)) count++; + } + // Restore 2D gaps file if backup exists + if (userRestoreFile("/2d-gaps.json")) count++; + return count; +} + +bool userBackupMappingsExist() { + // Check ledmap files + for (int i = 1; i < WLED_MAX_LEDMAPS; i++) { + char filename[32]; + sprintf_P(filename, PSTR("/ledmap%d.json"), i); + if (userBackupExists(filename)) return true; + } + // Check 2D gaps file + if (userBackupExists("/2d-gaps.json")) return true; + return false; +} + // print contents of all files in root dir to Serial except wsec files void dumpFilesToSerial() { File rootdir = WLED_FS.open("/", "r"); diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 6cd85188f5..312fab0802 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -376,6 +376,110 @@ void initServer() request->send(200, FPSTR(CONTENT_TYPE_PLAIN), (String)getFreeHeapSize()); }); + // User backup endpoints + server.on(F("/backup/config"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + bool success = userBackupConfig(); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), success ? F("Config backup created") : F("Config backup failed")); + }); + + server.on(F("/restore/config"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + bool success = userRestoreConfig(); + if (success) { + serveMessage(request, 200, F("Configuration restored."), F("Rebooting..."), 131); + doReboot = true; + } else { + request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Config restore failed or no backup found")); + } + }); + + server.on(F("/backup/presets"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + bool success = userBackupPresets(); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), success ? F("Presets backup created") : F("Presets backup failed")); + }); + + server.on(F("/restore/presets"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + bool success = userRestorePresets(); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), success ? F("Presets restored") : F("Presets restore failed or no backup found")); + }); + + server.on(F("/backup/palettes"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + int count = userBackupPalettes(); + String response = F("Palettes backup created: "); + response += count; + response += F(" files"); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response); + }); + + server.on(F("/restore/palettes"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + int count = userRestorePalettes(); + String response = F("Palettes restored: "); + response += count; + response += F(" files"); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response); + }); + + server.on(F("/backup/mappings"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + int count = userBackupMappings(); + String response = F("Mappings backup created: "); + response += count; + response += F(" files"); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response); + }); + + server.on(F("/restore/mappings"), HTTP_POST, [](AsyncWebServerRequest *request){ + if (!correctPIN) { + serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254); + return; + } + int count = userRestoreMappings(); + String response = F("Mappings restored: "); + response += count; + response += F(" files"); + request->send(200, FPSTR(CONTENT_TYPE_PLAIN), response); + }); + + // Check backup status endpoint + server.on(F("/backup/status"), HTTP_GET, [](AsyncWebServerRequest *request){ + String response = F("{\"config\":"); + response += userBackupConfigExists() ? F("true") : F("false"); + response += F(",\"presets\":"); + response += userBackupPresetsExists() ? F("true") : F("false"); + response += F(",\"palettes\":"); + response += userBackupPalettesExist() ? F("true") : F("false"); + response += F(",\"mappings\":"); + response += userBackupMappingsExist() ? F("true") : F("false"); + response += F("}"); + request->send(200, F("application/json"), response); + }); + #ifdef WLED_ENABLE_USERMOD_PAGE server.on("/u", HTTP_GET, [](AsyncWebServerRequest *request) { handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_usermod, PAGE_usermod_length);