-
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 6 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,51 @@ | ||
| <?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 heavy lifting is done by | ||
| * webGui/scripts/flash_backup, which writes the zip to stdout; we just set the | ||
| * download headers and pass it through. | ||
| * | ||
| * Query params: | ||
| * level zip compression level 0..9 (default 6) | ||
| * download opaque token echoed back as a cookie once the stream starts, so | ||
| * the GUI can tell the download began and clear its status spinner. | ||
| */ | ||
| $docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp'); | ||
|
|
||
| $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 |
||
|
|
||
| // Echo the download token back as a (non-HttpOnly) cookie so the page can detect | ||
| // the download has started. Numeric-only to keep it harmless. Must be set before | ||
| // any body output. | ||
| $token = preg_replace('/\D/', '', $_GET['download'] ?? ''); | ||
| if ($token !== '') setcookie('flashBackup', $token, ['path' => '/']); | ||
|
|
||
| // Stream for as long as it takes, and stop zip if the user cancels the download. | ||
| 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(); | ||
|
|
||
| passthru(escapeshellarg("$docroot/webGui/scripts/flash_backup")." ".(int)$level); | ||
| 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,39 @@ | |||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| ?> | ||||||||||||||||||||||||||||||||||
| <? | ||||||||||||||||||||||||||||||||||
| $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 | ||||||||||||||||||||||||||||||||||
| * 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. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $var = (array)@parse_ini_file('/var/local/emhttp/var.ini'); | ||||||||||||||||||||||||||||||||||
| $dir = ['system','appdata','isos','domains']; | ||||||||||||||||||||||||||||||||||
| $out = ['prev','previous']; | ||||||||||||||||||||||||||||||||||
| // 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;} | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| 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; | ||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||
|
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 |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Stream the archive to stdout. zip writes a valid streamable archive (data | ||||||||||||||||||||||||||||||||||
| // descriptors) when its output is not seekable. | ||||||||||||||||||||||||||||||||||
| $args = implode(' ', array_map('escapeshellarg', $items)); | ||||||||||||||||||||||||||||||||||
| 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.