From c67d07e2528999e92fa7b69963b62c76df99dc2c Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Sat, 17 Oct 2026 19:35:48 -0400 Subject: [PATCH] Backups: one meshcore-backup- name, no overwrite without a clock (#686) With the clock unset, Export backup named the file after uptime seconds (meshcore-backup-86.json), which read as a broken date and repeated after every reboot, so a later export could overwrite an earlier backup. Every timestamped backup is now meshcore-backup-YYYYMMDD-HHMMSS.json (was meshcore-YYYYMMDD-HHMMSS.json), so the date always ends the name. Without a set clock it is meshcore-backup-nodate-N.json; the Backups page picks the first N not already on the card or in internal storage. The web download uses the same pattern without the storage probe. Older names still list and restore (the filters match meshcore-*.json). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MjAKa5hXgyBykCLqnwcjEy --- src/ui-touch/UITask.cpp | 28 ++++++++++++++++-------- test/test_web_file_transfer_protocol.cpp | 2 ++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index 55e91cd..98e5d15 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -30147,7 +30147,7 @@ static constexpr uint32_t kFileTransferRestoreMaxBytes = 4u * 1024u * 1024u; static bool s_file_transfer_restore_upload = false; static bool s_file_transfer_backup_download = false; // Settings backup helpers, defined with the Backups settings page further down. -static void backupMakeFilename(char* out, size_t cap); +static void backupMakeFilename(char* out, size_t cap, bool probe_storage); static bool backupImportAndReboot(fs::FS* fsp, const char* path, void (*on_imported)(int nch, int nco)); static constexpr uint32_t kFileTransferMaxBytes = 512u * 1024u * 1024u; @@ -30511,7 +30511,7 @@ static void fileTransferBackupBegin() { return; } char name[48]; - backupMakeFilename(name, sizeof name); + backupMakeFilename(name, sizeof name, false); // lands on the computer, not device storage s_file_transfer_downloading = true; s_file_transfer_download_size = static_cast(size); s_file_transfer_download_offset = 0; @@ -49277,17 +49277,27 @@ static void backupsRebuildAsyncCb(void* /*p*/) { if (s_settings_open_cat == CAT_BACKUPS) openSettingsCategory(CAT_BACKUPS); } -// Unique backup filename from the device clock (local time) when it's set, else a -// uptime fallback. e.g. "meshcore-20260612-143205.json". -static void backupMakeFilename(char* out, size_t cap) { +// Unique backup filename, always "meshcore-backup-.json". The stamp is the +// device clock (local time) when it's set, e.g. "meshcore-backup-20260612-143205.json". +// Without a set clock it's "nodate-N"; with `probe_storage` N is the first one not +// already on the card or in internal storage, so an export after a reboot can't +// overwrite an older backup (the old uptime-seconds stamp repeated, #686). +static void backupMakeFilename(char* out, size_t cap, bool probe_storage) { uint32_t t = 0; if (mesh::RTCClock* rtc = the_mesh.getRTCClock()) t = rtc->getCurrentTime(); if (wallClockIsCurrent() && t > ClockFloorRTC::MIN_VALID_EPOCH) { time_t tt = (time_t)t; struct tm v; localtime_r(&tt, &v); - snprintf(out, cap, "meshcore-%04d%02d%02d-%02d%02d%02d.json", + snprintf(out, cap, "meshcore-backup-%04d%02d%02d-%02d%02d%02d.json", v.tm_year + 1900, v.tm_mon + 1, v.tm_mday, v.tm_hour, v.tm_min, v.tm_sec); - } else { - snprintf(out, cap, "meshcore-backup-%lu.json", (unsigned long)(millis() / 1000)); + return; + } + fs::FS* sd_fs = probe_storage ? backupSdFs() : nullptr; + fs::FS* int_fs = backupInternalFs(); + for (unsigned n = 1; ; n++) { + snprintf(out, cap, "meshcore-backup-nodate-%u.json", n); + if (!probe_storage || n >= 999) return; + char path[64]; snprintf(path, sizeof path, "/%s", out); + if (!(sd_fs && sd_fs->exists(path)) && !int_fs->exists(path)) return; } } @@ -49448,7 +49458,7 @@ static void buildBackupsSettings() { char timestamped[48]; const char* filename = "meshcore-backup.json"; if (s_backup_append_timestamp) { - backupMakeFilename(timestamped, sizeof timestamped); + backupMakeFilename(timestamped, sizeof timestamped, true); filename = timestamped; } doExportBackupFile(filename); diff --git a/test/test_web_file_transfer_protocol.cpp b/test/test_web_file_transfer_protocol.cpp index c2d6dbc..f342ba8 100644 --- a/test/test_web_file_transfer_protocol.cpp +++ b/test/test_web_file_transfer_protocol.cpp @@ -78,6 +78,8 @@ int main() { assert(readablePath("/meshcore-backup.json")); assert(readablePath("/meshcore-20260907-120000.json")); assert(readablePath("/meshcore-backup-123.json")); + assert(readablePath("/meshcore-backup-20261017-143205.json")); + assert(readablePath("/meshcore-backup-nodate-1.json")); assert(!readablePath("/transfer/.upload.part")); assert(!readablePath("/transfer/../secret.txt")); assert(!readablePath("/transfer/nested/file.txt"));