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 Upload
+ 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
+ Create Config Backup
+ Restore Config
+ Presets
+ Create Presets Backup
+ Restore Presets
+ Custom Palettes
+ Create Palettes Backup
+ Restore Palettes
+ Custom Mappings
+ Create Mappings Backup
+ Restore 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);