package app import ( "context" "fmt" "os" "github.com/owen/vibedns/internal/auditlog" "github.com/owen/vibedns/internal/backup" ) // BackupStatus returns the backup configuration and inventory. func (a *App) BackupStatus() backup.Status { return a.Backups.Status() } // BackupList lists the available backup files, newest first. func (a *App) BackupList() ([]backup.Info, error) { list, err := a.Backups.List() if err != nil { return nil, Internal(err, "The backup directory could not be read.") } return list, nil } // RunBackup creates a backup immediately. func (a *App) RunBackup(ctx context.Context, actor auditlog.Actor) (backup.Info, error) { info, err := a.Backups.Run(ctx) if err != nil { return info, Invalid("Database backup failed: %s", err.Error()) } a.Audit.Record(ctx, actor, "backup.create", auditlog.ObjectBackup, info.Name, info.Name, auditlog.Changes("bytes", fmt.Sprint(info.SizeBytes))) return info, nil } // OpenBackup opens a backup file for download. func (a *App) OpenBackup(name string) (*os.File, backup.Info, error) { path, err := backup.Resolve(a.Backups.Directory(), name) if err != nil { return nil, backup.Info{}, NotFound("%s", err.Error()) } f, err := os.Open(path) if err != nil { return nil, backup.Info{}, Internal(err, "The backup could not be opened.") } fi, err := f.Stat() if err != nil { f.Close() return nil, backup.Info{}, Internal(err, "The backup could not be read.") } return f, backup.Info{Name: name, Path: path, SizeBytes: fi.Size(), CreatedAt: fi.ModTime()}, nil } // DeleteBackup removes a backup file. func (a *App) DeleteBackup(ctx context.Context, actor auditlog.Actor, name string) error { if err := backup.Delete(a.Backups.Directory(), name); err != nil { return NotFound("%s", err.Error()) } a.Audit.Record(ctx, actor, "backup.delete", auditlog.ObjectBackup, name, name, "") return nil } // StageRestore validates a backup and schedules it to replace the live // database on the next start. // // The swap is deliberately deferred: overwriting the database file while // connections are open would leave the running process reading a file that no // longer exists. The operator restarts, and the restore is applied cleanly // before anything opens the database. func (a *App) StageRestore(ctx context.Context, actor auditlog.Actor, name string) error { path, err := backup.Resolve(a.Backups.Directory(), name) if err != nil { return NotFound("%s", err.Error()) } if err := backup.StageRestore(a.DB.Path(), path); err != nil { return Invalid("%s", err.Error()) } a.Audit.Record(ctx, actor, "backup.restore_staged", auditlog.ObjectBackup, name, name, "applies on the next restart") a.Log.Warn("database restore staged; it will be applied on the next start", "backup", name) return nil } // PendingRestore reports whether a restore is waiting for a restart. func (a *App) PendingRestore() bool { _, pending := backup.PendingRestore(a.DB.Path()) return pending } // CancelRestore discards a staged restore. func (a *App) CancelRestore(ctx context.Context, actor auditlog.Actor) error { if err := backup.CancelRestore(a.DB.Path()); err != nil { return Internal(err, "The staged restore could not be cancelled.") } a.Audit.Record(ctx, actor, "backup.restore_cancelled", auditlog.ObjectBackup, "", "", "") return nil }