The local index tracks which chunks and blobs already exist at the backup destination. Nothing was recording *which* destination, so changing storage_url and running a backup left the scanner treating every already-seen chunk as still-present at the new (empty) location. Uploads were skipped silently and the resulting snapshots pointed at blobs that don't exist at the new destination. Fix: record storage_url in a new local_meta key-value table on first mutating command, and refuse to proceed when the configured URL later differs from the stored one. The error explains the two recovery paths (revert the config, or run 'vaultik database purge' to discard the index and rebuild from a fresh full backup). Wired into snapshot create / prune / snapshot remove / snapshot purge / snapshot cleanup. Read-only inspection commands (snapshot list, remote info, store info) are exempt.
118 lines
3.4 KiB
Go
118 lines
3.4 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
// Repositories provides access to all database repositories.
|
|
// It serves as a centralized access point for all database operations
|
|
// and manages transaction coordination across repositories.
|
|
type Repositories struct {
|
|
db *DB
|
|
Files *FileRepository
|
|
Chunks *ChunkRepository
|
|
Blobs *BlobRepository
|
|
FileChunks *FileChunkRepository
|
|
BlobChunks *BlobChunkRepository
|
|
ChunkFiles *ChunkFileRepository
|
|
Snapshots *SnapshotRepository
|
|
Uploads *UploadRepository
|
|
LocalMeta *LocalMetaRepository
|
|
}
|
|
|
|
// NewRepositories creates a new Repositories instance with all repository types.
|
|
// Each repository shares the same database connection for coordinated transactions.
|
|
func NewRepositories(db *DB) *Repositories {
|
|
return &Repositories{
|
|
db: db,
|
|
Files: NewFileRepository(db),
|
|
Chunks: NewChunkRepository(db),
|
|
Blobs: NewBlobRepository(db),
|
|
FileChunks: NewFileChunkRepository(db),
|
|
BlobChunks: NewBlobChunkRepository(db),
|
|
ChunkFiles: NewChunkFileRepository(db),
|
|
Snapshots: NewSnapshotRepository(db),
|
|
Uploads: NewUploadRepository(db.conn),
|
|
LocalMeta: NewLocalMetaRepository(db),
|
|
}
|
|
}
|
|
|
|
// TxFunc is a function that executes within a database transaction.
|
|
// The transaction is automatically committed if the function returns nil,
|
|
// or rolled back if it returns an error.
|
|
type TxFunc func(ctx context.Context, tx *sql.Tx) error
|
|
|
|
// WithTx executes a function within a write transaction.
|
|
// SQLite handles its own locking internally, so no explicit locking is needed.
|
|
// The transaction is automatically committed on success or rolled back on error.
|
|
// This method should be used for all write operations to ensure atomicity.
|
|
func (r *Repositories) WithTx(ctx context.Context, fn TxFunc) error {
|
|
LogSQL("WithTx", "Beginning transaction", "")
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("beginning transaction: %w", err)
|
|
}
|
|
LogSQL("WithTx", "Transaction started", "")
|
|
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
|
Fatal("failed to rollback transaction: %v", rollbackErr)
|
|
}
|
|
panic(p)
|
|
} else if err != nil {
|
|
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
|
Fatal("failed to rollback transaction: %v", rollbackErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = fn(ctx, tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// DB returns the underlying database for direct queries
|
|
func (r *Repositories) DB() *DB {
|
|
return r.db
|
|
}
|
|
|
|
// WithReadTx executes a function within a read-only transaction.
|
|
// Read transactions can run concurrently with other read transactions
|
|
// but will be blocked by write transactions. The transaction is
|
|
// automatically committed on success or rolled back on error.
|
|
func (r *Repositories) WithReadTx(ctx context.Context, fn TxFunc) error {
|
|
opts := &sql.TxOptions{
|
|
ReadOnly: true,
|
|
}
|
|
tx, err := r.db.BeginTx(ctx, opts)
|
|
if err != nil {
|
|
return fmt.Errorf("beginning read transaction: %w", err)
|
|
}
|
|
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
|
Fatal("failed to rollback transaction: %v", rollbackErr)
|
|
}
|
|
panic(p)
|
|
} else if err != nil {
|
|
if rollbackErr := tx.Rollback(); rollbackErr != nil {
|
|
Fatal("failed to rollback transaction: %v", rollbackErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
err = fn(ctx, tx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|