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.
253 lines
5.8 KiB
Go
253 lines
5.8 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
)
|
|
|
|
// BlobRepository provides access to the blobs table, which tracks the
|
|
// packed, encrypted storage units uploaded to the destination.
|
|
type BlobRepository struct {
|
|
db *DB
|
|
}
|
|
|
|
// NewBlobRepository creates a BlobRepository backed by db.
|
|
func NewBlobRepository(db *DB) *BlobRepository {
|
|
return &BlobRepository{db: db}
|
|
}
|
|
|
|
// Create inserts a blob row, using tx when non-nil.
|
|
func (r *BlobRepository) Create(ctx context.Context, tx *sql.Tx, blob *Blob) error {
|
|
query := `
|
|
INSERT INTO blobs (id, blob_hash, created_ts, finished_ts,
|
|
uncompressed_size, compressed_size, uploaded_ts)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`
|
|
|
|
var finishedTS, uploadedTS *int64
|
|
|
|
if blob.FinishedTS != nil {
|
|
ts := blob.FinishedTS.Unix()
|
|
finishedTS = &ts
|
|
}
|
|
|
|
if blob.UploadedTS != nil {
|
|
ts := blob.UploadedTS.Unix()
|
|
uploadedTS = &ts
|
|
}
|
|
|
|
var err error
|
|
if tx != nil {
|
|
_, err = tx.ExecContext(ctx, query, blob.ID, blob.Hash, blob.CreatedTS.Unix(),
|
|
finishedTS, blob.UncompressedSize, blob.CompressedSize, uploadedTS)
|
|
} else {
|
|
_, err = r.db.ExecWithLog(ctx, query, blob.ID, blob.Hash, blob.CreatedTS.Unix(),
|
|
finishedTS, blob.UncompressedSize, blob.CompressedSize, uploadedTS)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("inserting blob: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetByHash returns the blob with the given content hash, or nil if no
|
|
// such blob exists.
|
|
func (r *BlobRepository) GetByHash(ctx context.Context, hash string) (*Blob, error) {
|
|
return r.getOne(ctx, "blob_hash", hash)
|
|
}
|
|
|
|
// GetByID retrieves a blob by its ID
|
|
func (r *BlobRepository) GetByID(ctx context.Context, id string) (*Blob, error) {
|
|
return r.getOne(ctx, "id", id)
|
|
}
|
|
|
|
// GetAll returns every blob row keyed by blob ID. Useful at restore
|
|
// start to translate the per-chunk blob_id references in chunkToBlobMap
|
|
// into blob hashes without doing one GetByID query per chunk.
|
|
func (r *BlobRepository) GetAll(ctx context.Context) (map[string]*Blob, error) {
|
|
query := `
|
|
SELECT id, blob_hash, created_ts, finished_ts,
|
|
uncompressed_size, compressed_size, uploaded_ts
|
|
FROM blobs
|
|
`
|
|
|
|
rows, err := r.db.conn.QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying blobs: %w", err)
|
|
}
|
|
defer CloseRows(rows)
|
|
|
|
out := make(map[string]*Blob)
|
|
|
|
for rows.Next() {
|
|
var (
|
|
blob Blob
|
|
createdTSUnix int64
|
|
finishedTSUnix, uploadedTSUnix sql.NullInt64
|
|
)
|
|
|
|
err := rows.Scan(
|
|
&blob.ID,
|
|
&blob.Hash,
|
|
&createdTSUnix,
|
|
&finishedTSUnix,
|
|
&blob.UncompressedSize,
|
|
&blob.CompressedSize,
|
|
&uploadedTSUnix,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scanning blob: %w", err)
|
|
}
|
|
|
|
blob.CreatedTS = time.Unix(createdTSUnix, 0).UTC()
|
|
if finishedTSUnix.Valid {
|
|
ts := time.Unix(finishedTSUnix.Int64, 0).UTC()
|
|
blob.FinishedTS = &ts
|
|
}
|
|
|
|
if uploadedTSUnix.Valid {
|
|
ts := time.Unix(uploadedTSUnix.Int64, 0).UTC()
|
|
blob.UploadedTS = &ts
|
|
}
|
|
|
|
out[blob.ID.String()] = &blob
|
|
}
|
|
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// UpdateFinished updates a blob when it's finalized
|
|
func (r *BlobRepository) UpdateFinished(
|
|
ctx context.Context,
|
|
tx *sql.Tx,
|
|
id string,
|
|
hash string,
|
|
uncompressedSize, compressedSize int64,
|
|
) error {
|
|
query := `
|
|
UPDATE blobs
|
|
SET blob_hash = ?, finished_ts = ?, uncompressed_size = ?, compressed_size = ?
|
|
WHERE id = ?
|
|
`
|
|
|
|
now := time.Now().UTC().Unix()
|
|
|
|
var err error
|
|
if tx != nil {
|
|
_, err = tx.ExecContext(ctx, query, hash, now, uncompressedSize, compressedSize, id)
|
|
} else {
|
|
_, err = r.db.ExecWithLog(ctx, query, hash, now, uncompressedSize, compressedSize, id)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("updating blob: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateUploaded marks a blob as uploaded
|
|
func (r *BlobRepository) UpdateUploaded(
|
|
ctx context.Context, tx *sql.Tx, id string,
|
|
) error {
|
|
query := `
|
|
UPDATE blobs
|
|
SET uploaded_ts = ?
|
|
WHERE id = ?
|
|
`
|
|
|
|
now := time.Now().UTC().Unix()
|
|
|
|
var err error
|
|
if tx != nil {
|
|
_, err = tx.ExecContext(ctx, query, now, id)
|
|
} else {
|
|
_, err = r.db.ExecWithLog(ctx, query, now, id)
|
|
}
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("marking blob as uploaded: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DeleteOrphaned deletes blobs that are not referenced by any snapshot
|
|
func (r *BlobRepository) DeleteOrphaned(ctx context.Context) error {
|
|
query := `
|
|
DELETE FROM blobs
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM snapshot_blobs
|
|
WHERE snapshot_blobs.blob_id = blobs.id
|
|
)
|
|
`
|
|
|
|
result, err := r.db.ExecWithLog(ctx, query)
|
|
if err != nil {
|
|
return fmt.Errorf("deleting orphaned blobs: %w", err)
|
|
}
|
|
|
|
rowsAffected, _ := result.RowsAffected()
|
|
if rowsAffected > 0 {
|
|
log.Debug("Deleted orphaned blobs", "count", rowsAffected)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getOne fetches a single blob row matched on the given column, or
|
|
// (nil, nil) when no row matches.
|
|
func (r *BlobRepository) getOne(
|
|
ctx context.Context, column, value string,
|
|
) (*Blob, error) {
|
|
query := `
|
|
SELECT id, blob_hash, created_ts, finished_ts,
|
|
uncompressed_size, compressed_size, uploaded_ts
|
|
FROM blobs
|
|
WHERE ` + column + ` = ?`
|
|
|
|
var (
|
|
blob Blob
|
|
createdTSUnix int64
|
|
finishedTSUnix, uploadedTSUnix sql.NullInt64
|
|
)
|
|
|
|
err := r.db.conn.QueryRowContext(ctx, query, value).Scan(
|
|
&blob.ID,
|
|
&blob.Hash,
|
|
&createdTSUnix,
|
|
&finishedTSUnix,
|
|
&blob.UncompressedSize,
|
|
&blob.CompressedSize,
|
|
&uploadedTSUnix,
|
|
)
|
|
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil //nolint:nilnil // nil,nil signals not-found; callers check nil
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("querying blob: %w", err)
|
|
}
|
|
|
|
blob.CreatedTS = time.Unix(createdTSUnix, 0).UTC()
|
|
if finishedTSUnix.Valid {
|
|
ts := time.Unix(finishedTSUnix.Int64, 0).UTC()
|
|
blob.FinishedTS = &ts
|
|
}
|
|
|
|
if uploadedTSUnix.Valid {
|
|
ts := time.Unix(uploadedTSUnix.Int64, 0).UTC()
|
|
blob.UploadedTS = &ts
|
|
}
|
|
|
|
return &blob, nil
|
|
}
|