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