@@ -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