mirror of
https://github.com/wled/WLED.git
synced 2025-11-21 16:57:32 +00:00
Compare commits
3 Commits
copilot/ad
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9672b9bee0 | ||
|
|
6d0921bdd7 | ||
|
|
ea4661e907 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ wled-update.sh
|
||||
/wled00/Release
|
||||
/wled00/wled00.ino.cpp
|
||||
/wled00/html_*.h
|
||||
compile_commands.json
|
||||
|
||||
@@ -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">⚠ 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> 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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
145
wled00/file.cpp
145
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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user