@@ -276,13 +276,15 @@ class ScriptExecutionHandler {
276276 * @param {WebSocketMessage } message
277277 */
278278 async handleMessage ( ws , message ) {
279- const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, containerId } = message ;
279+ const { action, scriptPath, executionId, input, mode, server, isUpdate, isShell, isBackup , containerId, storage , backupStorage } = message ;
280280
281281 switch ( action ) {
282282 case 'start' :
283283 if ( scriptPath && executionId ) {
284- if ( isUpdate && containerId ) {
285- await this . startUpdateExecution ( ws , containerId , executionId , mode , server ) ;
284+ if ( isBackup && containerId && storage ) {
285+ await this . startBackupExecution ( ws , containerId , executionId , storage , mode , server ) ;
286+ } else if ( isUpdate && containerId ) {
287+ await this . startUpdateExecution ( ws , containerId , executionId , mode , server , backupStorage ) ;
286288 } else if ( isShell && containerId ) {
287289 await this . startShellExecution ( ws , containerId , executionId , mode , server ) ;
288290 } else {
@@ -660,18 +662,220 @@ class ScriptExecutionHandler {
660662 }
661663 }
662664
665+ /**
666+ * Start backup execution
667+ * @param {ExtendedWebSocket } ws
668+ * @param {string } containerId
669+ * @param {string } executionId
670+ * @param {string } storage
671+ * @param {string } mode
672+ * @param {ServerInfo|null } server
673+ */
674+ async startBackupExecution ( ws , containerId , executionId , storage , mode = 'local' , server = null ) {
675+ try {
676+ // Send start message
677+ this . sendMessage ( ws , {
678+ type : 'start' ,
679+ data : `Starting backup for container ${ containerId } to storage ${ storage } ...` ,
680+ timestamp : Date . now ( )
681+ } ) ;
682+
683+ if ( mode === 'ssh' && server ) {
684+ await this . startSSHBackupExecution ( ws , containerId , executionId , storage , server ) ;
685+ } else {
686+ this . sendMessage ( ws , {
687+ type : 'error' ,
688+ data : 'Backup is only supported via SSH' ,
689+ timestamp : Date . now ( )
690+ } ) ;
691+ }
692+ } catch ( error ) {
693+ this . sendMessage ( ws , {
694+ type : 'error' ,
695+ data : `Failed to start backup: ${ error instanceof Error ? error . message : String ( error ) } ` ,
696+ timestamp : Date . now ( )
697+ } ) ;
698+ }
699+ }
700+
701+ /**
702+ * Start SSH backup execution
703+ * @param {ExtendedWebSocket } ws
704+ * @param {string } containerId
705+ * @param {string } executionId
706+ * @param {string } storage
707+ * @param {ServerInfo } server
708+ * @param {Function } [onComplete] - Optional callback when backup completes
709+ */
710+ startSSHBackupExecution ( ws , containerId , executionId , storage , server , onComplete = null ) {
711+ const sshService = getSSHExecutionService ( ) ;
712+
713+ return new Promise ( ( resolve , reject ) => {
714+ try {
715+ const backupCommand = `vzdump ${ containerId } --storage ${ storage } --mode snapshot` ;
716+
717+ // Wrap the onExit callback to resolve our promise
718+ let promiseResolved = false ;
719+
720+ sshService . executeCommand (
721+ server ,
722+ backupCommand ,
723+ /** @param {string } data */
724+ ( data ) => {
725+ this . sendMessage ( ws , {
726+ type : 'output' ,
727+ data : data ,
728+ timestamp : Date . now ( )
729+ } ) ;
730+ } ,
731+ /** @param {string } error */
732+ ( error ) => {
733+ this . sendMessage ( ws , {
734+ type : 'error' ,
735+ data : error ,
736+ timestamp : Date . now ( )
737+ } ) ;
738+ } ,
739+ /** @param {number } code */
740+ ( code ) => {
741+ // Don't send 'end' message here if this is part of a backup+update flow
742+ // The update flow will handle completion messages
743+ const success = code === 0 ;
744+
745+ if ( ! success ) {
746+ this . sendMessage ( ws , {
747+ type : 'error' ,
748+ data : `Backup failed with exit code: ${ code } ` ,
749+ timestamp : Date . now ( )
750+ } ) ;
751+ }
752+
753+ // Send a completion message (but not 'end' type to avoid stopping terminal)
754+ this . sendMessage ( ws , {
755+ type : 'output' ,
756+ data : `\n[Backup ${ success ? 'completed' : 'failed' } with exit code: ${ code } ]\n` ,
757+ timestamp : Date . now ( )
758+ } ) ;
759+
760+ if ( onComplete ) onComplete ( success ) ;
761+
762+ // Resolve the promise when backup completes
763+ // Use setImmediate to ensure resolution happens in the right execution context
764+ if ( ! promiseResolved ) {
765+ promiseResolved = true ;
766+ const result = { success, code } ;
767+
768+ // Use setImmediate to ensure promise resolution happens in the next tick
769+ // This ensures the await in startUpdateExecution can properly resume
770+ setImmediate ( ( ) => {
771+ try {
772+ resolve ( result ) ;
773+ } catch ( resolveError ) {
774+ console . error ( 'Error resolving backup promise:' , resolveError ) ;
775+ reject ( resolveError ) ;
776+ }
777+ } ) ;
778+ }
779+
780+ this . activeExecutions . delete ( executionId ) ;
781+ }
782+ ) . then ( ( execution ) => {
783+ // Store the execution
784+ this . activeExecutions . set ( executionId , {
785+ process : /** @type {any } */ ( execution ) . process ,
786+ ws
787+ } ) ;
788+ // Note: Don't resolve here - wait for onExit callback
789+ } ) . catch ( ( error ) => {
790+ console . error ( 'Error starting backup execution:' , error ) ;
791+ this . sendMessage ( ws , {
792+ type : 'error' ,
793+ data : `SSH backup execution failed: ${ error instanceof Error ? error . message : String ( error ) } ` ,
794+ timestamp : Date . now ( )
795+ } ) ;
796+ if ( onComplete ) onComplete ( false ) ;
797+ if ( ! promiseResolved ) {
798+ promiseResolved = true ;
799+ reject ( error ) ;
800+ }
801+ } ) ;
802+
803+ } catch ( error ) {
804+ console . error ( 'Error in startSSHBackupExecution:' , error ) ;
805+ this . sendMessage ( ws , {
806+ type : 'error' ,
807+ data : `SSH backup execution failed: ${ error instanceof Error ? error . message : String ( error ) } ` ,
808+ timestamp : Date . now ( )
809+ } ) ;
810+ if ( onComplete ) onComplete ( false ) ;
811+ reject ( error ) ;
812+ }
813+ } ) ;
814+ }
815+
663816 /**
664817 * Start update execution (pct enter + update command)
665818 * @param {ExtendedWebSocket } ws
666819 * @param {string } containerId
667820 * @param {string } executionId
668821 * @param {string } mode
669822 * @param {ServerInfo|null } server
823+ * @param {string } [backupStorage] - Optional storage to backup to before update
670824 */
671- async startUpdateExecution ( ws , containerId , executionId , mode = 'local' , server = null ) {
825+ async startUpdateExecution ( ws , containerId , executionId , mode = 'local' , server = null , backupStorage = null ) {
672826 try {
827+ // If backup storage is provided, run backup first
828+ if ( backupStorage && mode === 'ssh' && server ) {
829+ this . sendMessage ( ws , {
830+ type : 'start' ,
831+ data : `Starting backup before update for container ${ containerId } ...` ,
832+ timestamp : Date . now ( )
833+ } ) ;
834+
835+ // Create a separate execution ID for backup
836+ const backupExecutionId = `backup_${ executionId } ` ;
837+
838+ // Run backup and wait for it to complete
839+ try {
840+ const backupResult = await this . startSSHBackupExecution (
841+ ws ,
842+ containerId ,
843+ backupExecutionId ,
844+ backupStorage ,
845+ server
846+ ) ;
847+
848+ // Backup completed (successfully or not)
849+ if ( ! backupResult || ! backupResult . success ) {
850+ // Backup failed, but we'll still allow update (per requirement 1b)
851+ this . sendMessage ( ws , {
852+ type : 'output' ,
853+ data : '\n⚠️ Backup failed, but proceeding with update as requested...\n' ,
854+ timestamp : Date . now ( )
855+ } ) ;
856+ } else {
857+ // Backup succeeded
858+ this . sendMessage ( ws , {
859+ type : 'output' ,
860+ data : '\n✅ Backup completed successfully. Starting update...\n' ,
861+ timestamp : Date . now ( )
862+ } ) ;
863+ }
864+ } catch ( error ) {
865+ console . error ( 'Backup error before update:' , error ) ;
866+ // Backup failed to start, but allow update to proceed
867+ this . sendMessage ( ws , {
868+ type : 'output' ,
869+ data : `\n⚠️ Backup error: ${ error instanceof Error ? error . message : String ( error ) } . Proceeding with update...\n` ,
870+ timestamp : Date . now ( )
871+ } ) ;
872+ }
873+
874+ // Small delay before starting update
875+ await new Promise ( resolve => setTimeout ( resolve , 1000 ) ) ;
876+ }
673877
674- // Send start message
878+ // Send start message for update (only if we're actually starting an update)
675879 this . sendMessage ( ws , {
676880 type : 'start' ,
677881 data : `Starting update for container ${ containerId } ...` ,
0 commit comments