-
Notifications
You must be signed in to change notification settings - Fork 98
feat(flash-backup): streaming download + save-to-server, with compression & progress #2677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
6094f98
d3f7954
d3f4a7f
c90b023
6bcfefe
f1dbf81
71ae4cf
f53ce1d
56cfc02
6ad7a4c
b8a12d4
8ef16fd
0fd3c21
e5a9ced
afbb87c
f4bedbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| <?PHP | ||
| /* Copyright 2005-2025, Lime Technology | ||
| * Copyright 2012-2023, Bergware International. | ||
| * | ||
| * This program is free software; you can redistribute it and/or | ||
| * modify it under the terms of the GNU General Public License version 2, | ||
| * as published by the Free Software Foundation. | ||
| * | ||
| * The above copyright notice and this permission notice shall be included in | ||
| * all copies or substantial portions of the Software. | ||
| */ | ||
| ?> | ||
| <? | ||
| /* Boot-device (flash) backup download endpoint. | ||
| * | ||
| * ?level=0..9 stream a freshly-built zip of the boot device straight to the | ||
| * browser as it is built (nothing staged on disk/RAM). The | ||
| * browser's own download indicator shows progress. | ||
| * ?serve=<name> stream an already-saved backup file (the save-to-server | ||
| * Download button / notification link). Strictly validated. | ||
| * | ||
| * `X-Accel-Buffering: no` tells nginx to stream the response instead of spooling | ||
| * it to a temp file. nginx requires a logged-in session to reach this endpoint. | ||
| */ | ||
| $docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'); | ||
|
|
||
| // "serve" mode: stream an already-saved backup. Strictly validated - only a file | ||
| // whose name matches the flash-backup pattern AND that resolves onto a | ||
| // pool/array/boot mount - so it can't be coerced into an arbitrary file read. | ||
| if (isset($_GET['serve'])) { | ||
| $name = basename($_GET['serve']); | ||
| if (!preg_match('/-boot-backup-[0-9-]+\.zip$/', $name)) { http_response_code(404); exit; } | ||
| $found = null; | ||
| foreach (array_merge(glob("/mnt/*/$name"), glob("/mnt/user/*/$name"), ["/boot/$name"]) as $cand) { | ||
| $rp = realpath($cand); | ||
| if ($rp && is_file($rp) && basename($rp) === $name && | ||
| (strncmp($rp, '/mnt/', 5) === 0 || strncmp($rp, '/boot/', 6) === 0)) { $found = $rp; break; } | ||
| } | ||
| if (!$found) { http_response_code(404); exit; } | ||
| set_time_limit(0); | ||
| header('Content-Type: application/zip'); | ||
| header('Content-Disposition: attachment; filename="'.$name.'"'); | ||
| header('Content-Length: '.filesize($found)); | ||
| header('X-Accel-Buffering: no'); | ||
| while (ob_get_level()) ob_end_clean(); | ||
| readfile($found); | ||
| exit; | ||
| } | ||
|
|
||
| // Stream a freshly-built backup. Named server-version-date.zip. | ||
| $var = (array)@parse_ini_file('/var/local/emhttp/var.ini'); | ||
| $level = isset($_GET['level']) && is_numeric($_GET['level']) ? max(0, min(9, (int)$_GET['level'])) : 6; | ||
| $server = isset($var['NAME']) ? str_replace(' ', '_', strtolower($var['NAME'])) : 'tower'; | ||
| $osVersion = $var['version'] ?? 'unknown'; | ||
| $name = "$server-v$osVersion-boot-backup-".date('Ymd-Hi').".zip"; | ||
|
|
||
| set_time_limit(0); | ||
| ignore_user_abort(false); // let zip die (SIGPIPE) if the user cancels the download | ||
|
|
||
| header('Content-Type: application/zip'); | ||
| header('Content-Disposition: attachment; filename="'.$name.'"'); | ||
| header('X-Accel-Buffering: no'); | ||
| header('Cache-Control: no-store'); | ||
| while (ob_get_level()) ob_end_clean(); | ||
|
|
||
| passthru(escapeshellarg("$docroot/webGui/scripts/flash_backup").' '.(int)$level.' 2>/dev/null'); | ||
| exit; | ||
| ?> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||||||
| #!/usr/bin/php -q | ||||||||||||||||||||||||||||||||||
| <?PHP | ||||||||||||||||||||||||||||||||||
| /* Copyright 2005-2023, Lime Technology | ||||||||||||||||||||||||||||||||||
| /* Copyright 2005-2025, Lime Technology | ||||||||||||||||||||||||||||||||||
| * Copyright 2012-2023, Bergware International. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * This program is free software; you can redistribute it and/or | ||||||||||||||||||||||||||||||||||
|
|
@@ -12,38 +12,50 @@ | |||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||
| <? | ||||||||||||||||||||||||||||||||||
| $docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'); | ||||||||||||||||||||||||||||||||||
| require_once "$docroot/webGui/include/Wrappers.php"; | ||||||||||||||||||||||||||||||||||
| /* Stream a zip backup of the boot device (USB flash) to stdout. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Usage: flash_backup [compression-level] > backup.zip (stream the archive) | ||||||||||||||||||||||||||||||||||
| * flash_backup size (print total bytes) | ||||||||||||||||||||||||||||||||||
| * compression-level 0 (store, fastest) .. 9 (smallest, slowest), default 6 | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * The archive is written straight to stdout so it can be streamed directly to a | ||||||||||||||||||||||||||||||||||
| * browser (see include/FlashBackup.php) without ever staging a file on disk or | ||||||||||||||||||||||||||||||||||
| * in RAM - this avoids any size limit, works with the array stopped, and needs | ||||||||||||||||||||||||||||||||||
| * no cleanup. zip runs under nice + best-effort-low ionice to stay gentle on | ||||||||||||||||||||||||||||||||||
| * CPU and disk. The prior-release dirs (previous/prev) and desktop junk are | ||||||||||||||||||||||||||||||||||
| * excluded so the archive stays small. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * The 'size' mode prints the total byte size of the same included set; the | ||||||||||||||||||||||||||||||||||
| * endpoint uses it as the denominator for a download progress bar. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $var = (array)@parse_ini_file('/var/local/emhttp/var.ini'); | ||||||||||||||||||||||||||||||||||
| $dir = ['system','appdata','isos','domains']; | ||||||||||||||||||||||||||||||||||
| $out = ['prev','previous']; | ||||||||||||||||||||||||||||||||||
| $sizeOnly = isset($argv[1]) && $argv[1] === 'size'; | ||||||||||||||||||||||||||||||||||
| // Clamp the requested compression level to a valid zip range (default 6). | ||||||||||||||||||||||||||||||||||
| $level = isset($argv[1]) && is_numeric($argv[1]) ? max(0, min(9, (int)$argv[1])) : 6; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $server = isset($var['NAME']) ? str_replace(' ','_',strtolower($var['NAME'])) : 'tower'; | ||||||||||||||||||||||||||||||||||
| $mydate = date('Ymd-Hi'); | ||||||||||||||||||||||||||||||||||
| $osVersion = _var($var,'version','_unknown'); | ||||||||||||||||||||||||||||||||||
| $backup = "$server-v$osVersion-boot-backup-$mydate.zip"; | ||||||||||||||||||||||||||||||||||
| // Top-level entries to exclude. 'previous'/'prev' hold the entire prior OS | ||||||||||||||||||||||||||||||||||
| // release (often >1GB); the rest is desktop junk created when the flash is | ||||||||||||||||||||||||||||||||||
| // mounted on a Mac/PC. | ||||||||||||||||||||||||||||||||||
| $out = ['prev','previous','System Volume Information','.Trash-0','.Trashes','.Spotlight-V100','.fseventsd','.TemporaryItems']; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $used = exec("df /boot|awk 'END{print $3}'") * 1.5; | ||||||||||||||||||||||||||||||||||
| $free = exec("df /|awk 'END{print $4}'"); | ||||||||||||||||||||||||||||||||||
| if ($free > $used) $zip = "/$backup"; else { | ||||||||||||||||||||||||||||||||||
| foreach ($dir as $share) { | ||||||||||||||||||||||||||||||||||
| if (!is_dir("/mnt/user/$share")) continue; | ||||||||||||||||||||||||||||||||||
| $free = exec("df /mnt/user/$share|awk 'END{print $4}'"); | ||||||||||||||||||||||||||||||||||
| if ($free > $used) {$zip = "/mnt/user/$share/$backup"; break;} | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| 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 (isset($zip)) { | ||||||||||||||||||||||||||||||||||
| chdir("/boot"); | ||||||||||||||||||||||||||||||||||
| foreach (glob("*",GLOB_NOSORT+GLOB_ONLYDIR) as $folder) { | ||||||||||||||||||||||||||||||||||
| if (in_array($folder,$out)) continue; | ||||||||||||||||||||||||||||||||||
| exec("zip -qr ".escapeshellarg($zip)." ".escapeshellarg($folder)); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| foreach (glob("*",GLOB_NOSORT) as $file) { | ||||||||||||||||||||||||||||||||||
| if (is_dir($file)) continue; | ||||||||||||||||||||||||||||||||||
| exec("zip -q ".escapeshellarg($zip)." ".escapeshellarg($file)); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| symlink($zip,"$docroot/$backup"); | ||||||||||||||||||||||||||||||||||
| echo $backup; | ||||||||||||||||||||||||||||||||||
| if (!$items) exit(1); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+48
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Fail fast if Line 36 does not check Suggested patch-chdir('/boot');
+if (!chdir('/boot')) exit(1);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| $args = implode(' ', array_map('escapeshellarg', $items)); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // 'size' mode: print the total apparent byte size of the included set and exit. | ||||||||||||||||||||||||||||||||||
| if ($sizeOnly) { | ||||||||||||||||||||||||||||||||||
| echo (int)trim(exec("du -sbc $args 2>/dev/null | awk 'END{print $1}'")); | ||||||||||||||||||||||||||||||||||
| exit(0); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Stream the archive to stdout. zip writes a valid streamable archive (data | ||||||||||||||||||||||||||||||||||
| // descriptors) when its output is not seekable. | ||||||||||||||||||||||||||||||||||
| passthru("nice -n 19 ionice -c2 -n7 zip -$level -qr - $args 2>/dev/null", $rc); | ||||||||||||||||||||||||||||||||||
| exit($rc); | ||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Sanitize filename parts before sending
Content-Disposition.Lines 29-31 use config-derived
NAME/versiondirectly in the attachment filename. Special characters (quotes, separators, control chars) can break filename parsing across clients.Suggested patch
Also applies to: 44-44
🤖 Prompt for AI Agents