Skip to content

Commit 2465122

Browse files
committed
fix(backup): support older rclone on restore and add a --list preflight
Two robustness follow-ups to the rclone memory-budgeting work merged in #477: 1) Restore on older rclone (regression). rclone_download() unconditionally passed --multi-thread-write-buffer-size (added in rclone v1.63.0) and --multi-thread-chunk-size (added in v1.64.0). Older rclone hard-fails on unknown flags (Fatal error: unknown flag, exit code 2), so any rclone < 1.63 -- common in distro packages -- broke restore/rollback. Both flags are now gated behind the detected rclone version (get_rclone_version()), with thresholds in RCLONE_MIN_VERSION_MT_* constants. The memory budget already assumes their defaults, so gating them out changes only which knobs reach rclone, not the computed footprint; --transfers/--buffer-size/--multi-thread-streams (all >= v1.48, long-standing) are always passed. When the version cannot be determined, the newer flags are skipped (safe). 2) Friendly preflight on read paths. The rclone-presence and backend-config checks lived inside pre_backup_restore_checks(), which the 'backup --list' branch and the 'restore --id' branch (via verify_backup_id() -> rclone lsf) both reach before, so a missing rclone surfaced as a raw 'sh: 1: rclone: not found' (--list) or a misleading 'Invalid backup ID' (restore --id). Extracted the checks into check_rclone_available() and call it on both read paths (and from pre_backup_restore_checks() for the main backup/restore flows).
1 parent eb42f07 commit 2465122

1 file changed

Lines changed: 57 additions & 3 deletions

File tree

src/helper/Site_Backup_Restore.php

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ class Site_Backup_Restore {
2323
const ERROR_TYPE_INTERRUPTED = 'interrupted'; // Killed/stopped
2424
const ERROR_TYPE_UNKNOWN = 'unknown_error'; // Unexpected
2525

26+
// Minimum rclone version that supports each download flag added after the
27+
// original multi-thread support. Older rclone rejects unknown flags and
28+
// aborts the copy, so these are only passed when the installed rclone is new
29+
// enough. (Update if upstream confirms different introduction versions.)
30+
const RCLONE_MIN_VERSION_MT_WRITE_BUFFER = '1.63.0'; // --multi-thread-write-buffer-size (rclone v1.63.0)
31+
const RCLONE_MIN_VERSION_MT_CHUNK_SIZE = '1.64.0'; // --multi-thread-chunk-size (rclone v1.64.0)
32+
2633
private $fs;
2734
public $site_data;
2835
private $rclone_config_path;
@@ -65,6 +72,7 @@ public function backup( $args, $assoc_args = [] ) {
6572

6673
// Handle --list flag to display available backups
6774
if ( $list_backups ) {
75+
$this->check_rclone_available();
6876
$this->list_remote_backups();
6977

7078
return; // Exit after listing backups
@@ -263,6 +271,12 @@ public function restore( $args, $assoc_args = [] ) {
263271

264272
if ( $backup_id ) {
265273

274+
// verify_backup_id() lists remote backups (rclone lsf) before the
275+
// pre_restore_check() preflight below, so check rclone here too --
276+
// otherwise a missing rclone surfaces as a misleading "Invalid backup
277+
// ID" instead of the friendly "rclone is not installed" message.
278+
$this->check_rclone_available();
279+
266280
if ( ! $this->verify_backup_id( $backup_id ) ) {
267281
EE::error( "Invalid backup ID provided.\nPlease provide a valid ID from the list using 'ee site backup --list " . $this->site_data['site_url'] . "'." );
268282
}
@@ -865,7 +879,14 @@ private function restore_wp( $backup_dir ) {
865879
EE::run_command( $args, $assoc_args, $options );
866880
}
867881

868-
private function pre_backup_restore_checks() {
882+
/**
883+
* Verify rclone is installed and the configured backend exists.
884+
*
885+
* Extracted from pre_backup_restore_checks() so read-only paths (e.g.
886+
* `ee site backup --list`) can run it before invoking rclone and surface a
887+
* friendly message instead of a raw "sh: 1: rclone: not found".
888+
*/
889+
private function check_rclone_available() {
869890
$command = 'rclone --version';
870891
$return_code = EE::exec( $command );
871892

@@ -893,6 +914,10 @@ private function pre_backup_restore_checks() {
893914
);
894915
EE::error( sprintf( 'rclone backend "%s" does not exist. Please create it using `rclone config`', $rclone_backend ) );
895916
}
917+
}
918+
919+
private function pre_backup_restore_checks() {
920+
$this->check_rclone_available();
896921

897922
$this->check_and_install( 'zip', 'zip' );
898923
$this->check_and_install( '7z', 'p7zip-full' );
@@ -1286,6 +1311,20 @@ private function get_available_ram_mb() {
12861311
return intval( EE::launch( $command )->stdout );
12871312
}
12881313

1314+
/**
1315+
* Installed rclone version as a dotted string (e.g. "1.66.0"), or '' if it
1316+
* cannot be determined. Used to gate download flags that older rclone lacks.
1317+
*
1318+
* @return string
1319+
*/
1320+
private function get_rclone_version() {
1321+
// `rclone version` prints e.g. "rclone v1.66.0" on its first line.
1322+
$output = EE::launch( "rclone version | head -n1 | awk '{print $2}'" )->stdout;
1323+
$version = ltrim( trim( $output ), 'v' );
1324+
1325+
return preg_match( '/^[0-9]+\.[0-9]+/', $version ) ? $version : '';
1326+
}
1327+
12891328
private function rclone_download( $path ) {
12901329
$cpu_cores = intval( EE::launch( 'nproc' )->stdout );
12911330
$available_ram = $this->get_available_ram_mb();
@@ -1333,8 +1372,23 @@ private function rclone_download( $path ) {
13331372
$mt_streams = max( 1, min( $cpu_cores * 2, 32, intval( floor( $stream_budget / $per_stream_mem ) ) ) );
13341373
$buffer_size = $buffer_mb . 'M';
13351374

1375+
// --multi-thread-write-buffer-size and --multi-thread-chunk-size were added
1376+
// after the original multi-thread support; older rclone aborts on unknown
1377+
// flags, so only pass them when the installed rclone is new enough. The
1378+
// budget above already assumes their defaults, so gating them out changes
1379+
// only which knobs reach rclone, not the computed footprint.
1380+
$rclone_version = $this->get_rclone_version();
1381+
$mt_flags = sprintf( '--multi-thread-streams %d', $mt_streams );
1382+
if ( $rclone_version && version_compare( $rclone_version, self::RCLONE_MIN_VERSION_MT_WRITE_BUFFER, '>=' ) ) {
1383+
$mt_flags .= sprintf( ' --multi-thread-write-buffer-size %dKi', $mt_write_buffer );
1384+
}
1385+
if ( $rclone_version && version_compare( $rclone_version, self::RCLONE_MIN_VERSION_MT_CHUNK_SIZE, '>=' ) ) {
1386+
$mt_flags .= sprintf( ' --multi-thread-chunk-size %dM', $mt_chunk_size );
1387+
}
1388+
13361389
EE::debug( sprintf(
1337-
'rclone download tuning: available_ram=%dMB budget=%dMB transfers=%d buffer-size=%s multi-thread-streams=%d mt-write-buffer=%dKi mt-chunk-size=%dM (est. peak ~%dMB)',
1390+
'rclone download tuning: rclone=%s available_ram=%dMB budget=%dMB transfers=%d buffer-size=%s multi-thread-streams=%d mt-write-buffer=%dKi mt-chunk-size=%dM (est. peak ~%dMB)',
1391+
$rclone_version ?: 'unknown',
13381392
$available_ram,
13391393
$budget,
13401394
$transfers,
@@ -1345,7 +1399,7 @@ private function rclone_download( $path ) {
13451399
(int) ( $transfers * ( $buffer_mb + $mt_streams * $per_stream_mem ) )
13461400
) );
13471401

1348-
$command = sprintf( "rclone copy -P --transfers %d --buffer-size %s --multi-thread-streams %d --multi-thread-write-buffer-size %dKi --multi-thread-chunk-size %dM %s %s", $transfers, $buffer_size, $mt_streams, $mt_write_buffer, $mt_chunk_size, escapeshellarg( $this->get_remote_path( false ) ), escapeshellarg( $path ) );
1402+
$command = sprintf( "rclone copy -P --transfers %d --buffer-size %s %s %s %s", $transfers, $buffer_size, $mt_flags, escapeshellarg( $this->get_remote_path( false ) ), escapeshellarg( $path ) );
13491403
$output = EE::launch( $command );
13501404

13511405
if ( $output->return_code ) {

0 commit comments

Comments
 (0)