bsdaily/internal/bsdaily/run.go
2026-02-09 00:42:06 -08:00

122 lines
3.7 KiB
Go

package bsdaily
import (
"fmt"
"log/slog"
"os"
"path/filepath"
)
func Run() error {
snapshotDir, snapshotDate, err := FindLatestDailySnapshot()
if err != nil {
return fmt.Errorf("finding latest snapshot: %w", err)
}
slog.Info("found latest daily snapshot", "dir", snapshotDir, "snapshot_date", snapshotDate.Format("2006-01-02"))
targetDay := snapshotDate.AddDate(0, 0, -1)
slog.Info("target day for extraction", "date", targetDay.Format("2006-01-02"))
// Check if output already exists
outputDir := filepath.Join(DailiesBase, targetDay.Format("2006-01"))
outputFinal := filepath.Join(outputDir, targetDay.Format("2006-01-02")+".sql.zst")
if _, err := os.Stat(outputFinal); err == nil {
return fmt.Errorf("output file already exists: %s", outputFinal)
}
// Check disk space
if err := CheckFreeSpace(TmpBase, MinTmpFreeBytes, "tmpBase"); err != nil {
return err
}
if err := CheckFreeSpace(DailiesBase, MinDailiesFreeBytes, "dailiesBase"); err != nil {
return err
}
// Create temp directory
tmpDir, err := os.MkdirTemp(TmpBase, "bsarchivesegment-*")
if err != nil {
return fmt.Errorf("creating temp directory in %s: %w", TmpBase, err)
}
slog.Info("created temp directory", "path", tmpDir)
defer func() {
slog.Info("cleaning up temp directory", "path", tmpDir)
if err := os.RemoveAll(tmpDir); err != nil {
slog.Error("failed to remove temp directory", "path", tmpDir, "error", err)
}
}()
// Copy database files from snapshot to temp
srcDB := filepath.Join(snapshotDir, DBFilename)
srcWAL := filepath.Join(snapshotDir, WALFilename)
srcSHM := filepath.Join(snapshotDir, SHMFilename)
dstDB := filepath.Join(tmpDir, DBFilename)
dstWAL := filepath.Join(tmpDir, WALFilename)
dstSHM := filepath.Join(tmpDir, SHMFilename)
for _, f := range []string{srcDB, srcWAL} {
info, err := os.Stat(f)
if err != nil {
return fmt.Errorf("source file missing: %s: %w", f, err)
}
if info.Size() == 0 {
return fmt.Errorf("source file is empty: %s", f)
}
slog.Info("source file", "path", f, "size_bytes", info.Size())
}
if err := CopyFile(srcDB, dstDB); err != nil {
return fmt.Errorf("copying database: %w", err)
}
if err := CopyFile(srcWAL, dstWAL); err != nil {
return fmt.Errorf("copying WAL: %w", err)
}
if _, err := os.Stat(srcSHM); err == nil {
if err := CopyFile(srcSHM, dstSHM); err != nil {
return fmt.Errorf("copying SHM: %w", err)
}
}
if err := CheckFreeSpace(TmpBase, PostCopyTmpMinFree, "tmpBase (post-copy)"); err != nil {
return err
}
// Prune database to target day only
slog.Info("opening database for pruning", "path", dstDB)
if err := PruneDatabase(dstDB, targetDay); err != nil {
return fmt.Errorf("pruning database: %w", err)
}
// Dump to SQL and compress
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("creating output directory %s: %w", outputDir, err)
}
outputTmp := filepath.Join(outputDir, "."+targetDay.Format("2006-01-02")+".sql.zst.tmp")
slog.Info("dumping and compressing", "tmp_output", outputTmp)
if err := DumpAndCompress(dstDB, outputTmp); err != nil {
os.Remove(outputTmp)
return fmt.Errorf("dump and compress: %w", err)
}
slog.Info("verifying compressed output")
if err := VerifyOutput(outputTmp); err != nil {
os.Remove(outputTmp)
return fmt.Errorf("verification failed: %w", err)
}
// Atomic rename to final path
slog.Info("renaming to final output", "from", outputTmp, "to", outputFinal)
if err := os.Rename(outputTmp, outputFinal); err != nil {
return fmt.Errorf("atomic rename: %w", err)
}
info, err := os.Stat(outputFinal)
if err != nil {
return fmt.Errorf("stat final output: %w", err)
}
slog.Info("final output written", "path", outputFinal, "size_bytes", info.Size())
return nil
}