Skip to content

Commit 58a9e94

Browse files
authored
Merge pull request #470 from mrrobot47/feat/api-retry-cleanup
Backup cleanup improvement
2 parents f578b57 + 41c8240 commit 58a9e94

File tree

1 file changed

+128
-32
lines changed

1 file changed

+128
-32
lines changed

src/helper/Site_Backup_Restore.php

Lines changed: 128 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class Site_Backup_Restore {
2222
private $dash_api_url;
2323
private $dash_backup_metadata;
2424
private $dash_backup_completed = false;
25+
private $dash_new_backup_path; // Track new backup path for potential rollback
2526

2627
public function __construct() {
2728
$this->fs = new Filesystem();
@@ -102,12 +103,20 @@ public function backup( $args, $assoc_args = [] ) {
102103
// Mark backup as completed and send success callback
103104
$this->dash_backup_completed = true;
104105
if ( $this->dash_auth_enabled ) {
105-
$this->send_dash_success_callback(
106+
$api_success = $this->send_dash_success_callback(
106107
$this->dash_api_url,
107108
$this->dash_backup_id,
108109
$this->dash_verify_token,
109110
$this->dash_backup_metadata
110111
);
112+
113+
// Only cleanup old backups if API callback succeeded
114+
// If API failed, rollback the newly uploaded backup
115+
if ( $api_success ) {
116+
$this->cleanup_old_backups();
117+
} else {
118+
$this->rollback_failed_backup();
119+
}
111120
}
112121

113122
delem_log( 'site backup end' );
@@ -995,23 +1004,13 @@ private function get_remote_path( $upload = true ) {
9951004

9961005
$this->rclone_config_path = $this->get_rclone_config_path();
9971006

998-
$no_of_backups = intval( get_config_value( 'no-of-backups', 7 ) );
999-
10001007
$backups = $this->list_remote_backups( true );
10011008
$timestamp = time() . '_' . date( 'Y-m-d-H-i-s' );
10021009

10031010
if ( ! empty( $backups ) ) {
10041011

1005-
if ( $upload ) {
1006-
if ( count( $backups ) > $no_of_backups ) {
1007-
$backups_to_delete = array_slice( $backups, $no_of_backups );
1008-
foreach ( $backups_to_delete as $backup ) {
1009-
EE::log( 'Deleting old backup: ' . $backup );
1010-
EE::launch( sprintf( 'rclone purge %s/%s', $this->rclone_config_path, $backup ) );
1011-
}
1012-
}
1013-
} else {
1014-
1012+
if ( ! $upload ) {
1013+
// For restore: use the most recent backup
10151014
$timestamp = $backups[0];
10161015
EE::log( 'Restoring from backup: ' . $timestamp );
10171016
}
@@ -1066,6 +1065,76 @@ private function rclone_upload( $path ) {
10661065
$output = EE::launch( $command );
10671066
$remote_path = $output->stdout;
10681067
EE::success( 'Backup uploaded to remote storage. Remote path: ' . $remote_path );
1068+
1069+
// Store the new backup path for potential rollback (only when using dash-auth)
1070+
if ( $this->dash_auth_enabled ) {
1071+
$this->dash_new_backup_path = $this->get_remote_path();
1072+
}
1073+
1074+
// Only delete old backups immediately if NOT using dash-auth
1075+
// If using dash-auth, cleanup happens after API callback succeeds
1076+
if ( ! $this->dash_auth_enabled ) {
1077+
$this->cleanup_old_backups();
1078+
}
1079+
}
1080+
}
1081+
1082+
/**
1083+
* Delete old backups from remote storage after successful upload.
1084+
* Keeps only the configured number of most recent backups.
1085+
*/
1086+
private function cleanup_old_backups() {
1087+
$no_of_backups = intval( get_config_value( 'no-of-backups', 7 ) );
1088+
1089+
// Get fresh list of backups after the new upload
1090+
$backups = $this->list_remote_backups( true );
1091+
1092+
if ( empty( $backups ) ) {
1093+
return;
1094+
}
1095+
1096+
// Check if we have more backups than allowed
1097+
if ( count( $backups ) > ( $no_of_backups + 1 ) ) {
1098+
$backups_to_delete = array_slice( $backups, $no_of_backups );
1099+
1100+
EE::log( sprintf( 'Cleaning up old backups. Keeping %d most recent backups.', $no_of_backups ) );
1101+
foreach ( $backups_to_delete as $backup ) {
1102+
EE::log( 'Deleting old backup: ' . $backup );
1103+
$result = EE::launch( sprintf( 'rclone purge %s/%s', escapeshellarg( $this->get_rclone_config_path() ), escapeshellarg( $backup ) ) );
1104+
if ( $result->return_code ) {
1105+
EE::warning( 'Failed to delete old backup: ' . $backup );
1106+
} else {
1107+
EE::debug( 'Successfully deleted old backup: ' . $backup );
1108+
}
1109+
}
1110+
EE::success( sprintf( 'Cleaned up %d old backup(s).', count( $backups_to_delete ) ) );
1111+
} else {
1112+
EE::debug( sprintf( 'No cleanup needed. Current backups: %d, Maximum allowed: %d', count( $backups ), $no_of_backups ) );
1113+
}
1114+
}
1115+
1116+
/**
1117+
* Rollback (delete) the newly uploaded backup when EasyDash API callback fails.
1118+
* This prevents orphaned backups in remote storage that aren't tracked by EasyDash.
1119+
*/
1120+
private function rollback_failed_backup() {
1121+
if ( empty( $this->dash_new_backup_path ) ) {
1122+
EE::warning( 'Cannot rollback backup: backup path not found.' );
1123+
return;
1124+
}
1125+
1126+
EE::warning( 'EasyDash API callback failed. Rolling back newly uploaded backup...' );
1127+
EE::log( 'Deleting unregistered backup: ' . $this->dash_new_backup_path );
1128+
1129+
$result = EE::launch( sprintf( 'rclone purge %s', escapeshellarg( $this->dash_new_backup_path ) ) );
1130+
1131+
if ( $result->return_code ) {
1132+
EE::warning( sprintf(
1133+
'Failed to delete backup from remote storage. Please manually delete: %s',
1134+
$this->dash_new_backup_path
1135+
) );
1136+
} else {
1137+
EE::success( 'Successfully removed unregistered backup from remote storage.' );
10691138
}
10701139
}
10711140

@@ -1119,6 +1188,7 @@ private function restore_php_conf( $backup_dir ) {
11191188
* @param string $backup_id The backup ID.
11201189
* @param string $verify_token The verification token.
11211190
* @param array $backup_metadata The backup metadata.
1191+
* @return bool True if API request succeeded, false otherwise.
11221192
*/
11231193
private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_token, $backup_metadata ) {
11241194
$endpoint = rtrim( $ed_api_url, '/' ) . '/easydash.easydash.doctype.site_backup.site_backup.on_ee_backup_success';
@@ -1150,7 +1220,7 @@ private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_to
11501220

11511221
EE::debug( 'Payload being sent: ' . json_encode( $payload ) );
11521222

1153-
$this->send_dash_request( $endpoint, $payload );
1223+
return $this->send_dash_request( $endpoint, $payload );
11541224
}
11551225

11561226
/**
@@ -1173,10 +1243,11 @@ private function send_dash_failure_callback( $ed_api_url, $backup_id, $verify_to
11731243
}
11741244

11751245
/**
1176-
* Send HTTP request to EasyEngine Dashboard API with retry logic for 5xx errors.
1246+
* Send HTTP request to EasyEngine Dashboard API with retry logic for 5xx errors and connection errors.
11771247
*
11781248
* @param string $endpoint The API endpoint URL.
11791249
* @param array $payload The request payload.
1250+
* @return bool True if request succeeded, false otherwise.
11801251
*/
11811252
private function send_dash_request( $endpoint, $payload ) {
11821253
$max_retries = 3;
@@ -1208,28 +1279,47 @@ private function send_dash_request( $endpoint, $payload ) {
12081279
if ( ! $error && $http_code >= 200 && $http_code < 300 ) {
12091280
EE::log( 'EasyEngine Dashboard callback sent successfully.' );
12101281
EE::debug( 'EasyEngine Dashboard response: ' . $response_text );
1211-
return; // Success, exit the retry loop
1282+
return true; // Success
12121283
}
12131284

1214-
// Check if it's a 5xx error (server error) that should be retried
1285+
// Determine if this is a retryable error
12151286
$is_5xx_error = $http_code >= 500 && $http_code < 600;
1287+
$is_connection_error = ! empty( $error ) || $http_code === 0;
1288+
$should_retry = ( $is_5xx_error || $is_connection_error ) && $attempt < $max_attempts;
12161289

1217-
if ( $is_5xx_error && $attempt < $max_attempts ) {
1218-
EE::warning( sprintf(
1219-
'EasyEngine Dashboard callback failed with HTTP %d (attempt %d/%d). Retrying in %d seconds...',
1220-
$http_code,
1221-
$attempt,
1222-
$max_attempts,
1223-
$retry_delay
1224-
) );
1225-
EE::debug( 'Response: ' . $response_text );
1290+
if ( $should_retry ) {
1291+
// Retry on 5xx errors or connection errors
1292+
if ( $is_5xx_error ) {
1293+
EE::warning( sprintf(
1294+
'EasyEngine Dashboard callback failed with HTTP %d (attempt %d/%d). Retrying in %d seconds...',
1295+
$http_code,
1296+
$attempt,
1297+
$max_attempts,
1298+
$retry_delay
1299+
) );
1300+
EE::debug( 'Response: ' . $response_text );
1301+
} else {
1302+
// Connection error
1303+
$error_message = ! empty( $error ) ? $error : 'No HTTP response received';
1304+
EE::warning( sprintf(
1305+
'EasyEngine Dashboard connection error: %s (attempt %d/%d). Retrying in %d seconds...',
1306+
$error_message,
1307+
$attempt,
1308+
$max_attempts,
1309+
$retry_delay
1310+
) );
1311+
}
12261312
sleep( $retry_delay );
12271313
$attempt++; // Increment at end of loop iteration
12281314
} else {
1229-
// Either not a 5xx error, or we've exhausted all retries
1315+
// Either not a retryable error, or we've exhausted all retries
12301316
if ( $error ) {
1231-
// cURL error occurred (network, DNS, timeout, etc.)
1232-
EE::warning( 'Failed to send callback to EasyEngine Dashboard: ' . $error );
1317+
// cURL error occurred after all retries (network, DNS, timeout, etc.)
1318+
EE::warning( sprintf(
1319+
'Failed to send callback to EasyEngine Dashboard after %d retries: %s',
1320+
$max_retries,
1321+
$error
1322+
) );
12331323
} elseif ( $is_5xx_error ) {
12341324
// 5xx error after all retries exhausted
12351325
EE::warning( sprintf(
@@ -1239,15 +1329,21 @@ private function send_dash_request( $endpoint, $payload ) {
12391329
$response_text
12401330
) );
12411331
} elseif ( $http_code === 0 ) {
1242-
// No HTTP response received (may indicate network/cURL issue without explicit error)
1243-
EE::warning( 'EasyEngine Dashboard callback failed: No HTTP response received. This may indicate a network or cURL error. Response: ' . $response_text );
1332+
// No HTTP response received after all retries
1333+
EE::warning( sprintf(
1334+
'EasyEngine Dashboard callback failed after %d retries: No HTTP response received. Response: %s',
1335+
$max_retries,
1336+
$response_text
1337+
) );
12441338
} else {
12451339
// 4xx or other HTTP error codes that shouldn't be retried
12461340
EE::warning( 'EasyEngine Dashboard callback returned HTTP ' . $http_code . '. Response: ' . $response_text );
12471341
}
1248-
break; // Exit the retry loop
1342+
return false; // Failure
12491343
}
12501344
}
1345+
1346+
return false; // Should never reach here, but return false as fallback
12511347
}
12521348

12531349
/**

0 commit comments

Comments
 (0)