feat(flash-backup): stream boot device backup download with selectable compression#2677
feat(flash-backup): stream boot device backup download with selectable compression#2677elibosley wants to merge 13 commits into
Conversation
Run the boot device (USB flash) backup as a detached background task instead of blocking the HTTP request for the entire zip creation. - Add a compression-level selector (None/Low/Normal/Maximum) on the Boot Device page; the chosen zip level (0/1/6/9) is passed to flash_backup. - Stream live progress over a new 'flash_backup' nchan channel and trigger the download automatically on completion (mirrors the Diagnostics flow). - Run zip under nice/ionice so the backup stays gentle on CPU and disk. - Prefer a real disk (array/pool share) over the RAM-backed rootfs for the archive so a large backup does not consume system memory; fall back to / only when no share has room. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughBoot device backup is converted from disk staging to direct browser streaming with real-time progress updates. A new ChangesBoot Backup Streaming Architecture
Sequence DiagramsequenceDiagram
participant User
participant BootInfo as BootInfo.page
participant FlashBackup as FlashBackup.php endpoint
participant flashBackup as flash_backup script
participant nchan as nchan channel
participant Browser
User->>BootInfo: Click Backup, select compression level
BootInfo->>BootInfo: show SweetAlert with progress bar
BootInfo->>nchan: subscribe to /sub/flash_backup
BootInfo->>Browser: location = /webGui/include/FlashBackup.php?level=N
Browser->>FlashBackup: GET request with level parameter
FlashBackup->>flashBackup: invoke "flash_backup size"
flashBackup-->>FlashBackup: expected total bytes
FlashBackup->>FlashBackup: set ZIP headers, disable buffering
FlashBackup->>flashBackup: popen "flash_backup level"
flashBackup->>Browser: stream ZIP chunks to stdout
loop Every chunk streamed
FlashBackup->>FlashBackup: calculate sent/total %
FlashBackup->>nchan: publish numeric progress
nchan->>BootInfo: progress message
BootInfo->>BootInfo: update `#fbBar/`#fbPct
end
FlashBackup->>nchan: publish _DONE_
nchan->>BootInfo: _DONE_ message
BootInfo->>BootInfo: set progress 100%, close alert
Browser->>Browser: auto-download ZIP file
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
🔧 PR Test Plugin AvailableA test plugin has been generated for this PR that includes the modified files. Version: 📥 Installation Instructions:Install via Unraid Web UI:
Alternative: Direct Download
|
…d nchan window Switch the boot backup from a bespoke modal to the same background-job mechanism plugin/docker operations use: - Launch via StartCommand.php (tracked PID, run de-duplication, abortable) instead of a one-off nohup in Download.php. - Stream into the standard progress window (pre#swaltext / pluginProgressTitle) and reuse the shared openDone()/openError() sentinel handlers; the script now emits the bare _DONE_/_ERROR_ sentinels they expect. - Keep the _FILE_<name> message so the GUI still auto-downloads the archive on completion, then cleans it up. - Drop the now-unused cmd=backup branch from Download.php. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rst target Issues found by running the job end-to-end on a live server: - Fatal "Call to undefined function _()": the job runs under php-cli (launched via StartCommand.php) where the GUI translation layer isn't loaded, so every _() call aborted the script immediately. Add a no-op _() fallback like other CLI scripts (monitor, InstallKey). This broke the feature entirely. - ionice -c3 (idle) could starve throughput to a crawl; use best-effort lowest priority (-c2 -n7) instead. (Note: ionice is a no-op on ZFS, which has its own scheduler.) - Storage target now prefers a cache/SSD pool, then RAM, then an array share - and writes straight to the pool mount, bypassing the slower /mnt/user FUSE layer. The previous disk-share-first choice routed through FUSE onto array/ZFS and was very slow; this keeps it fast on typical systems while still avoiding a large archive in RAM unless nothing else fits. Verified on a live 7.3.2 box: archive builds, integrity OK, prev/previous excluded, progress streams over nchan, _FILE_/_DONE_ drive the download. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@emhttp/plugins/dynamix/BootInfo.page`:
- Around line 29-32: The nchan_backup.stop() method is called multiple times in
the BootInfo.page file, which causes errors when attempting to stop an
already-stopped subscriber. Create a helper function that guards the stop call
by checking if nchan_backup exists and is in a running state before calling
stop(), then replace all three instances of nchan_backup.stop() calls (at the
locations where openError(data) is checked, openDone(data) is checked, and in
the SweetAlert dialog close callback) with this new helper function to prevent
attempting to stop an already-stopped subscriber.
- Around line 39-41: The box element is using `.html()` method to inject data
that comes from archive entry names (externally-derived content), which creates
a vulnerability to HTML/markup injection. Replace the `.html()` method calls in
line 40 with `.text()` method to safely escape the injected data. Since the
content is in a `<pre>` tag, `.text()` will preserve line breaks naturally
without needing the explicit `<br>` tag, while protecting against any malicious
markup in filenames.
In `@emhttp/plugins/dynamix/scripts/flash_backup`:
- Around line 124-128: The symlink() function call that creates the archive link
does not check for success before publishing the completion markers _FILE_ and
_DONE_. Since PHP's symlink() returns false when the destination already exists,
the code currently reports success even when the symlink creation fails. Add a
return value check on the symlink() call and implement error handling before the
write() calls for _FILE_ and _DONE_, following the same error handling pattern
already established earlier in the script (similar to lines 117-121), so that
failures are properly reported instead of silently continuing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: c3dbb551-7813-45c1-8ada-abbf4bfcab49
📒 Files selected for processing (4)
emhttp/languages/en_US/helptext.txtemhttp/plugins/dynamix/BootInfo.pageemhttp/plugins/dynamix/include/Download.phpemhttp/plugins/dynamix/scripts/flash_backup
💤 Files with no reviewable changes (1)
- emhttp/plugins/dynamix/include/Download.php
…ray stopped The backup archive is a throwaway staging file (built, downloaded, deleted), so the destination should be the fastest always-available location. Restore the original RAM-first behavior: rootfs (always mounted, even with the array stopped) -> cache/SSD pool -> array share. Pools only mount with the array, so pool-first failed to deliver a backup when the array was stopped. Disk tiers are kept only for flashes too large to stage in RAM. Process memory stays low regardless via the streaming zip binary. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… excludes - Add the boot device's own pool as a last-resort staging target. /boot is always mounted (even with the array stopped), so this guarantees a target when RAM is full and no pool/array share is available. Gated to real pool filesystems (zfs/btrfs/xfs) so a FAT/USB flash - which is not a pool and can't hold a >4GB file - is never chosen. - Reap any leftover staging archive at the start of each run (readlink the docroot symlink + delete its target), so a copy can't linger - especially in RAM - if the browser-side cleanup didn't fire (e.g. tab closed mid-download). The normal post-download Download.php cmd=unlink still runs. - Exclude more: prior-release dirs already skipped (previous/prev, ~1GB+), now also common desktop junk (System Volume Information, .Trashes, .fseventsd, .Spotlight-V100, .TemporaryItems) and any stray *-boot-backup-*.zip a user left on the flash. Verified on a live ZFS box: previous/ (1.1G) excluded, reaper removes a seeded stale artifact, archive integrity OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the staged background-job model with direct streaming: the zip is piped straight to the browser as it is built, so nothing is ever written to disk or RAM on the server. - scripts/flash_backup: now writes the zip to stdout (`zip -qr - ...`), usable both from the new endpoint and the CLI (`flash_backup 6 > out.zip`). No target selection, no nchan, no _() — just the exclusions and a single streaming zip. - include/FlashBackup.php: new download endpoint. Sets the attachment headers and `X-Accel-Buffering: no` (so nginx streams instead of spooling to a temp file), echoes the request's download token back as a cookie so the GUI can tell the stream started, then passes the script through. - BootInfo.page: clicking backup now navigates to the endpoint (browser handles the download, page isn't blocked) and shows a light "preparing…" status that clears as soon as the cookie appears. Removed the nchan/StartCommand path. Benefits: no staging file → no size/FAT limit, no cleanup/reaper, no target-selection logic, works with the array stopped, and it's faster (single pass; one zip invocation instead of per-item appends — ~44s vs ~115s on a test ZFS box). Trade-off: browser's native download indicator instead of an in-page progress log, and errors after streaming starts yield a truncated download. Verified on a live ZFS box: streamed archive validates, stderr clean (no zip pollution), previous/junk excluded, nothing left staged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@emhttp/plugins/dynamix/BootInfo.page`:
- Around line 32-37: The cookie check in the setInterval function uses indexOf
with substring matching on the flashBackup cookie, which can incorrectly match
partial or stale cookie values and close the modal prematurely. Instead of using
indexOf('flashBackup='+token) to check if the cookie exists, implement exact
matching by parsing the cookie string properly to extract the full flashBackup
value and compare it exactly against the token, ensuring partial matches don't
trigger an early clearInterval call.
In `@emhttp/plugins/dynamix/include/FlashBackup.php`:
- Around line 29-31: The filename construction in the backup file creation
(variables $server, $osVersion, and $name) uses unsanitized values from the
config that may contain special characters like quotes, separators, or control
characters which can break Content-Disposition header parsing. Apply proper
filename sanitization to both the processed $var['NAME'] value (after
str_replace and strtolower) and the $var['version'] value before they are used
in the $name variable. Remove or escape any special characters that are invalid
in HTTP headers or filenames. Apply the same sanitization fix to line 44 which
also appears to have similar filename construction issues.
In `@emhttp/plugins/dynamix/scripts/flash_backup`:
- Around line 36-43: The chdir('/boot') call on line 36 does not verify that the
directory change succeeded before proceeding with glob('*'). If the chdir fails,
glob will execute from the current working directory instead of /boot,
potentially generating incorrect backup data. Add a check after the
chdir('/boot') call to verify it returns true, and exit with an error code if
the directory change fails. This ensures glob() only runs after successfully
changing to the /boot directory.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 6fd20889-aa72-4719-a45d-ebc56196dd00
📒 Files selected for processing (4)
emhttp/languages/en_US/helptext.txtemhttp/plugins/dynamix/BootInfo.pageemhttp/plugins/dynamix/include/FlashBackup.phpemhttp/plugins/dynamix/scripts/flash_backup
✅ Files skipped from review due to trivial changes (1)
- emhttp/languages/en_US/helptext.txt
| $server = isset($var['NAME']) ? str_replace(' ', '_', strtolower($var['NAME'])) : 'tower'; | ||
| $osVersion = $var['version'] ?? 'unknown'; | ||
| $name = "$server-v$osVersion-boot-backup-".date('Ymd-Hi').".zip"; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Sanitize filename parts before sending Content-Disposition.
Lines 29-31 use config-derived NAME/version directly in the attachment filename. Special characters (quotes, separators, control chars) can break filename parsing across clients.
Suggested patch
-$server = isset($var['NAME']) ? str_replace(' ', '_', strtolower($var['NAME'])) : 'tower';
-$osVersion = $var['version'] ?? 'unknown';
-$name = "$server-v$osVersion-boot-backup-".date('Ymd-Hi').".zip";
+$server = isset($var['NAME']) ? str_replace(' ', '_', strtolower($var['NAME'])) : 'tower';
+$osVersion = $var['version'] ?? 'unknown';
+$safeServer = preg_replace('/[^a-z0-9._-]+/i', '_', $server);
+$safeVersion = preg_replace('/[^a-z0-9._-]+/i', '_', $osVersion);
+$name = "$safeServer-v$safeVersion-boot-backup-".date('Ymd-Hi').".zip";Also applies to: 44-44
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@emhttp/plugins/dynamix/include/FlashBackup.php` around lines 29 - 31, The
filename construction in the backup file creation (variables $server,
$osVersion, and $name) uses unsanitized values from the config that may contain
special characters like quotes, separators, or control characters which can
break Content-Disposition header parsing. Apply proper filename sanitization to
both the processed $var['NAME'] value (after str_replace and strtolower) and the
$var['version'] value before they are used in the $name variable. Remove or
escape any special characters that are invalid in HTTP headers or filenames.
Apply the same sanitization fix to line 44 which also appears to have similar
filename construction issues.
| chdir('/boot'); | ||
| $items = []; | ||
| foreach (glob('*', GLOB_NOSORT) as $entry) { | ||
| if (in_array($entry, $out)) continue; | ||
| if (preg_match('/-boot-backup-.*\.zip$/', $entry)) continue; // stray old backups | ||
| $items[] = $entry; | ||
| } | ||
| if (!$items) exit(1); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fail fast if /boot cannot be set as the working directory.
Line 36 does not check chdir('/boot'). If /boot is unavailable, glob('*') runs from the current directory and can produce a valid-but-wrong backup payload.
Suggested patch
-chdir('/boot');
+if (!chdir('/boot')) exit(1);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| chdir('/boot'); | |
| $items = []; | |
| foreach (glob('*', GLOB_NOSORT) as $entry) { | |
| if (in_array($entry, $out)) continue; | |
| if (preg_match('/-boot-backup-.*\.zip$/', $entry)) continue; // stray old backups | |
| $items[] = $entry; | |
| } | |
| if (!$items) exit(1); | |
| chdir('/boot') or exit(1); | |
| $items = []; | |
| foreach (glob('*', GLOB_NOSORT) as $entry) { | |
| if (in_array($entry, $out)) continue; | |
| if (preg_match('/-boot-backup-.*\.zip$/', $entry)) continue; // stray old backups | |
| $items[] = $entry; | |
| } | |
| if (!$items) exit(1); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@emhttp/plugins/dynamix/scripts/flash_backup` around lines 36 - 43, The
chdir('/boot') call on line 36 does not verify that the directory change
succeeded before proceeding with glob('*'). If the chdir fails, glob will
execute from the current working directory instead of /boot, potentially
generating incorrect backup data. Add a check after the chdir('/boot') call to
verify it returns true, and exit with an error code if the directory change
fails. This ensures glob() only runs after successfully changing to the /boot
directory.
A streamed zip has no known final size, so the browser can't show a progress bar. Drive a real bar from the server instead, without giving up streaming: - scripts/flash_backup: add a `size` mode that prints the total byte size of the included set (single source of truth for the file list). - include/FlashBackup.php: pipe the zip to the client via popen and, as bytes flow, publish a percentage (bytes-sent / total) to the 'flash_backup' nchan channel, capped at 99% until the stream finishes (then _DONE_). The boot device is mostly already-compressed OS files, so output closely tracks input and the bar is accurate in practice. - BootInfo.page: subscribe to the channel and render a real progress bar that fills as the download streams, completing on _DONE_. Verified on a live ZFS box: size=2.55GB, bar climbs monotonically to ~96% then completes, streamed archive validates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
emhttp/plugins/dynamix/include/FlashBackup.php (1)
35-37: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winSanitize filename parts before placing them in
Content-Disposition.
NAME/versionfromvar.iniare used unsanitized in the attachment filename (also at Line 47). Quotes, separators, or control chars can break filename parsing across clients.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@emhttp/plugins/dynamix/include/FlashBackup.php` around lines 35 - 37, The filename constructed in the $name variable is using unsanitized values from var.ini ($var['NAME'] and $var['version']), which can contain quotes, control characters, or separators that break HTTP Content-Disposition header parsing. Sanitize both the $server variable (derived from $var['NAME']) and the $osVersion variable (from $var['version']) by removing or escaping problematic characters such as quotes, slashes, backslashes, and control characters before they are concatenated into the $name variable. Apply the same sanitization at line 47 where the filename is used in the Content-Disposition header.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@emhttp/plugins/dynamix/BootInfo.page`:
- Around line 23-33: The NchanSubscriber for nchan_backup is created with
default settings that cause it to replay the oldest buffered message from
previous backup runs, which means a stale _DONE_ message can immediately close a
fresh backup dialog when start() is called. Fix this by adding the
nchan_subscriber_first_message option set to 'newest' in the NchanSubscriber
constructor options object (the second parameter) to ensure only new messages
trigger the backup progress updates, preventing stale completion messages from
prior runs from interfering with new backups.
In `@emhttp/plugins/dynamix/include/FlashBackup.php`:
- Around line 52-69: The backup completion signal is published unconditionally
without checking the exit status of the child process. Capture the return value
from the pclose() call to get the exit status of the zip command, then
conditionally publish _DONE_ only when the exit status indicates success
(typically 0). When the exit status indicates failure or when popen() fails to
start the process, publish an appropriate error signal instead (such as _ERROR_)
to ensure the client-side handler can properly report backup failures and
prevent data corruption from incomplete backups.
---
Duplicate comments:
In `@emhttp/plugins/dynamix/include/FlashBackup.php`:
- Around line 35-37: The filename constructed in the $name variable is using
unsanitized values from var.ini ($var['NAME'] and $var['version']), which can
contain quotes, control characters, or separators that break HTTP
Content-Disposition header parsing. Sanitize both the $server variable (derived
from $var['NAME']) and the $osVersion variable (from $var['version']) by
removing or escaping problematic characters such as quotes, slashes,
backslashes, and control characters before they are concatenated into the $name
variable. Apply the same sanitization at line 47 where the filename is used in
the Content-Disposition header.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 17d978ce-7f0d-4e01-929c-85954e8b2c34
📒 Files selected for processing (3)
emhttp/plugins/dynamix/BootInfo.pageemhttp/plugins/dynamix/include/FlashBackup.phpemhttp/plugins/dynamix/scripts/flash_backup
🚧 Files skipped from review as they are similar to previous changes (1)
- emhttp/plugins/dynamix/scripts/flash_backup
…survives The progress bar stayed at 0%: triggering the download with `window.location` starts a top-level navigation, and NchanSubscriber stops itself on the window's unload/beforeunload — so the subscriber died the instant the download began and no progress (nor _DONE_) ever reached the page. Trigger the download from a hidden iframe instead, so the main window never navigates and the subscriber keeps receiving progress. Also give the dialog a Close button as a safety and fix the now-inaccurate status text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Backup mode selector. Download streams to the browser as before (include/FlashBackup.php). Save to the server reuses the job-based background backup (scripts/flash_backup_save via StartCommand.php): it writes the archive to a persistent disk target - cache/SSD pool, then an array share, then the boot device's own pool (no RAM, since a saved backup must survive a reboot) - keeps it, and reports the saved path. Useful when the connection to the server is slow. Both modes share the same nchan progress bar. flash_backup_save publishes a percentage per item, then _SAVED_<path> + _DONE_ (or _ERROR_<msg>); the page shows the path on completion for save mode and auto-downloads for download mode. Verified on a live ZFS box: save writes a valid 2.5GB archive to the pool, keeps it, integrity OK, progress published throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…upt progress
nchan retains the last message on the channel, so starting a backup made the
fresh subscriber immediately receive a stale "100"/"_DONE_" from a previous run
- the bar jumped to 100% on the first click. Compounding it, the per-click
start()/stop() of the subscriber raced ("already started"), leaving the next run
with a dead subscriber stuck at 0%.
Fix: tag each run with a token (Date.now()). The endpoint and save job publish
"<token>:<payload>"; the page keeps ONE subscriber for the page lifetime and
ignores any message whose token isn't the current run. No more per-click
start/stop, and stale buffered messages are filtered out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rop shared-file edits - Save mode is now much faster: pipe the single-pass streaming zip (scripts/flash_backup) into the destination file instead of appending each top-level item to a growing archive (one zip invocation vs many). - On completion the save job raises an Unraid notification (the tray bell) with a click-to-download link, exposes the saved file for download via a docroot symlink, and the GUI shows a Download button next to the saved path. - Revert the helptext.txt and .gitignore edits to base so this PR touches only its own files. helptext.txt was the single file shared with other in-flight PR test plugins (#2671, #2673), which made the stacked PR-plugin patch fail to apply; with it gone, #2677 stacks cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The save-to-server mode symlinked the finished backup into the webGui docroot so the Download button could fetch it at /<name>.zip. That path is session-gated by nginx (not open), but it left a predictable, session-reachable link to the full-flash backup (license, SSH/WireGuard keys, configs) sitting there for the whole uptime - longer exposure than stock Unraid, which removes its download symlink right after the download. Replace it with an authenticated, path-validated serve endpoint: - include/FlashBackup.php gains a `serve=<name>` mode that streams an already- saved backup only if the name matches the flash-backup pattern AND resolves onto a pool/array/boot mount (basename-stripped + realpath-checked), so it can't be turned into an arbitrary file read. It also sends Content-Length, so the browser shows a real progress bar for this download. - flash_backup_save no longer creates the docroot symlink; the notification's link and the GUI Download button point at the serve endpoint instead. Verified: traversal/bad names return nothing; a real saved backup serves (PK magic); the previously-exposed symlink removed from the live server. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make server-side saves discoverable and manageable: - The save-complete dialog now shows the path plus a Download link and an "Open in File Manager" link (/Main/Browse?dir=...) to where it landed. - The Boot Device page lists any boot backups already saved on the server (scanned from pool/array/boot mounts), each with its full path, size, a Download link (authenticated serve endpoint), and a File Manager link - so you can find them later and clean them up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Rework the manual Boot device backup (Main → boot device → Boot Device): no longer synchronous in the request, with a selectable mode, compression level, and a live progress bar.
include/FlashBackup.php); nothing staged on the server, no size/FAT limit, works with the array stopped.scripts/flash_backup_saveviaStartCommand.php) writes the archive to a persistent disk target (cache/SSD pool → array share → boot pool) and keeps it. On completion it raises an Unraid notification (the tray bell) with a click-to-download link, and the GUI shows a Download button next to the saved path. Runs to completion even if you close the dialog or navigate away — ideal when the link to the server is slow.Both modes share one progress bar fed by the
flash_backupnchan channel.Notable implementation points
0/1/6/9.zip -qr -pass piped to its destination (the download client, or the save file) instead of per-item appends — save dropped from ~95 s to ~8 s on a test box.window.location(a top-level navigation firesbeforeunload, which makesNchanSubscriberstop itself and freezes the bar).previous/prev, often >1 GB) and desktop junk.Files
emhttp/plugins/dynamix/include/FlashBackup.php(new) — streaming download endpointemhttp/plugins/dynamix/scripts/flash_backup— streams the zip to stdout (+sizemode)emhttp/plugins/dynamix/scripts/flash_backup_save(new) — background save-to-disk job + tray notificationemhttp/plugins/dynamix/BootInfo.page— mode + compression selectors, progress bar, download buttonemhttp/plugins/dynamix/include/Download.php— drop the old synchronouscmd=backupbranchTesting
Verified on a live 7.3.2 server:
notify get), download symlink created.sizemode reports total.Note on this dev box: it sits on a different subnet reached via NAT (~0.8 MB/s even wired), so the download mode is slow there — that's the network path, not the code (a static file over the same link is just as slow). Save-to-server (local disk speed) is the right tool in that situation.
🤖 Generated with Claude Code