-
Notifications
You must be signed in to change notification settings - Fork 99
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 7 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,71 @@ | ||
| <?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. | ||
| */ | ||
| ?> | ||
| <? | ||
| /* Stream a boot-device (flash) backup straight to the browser as it is built, | ||
| * so nothing is ever staged on disk or in RAM. The zip is produced by | ||
| * webGui/scripts/flash_backup (writing to stdout); we pipe it to the client and, | ||
| * as bytes flow, publish a percentage to the 'flash_backup' nchan channel so the | ||
| * GUI can show a real progress bar. The percentage is bytes-sent over the total | ||
| * flash size reported by `flash_backup size` (the boot device is mostly already- | ||
| * compressed OS files, so output closely tracks input - the bar is accurate in | ||
| * practice and we cap it at 99% until the stream actually finishes). | ||
| * | ||
| * Query params: | ||
| * level zip compression level 0..9 (default 6) | ||
| */ | ||
| $docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'); | ||
| require_once "$docroot/webGui/include/publish.php"; | ||
|
|
||
| $channel = 'flash_backup'; | ||
| $script = "$docroot/webGui/scripts/flash_backup"; | ||
|
|
||
| $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"; | ||
|
Comment on lines
+53
to
+55
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. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Sanitize filename parts before sending Lines 29-31 use config-derived 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 |
||
|
|
||
| // Total size of the included set, used as the progress denominator. | ||
| $total = (int)trim(shell_exec(escapeshellarg($script).' size 2>/dev/null')); | ||
|
|
||
| // Stream for as long as it takes, and let zip die if the user cancels. | ||
| set_time_limit(0); | ||
| ignore_user_abort(false); | ||
|
|
||
| header('Content-Type: application/zip'); | ||
| header('Content-Disposition: attachment; filename="'.$name.'"'); | ||
| header('X-Accel-Buffering: no'); // tell nginx to stream, not spool to a temp file | ||
| header('Cache-Control: no-store'); | ||
| while (ob_get_level()) ob_end_clean(); | ||
|
|
||
| $h = popen(escapeshellarg($script).' '.(int)$level.' 2>/dev/null', 'r'); | ||
| if ($h) { | ||
| $sent = 0; $last = 0; | ||
| while (!feof($h)) { | ||
| $buf = fread($h, 1 << 18); | ||
| if ($buf === '' || $buf === false) break; | ||
| echo $buf; | ||
| flush(); | ||
| $sent += strlen($buf); | ||
| $now = microtime(true); | ||
| if ($total > 0 && $now - $last > 0.4) { | ||
| publish($channel, (string)min(99, intdiv($sent * 100, $total)), 1, false); | ||
| $last = $now; | ||
| } | ||
| } | ||
| pclose($h); | ||
| } | ||
| publish($channel, '_DONE_', 1, false); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| 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); | ||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.