Skip to content

Commit 321dc30

Browse files
committed
feat(backup): implement conditional cleanup with EasyDash API integration
Implement intelligent backup cleanup that only deletes old backups when EasyDash API successfully registers the new backup. If API callback fails, rollback the newly uploaded backup to prevent orphaned backups in remote storage. Key changes: - Add conditional cleanup based on API callback success/failure - Implement rollback mechanism to delete unregistered backups - Extend retry logic to handle connection errors (DNS, timeouts, etc.) - Add proper return values to API callback methods - Remove dead code from error handling paths - Optimize backup path tracking to only when dash-auth is enabled Retry behavior: - Retries 5xx server errors (500-599) - Retries connection errors (connection refused, DNS failures, timeouts) - Retries HTTP 0 (no response received) - Does NOT retry 4xx client errors - Max 3 retries with 5-minute delays between attempts Error handling improvements: - Changed rollback deletion failure from error to warning for consistency - Clear error messages distinguish between retry attempts and final failures - Proper "after N retries" messaging for all retryable errors
1 parent 5cb93b7 commit 321dc30

File tree

1 file changed

+92
-23
lines changed

1 file changed

+92
-23
lines changed

src/helper/Site_Backup_Restore.php

Lines changed: 92 additions & 23 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' );
@@ -1057,8 +1066,16 @@ private function rclone_upload( $path ) {
10571066
$remote_path = $output->stdout;
10581067
EE::success( 'Backup uploaded to remote storage. Remote path: ' . $remote_path );
10591068

1060-
// Delete old backups AFTER successful upload
1061-
$this->cleanup_old_backups();
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+
}
10621079
}
10631080
}
10641081

@@ -1077,7 +1094,7 @@ private function cleanup_old_backups() {
10771094
}
10781095

10791096
// Check if we have more backups than allowed
1080-
if ( count( $backups ) > $no_of_backups ) {
1097+
if ( count( $backups ) > ( $no_of_backups + 1 ) ) {
10811098
$backups_to_delete = array_slice( $backups, $no_of_backups );
10821099

10831100
EE::log( sprintf( 'Cleaning up old backups. Keeping %d most recent backups.', $no_of_backups ) );
@@ -1099,6 +1116,31 @@ private function cleanup_old_backups() {
10991116
}
11001117
}
11011118

1119+
/**
1120+
* Rollback (delete) the newly uploaded backup when EasyDash API callback fails.
1121+
* This prevents orphaned backups in remote storage that aren't tracked by EasyDash.
1122+
*/
1123+
private function rollback_failed_backup() {
1124+
if ( empty( $this->dash_new_backup_path ) ) {
1125+
EE::warning( 'Cannot rollback backup: backup path not found.' );
1126+
return;
1127+
}
1128+
1129+
EE::warning( 'EasyDash API callback failed. Rolling back newly uploaded backup...' );
1130+
EE::log( 'Deleting unregistered backup: ' . $this->dash_new_backup_path );
1131+
1132+
$result = EE::launch( sprintf( 'rclone purge %s', escapeshellarg( $this->dash_new_backup_path ) ) );
1133+
1134+
if ( $result->return_code ) {
1135+
EE::warning( sprintf(
1136+
'Failed to delete backup from remote storage. Please manually delete: %s',
1137+
$this->dash_new_backup_path
1138+
) );
1139+
} else {
1140+
EE::success( 'Successfully removed unregistered backup from remote storage.' );
1141+
}
1142+
}
1143+
11021144
private function restore_nginx_conf( $backup_dir ) {
11031145
$backup_file = $backup_dir . '/conf.zip';
11041146

@@ -1149,6 +1191,7 @@ private function restore_php_conf( $backup_dir ) {
11491191
* @param string $backup_id The backup ID.
11501192
* @param string $verify_token The verification token.
11511193
* @param array $backup_metadata The backup metadata.
1194+
* @return bool True if API request succeeded, false otherwise.
11521195
*/
11531196
private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_token, $backup_metadata ) {
11541197
$endpoint = rtrim( $ed_api_url, '/' ) . '/easydash.easydash.doctype.site_backup.site_backup.on_ee_backup_success';
@@ -1180,7 +1223,7 @@ private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_to
11801223

11811224
EE::debug( 'Payload being sent: ' . json_encode( $payload ) );
11821225

1183-
$this->send_dash_request( $endpoint, $payload );
1226+
return $this->send_dash_request( $endpoint, $payload );
11841227
}
11851228

11861229
/**
@@ -1203,10 +1246,11 @@ private function send_dash_failure_callback( $ed_api_url, $backup_id, $verify_to
12031246
}
12041247

12051248
/**
1206-
* Send HTTP request to EasyEngine Dashboard API with retry logic for 5xx errors.
1249+
* Send HTTP request to EasyEngine Dashboard API with retry logic for 5xx errors and connection errors.
12071250
*
12081251
* @param string $endpoint The API endpoint URL.
12091252
* @param array $payload The request payload.
1253+
* @return bool True if request succeeded, false otherwise.
12101254
*/
12111255
private function send_dash_request( $endpoint, $payload ) {
12121256
$max_retries = 3;
@@ -1238,28 +1282,47 @@ private function send_dash_request( $endpoint, $payload ) {
12381282
if ( ! $error && $http_code >= 200 && $http_code < 300 ) {
12391283
EE::log( 'EasyEngine Dashboard callback sent successfully.' );
12401284
EE::debug( 'EasyEngine Dashboard response: ' . $response_text );
1241-
return; // Success, exit the retry loop
1285+
return true; // Success
12421286
}
12431287

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

1247-
if ( $is_5xx_error && $attempt < $max_attempts ) {
1248-
EE::warning( sprintf(
1249-
'EasyEngine Dashboard callback failed with HTTP %d (attempt %d/%d). Retrying in %d seconds...',
1250-
$http_code,
1251-
$attempt,
1252-
$max_attempts,
1253-
$retry_delay
1254-
) );
1255-
EE::debug( 'Response: ' . $response_text );
1293+
if ( $should_retry ) {
1294+
// Retry on 5xx errors or connection errors
1295+
if ( $is_5xx_error ) {
1296+
EE::warning( sprintf(
1297+
'EasyEngine Dashboard callback failed with HTTP %d (attempt %d/%d). Retrying in %d seconds...',
1298+
$http_code,
1299+
$attempt,
1300+
$max_attempts,
1301+
$retry_delay
1302+
) );
1303+
EE::debug( 'Response: ' . $response_text );
1304+
} else {
1305+
// Connection error
1306+
$error_message = ! empty( $error ) ? $error : 'No HTTP response received';
1307+
EE::warning( sprintf(
1308+
'EasyEngine Dashboard connection error: %s (attempt %d/%d). Retrying in %d seconds...',
1309+
$error_message,
1310+
$attempt,
1311+
$max_attempts,
1312+
$retry_delay
1313+
) );
1314+
}
12561315
sleep( $retry_delay );
12571316
$attempt++; // Increment at end of loop iteration
12581317
} else {
1259-
// Either not a 5xx error, or we've exhausted all retries
1318+
// Either not a retryable error, or we've exhausted all retries
12601319
if ( $error ) {
1261-
// cURL error occurred (network, DNS, timeout, etc.)
1262-
EE::warning( 'Failed to send callback to EasyEngine Dashboard: ' . $error );
1320+
// cURL error occurred after all retries (network, DNS, timeout, etc.)
1321+
EE::warning( sprintf(
1322+
'Failed to send callback to EasyEngine Dashboard after %d retries: %s',
1323+
$max_retries,
1324+
$error
1325+
) );
12631326
} elseif ( $is_5xx_error ) {
12641327
// 5xx error after all retries exhausted
12651328
EE::warning( sprintf(
@@ -1269,15 +1332,21 @@ private function send_dash_request( $endpoint, $payload ) {
12691332
$response_text
12701333
) );
12711334
} elseif ( $http_code === 0 ) {
1272-
// No HTTP response received (may indicate network/cURL issue without explicit error)
1273-
EE::warning( 'EasyEngine Dashboard callback failed: No HTTP response received. This may indicate a network or cURL error. Response: ' . $response_text );
1335+
// No HTTP response received after all retries
1336+
EE::warning( sprintf(
1337+
'EasyEngine Dashboard callback failed after %d retries: No HTTP response received. Response: %s',
1338+
$max_retries,
1339+
$response_text
1340+
) );
12741341
} else {
12751342
// 4xx or other HTTP error codes that shouldn't be retried
12761343
EE::warning( 'EasyEngine Dashboard callback returned HTTP ' . $http_code . '. Response: ' . $response_text );
12771344
}
1278-
break; // Exit the retry loop
1345+
return false; // Failure
12791346
}
12801347
}
1348+
1349+
return false; // Should never reach here, but return false as fallback
12811350
}
12821351

12831352
/**

0 commit comments

Comments
 (0)
SYSTEM_READY >> ...MS