Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ wled-update.sh
/wled00/Release
/wled00/wled00.ino.cpp
/wled00/html_*.h
compile_commands.json
84 changes: 84 additions & 0 deletions wled00/data/settings_sec.htm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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');
}
Expand Down Expand Up @@ -70,6 +138,22 @@ <h3>Backup & Restore</h3>
<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>
Expand Down
15 changes: 15 additions & 0 deletions wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
145 changes: 145 additions & 0 deletions wled00/file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down
Loading