Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 35 additions & 21 deletions internal/backups/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,37 +216,51 @@ func (s *Service) RestoreDrill(ctx context.Context) (RestoreDrillResult, error)
return RestoreDrillResult{}, fmt.Errorf("extract backup: %w", err)
}
result := RestoreDrillResult{BackupName: files[0].Name}
err = filepath.WalkDir(destination, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
filesChecked, err := validateRestoredDatabases(ctx, destination)
if err != nil {
return RestoreDrillResult{}, err
}
result.DatabaseFiles = filesChecked
result.IntegrityChecked = len(filesChecked)
if result.IntegrityChecked == 0 {
return RestoreDrillResult{}, errors.New("restored archive contains no SQLite database")
}
return result, nil
}

func validateRestoredDatabases(ctx context.Context, destination string) ([]string, error) {
entries, err := os.ReadDir(destination)
if err != nil {
return nil, err
}
files := make([]string, 0, 2)
for _, entry := range entries {
// A PocketBase restore boots only the root database files. Nested .db files
// are retained forensic/safety snapshots (for example quarantine/) and
// must not make an otherwise restorable backup fail its drill.
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".db") {
return nil
continue
}
relative, _ := filepath.Rel(destination, path)
result.DatabaseFiles = append(result.DatabaseFiles, relative)
path := filepath.Join(destination, entry.Name())
database, err := sql.Open("sqlite", "file:"+path+"?mode=ro")
if err != nil {
return fmt.Errorf("open restored database %s: %w", relative, err)
return nil, fmt.Errorf("open restored database %s: %w", entry.Name(), err)
}
defer database.Close()
var check string
if err := database.QueryRowContext(ctx, "PRAGMA integrity_check").Scan(&check); err != nil {
return fmt.Errorf("integrity check %s: %w", relative, err)
queryErr := database.QueryRowContext(ctx, "PRAGMA integrity_check").Scan(&check)
closeErr := database.Close()
if queryErr != nil {
return nil, fmt.Errorf("integrity check %s: %w", entry.Name(), queryErr)
}
if closeErr != nil {
return nil, closeErr
}
if !strings.EqualFold(strings.TrimSpace(check), "ok") {
return fmt.Errorf("integrity check %s returned %q", relative, check)
return nil, fmt.Errorf("integrity check %s returned %q", entry.Name(), check)
}
result.IntegrityChecked++
return nil
})
if err != nil {
return RestoreDrillResult{}, err
}
if result.IntegrityChecked == 0 {
return RestoreDrillResult{}, errors.New("restored archive contains no SQLite database")
files = append(files, entry.Name())
}
return result, nil
return files, nil
}

func (s *Service) download(ctx context.Context, name string) (string, func(), error) {
Expand Down
35 changes: 35 additions & 0 deletions internal/backups/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package backups

import (
"context"
"database/sql"
"os"
"path/filepath"
"testing"
"time"

"github.com/Phloraxx/payment-api/internal/config"
_ "github.com/Phloraxx/payment-api/migrations"
"github.com/pocketbase/pocketbase/tests"
_ "modernc.org/sqlite"
)

func TestConfigureCreateAndVerifyLocalBackup(t *testing.T) {
Expand Down Expand Up @@ -47,3 +51,34 @@ func TestConfigureCreateAndVerifyLocalBackup(t *testing.T) {
t.Fatalf("drill=%+v", drill)
}
}

func TestValidateRestoredDatabasesIgnoresNestedForensicSnapshots(t *testing.T) {
dir := t.TempDir()
for _, name := range []string{"data.db", "auxiliary.db"} {
db, err := sql.Open("sqlite", filepath.Join(dir, name))
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec("CREATE TABLE sample (id INTEGER PRIMARY KEY, value TEXT)"); err != nil {
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
}
quarantine := filepath.Join(dir, "quarantine", "20260811")
if err := os.MkdirAll(quarantine, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(quarantine, "auxiliary.db"), []byte("not sqlite"), 0o600); err != nil {
t.Fatal(err)
}

files, err := validateRestoredDatabases(context.Background(), dir)
if err != nil {
t.Fatal(err)
}
if len(files) != 2 {
t.Fatalf("validated files=%v", files)
}
}