package updater import ( "fmt" "path/filepath" ) func recoverLayout(layout updateLayout, syncer DirectorySyncer) error { record, exists, err := loadTransaction(layout) if err != nil || !exists { return err } layout.requestID = record.RequestID layout.staging = filepath.Join(layout.root, stagingDirectoryName, record.RequestID) layout.backup = filepath.Join(layout.root, backupsDirectoryName, record.RequestID) switch record.Phase { case phasePrepared: return removeTransaction(layout, syncer) case phaseTargetBackedUp: if err := restoreBackup(layout, syncer); err != nil { return err } return removeTransaction(layout, syncer) case phaseStagingActivated, phaseLaunched: if healthMatches(layout, record.RequestID) { if err := removeManagedTree(layout.backup, layout, syncer); err != nil { return fmt.Errorf("finalize recovered self-update: %w", err) } return removeTransaction(layout, syncer) } return rollbackLayout(layout, syncer) case phaseCommitted: if err := removeManagedTree(layout.backup, layout, syncer); err != nil { return fmt.Errorf("finalize committed self-update: %w", err) } return removeTransaction(layout, syncer) default: return ErrTransactionCorrupt } } func rollbackLayout(layout updateLayout, syncer DirectorySyncer) error { backupExists, err := realDirectoryState(layout.backup, "backup") if err != nil || !backupExists { return fmt.Errorf("%w: old app backup is unavailable", ErrRecoveryRequired) } targetExists, err := realDirectoryState(layout.target, "target") if err != nil { return err } if targetExists { stagingExists, err := realDirectoryState(layout.staging, "staging") if err != nil { return err } if stagingExists { return fmt.Errorf("%w: staged and active app both exist", ErrRecoveryRequired) } if err := renameDirectory(layout.target, layout.staging, layout, syncer, "preserve failed staged app"); err != nil { return fmt.Errorf("%w: %v", ErrRecoveryRequired, err) } } if err := renameDirectory(layout.backup, layout.target, layout, syncer, "restore previous app"); err != nil { return fmt.Errorf("%w: %v", ErrRecoveryRequired, err) } if err := removeTransaction(layout, syncer); err != nil { return err } if err := removeHealth(layout, syncer); err != nil { return err } return nil } func restoreBackup(layout updateLayout, syncer DirectorySyncer) error { targetExists, err := realDirectoryState(layout.target, "target") if err != nil { return err } if targetExists { return fmt.Errorf("%w: target exists before backup restoration", ErrRecoveryRequired) } if exists, err := realDirectoryState(layout.backup, "backup"); err != nil || !exists { if err != nil { return err } return fmt.Errorf("%w: backup is unavailable", ErrRecoveryRequired) } if err := renameDirectory(layout.backup, layout.target, layout, syncer, "restore previous app"); err != nil { return fmt.Errorf("%w: %v", ErrRecoveryRequired, err) } return nil } func healthMatches(layout updateLayout, requestID string) bool { record, err := readHealth(layout.health) return err == nil && record.RequestID == requestID } func removeHealth(layout updateLayout, syncer DirectorySyncer) error { return removeRegularFile(layout.health, layout.root, syncer) }