1111
1212class Site_Backup_Restore {
1313
14+ // Error type constants for categorized error reporting
15+ const ERROR_TYPE_VALIDATION = 'validation_error ' ; // Invalid input
16+ const ERROR_TYPE_CONFIG = 'configuration_error ' ; // Missing config
17+ const ERROR_TYPE_FILESYSTEM = 'filesystem_error ' ; // File/dir issues
18+ const ERROR_TYPE_NETWORK = 'network_error ' ; // Upload/download
19+ const ERROR_TYPE_DATABASE = 'database_error ' ; // DB operations
20+ const ERROR_TYPE_DISK_SPACE = 'disk_space_error ' ; // Insufficient space
21+ const ERROR_TYPE_LOCK = 'lock_error ' ; // Concurrent operation
22+ const ERROR_TYPE_FATAL = 'fatal_error ' ; // PHP fatal
23+ const ERROR_TYPE_INTERRUPTED = 'interrupted ' ; // Killed/stopped
24+ const ERROR_TYPE_UNKNOWN = 'unknown_error ' ; // Unexpected
25+
1426 private $ fs ;
1527 public $ site_data ;
1628 private $ rclone_config_path ;
@@ -24,6 +36,11 @@ class Site_Backup_Restore {
2436 private $ dash_backup_completed = false ;
2537 private $ dash_new_backup_path ; // Track new backup path for potential rollback
2638
39+ // Error tracking for EasyDash failure callbacks
40+ private $ dash_error_message = '' ;
41+ private $ dash_error_type = 'unknown ' ;
42+ private $ dash_error_code = 0 ;
43+
2744 public function __construct () {
2845 $ this ->fs = new Filesystem ();
2946 }
@@ -48,11 +65,16 @@ public function backup( $args, $assoc_args = [] ) {
4865 // Debug: Log the raw dash_auth value received
4966 EE ::debug ( 'Received --dash-auth value: ' . $ dash_auth );
5067
51- // Parse backup-id:backup-verification-token format
52- $ auth_parts = explode ( ': ' , $ dash_auth , 2 );
53- if ( count ( $ auth_parts ) !== 2 || empty ( $ auth_parts [0 ] ) || empty ( $ auth_parts [1 ] ) ) {
54- EE ::error ( 'Invalid --dash-auth format. Expected: backup-id:backup-verification-token ' );
55- }
68+ // Parse backup-id:backup-verification-token format
69+ $ auth_parts = explode ( ': ' , $ dash_auth , 2 );
70+ if ( count ( $ auth_parts ) !== 2 || empty ( $ auth_parts [0 ] ) || empty ( $ auth_parts [1 ] ) ) {
71+ $ this ->capture_error (
72+ 'Invalid --dash-auth format. Expected: backup-id:backup-verification-token ' ,
73+ self ::ERROR_TYPE_VALIDATION ,
74+ 1001
75+ );
76+ EE ::error ( 'Invalid --dash-auth format. Expected: backup-id:backup-verification-token ' );
77+ }
5678
5779 // Check for ed-api-url configuration
5880 $ ed_api_url = get_config_value ( 'ed-api-url ' , '' );
@@ -92,6 +114,11 @@ public function backup( $args, $assoc_args = [] ) {
92114 $ this ->backup_php_wp ( $ backup_dir );
93115 break ;
94116 default :
117+ $ this ->capture_error (
118+ sprintf ( 'Backup is not supported for site type: %s ' , $ this ->site_data ['site_type ' ] ),
119+ self ::ERROR_TYPE_VALIDATION ,
120+ 1003
121+ );
95122 EE ::error ( 'Backup is not supported for this site type. ' );
96123 }
97124
@@ -125,10 +152,40 @@ public function backup( $args, $assoc_args = [] ) {
125152 /**
126153 * Shutdown handler to send failure callback to EasyDash if backup didn't complete.
127154 * This is called when script terminates (including via EE::error which calls exit).
155+ *
156+ * Automatically captures fatal errors and interrupted processes if no error was
157+ * explicitly captured during backup execution.
128158 */
129159 public function dash_shutdown_handler () {
130160 // Only send failure callback if dash auth was enabled and backup didn't complete
131161 if ( $ this ->dash_auth_enabled && ! $ this ->dash_backup_completed ) {
162+
163+ // If no error was captured yet, try to capture shutdown error
164+ if ( empty ( $ this ->dash_error_message ) ) {
165+ $ last_error = error_get_last ();
166+
167+ // Check if this was a fatal PHP error
168+ if ( $ last_error && in_array ( $ last_error ['type ' ], [ E_ERROR , E_PARSE , E_CORE_ERROR , E_COMPILE_ERROR , E_USER_ERROR ] ) ) {
169+ $ this ->capture_error (
170+ sprintf (
171+ 'PHP Fatal Error: %s in %s:%d ' ,
172+ $ last_error ['message ' ],
173+ basename ( $ last_error ['file ' ] ),
174+ $ last_error ['line ' ]
175+ ),
176+ self ::ERROR_TYPE_FATAL ,
177+ $ last_error ['type ' ]
178+ );
179+ } else {
180+ // Script was killed, interrupted, or failed unexpectedly
181+ $ this ->capture_error (
182+ 'Backup process was interrupted or killed unexpectedly ' ,
183+ self ::ERROR_TYPE_INTERRUPTED ,
184+ 0
185+ );
186+ }
187+ }
188+
132189 $ this ->send_dash_failure_callback (
133190 $ this ->dash_api_url ,
134191 $ this ->dash_backup_id ,
@@ -137,6 +194,30 @@ public function dash_shutdown_handler() {
137194 }
138195 }
139196
197+ /**
198+ * Capture error details for EasyDash failure callback.
199+ * Stores error information to be sent when backup fails.
200+ *
201+ * @param string $message Error message describing what went wrong.
202+ * @param string $type Error type category (use ERROR_TYPE_* constants).
203+ * @param int $code Error code for additional context (optional).
204+ */
205+ private function capture_error ( $ message , $ type = self ::ERROR_TYPE_UNKNOWN , $ code = 0 ) {
206+ // Only capture the first error (root cause)
207+ if ( empty ( $ this ->dash_error_message ) ) {
208+ $ this ->dash_error_message = $ message ;
209+ $ this ->dash_error_type = $ type ;
210+ $ this ->dash_error_code = $ code ;
211+
212+ EE ::debug ( sprintf (
213+ 'Captured error for EasyDash: [%s] %s (code: %d) ' ,
214+ $ type ,
215+ $ message ,
216+ $ code
217+ ) );
218+ }
219+ }
220+
140221 public function restore ( $ args , $ assoc_args = [] ) {
141222
142223 delem_log ( 'site restore start ' );
@@ -659,6 +740,11 @@ private function pre_backup_restore_checks() {
659740 $ return_code = EE ::exec ( $ command );
660741
661742 if ( ! $ return_code ) {
743+ $ this ->capture_error (
744+ 'rclone is not installed ' ,
745+ self ::ERROR_TYPE_CONFIG ,
746+ 2001
747+ );
662748 EE ::error ( 'rclone is not installed. Please install rclone for backup/restore: https://rclone.org/downloads/#script-download-and-install ' );
663749 }
664750
@@ -669,6 +755,11 @@ private function pre_backup_restore_checks() {
669755 $ rclone_path = explode ( ': ' , $ rclone_path )[0 ] . ': ' ;
670756
671757 if ( strpos ( $ output ->stdout , $ rclone_path ) === false ) {
758+ $ this ->capture_error (
759+ 'rclone backend easyengine does not exist ' ,
760+ self ::ERROR_TYPE_CONFIG ,
761+ 2002
762+ );
672763 EE ::error ( 'rclone backend easyengine does not exist. Please create it using `rclone config` ' );
673764 }
674765
@@ -686,6 +777,11 @@ private function pre_backup_restore_checks() {
686777 $ lock_file = EE_BACKUP_DIR . '/ ' . $ this ->site_data ['site_url ' ] . '.lock ' ;
687778
688779 if ( $ this ->fs ->exists ( $ lock_file ) ) {
780+ $ this ->capture_error (
781+ 'Another backup/restore process is already running for this site ' ,
782+ self ::ERROR_TYPE_LOCK ,
783+ 2003
784+ );
689785 EE ::error ( 'Another backup/restore process is running. Please wait for it to complete. ' );
690786 } else {
691787 $ this ->fs ->dumpFile ( $ lock_file , 'lock ' );
@@ -711,6 +807,16 @@ private function pre_backup_check() {
711807 if ( $ site_size > $ free_space ) {
712808 $ error_message = $ this ->build_disk_space_error_message ( 'backup ' , $ site_size , $ free_space );
713809
810+ $ this ->capture_error (
811+ sprintf (
812+ 'Insufficient disk space for backup. Required: %s, Available: %s ' ,
813+ $ this ->format_bytes ( $ site_size ),
814+ $ this ->format_bytes ( $ free_space )
815+ ),
816+ self ::ERROR_TYPE_DISK_SPACE ,
817+ 3001
818+ );
819+
714820 $ this ->fs ->remove ( EE_BACKUP_DIR . '/ ' . $ this ->site_data ['site_url ' ] . '.lock ' );
715821 EE ::error ( $ error_message );
716822 }
@@ -751,13 +857,23 @@ private function check_and_install( $command, $name ) {
751857 $ status = EE ::exec ( "command -v $ command " );
752858 if ( ! $ status ) {
753859 if ( IS_DARWIN ) {
860+ $ this ->capture_error (
861+ sprintf ( '%s is not installed (required for backup/restore) ' , $ name ),
862+ self ::ERROR_TYPE_CONFIG ,
863+ 2010
864+ );
754865 EE ::error ( "$ name is not installed. Please install $ name for backup/restore. You can install it using `brew install $ name`. " );
755866 } else {
756867 $ status = EE ::exec ( 'apt-get --version ' );
757868 if ( $ status ) {
758869 EE ::exec ( 'apt-get update ' );
759870 EE ::exec ( "apt-get install -y $ name " );
760871 } else {
872+ $ this ->capture_error (
873+ sprintf ( '%s is not installed and could not be auto-installed (apt-get not available) ' , $ name ),
874+ self ::ERROR_TYPE_CONFIG ,
875+ 2011
876+ );
761877 EE ::error ( "$ name is not installed. Please install $ name for backup/restore. " );
762878 }
763879 }
@@ -1058,6 +1174,11 @@ private function rclone_upload( $path ) {
10581174 $ output = EE ::launch ( $ command );
10591175
10601176 if ( $ output ->return_code ) {
1177+ $ this ->capture_error (
1178+ 'Failed to upload backup to remote storage via rclone ' ,
1179+ self ::ERROR_TYPE_NETWORK ,
1180+ 4001
1181+ );
10611182 EE ::error ( 'Error uploading backup to remote storage. ' );
10621183 } else {
10631184
@@ -1225,6 +1346,7 @@ private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_to
12251346
12261347 /**
12271348 * Send failure callback to EasyDash API after failed backup.
1349+ * Includes error details (message, type, code) for debugging and user feedback.
12281350 *
12291351 * @param string $ed_api_url The EasyDash API URL.
12301352 * @param string $backup_id The backup ID.
@@ -1234,11 +1356,20 @@ private function send_dash_failure_callback( $ed_api_url, $backup_id, $verify_to
12341356 $ endpoint = rtrim ( $ ed_api_url , '/ ' ) . '/easydash.easydash.doctype.site_backup.site_backup.on_ee_backup_failure ' ;
12351357
12361358 $ payload = [
1237- 'site ' => $ this ->site_data ['site_url ' ],
1238- 'backup ' => $ backup_id ,
1239- 'verify ' => $ verify_token ,
1359+ 'site ' => $ this ->site_data ['site_url ' ],
1360+ 'backup ' => $ backup_id ,
1361+ 'verify ' => $ verify_token ,
1362+ 'error_message ' => $ this ->dash_error_message ?: 'Unknown error occurred ' ,
1363+ 'error_type ' => $ this ->dash_error_type ,
1364+ 'error_code ' => $ this ->dash_error_code ,
12401365 ];
12411366
1367+ EE ::debug ( 'Sending failure callback with error details: ' . json_encode ( [
1368+ 'error_message ' => $ payload ['error_message ' ],
1369+ 'error_type ' => $ payload ['error_type ' ],
1370+ 'error_code ' => $ payload ['error_code ' ],
1371+ ] ) );
1372+
12421373 $ this ->send_dash_request ( $ endpoint , $ payload );
12431374 }
12441375
0 commit comments