42 lines
1.3 KiB
Go
42 lines
1.3 KiB
Go
|
|
package storage
|
||
|
|
|
||
|
|
import (
|
||
|
|
"database/sql"
|
||
|
|
"fmt"
|
||
|
|
"strings"
|
||
|
|
)
|
||
|
|
|
||
|
|
// SnapshotTo writes a consistent point-in-time copy of the database to destPath
|
||
|
|
// using SQLite's VACUUM INTO (never touching/locking the live file). When
|
||
|
|
// includeBlobs is false, the image_data blobs are stripped from the copy,
|
||
|
|
// leaving only metadata — which is ~99% smaller.
|
||
|
|
//
|
||
|
|
// destPath must not already exist (VACUUM INTO requires this).
|
||
|
|
func (s *Store) SnapshotTo(destPath string, includeBlobs bool) error {
|
||
|
|
if _, err := s.db.Exec(`VACUUM INTO '` + escapeSQLiteString(destPath) + `'`); err != nil {
|
||
|
|
return fmt.Errorf("vacuum into snapshot: %w", err)
|
||
|
|
}
|
||
|
|
if includeBlobs {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// Strip blobs from the copy only. image_data is NOT NULL, so use an empty blob.
|
||
|
|
snap, err := sql.Open("sqlite", destPath)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("open snapshot: %w", err)
|
||
|
|
}
|
||
|
|
defer snap.Close()
|
||
|
|
if _, err := snap.Exec(`UPDATE receipts SET image_data = X''`); err != nil {
|
||
|
|
return fmt.Errorf("strip blobs: %w", err)
|
||
|
|
}
|
||
|
|
if _, err := snap.Exec(`VACUUM`); err != nil {
|
||
|
|
return fmt.Errorf("vacuum stripped snapshot: %w", err)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// escapeSQLiteString escapes a string for use inside single quotes in SQL.
|
||
|
|
func escapeSQLiteString(s string) string {
|
||
|
|
return strings.ReplaceAll(s, "'", "''")
|
||
|
|
}
|