Files
vibedns/internal/app/backups.go
T
2026-08-16 21:18:45 -05:00

97 lines
3.3 KiB
Go

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
}