Remediate all lint findings under the canonical golangci-lint config
Fix every finding surfaced by the canonical .golangci.yml with golangci-lint v2.12.2 (refs #61), behavior-preserving throughout: - err113: dynamic errors replaced with package-level sentinels and %w wrapping; direct comparisons converted to errors.Is - goprintffuncname: printf-style helpers renamed with an f suffix (ui.Writer message methods, cli.ReportErrorf, database.Fatalf, vaultik stdoutf) and all call sites updated - revive: stuttering type names renamed (blob.Handler, blob.WithReader, blob.ChunkPosition, storage.URL, storage.Info), doc comments added, unused parameters blanked, package comments added - contextcheck/noctx: ctx threaded through blob.Packer (AddChunk/Flush/FinalizeBlob/PackChunks) and scanner call sites; context-aware exec and sql variants used - funlen/cyclop/gocognit/nestif/dupl: oversized or duplicated functions split into focused helpers across production and test code - paralleltest/tparallel/thelper/usetesting/testpackage: tests parallelized where safe (global log.Initialize kept in the serial phase), helpers marked, t.TempDir adopted, external test packages where only exported API is used - gosec: integer conversions clamped or justified, header timeouts added, remaining findings suppressed with per-site justifications - mnd/goconst/lll/wsl_v5/nlreturn/noinlineerr/errcheck and other mechanical findings fixed directly Remove the deprecated log.LogOptions alias (callers migrated to log.Options). make check is green.
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
// Package snapshot implements snapshot creation: scanning source
|
||||
// directories, chunking and deduplicating file data, packing chunks into
|
||||
// encrypted blobs, and exporting per-snapshot metadata to remote storage.
|
||||
package snapshot
|
||||
|
||||
// Snapshot Metadata Export Process
|
||||
@@ -58,6 +61,8 @@ import (
|
||||
)
|
||||
|
||||
// SnapshotManager handles snapshot creation and metadata export
|
||||
//
|
||||
//nolint:revive // renaming snapshot.SnapshotManager is a cross-package API change
|
||||
type SnapshotManager struct {
|
||||
repos *database.Repositories
|
||||
storage storage.Storer
|
||||
@@ -66,6 +71,8 @@ type SnapshotManager struct {
|
||||
}
|
||||
|
||||
// SnapshotManagerParams holds dependencies for NewSnapshotManager
|
||||
//
|
||||
//nolint:revive // renaming this alongside SnapshotManager is a cross-package API change
|
||||
type SnapshotManagerParams struct {
|
||||
fx.In
|
||||
|
||||
@@ -88,15 +95,22 @@ func (sm *SnapshotManager) SetFilesystem(fs afero.Fs) {
|
||||
sm.fs = fs
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a new snapshot record in the database at the start of a backup.
|
||||
// CreateSnapshot creates a new snapshot record in the database at the
|
||||
// start of a backup.
|
||||
//
|
||||
// Deprecated: Use CreateSnapshotWithName instead for multi-snapshot support.
|
||||
func (sm *SnapshotManager) CreateSnapshot(ctx context.Context, hostname, version, gitRevision string) (string, error) {
|
||||
func (sm *SnapshotManager) CreateSnapshot(
|
||||
ctx context.Context, hostname, version, gitRevision string,
|
||||
) (string, error) {
|
||||
return sm.CreateSnapshotWithName(ctx, hostname, "", version, gitRevision)
|
||||
}
|
||||
|
||||
// CreateSnapshotWithName creates a new snapshot record with an optional snapshot name.
|
||||
// The snapshot ID format is: hostname_name_timestamp or hostname_timestamp if name is empty.
|
||||
func (sm *SnapshotManager) CreateSnapshotWithName(ctx context.Context, hostname, name, version, gitRevision string) (string, error) {
|
||||
// CreateSnapshotWithName creates a new snapshot record with an optional
|
||||
// snapshot name. The snapshot ID format is: hostname_name_timestamp or
|
||||
// hostname_timestamp if name is empty.
|
||||
func (sm *SnapshotManager) CreateSnapshotWithName(
|
||||
ctx context.Context, hostname, name, version, gitRevision string,
|
||||
) (string, error) {
|
||||
// Use short hostname (strip domain if present)
|
||||
shortHostname := hostname
|
||||
if before, _, ok := strings.Cut(hostname, "."); ok {
|
||||
@@ -141,7 +155,9 @@ func (sm *SnapshotManager) CreateSnapshotWithName(ctx context.Context, hostname,
|
||||
}
|
||||
|
||||
// UpdateSnapshotStats updates the statistics for a snapshot during backup
|
||||
func (sm *SnapshotManager) UpdateSnapshotStats(ctx context.Context, snapshotID string, stats BackupStats) error {
|
||||
func (sm *SnapshotManager) UpdateSnapshotStats(
|
||||
ctx context.Context, snapshotID string, stats BackupStats,
|
||||
) error {
|
||||
err := sm.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
return sm.repos.Snapshots.UpdateCounts(ctx, tx, snapshotID,
|
||||
int64(stats.FilesScanned),
|
||||
@@ -160,7 +176,9 @@ func (sm *SnapshotManager) UpdateSnapshotStats(ctx context.Context, snapshotID s
|
||||
|
||||
// UpdateSnapshotStatsExtended updates snapshot statistics with extended metrics.
|
||||
// This includes compression level, uncompressed blob size, and upload duration.
|
||||
func (sm *SnapshotManager) UpdateSnapshotStatsExtended(ctx context.Context, snapshotID string, stats ExtendedBackupStats) error {
|
||||
func (sm *SnapshotManager) UpdateSnapshotStatsExtended(
|
||||
ctx context.Context, snapshotID string, stats ExtendedBackupStats,
|
||||
) error {
|
||||
return sm.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
// First update basic stats
|
||||
err := sm.repos.Snapshots.UpdateCounts(ctx, tx, snapshotID,
|
||||
@@ -187,7 +205,9 @@ func (sm *SnapshotManager) UpdateSnapshotStatsExtended(ctx context.Context, snap
|
||||
// is populated with every blob holding any chunk referenced by the
|
||||
// snapshot's files (including deduplicated blobs uploaded by prior
|
||||
// snapshots). Without this, fully-deduplicated snapshots are unrestorable.
|
||||
func (sm *SnapshotManager) CompleteSnapshot(ctx context.Context, snapshotID string) error {
|
||||
func (sm *SnapshotManager) CompleteSnapshot(
|
||||
ctx context.Context, snapshotID string,
|
||||
) error {
|
||||
err := sm.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
added, err := sm.repos.Snapshots.PopulateReferencedBlobs(ctx, tx, snapshotID)
|
||||
if err != nil {
|
||||
@@ -226,8 +246,11 @@ func (sm *SnapshotManager) CompleteSnapshot(ctx context.Context, snapshotID stri
|
||||
// - Reopening the main database after this method returns
|
||||
//
|
||||
// This ensures database consistency during the copy operation.
|
||||
func (sm *SnapshotManager) ExportSnapshotMetadata(ctx context.Context, dbPath string, snapshotID string) error {
|
||||
log.Info("Phase 3/3: Exporting snapshot metadata", "snapshot_id", snapshotID, "source_db", dbPath)
|
||||
func (sm *SnapshotManager) ExportSnapshotMetadata(
|
||||
ctx context.Context, dbPath string, snapshotID string,
|
||||
) error {
|
||||
log.Info("Phase 3/3: Exporting snapshot metadata",
|
||||
"snapshot_id", snapshotID, "source_db", dbPath)
|
||||
|
||||
// Create temp directory for all temporary files
|
||||
tempDir, err := afero.TempDir(sm.fs, "", "vaultik-snapshot-*")
|
||||
@@ -258,7 +281,8 @@ func (sm *SnapshotManager) ExportSnapshotMetadata(ctx context.Context, dbPath st
|
||||
}
|
||||
|
||||
// Step 7: Upload to S3 in snapshot subdirectory
|
||||
if err := sm.uploadSnapshotArtifacts(ctx, snapshotID, finalData, blobManifest); err != nil {
|
||||
err = sm.uploadSnapshotArtifacts(ctx, snapshotID, finalData, blobManifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -270,15 +294,130 @@ func (sm *SnapshotManager) ExportSnapshotMetadata(ctx context.Context, dbPath st
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareExportDB copies, cleans, vacuums, and compresses the snapshot database for export.
|
||||
// Returns the compressed data and the path to the temporary database (needed for manifest generation).
|
||||
func (sm *SnapshotManager) prepareExportDB(ctx context.Context, dbPath, snapshotID, tempDir string) ([]byte, string, error) {
|
||||
// CleanupIncompleteSnapshots removes incomplete snapshots that don't have
|
||||
// metadata in S3. This is critical for data safety: incomplete snapshots
|
||||
// can cause deduplication to skip files that were never successfully
|
||||
// backed up, resulting in data loss.
|
||||
func (sm *SnapshotManager) CleanupIncompleteSnapshots(
|
||||
ctx context.Context, hostname string,
|
||||
) error {
|
||||
log.Info("Checking for incomplete snapshots", "hostname", hostname)
|
||||
|
||||
// Get all incomplete snapshots for this hostname
|
||||
incompleteSnapshots, err := sm.repos.Snapshots.GetIncompleteByHostname(ctx, hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting incomplete snapshots: %w", err)
|
||||
}
|
||||
|
||||
if len(incompleteSnapshots) == 0 {
|
||||
log.Debug("No incomplete snapshots found")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Found incomplete snapshots", "count", len(incompleteSnapshots))
|
||||
|
||||
// Check each incomplete snapshot for metadata in storage
|
||||
for _, snapshot := range incompleteSnapshots {
|
||||
// Check if metadata exists in storage (paths use the hashed
|
||||
// remote key so we don't leak host info to the listing).
|
||||
metadataKey := fmt.Sprintf("metadata/%s/db.zst",
|
||||
RemoteSnapshotKey(snapshot.ID.String()))
|
||||
|
||||
_, err := sm.storage.Stat(ctx, metadataKey)
|
||||
if err != nil {
|
||||
// Metadata doesn't exist in S3 - this is an incomplete snapshot
|
||||
log.Info("Cleaning up incomplete snapshot record",
|
||||
"snapshot_id", snapshot.ID, "started_at", snapshot.StartedAt)
|
||||
|
||||
// Delete the snapshot and all its associations
|
||||
err := sm.deleteSnapshot(ctx, snapshot.ID.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting incomplete snapshot %s: %w",
|
||||
snapshot.ID, err)
|
||||
}
|
||||
|
||||
log.Info("Deleted incomplete snapshot record and associated data",
|
||||
"snapshot_id", snapshot.ID)
|
||||
} else {
|
||||
// Metadata exists - this snapshot was completed but database wasn't updated
|
||||
// This shouldn't happen in normal operation, but mark it complete
|
||||
log.Warn("Found snapshot with remote metadata but incomplete in database",
|
||||
"snapshot_id", snapshot.ID)
|
||||
|
||||
err := sm.repos.Snapshots.MarkComplete(ctx, nil, snapshot.ID.String())
|
||||
if err != nil {
|
||||
log.Error("Failed to mark snapshot as complete in database",
|
||||
"snapshot_id", snapshot.ID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupOrphanedData removes files, chunks, and blobs that are no longer
|
||||
// referenced by any snapshot. This should be called periodically to clean
|
||||
// up data from deleted or incomplete snapshots.
|
||||
func (sm *SnapshotManager) CleanupOrphanedData(ctx context.Context) error {
|
||||
// Order is important to respect foreign key constraints:
|
||||
// 1. Delete orphaned files (will cascade delete file_chunks)
|
||||
// 2. Delete orphaned blobs (will cascade delete blob_chunks for deleted blobs)
|
||||
// 3. Delete orphaned blob_chunks (where blob exists but chunk doesn't)
|
||||
// 4. Delete orphaned chunks (now safe after all blob_chunks are gone)
|
||||
|
||||
// Delete orphaned files (files not in any snapshot)
|
||||
log.Debug("Deleting orphaned file records from database")
|
||||
|
||||
err := sm.repos.Files.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned files: %w", err)
|
||||
}
|
||||
|
||||
// Delete orphaned blobs (blobs not in any snapshot)
|
||||
// This will cascade delete blob_chunks for deleted blobs
|
||||
log.Debug("Deleting orphaned blob records from database")
|
||||
|
||||
err = sm.repos.Blobs.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned blobs: %w", err)
|
||||
}
|
||||
|
||||
// Delete orphaned blob_chunks entries
|
||||
// This handles cases where the blob still exists but chunks were deleted
|
||||
log.Debug("Deleting orphaned blob_chunks associations from database")
|
||||
|
||||
err = sm.repos.BlobChunks.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned blob_chunks: %w", err)
|
||||
}
|
||||
|
||||
// Delete orphaned chunks (chunks not referenced by any file)
|
||||
// This must come after cleaning up blob_chunks to avoid foreign key violations
|
||||
log.Debug("Deleting orphaned chunk records from database")
|
||||
|
||||
err = sm.repos.Chunks.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned chunks: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareExportDB copies, cleans, vacuums, and compresses the snapshot
|
||||
// database for export. Returns the compressed data and the path to the
|
||||
// temporary database (needed for manifest generation).
|
||||
func (sm *SnapshotManager) prepareExportDB(
|
||||
ctx context.Context, dbPath, snapshotID, tempDir string,
|
||||
) ([]byte, string, error) {
|
||||
// Step 1: Copy database to temp file
|
||||
// The main database should be closed at this point
|
||||
tempDBPath := filepath.Join(tempDir, "snapshot.db")
|
||||
log.Debug("Copying database to temporary location", "source", dbPath, "destination", tempDBPath)
|
||||
log.Debug("Copying database to temporary location",
|
||||
"source", dbPath, "destination", tempDBPath)
|
||||
|
||||
if err := sm.copyFile(dbPath, tempDBPath); err != nil {
|
||||
err := sm.copyFile(dbPath, tempDBPath)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("copying database: %w", err)
|
||||
}
|
||||
|
||||
@@ -294,31 +433,36 @@ func (sm *SnapshotManager) prepareExportDB(ctx context.Context, dbPath, snapshot
|
||||
|
||||
log.Info("Temporary database cleanup complete",
|
||||
"db_path", tempDBPath,
|
||||
"size_after_clean", humanize.Bytes(uint64(sm.getFileSize(tempDBPath))),
|
||||
"size_after_clean", humanize.Bytes(safeUint64(sm.getFileSize(tempDBPath))),
|
||||
"files", stats.FileCount,
|
||||
"chunks", stats.ChunkCount,
|
||||
"blobs", stats.BlobCount,
|
||||
"total_compressed_size", humanize.Bytes(uint64(stats.CompressedSize)),
|
||||
"total_uncompressed_size", humanize.Bytes(uint64(stats.UncompressedSize)),
|
||||
"compression_ratio", fmt.Sprintf("%.2fx", float64(stats.UncompressedSize)/float64(stats.CompressedSize)))
|
||||
"total_compressed_size", humanize.Bytes(safeUint64(stats.CompressedSize)),
|
||||
"total_uncompressed_size", humanize.Bytes(safeUint64(stats.UncompressedSize)),
|
||||
"compression_ratio", fmt.Sprintf("%.2fx",
|
||||
float64(stats.UncompressedSize)/float64(stats.CompressedSize)))
|
||||
|
||||
// Step 3: VACUUM the database to remove deleted data and compact
|
||||
// This is critical for security - ensures no stale/deleted data is uploaded
|
||||
if err := sm.vacuumDatabase(tempDBPath); err != nil {
|
||||
err = sm.vacuumDatabase(ctx, tempDBPath)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("vacuuming database: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("Database vacuumed", "size", humanize.Bytes(uint64(sm.getFileSize(tempDBPath))))
|
||||
log.Debug("Database vacuumed",
|
||||
"size", humanize.Bytes(safeUint64(sm.getFileSize(tempDBPath))))
|
||||
|
||||
// Step 4: Compress and encrypt the binary database file
|
||||
compressedPath := filepath.Join(tempDir, "db.zst.age")
|
||||
if err := sm.compressFile(tempDBPath, compressedPath); err != nil {
|
||||
|
||||
err = sm.compressFile(tempDBPath, compressedPath)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("compressing database: %w", err)
|
||||
}
|
||||
|
||||
log.Debug("Compression complete",
|
||||
"original_size", humanize.Bytes(uint64(sm.getFileSize(tempDBPath))),
|
||||
"compressed_size", humanize.Bytes(uint64(sm.getFileSize(compressedPath))))
|
||||
"original_size", humanize.Bytes(safeUint64(sm.getFileSize(tempDBPath))),
|
||||
"compressed_size", humanize.Bytes(safeUint64(sm.getFileSize(compressedPath))))
|
||||
|
||||
// Step 5: Read compressed and encrypted data for upload
|
||||
finalData, err := afero.ReadFile(sm.fs, compressedPath)
|
||||
@@ -335,7 +479,9 @@ func (sm *SnapshotManager) prepareExportDB(ctx context.Context, dbPath, snapshot
|
||||
// We never write the human-readable snapshot ID into any unencrypted
|
||||
// part of remote storage so a listing of the destination bucket leaks
|
||||
// no host, configuration, or scheduling information.
|
||||
func (sm *SnapshotManager) uploadSnapshotArtifacts(ctx context.Context, snapshotID string, dbData, manifestData []byte) error {
|
||||
func (sm *SnapshotManager) uploadSnapshotArtifacts(
|
||||
ctx context.Context, snapshotID string, dbData, manifestData []byte,
|
||||
) error {
|
||||
remoteKey := RemoteSnapshotKey(snapshotID)
|
||||
|
||||
// Upload database backup (compressed and encrypted)
|
||||
@@ -349,7 +495,8 @@ func (sm *SnapshotManager) uploadSnapshotArtifacts(ctx context.Context, snapshot
|
||||
}
|
||||
|
||||
dbUploadDuration := time.Since(dbUploadStart)
|
||||
dbUploadSpeed := float64(len(dbData)) * 8 / dbUploadDuration.Seconds() // bits per second
|
||||
// bits per second
|
||||
dbUploadSpeed := float64(len(dbData)) * bitsPerByte / dbUploadDuration.Seconds()
|
||||
log.Info("Uploaded snapshot database",
|
||||
"path", dbKey,
|
||||
"size", humanize.Bytes(uint64(len(dbData))),
|
||||
@@ -366,7 +513,9 @@ func (sm *SnapshotManager) uploadSnapshotArtifacts(ctx context.Context, snapshot
|
||||
}
|
||||
|
||||
manifestUploadDuration := time.Since(manifestUploadStart)
|
||||
manifestUploadSpeed := float64(len(manifestData)) * 8 / manifestUploadDuration.Seconds() // bits per second
|
||||
// bits per second
|
||||
manifestUploadSpeed := float64(len(manifestData)) * bitsPerByte /
|
||||
manifestUploadDuration.Seconds()
|
||||
log.Info("Uploaded blob manifest",
|
||||
"path", manifestKey,
|
||||
"size", humanize.Bytes(uint64(len(manifestData))),
|
||||
@@ -388,16 +537,19 @@ type CleanupStats struct {
|
||||
// cleanSnapshotDB removes all data except for the specified snapshot
|
||||
//
|
||||
// The cleanup is performed in a specific order to maintain referential integrity:
|
||||
// 1. Delete other snapshots
|
||||
// 2. Delete orphaned snapshot associations (snapshot_files, snapshot_blobs) for deleted snapshots
|
||||
// 3. Delete orphaned files (not in the current snapshot)
|
||||
// 4. Delete orphaned chunk-to-file mappings (references to deleted files)
|
||||
// 5. Delete orphaned blobs (not in the current snapshot)
|
||||
// 6. Delete orphaned blob-to-chunk mappings (references to deleted chunks)
|
||||
// 7. Delete orphaned chunks (not referenced by any file)
|
||||
// 1. Delete other snapshots
|
||||
// 2. Delete orphaned snapshot associations (snapshot_files, snapshot_blobs)
|
||||
// for deleted snapshots
|
||||
// 3. Delete orphaned files (not in the current snapshot)
|
||||
// 4. Delete orphaned chunk-to-file mappings (references to deleted files)
|
||||
// 5. Delete orphaned blobs (not in the current snapshot)
|
||||
// 6. Delete orphaned blob-to-chunk mappings (references to deleted chunks)
|
||||
// 7. Delete orphaned chunks (not referenced by any file)
|
||||
//
|
||||
// Each step is implemented as a separate method for clarity and maintainability.
|
||||
func (sm *SnapshotManager) cleanSnapshotDB(ctx context.Context, dbPath string, snapshotID string) (*CleanupStats, error) {
|
||||
func (sm *SnapshotManager) cleanSnapshotDB(
|
||||
ctx context.Context, dbPath string, snapshotID string,
|
||||
) (*CleanupStats, error) {
|
||||
// Open the temp database
|
||||
db, err := database.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
@@ -423,48 +575,54 @@ func (sm *SnapshotManager) cleanSnapshotDB(ctx context.Context, dbPath string, s
|
||||
}()
|
||||
|
||||
// Execute cleanup steps in order
|
||||
if err := sm.deleteOtherSnapshots(ctx, tx, snapshotID); err != nil {
|
||||
return nil, fmt.Errorf("step 1 - delete other snapshots: %w", err)
|
||||
steps := []struct {
|
||||
name string
|
||||
fn func() error
|
||||
}{
|
||||
{"delete other snapshots",
|
||||
func() error { return sm.deleteOtherSnapshots(ctx, tx, snapshotID) }},
|
||||
{"delete orphaned snapshot associations",
|
||||
func() error { return sm.deleteOrphanedSnapshotAssociations(ctx, tx, snapshotID) }},
|
||||
{"delete orphaned files",
|
||||
func() error { return sm.deleteOrphanedFiles(ctx, tx, snapshotID) }},
|
||||
{"delete orphaned chunk-to-file mappings",
|
||||
func() error { return sm.deleteOrphanedChunkToFileMappings(ctx, tx) }},
|
||||
{"delete orphaned blobs",
|
||||
func() error { return sm.deleteOrphanedBlobs(ctx, tx, snapshotID) }},
|
||||
{"delete orphaned blob-to-chunk mappings",
|
||||
func() error { return sm.deleteOrphanedBlobToChunkMappings(ctx, tx) }},
|
||||
{"delete orphaned chunks",
|
||||
func() error { return sm.deleteOrphanedChunks(ctx, tx) }},
|
||||
}
|
||||
|
||||
if err := sm.deleteOrphanedSnapshotAssociations(ctx, tx, snapshotID); err != nil {
|
||||
return nil, fmt.Errorf("step 2 - delete orphaned snapshot associations: %w", err)
|
||||
}
|
||||
|
||||
if err := sm.deleteOrphanedFiles(ctx, tx, snapshotID); err != nil {
|
||||
return nil, fmt.Errorf("step 3 - delete orphaned files: %w", err)
|
||||
}
|
||||
|
||||
if err := sm.deleteOrphanedChunkToFileMappings(ctx, tx); err != nil {
|
||||
return nil, fmt.Errorf("step 4 - delete orphaned chunk-to-file mappings: %w", err)
|
||||
}
|
||||
|
||||
if err := sm.deleteOrphanedBlobs(ctx, tx, snapshotID); err != nil {
|
||||
return nil, fmt.Errorf("step 5 - delete orphaned blobs: %w", err)
|
||||
}
|
||||
|
||||
if err := sm.deleteOrphanedBlobToChunkMappings(ctx, tx); err != nil {
|
||||
return nil, fmt.Errorf("step 6 - delete orphaned blob-to-chunk mappings: %w", err)
|
||||
}
|
||||
|
||||
if err := sm.deleteOrphanedChunks(ctx, tx); err != nil {
|
||||
return nil, fmt.Errorf("step 7 - delete orphaned chunks: %w", err)
|
||||
for i, step := range steps {
|
||||
err = step.fn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("step %d - %s: %w", i+1, step.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
log.Debug("[Temp DB Cleanup] Committing cleanup transaction")
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("committing transaction: %w", err)
|
||||
}
|
||||
|
||||
// Collect statistics about the cleaned database
|
||||
return sm.collectCleanupStats(ctx, db, snapshotID)
|
||||
}
|
||||
|
||||
// collectCleanupStats gathers statistics about the cleaned database.
|
||||
func (sm *SnapshotManager) collectCleanupStats(
|
||||
ctx context.Context, db *database.DB, snapshotID string,
|
||||
) (*CleanupStats, error) {
|
||||
stats := &CleanupStats{}
|
||||
|
||||
// Count files
|
||||
var fileCount int
|
||||
|
||||
err = db.QueryRowWithLog(ctx, "SELECT COUNT(*) FROM files").Scan(&fileCount)
|
||||
err := db.QueryRowWithLog(ctx, "SELECT COUNT(*) FROM files").Scan(&fileCount)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("counting files: %w", err)
|
||||
}
|
||||
@@ -488,9 +646,12 @@ func (sm *SnapshotManager) cleanSnapshotDB(ctx context.Context, dbPath string, s
|
||||
)
|
||||
|
||||
err = db.QueryRowWithLog(ctx, `
|
||||
SELECT COUNT(*), COALESCE(SUM(compressed_size), 0), COALESCE(SUM(uncompressed_size), 0)
|
||||
FROM blobs
|
||||
WHERE blob_hash IN (SELECT blob_hash FROM snapshot_blobs WHERE snapshot_id = ?)
|
||||
SELECT COUNT(*),
|
||||
COALESCE(SUM(compressed_size), 0),
|
||||
COALESCE(SUM(uncompressed_size), 0)
|
||||
FROM blobs
|
||||
WHERE blob_hash IN
|
||||
(SELECT blob_hash FROM snapshot_blobs WHERE snapshot_id = ?)
|
||||
`, snapshotID).Scan(&blobCount, &compressedSize, &uncompressedSize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("counting blobs and sizes: %w", err)
|
||||
@@ -505,11 +666,13 @@ func (sm *SnapshotManager) cleanSnapshotDB(ctx context.Context, dbPath string, s
|
||||
|
||||
// vacuumDatabase runs VACUUM on the database to remove deleted data and compact
|
||||
// This is critical for security - ensures no stale/deleted data pages are uploaded
|
||||
func (sm *SnapshotManager) vacuumDatabase(dbPath string) error {
|
||||
func (sm *SnapshotManager) vacuumDatabase(ctx context.Context, dbPath string) error {
|
||||
log.Debug("Running VACUUM on database", "path", dbPath)
|
||||
cmd := exec.Command("sqlite3", dbPath, "VACUUM;")
|
||||
//nolint:gosec // G204: fixed argv; dbPath is our own temp file path
|
||||
cmd := exec.CommandContext(ctx, "sqlite3", dbPath, "VACUUM;")
|
||||
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running VACUUM: %w (output: %s)", err, string(output))
|
||||
}
|
||||
|
||||
@@ -543,7 +706,8 @@ func (sm *SnapshotManager) compressFile(inputPath, outputPath string) error {
|
||||
// Use blobgen for compression and encryption
|
||||
log.Debug("Compressing and encrypting data")
|
||||
|
||||
writer, err := blobgen.NewWriter(output, sm.config.CompressionLevel, sm.config.AgeRecipients)
|
||||
writer, err := blobgen.NewWriter(output, sm.config.CompressionLevel,
|
||||
sm.config.AgeRecipients)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating blobgen writer: %w", err)
|
||||
}
|
||||
@@ -559,12 +723,14 @@ func (sm *SnapshotManager) compressFile(inputPath, outputPath string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(writer, input); err != nil {
|
||||
_, err = io.Copy(writer, input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compressing data: %w", err)
|
||||
}
|
||||
|
||||
// Close writer to flush all data
|
||||
if err := writer.Close(); err != nil {
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing writer: %w", err)
|
||||
}
|
||||
|
||||
@@ -620,7 +786,9 @@ func (sm *SnapshotManager) copyFile(src, dst string) error {
|
||||
}
|
||||
|
||||
// generateBlobManifest creates a compressed JSON list of all blobs in the snapshot
|
||||
func (sm *SnapshotManager) generateBlobManifest(ctx context.Context, dbPath string, snapshotID string) ([]byte, error) {
|
||||
func (sm *SnapshotManager) generateBlobManifest(
|
||||
ctx context.Context, dbPath string, snapshotID string,
|
||||
) ([]byte, error) {
|
||||
// Open the cleaned database using the database package
|
||||
db, err := database.New(ctx, dbPath)
|
||||
if err != nil {
|
||||
@@ -718,61 +886,10 @@ type ExtendedBackupStats struct {
|
||||
UploadDurationMs int64 // Total milliseconds spent uploading to S3
|
||||
}
|
||||
|
||||
// CleanupIncompleteSnapshots removes incomplete snapshots that don't have metadata in S3.
|
||||
// This is critical for data safety: incomplete snapshots can cause deduplication to skip
|
||||
// files that were never successfully backed up, resulting in data loss.
|
||||
func (sm *SnapshotManager) CleanupIncompleteSnapshots(ctx context.Context, hostname string) error {
|
||||
log.Info("Checking for incomplete snapshots", "hostname", hostname)
|
||||
|
||||
// Get all incomplete snapshots for this hostname
|
||||
incompleteSnapshots, err := sm.repos.Snapshots.GetIncompleteByHostname(ctx, hostname)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting incomplete snapshots: %w", err)
|
||||
}
|
||||
|
||||
if len(incompleteSnapshots) == 0 {
|
||||
log.Debug("No incomplete snapshots found")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Found incomplete snapshots", "count", len(incompleteSnapshots))
|
||||
|
||||
// Check each incomplete snapshot for metadata in storage
|
||||
for _, snapshot := range incompleteSnapshots {
|
||||
// Check if metadata exists in storage (paths use the hashed
|
||||
// remote key so we don't leak host info to the listing).
|
||||
metadataKey := fmt.Sprintf("metadata/%s/db.zst", RemoteSnapshotKey(snapshot.ID.String()))
|
||||
|
||||
_, err := sm.storage.Stat(ctx, metadataKey)
|
||||
if err != nil {
|
||||
// Metadata doesn't exist in S3 - this is an incomplete snapshot
|
||||
log.Info("Cleaning up incomplete snapshot record", "snapshot_id", snapshot.ID, "started_at", snapshot.StartedAt)
|
||||
|
||||
// Delete the snapshot and all its associations
|
||||
err := sm.deleteSnapshot(ctx, snapshot.ID.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting incomplete snapshot %s: %w", snapshot.ID, err)
|
||||
}
|
||||
|
||||
log.Info("Deleted incomplete snapshot record and associated data", "snapshot_id", snapshot.ID)
|
||||
} else {
|
||||
// Metadata exists - this snapshot was completed but database wasn't updated
|
||||
// This shouldn't happen in normal operation, but mark it complete
|
||||
log.Warn("Found snapshot with remote metadata but incomplete in database", "snapshot_id", snapshot.ID)
|
||||
|
||||
err := sm.repos.Snapshots.MarkComplete(ctx, nil, snapshot.ID.String())
|
||||
if err != nil {
|
||||
log.Error("Failed to mark snapshot as complete in database", "snapshot_id", snapshot.ID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteSnapshot removes a snapshot and all its associations from the database
|
||||
func (sm *SnapshotManager) deleteSnapshot(ctx context.Context, snapshotID string) error {
|
||||
func (sm *SnapshotManager) deleteSnapshot(
|
||||
ctx context.Context, snapshotID string,
|
||||
) error {
|
||||
// Delete snapshot_files entries
|
||||
err := sm.repos.Snapshots.DeleteSnapshotFiles(ctx, snapshotID)
|
||||
if err != nil {
|
||||
@@ -808,61 +925,20 @@ func (sm *SnapshotManager) deleteSnapshot(ctx context.Context, snapshotID string
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupOrphanedData removes files, chunks, and blobs that are no longer referenced by any snapshot.
|
||||
// This should be called periodically to clean up data from deleted or incomplete snapshots.
|
||||
func (sm *SnapshotManager) CleanupOrphanedData(ctx context.Context) error {
|
||||
// Order is important to respect foreign key constraints:
|
||||
// 1. Delete orphaned files (will cascade delete file_chunks)
|
||||
// 2. Delete orphaned blobs (will cascade delete blob_chunks for deleted blobs)
|
||||
// 3. Delete orphaned blob_chunks (where blob exists but chunk doesn't)
|
||||
// 4. Delete orphaned chunks (now safe after all blob_chunks are gone)
|
||||
|
||||
// Delete orphaned files (files not in any snapshot)
|
||||
log.Debug("Deleting orphaned file records from database")
|
||||
|
||||
err := sm.repos.Files.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned files: %w", err)
|
||||
}
|
||||
|
||||
// Delete orphaned blobs (blobs not in any snapshot)
|
||||
// This will cascade delete blob_chunks for deleted blobs
|
||||
log.Debug("Deleting orphaned blob records from database")
|
||||
|
||||
err = sm.repos.Blobs.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned blobs: %w", err)
|
||||
}
|
||||
|
||||
// Delete orphaned blob_chunks entries
|
||||
// This handles cases where the blob still exists but chunks were deleted
|
||||
log.Debug("Deleting orphaned blob_chunks associations from database")
|
||||
|
||||
err = sm.repos.BlobChunks.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned blob_chunks: %w", err)
|
||||
}
|
||||
|
||||
// Delete orphaned chunks (chunks not referenced by any file)
|
||||
// This must come after cleaning up blob_chunks to avoid foreign key violations
|
||||
log.Debug("Deleting orphaned chunk records from database")
|
||||
|
||||
err = sm.repos.Chunks.DeleteOrphaned(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned chunks: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteOtherSnapshots deletes all snapshots except the current one
|
||||
func (sm *SnapshotManager) deleteOtherSnapshots(ctx context.Context, tx *sql.Tx, currentSnapshotID string) error {
|
||||
log.Debug("[Temp DB Cleanup] Deleting all snapshot records except current", "keeping", currentSnapshotID)
|
||||
func (sm *SnapshotManager) deleteOtherSnapshots(
|
||||
ctx context.Context, tx *sql.Tx, currentSnapshotID string,
|
||||
) error {
|
||||
log.Debug("[Temp DB Cleanup] Deleting all snapshot records except current",
|
||||
"keeping", currentSnapshotID)
|
||||
|
||||
// First delete uploads that reference other snapshots (no CASCADE DELETE on this FK)
|
||||
database.LogSQL("Execute", "DELETE FROM uploads WHERE snapshot_id != ?", currentSnapshotID)
|
||||
// First delete uploads that reference other snapshots (no CASCADE DELETE
|
||||
// on this FK)
|
||||
database.LogSQL("Execute", "DELETE FROM uploads WHERE snapshot_id != ?",
|
||||
currentSnapshotID)
|
||||
|
||||
uploadResult, err := tx.ExecContext(ctx, "DELETE FROM uploads WHERE snapshot_id != ?", currentSnapshotID)
|
||||
uploadResult, err := tx.ExecContext(ctx,
|
||||
"DELETE FROM uploads WHERE snapshot_id != ?", currentSnapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting uploads for other snapshots: %w", err)
|
||||
}
|
||||
@@ -871,66 +947,84 @@ func (sm *SnapshotManager) deleteOtherSnapshots(ctx context.Context, tx *sql.Tx,
|
||||
log.Debug("[Temp DB Cleanup] Deleted upload records", "count", uploadsDeleted)
|
||||
|
||||
// Now we can safely delete the snapshots
|
||||
database.LogSQL("Execute", "DELETE FROM snapshots WHERE id != ?", currentSnapshotID)
|
||||
database.LogSQL("Execute", "DELETE FROM snapshots WHERE id != ?",
|
||||
currentSnapshotID)
|
||||
|
||||
result, err := tx.ExecContext(ctx, "DELETE FROM snapshots WHERE id != ?", currentSnapshotID)
|
||||
result, err := tx.ExecContext(ctx, "DELETE FROM snapshots WHERE id != ?",
|
||||
currentSnapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting other snapshots: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted snapshot records from database", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted snapshot records from database",
|
||||
"count", rowsAffected)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteOrphanedSnapshotAssociations deletes snapshot_files and snapshot_blobs for deleted snapshots
|
||||
func (sm *SnapshotManager) deleteOrphanedSnapshotAssociations(ctx context.Context, tx *sql.Tx, currentSnapshotID string) error {
|
||||
// deleteOrphanedSnapshotAssociations deletes snapshot_files and
|
||||
// snapshot_blobs for deleted snapshots
|
||||
func (sm *SnapshotManager) deleteOrphanedSnapshotAssociations(
|
||||
ctx context.Context, tx *sql.Tx, currentSnapshotID string,
|
||||
) error {
|
||||
// Delete orphaned snapshot_files
|
||||
log.Debug("[Temp DB Cleanup] Deleting orphaned snapshot_files associations")
|
||||
database.LogSQL("Execute", "DELETE FROM snapshot_files WHERE snapshot_id != ?", currentSnapshotID)
|
||||
database.LogSQL("Execute", "DELETE FROM snapshot_files WHERE snapshot_id != ?",
|
||||
currentSnapshotID)
|
||||
|
||||
result, err := tx.ExecContext(ctx, "DELETE FROM snapshot_files WHERE snapshot_id != ?", currentSnapshotID)
|
||||
result, err := tx.ExecContext(ctx,
|
||||
"DELETE FROM snapshot_files WHERE snapshot_id != ?", currentSnapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned snapshot_files: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted snapshot_files associations", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted snapshot_files associations",
|
||||
"count", rowsAffected)
|
||||
|
||||
// Delete orphaned snapshot_blobs
|
||||
log.Debug("[Temp DB Cleanup] Deleting orphaned snapshot_blobs associations")
|
||||
database.LogSQL("Execute", "DELETE FROM snapshot_blobs WHERE snapshot_id != ?", currentSnapshotID)
|
||||
database.LogSQL("Execute", "DELETE FROM snapshot_blobs WHERE snapshot_id != ?",
|
||||
currentSnapshotID)
|
||||
|
||||
result, err = tx.ExecContext(ctx, "DELETE FROM snapshot_blobs WHERE snapshot_id != ?", currentSnapshotID)
|
||||
result, err = tx.ExecContext(ctx,
|
||||
"DELETE FROM snapshot_blobs WHERE snapshot_id != ?", currentSnapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned snapshot_blobs: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, _ = result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted snapshot_blobs associations", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted snapshot_blobs associations",
|
||||
"count", rowsAffected)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteOrphanedFiles deletes files not in the current snapshot
|
||||
func (sm *SnapshotManager) deleteOrphanedFiles(ctx context.Context, tx *sql.Tx, currentSnapshotID string) error {
|
||||
log.Debug("[Temp DB Cleanup] Deleting file records not referenced by current snapshot")
|
||||
database.LogSQL("Execute", `DELETE FROM files WHERE NOT EXISTS (SELECT 1 FROM snapshot_files WHERE snapshot_files.file_id = files.id AND snapshot_files.snapshot_id = ?)`, currentSnapshotID)
|
||||
func (sm *SnapshotManager) deleteOrphanedFiles(
|
||||
ctx context.Context, tx *sql.Tx, currentSnapshotID string,
|
||||
) error {
|
||||
log.Debug(
|
||||
"[Temp DB Cleanup] Deleting file records not referenced by current snapshot")
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM files
|
||||
query := `
|
||||
DELETE FROM files
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM snapshot_files
|
||||
WHERE snapshot_files.file_id = files.id
|
||||
SELECT 1 FROM snapshot_files
|
||||
WHERE snapshot_files.file_id = files.id
|
||||
AND snapshot_files.snapshot_id = ?
|
||||
)`, currentSnapshotID)
|
||||
)`
|
||||
database.LogSQL("Execute", query, currentSnapshotID)
|
||||
|
||||
result, err := tx.ExecContext(ctx, query, currentSnapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned files: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted file records from database", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted file records from database",
|
||||
"count", rowsAffected)
|
||||
|
||||
// Note: file_chunks will be deleted via CASCADE
|
||||
log.Debug("[Temp DB Cleanup] file_chunks associations deleted via CASCADE")
|
||||
@@ -939,65 +1033,81 @@ func (sm *SnapshotManager) deleteOrphanedFiles(ctx context.Context, tx *sql.Tx,
|
||||
}
|
||||
|
||||
// deleteOrphanedChunkToFileMappings deletes chunk_files entries for deleted files
|
||||
func (sm *SnapshotManager) deleteOrphanedChunkToFileMappings(ctx context.Context, tx *sql.Tx) error {
|
||||
func (sm *SnapshotManager) deleteOrphanedChunkToFileMappings(
|
||||
ctx context.Context, tx *sql.Tx,
|
||||
) error {
|
||||
log.Debug("[Temp DB Cleanup] Deleting orphaned chunk_files associations")
|
||||
database.LogSQL("Execute", `DELETE FROM chunk_files WHERE NOT EXISTS (SELECT 1 FROM files WHERE files.id = chunk_files.file_id)`)
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM chunk_files
|
||||
query := `
|
||||
DELETE FROM chunk_files
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM files
|
||||
SELECT 1 FROM files
|
||||
WHERE files.id = chunk_files.file_id
|
||||
)`)
|
||||
)`
|
||||
database.LogSQL("Execute", query)
|
||||
|
||||
result, err := tx.ExecContext(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned chunk_files: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted chunk_files associations", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted chunk_files associations",
|
||||
"count", rowsAffected)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteOrphanedBlobs deletes blobs not in the current snapshot
|
||||
func (sm *SnapshotManager) deleteOrphanedBlobs(ctx context.Context, tx *sql.Tx, currentSnapshotID string) error {
|
||||
log.Debug("[Temp DB Cleanup] Deleting blob records not referenced by current snapshot")
|
||||
database.LogSQL("Execute", `DELETE FROM blobs WHERE NOT EXISTS (SELECT 1 FROM snapshot_blobs WHERE snapshot_blobs.blob_hash = blobs.blob_hash AND snapshot_blobs.snapshot_id = ?)`, currentSnapshotID)
|
||||
func (sm *SnapshotManager) deleteOrphanedBlobs(
|
||||
ctx context.Context, tx *sql.Tx, currentSnapshotID string,
|
||||
) error {
|
||||
log.Debug(
|
||||
"[Temp DB Cleanup] Deleting blob records not referenced by current snapshot")
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM blobs
|
||||
query := `
|
||||
DELETE FROM blobs
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM snapshot_blobs
|
||||
WHERE snapshot_blobs.blob_hash = blobs.blob_hash
|
||||
SELECT 1 FROM snapshot_blobs
|
||||
WHERE snapshot_blobs.blob_hash = blobs.blob_hash
|
||||
AND snapshot_blobs.snapshot_id = ?
|
||||
)`, currentSnapshotID)
|
||||
)`
|
||||
database.LogSQL("Execute", query, currentSnapshotID)
|
||||
|
||||
result, err := tx.ExecContext(ctx, query, currentSnapshotID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned blobs: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted blob records from database", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted blob records from database",
|
||||
"count", rowsAffected)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteOrphanedBlobToChunkMappings deletes blob_chunks entries for deleted blobs
|
||||
func (sm *SnapshotManager) deleteOrphanedBlobToChunkMappings(ctx context.Context, tx *sql.Tx) error {
|
||||
func (sm *SnapshotManager) deleteOrphanedBlobToChunkMappings(
|
||||
ctx context.Context, tx *sql.Tx,
|
||||
) error {
|
||||
log.Debug("[Temp DB Cleanup] Deleting orphaned blob_chunks associations")
|
||||
database.LogSQL("Execute", `DELETE FROM blob_chunks WHERE NOT EXISTS (SELECT 1 FROM blobs WHERE blobs.id = blob_chunks.blob_id)`)
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
DELETE FROM blob_chunks
|
||||
query := `
|
||||
DELETE FROM blob_chunks
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM blobs
|
||||
SELECT 1 FROM blobs
|
||||
WHERE blobs.id = blob_chunks.blob_id
|
||||
)`)
|
||||
)`
|
||||
database.LogSQL("Execute", query)
|
||||
|
||||
result, err := tx.ExecContext(ctx, query)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting orphaned blob_chunks: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted blob_chunks associations", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted blob_chunks associations",
|
||||
"count", rowsAffected)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1024,7 +1134,8 @@ func (sm *SnapshotManager) deleteOrphanedChunks(ctx context.Context, tx *sql.Tx)
|
||||
}
|
||||
|
||||
rowsAffected, _ := result.RowsAffected()
|
||||
log.Debug("[Temp DB Cleanup] Deleted chunk records from database", "count", rowsAffected)
|
||||
log.Debug("[Temp DB Cleanup] Deleted chunk records from database",
|
||||
"count", rowsAffected)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user