Files
bsdaily/internal/bsdaily/dump.go
2026-06-28 09:42:31 +02:00

84 lines
2.2 KiB
Go

package bsdaily
import (
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
)
func DumpAndCompress(dbPath, outputPath string) (err error) {
for _, tool := range []string{"sqlite3", "zstdmt"} {
if _, err := exec.LookPath(tool); err != nil {
return fmt.Errorf("required tool %q not found in PATH: %w", tool, err)
}
}
if err := CheckFreeSpace(filepath.Dir(outputPath), MinDailiesFreeBytes, "dailiesBase (pre-dump)"); err != nil {
return err
}
outFile, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("creating output file: %w", err)
}
defer func() {
if cerr := outFile.Close(); cerr != nil && err == nil {
err = fmt.Errorf("closing output: %w", cerr)
}
}()
// Dump all tables but use INSERT OR IGNORE for mergeable imports
// This preserves all data while allowing multiple dumps to be merged
// Users should import with: zstdcat *.sql.zst | sed 's/INSERT INTO/INSERT OR IGNORE INTO/g' | sqlite3 merged.db
dumpCmd := exec.Command("sqlite3", dbPath, ".dump")
zstdCmd := exec.Command("zstdmt", fmt.Sprintf("-%d", zstdCompressionLevel))
pipe, err := dumpCmd.StdoutPipe()
if err != nil {
return fmt.Errorf("creating dump stdout pipe: %w", err)
}
zstdCmd.Stdin = pipe
zstdCmd.Stdout = outFile
var dumpStderr, zstdStderr strings.Builder
dumpCmd.Stderr = &dumpStderr
zstdCmd.Stderr = &zstdStderr
slog.Info("starting sqlite3 dump and zstdmt compression")
if err := zstdCmd.Start(); err != nil {
return fmt.Errorf("starting zstdmt: %w", err)
}
if err := dumpCmd.Start(); err != nil {
return fmt.Errorf("starting sqlite3 dump: %w", err)
}
if err := dumpCmd.Wait(); err != nil {
return fmt.Errorf("sqlite3 dump failed: %w; stderr: %s", err, dumpStderr.String())
}
if err := zstdCmd.Wait(); err != nil {
return fmt.Errorf("zstdmt failed: %w; stderr: %s", err, zstdStderr.String())
}
if err := outFile.Sync(); err != nil {
return fmt.Errorf("syncing output: %w", err)
}
info, err := os.Stat(outputPath)
if err != nil {
return fmt.Errorf("stat output: %w", err)
}
const bytesPerMB = 1024 * 1024
slog.Info("compressed output written", "path", outputPath,
"size_bytes", info.Size(), "size_mb", info.Size()/bytesPerMB)
if info.Size() == 0 {
return fmt.Errorf("compressed output is empty")
}
return nil
}