Compare commits

...

3 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
9672b9bee0 Final improvements: enhanced UI feedback and cleanup
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-18 08:01:52 +00:00
copilot-swe-agent[bot]
6d0921bdd7 Implement user backup functionality with web API and UI
Co-authored-by: DedeHai <6280424+DedeHai@users.noreply.github.com>
2025-09-18 07:59:15 +00:00
copilot-swe-agent[bot]
ea4661e907 Initial plan 2025-09-18 07:47:38 +00:00
5 changed files with 349 additions and 0 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ wled-update.sh
/wled00/Release
/wled00/wled00.ino.cpp
/wled00/html_*.h
compile_commands.json

View File

@@ -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 @@
<a class="btn lnk" id="bckpresets" href="/cfg.json" download="cfg">Backup configuration</a><br>
<div>Restore configuration<br><input type="file" name="data2" accept=".json"> <button type="button" onclick="uploadFile(d.Sf.data2,'/cfg.json');">Upload</button><br></div>
<hr>
<h3>Internal User Backup</h3>
<div class="warn">&#9888; Internal backups are stored on the device filesystem and will be lost if the device is reset or reflashed.<br>
User backups will OVERWRITE existing backups of the same type.</div>
<h4>Configuration</h4>
<button type="button" id="ubkCfg" onclick="userBackup('config')">Create Config Backup</button>
<button type="button" id="uresCfg" onclick="userRestore('config')" style="display:none;">Restore Config</button><br><br>
<h4>Presets</h4>
<button type="button" id="ubkPresets" onclick="userBackup('presets')">Create Presets Backup</button>
<button type="button" id="uresPresets" onclick="userRestore('presets')" style="display:none;">Restore Presets</button><br><br>
<h4>Custom Palettes</h4>
<button type="button" id="ubkPalettes" onclick="userBackup('palettes')">Create Palettes Backup</button>
<button type="button" id="uresPalettes" onclick="userRestore('palettes')" style="display:none;">Restore Palettes</button><br><br>
<h4>Custom Mappings</h4>
<button type="button" id="ubkMappings" onclick="userBackup('mappings')">Create Mappings Backup</button>
<button type="button" id="uresMappings" onclick="userRestore('mappings')" style="display:none;">Restore Mappings</button><br><br>
<hr>
<h3>About</h3>
<a href="https://github.com/wled-dev/WLED/" target="_blank">WLED</a>&#32;version ##VERSION##<!-- Autoreplaced from package.json --><br><br>
<a href="https://kno.wled.ge/about/contributors/" target="_blank">Contributors, dependencies and special thanks</a><br>

View File

@@ -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();

View File

@@ -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");

View File

@@ -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);