up
This commit is contained in:
+33
-18
@@ -36,13 +36,14 @@ func DefaultFolder(dataDir string) string {
|
||||
return filepath.Join(dataDir, "backups")
|
||||
}
|
||||
|
||||
// Run executes one backup pass: checkpoint WAL → copy the database file
|
||||
// → optionally zip → rotate. Returns the path of the file that was
|
||||
// written so the caller can surface it to the UI.
|
||||
// Run executes one backup pass: VACUUM INTO a fresh snapshot → optionally
|
||||
// zip → rotate. Returns the path of the file that was written so the caller
|
||||
// can surface it to the UI.
|
||||
//
|
||||
// dbConn is used to issue the WAL checkpoint so the on-disk file is
|
||||
// self-consistent before we copy it. It's the same *sql.DB the app uses;
|
||||
// SQLite tolerates concurrent reads during the copy.
|
||||
// VACUUM INTO produces a transactionally-consistent copy in a single SQL
|
||||
// statement (no torn-copy window while the app keeps writing), and compacts
|
||||
// the destination as a bonus. It replaces the old "checkpoint + raw io.Copy",
|
||||
// which could capture a half-written page during a concurrent write.
|
||||
func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation int, doZip bool) (string, error) {
|
||||
if dbConn == nil {
|
||||
return "", fmt.Errorf("nil db connection")
|
||||
@@ -56,24 +57,38 @@ func Run(ctx context.Context, dbConn *sql.DB, dbPath, folder string, rotation in
|
||||
if err := os.MkdirAll(folder, 0o755); err != nil {
|
||||
return "", fmt.Errorf("create backup folder: %w", err)
|
||||
}
|
||||
// Flush WAL into the main file so a raw copy is a complete database.
|
||||
// TRUNCATE removes the -wal file's contents after checkpointing.
|
||||
if _, err := dbConn.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE);`); err != nil {
|
||||
return "", fmt.Errorf("wal_checkpoint: %w", err)
|
||||
}
|
||||
|
||||
stamp := time.Now().Format("2006-01-02")
|
||||
base := fmt.Sprintf("opslog-%s", stamp)
|
||||
|
||||
// VACUUM INTO requires a non-existent target → use a temp file, then
|
||||
// move/zip it into place.
|
||||
tmp := filepath.Join(folder, base+".vacuum.tmp")
|
||||
_ = os.Remove(tmp)
|
||||
if _, err := dbConn.ExecContext(ctx, `VACUUM INTO ?;`, tmp); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", fmt.Errorf("vacuum into: %w", err)
|
||||
}
|
||||
|
||||
var dstPath string
|
||||
if doZip {
|
||||
dstPath = filepath.Join(folder, base+".db.zip")
|
||||
if err := copyZipped(dbPath, dstPath); err != nil {
|
||||
// Inner name = the live DB's base so unzip restores e.g. "opslog.db".
|
||||
if err := copyZipped(tmp, dstPath, filepath.Base(dbPath)); err != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", err
|
||||
}
|
||||
_ = os.Remove(tmp)
|
||||
} else {
|
||||
dstPath = filepath.Join(folder, base+".db")
|
||||
if err := copyFile(dbPath, dstPath); err != nil {
|
||||
return "", err
|
||||
_ = os.Remove(dstPath)
|
||||
if err := os.Rename(tmp, dstPath); err != nil {
|
||||
// Rename can fail across filesystems — fall back to a copy.
|
||||
if cerr := copyFile(tmp, dstPath); cerr != nil {
|
||||
_ = os.Remove(tmp)
|
||||
return "", cerr
|
||||
}
|
||||
_ = os.Remove(tmp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +120,9 @@ func copyFile(src, dst string) error {
|
||||
}
|
||||
|
||||
// copyZipped writes a single-entry deflate zip containing the database.
|
||||
// The inner filename is just the base of the source so unzip restores
|
||||
// "hamlog.db" wherever the user extracts.
|
||||
func copyZipped(src, dst string) error {
|
||||
// innerName is the entry name inside the zip (the live DB's base name) so
|
||||
// unzip restores e.g. "opslog.db" wherever the user extracts.
|
||||
func copyZipped(src, dst, innerName string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
@@ -119,7 +134,7 @@ func copyZipped(src, dst string) error {
|
||||
return fmt.Errorf("create dest: %w", err)
|
||||
}
|
||||
zw := zip.NewWriter(out)
|
||||
w, err := zw.Create(filepath.Base(src))
|
||||
w, err := zw.Create(innerName)
|
||||
if err != nil {
|
||||
zw.Close()
|
||||
out.Close()
|
||||
|
||||
Reference in New Issue
Block a user