From 047bd7f1c4e847d11cfa4911d40924b79d673d26 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 9 Aug 2026 01:39:13 +0000 Subject: [PATCH 1/4] Fix remaining wsl_v5 whitespace findings (refs #61) Insert the blank line wsl_v5 requires above `defer` and `go` statements that share no variables with the statement above them. Applied mechanically via `make lint-fix`; the diff is 60 added blank lines and nothing else. --- cmd/vaultik/main.go | 2 ++ internal/blobgen/compress.go | 1 + internal/chunker/chunker.go | 1 + internal/cli/app.go | 1 + internal/database/database_test.go | 4 ++++ internal/database/repository_debug_test.go | 1 + internal/database/uploads.go | 1 + internal/pidlock/pidlock_test.go | 3 +++ internal/s3/client_test.go | 2 ++ internal/s3/s3_test.go | 4 ++++ internal/snapshot/backup_test.go | 1 + internal/snapshot/file_change_test.go | 2 ++ internal/snapshot/progress.go | 1 + internal/snapshot/scanner.go | 1 + internal/snapshot/scanner_test.go | 2 ++ internal/snapshot/snapshot.go | 9 +++++++++ internal/snapshot/snapshot_test.go | 3 +++ internal/storage/file.go | 3 +++ internal/storage/s3.go | 1 + internal/vaultik/blobcache.go | 1 + internal/vaultik/blobcache_test.go | 5 +++++ internal/vaultik/integration_test.go | 4 ++++ internal/vaultik/restore.go | 3 +++ internal/vaultik/restore_sweeper.go | 1 + internal/vaultik/snapshot.go | 1 + internal/vaultik/verify.go | 2 ++ 26 files changed, 60 insertions(+) diff --git a/cmd/vaultik/main.go b/cmd/vaultik/main.go index c9cbacc..4cda5a9 100644 --- a/cmd/vaultik/main.go +++ b/cmd/vaultik/main.go @@ -16,6 +16,7 @@ func main() { if err != nil { panic("could not create CPU profile: " + err.Error()) } + defer func() { _ = f.Close() }() err = pprof.StartCPUProfile(f) @@ -33,6 +34,7 @@ func main() { if err != nil { panic("could not create memory profile: " + err.Error()) } + defer func() { _ = f.Close() }() runtime.GC() // get up-to-date statistics diff --git a/internal/blobgen/compress.go b/internal/blobgen/compress.go index 8eefc77..40be29c 100644 --- a/internal/blobgen/compress.go +++ b/internal/blobgen/compress.go @@ -64,6 +64,7 @@ func CompressStream( } closed := false + defer func() { if !closed { _ = w.Close() diff --git a/internal/chunker/chunker.go b/internal/chunker/chunker.go index 8fedb30..c5cee7b 100644 --- a/internal/chunker/chunker.go +++ b/internal/chunker/chunker.go @@ -163,6 +163,7 @@ func (c *Chunker) ChunkFile(path string) ([]Chunk, error) { if err != nil { return nil, fmt.Errorf("opening file: %w", err) } + defer func() { err := file.Close() if err != nil && err.Error() != "invalid argument" { diff --git a/internal/cli/app.go b/internal/cli/app.go index e45596c..492f7e4 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -157,6 +157,7 @@ func RunApp(ctx context.Context, app *fx.App) error { // Handle shutdown shutdownComplete := make(chan struct{}) + go func() { defer close(shutdownComplete) diff --git a/internal/database/database_test.go b/internal/database/database_test.go index 9e1c214..24cf416 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -19,6 +19,7 @@ func TestDatabase(t *testing.T) { if err != nil { t.Fatalf("failed to create database: %v", err) } + defer func() { err := db.Close() if err != nil { @@ -73,6 +74,7 @@ func TestDatabaseConcurrentAccess(t *testing.T) { if err != nil { t.Fatalf("failed to create database: %v", err) } + defer func() { err := db.Close() if err != nil { @@ -182,6 +184,7 @@ func TestApplyMigrations_Idempotent(t *testing.T) { if err != nil { t.Fatalf("failed to open database: %v", err) } + defer func() { err := conn.Close() if err != nil { @@ -239,6 +242,7 @@ func TestBootstrapMigrationsTable_FreshDatabase(t *testing.T) { if err != nil { t.Fatalf("failed to open database: %v", err) } + defer func() { err := conn.Close() if err != nil { diff --git a/internal/database/repository_debug_test.go b/internal/database/repository_debug_test.go index db541ed..4a4e2aa 100644 --- a/internal/database/repository_debug_test.go +++ b/internal/database/repository_debug_test.go @@ -17,6 +17,7 @@ func logSnapshotFileIDs(t *testing.T, db *DB) { if err != nil { t.Fatal(err) } + defer func() { err := rows.Close() if err != nil { diff --git a/internal/database/uploads.go b/internal/database/uploads.go index 1070550..312373e 100644 --- a/internal/database/uploads.go +++ b/internal/database/uploads.go @@ -96,6 +96,7 @@ func (r *UploadRepository) GetRecentUploads( if err != nil { return nil, err } + defer func() { err := rows.Close() if err != nil { diff --git a/internal/pidlock/pidlock_test.go b/internal/pidlock/pidlock_test.go index 3adc2e0..1f49372 100644 --- a/internal/pidlock/pidlock_test.go +++ b/internal/pidlock/pidlock_test.go @@ -48,6 +48,7 @@ func TestAcquireBlocksSecondInstance(t *testing.T) { require.NoError(t, err) require.NotNil(t, lock1) + defer func() { _ = lock1.Release() }() // Try to acquire second lock - should fail @@ -72,6 +73,7 @@ func TestAcquireWithStaleLock(t *testing.T) { require.NoError(t, err) require.NotNil(t, lock) + defer func() { _ = lock.Release() }() // Verify our PID is now in the file @@ -117,6 +119,7 @@ func TestAcquireCreatesDirectory(t *testing.T) { require.NoError(t, err) require.NotNil(t, lock) + defer func() { _ = lock.Release() }() // Verify directory was created diff --git a/internal/s3/client_test.go b/internal/s3/client_test.go index 81f986f..75ee0d8 100644 --- a/internal/s3/client_test.go +++ b/internal/s3/client_test.go @@ -12,6 +12,7 @@ import ( //nolint:paralleltest // test servers share a fixed localhost port func TestClient(t *testing.T) { ts := NewTestServer(t) + defer func() { err := ts.Cleanup() if err != nil { @@ -58,6 +59,7 @@ func verifyPutGetHead( if err != nil { t.Fatalf("failed to get object: %v", err) } + defer func() { err := reader.Close() if err != nil { diff --git a/internal/s3/s3_test.go b/internal/s3/s3_test.go index 38dc911..0ab3738 100644 --- a/internal/s3/s3_test.go +++ b/internal/s3/s3_test.go @@ -147,6 +147,7 @@ func (ts *TestServer) Client() *s3.Client { //nolint:paralleltest // test servers share a fixed localhost port func TestBasicS3Operations(t *testing.T) { ts := NewTestServer(t) + defer func() { err := ts.Cleanup() if err != nil { @@ -179,6 +180,7 @@ func TestBasicS3Operations(t *testing.T) { if err != nil { t.Fatalf("failed to get object: %v", err) } + defer func() { err := result.Body.Close() if err != nil { @@ -202,6 +204,7 @@ func TestBasicS3Operations(t *testing.T) { //nolint:paralleltest // test servers share a fixed localhost port func TestBlobOperations(t *testing.T) { ts := NewTestServer(t) + defer func() { err := ts.Cleanup() if err != nil { @@ -268,6 +271,7 @@ func TestBlobOperations(t *testing.T) { //nolint:paralleltest // test servers share a fixed localhost port func TestMetadataOperations(t *testing.T) { ts := NewTestServer(t) + defer func() { err := ts.Cleanup() if err != nil { diff --git a/internal/snapshot/backup_test.go b/internal/snapshot/backup_test.go index a6e2a80..80e0b38 100644 --- a/internal/snapshot/backup_test.go +++ b/internal/snapshot/backup_test.go @@ -461,6 +461,7 @@ func (b *BackupEngine) backupOneFile( if err != nil { return err } + defer func() { err := f.Close() if err != nil { diff --git a/internal/snapshot/file_change_test.go b/internal/snapshot/file_change_test.go index 501b69b..fcd860a 100644 --- a/internal/snapshot/file_change_test.go +++ b/internal/snapshot/file_change_test.go @@ -75,6 +75,7 @@ func TestFileContentChange(t *testing.T) { db, err := database.NewTestDB() require.NoError(t, err) + defer func() { err := db.Close() if err != nil { @@ -165,6 +166,7 @@ func TestMultipleFileChanges(t *testing.T) { db, err := database.NewTestDB() require.NoError(t, err) + defer func() { err := db.Close() if err != nil { diff --git a/internal/snapshot/progress.go b/internal/snapshot/progress.go index 71fbb98..5cd151a 100644 --- a/internal/snapshot/progress.go +++ b/internal/snapshot/progress.go @@ -127,6 +127,7 @@ func NewProgressReporter() *ProgressReporter { // Start begins the progress reporting func (pr *ProgressReporter) Start() { pr.wg.Add(1) + go pr.run() // Print initial multi-line status diff --git a/internal/snapshot/scanner.go b/internal/snapshot/scanner.go index 9a8eb27..8e4b5cd 100644 --- a/internal/snapshot/scanner.go +++ b/internal/snapshot/scanner.go @@ -1673,6 +1673,7 @@ func (s *Scanner) processFileStreaming( if err != nil { return fmt.Errorf("opening file: %w", wrapPermissionError(fileToProcess.Path, err)) } + defer func() { _ = file.Close() }() var chunks []streamingChunkInfo diff --git a/internal/snapshot/scanner_test.go b/internal/snapshot/scanner_test.go index 96c3e83..dfac28e 100644 --- a/internal/snapshot/scanner_test.go +++ b/internal/snapshot/scanner_test.go @@ -164,6 +164,7 @@ func TestScannerSimpleDirectory(t *testing.T) { if err != nil { t.Fatalf("failed to create test database: %v", err) } + defer func() { err := db.Close() if err != nil { @@ -241,6 +242,7 @@ func TestScannerLargeFile(t *testing.T) { if err != nil { t.Fatalf("failed to create test database: %v", err) } + defer func() { err := db.Close() if err != nil { diff --git a/internal/snapshot/snapshot.go b/internal/snapshot/snapshot.go index 2efe8d8..f445e4f 100644 --- a/internal/snapshot/snapshot.go +++ b/internal/snapshot/snapshot.go @@ -259,6 +259,7 @@ func (sm *SnapshotManager) ExportSnapshotMetadata( } log.Debug("Created temporary directory", "path", tempDir) + defer func() { log.Debug("Cleaning up temporary directory", "path", tempDir) @@ -555,6 +556,7 @@ func (sm *SnapshotManager) cleanSnapshotDB( if err != nil { return nil, fmt.Errorf("opening temp database: %w", err) } + defer func() { err := db.Close() if err != nil { @@ -567,6 +569,7 @@ func (sm *SnapshotManager) cleanSnapshotDB( if err != nil { return nil, fmt.Errorf("beginning transaction: %w", err) } + defer func() { rbErr := tx.Rollback() if rbErr != nil && !errors.Is(rbErr, sql.ErrTxDone) { @@ -685,6 +688,7 @@ func (sm *SnapshotManager) compressFile(inputPath, outputPath string) error { if err != nil { return fmt.Errorf("opening input file: %w", err) } + defer func() { err := input.Close() if err != nil { @@ -696,6 +700,7 @@ func (sm *SnapshotManager) compressFile(inputPath, outputPath string) error { if err != nil { return fmt.Errorf("creating output file: %w", err) } + defer func() { err := output.Close() if err != nil { @@ -714,6 +719,7 @@ func (sm *SnapshotManager) compressFile(inputPath, outputPath string) error { // Track if writer has been closed to avoid double-close writerClosed := false + defer func() { if !writerClosed { err := writer.Close() @@ -749,6 +755,7 @@ func (sm *SnapshotManager) copyFile(src, dst string) error { if err != nil { return err } + defer func() { log.Debug("Closing source file", "path", src) @@ -764,6 +771,7 @@ func (sm *SnapshotManager) copyFile(src, dst string) error { if err != nil { return err } + defer func() { log.Debug("Closing destination file", "path", dst) @@ -794,6 +802,7 @@ func (sm *SnapshotManager) generateBlobManifest( if err != nil { return nil, fmt.Errorf("opening database: %w", err) } + defer func() { _ = db.Close() }() // Create repositories to access the data diff --git a/internal/snapshot/snapshot_test.go b/internal/snapshot/snapshot_test.go index fd83d72..878366e 100644 --- a/internal/snapshot/snapshot_test.go +++ b/internal/snapshot/snapshot_test.go @@ -25,12 +25,14 @@ func copyFile(fs afero.Fs, src, dst string) error { if err != nil { return err } + defer func() { _ = sourceFile.Close() }() destFile, err := fs.Create(dst) if err != nil { return err } + defer func() { _ = destFile.Close() }() _, err = io.Copy(destFile, sourceFile) @@ -53,6 +55,7 @@ func verifyCleanedDB( if err != nil { t.Fatalf("failed to open cleaned database: %v", err) } + defer func() { err := cleanedDB.Close() if err != nil { diff --git a/internal/storage/file.go b/internal/storage/file.go index 1dab239..36ed92e 100644 --- a/internal/storage/file.go +++ b/internal/storage/file.go @@ -62,6 +62,7 @@ func (f *FileStorer) Put(_ context.Context, key string, data io.Reader) error { if err != nil { return fmt.Errorf("creating file: %w", err) } + defer func() { _ = file.Close() }() _, err = io.Copy(file, data) @@ -91,6 +92,7 @@ func (f *FileStorer) PutWithProgress( if err != nil { return fmt.Errorf("creating file: %w", err) } + defer func() { _ = file.Close() }() // Wrap with progress tracking @@ -209,6 +211,7 @@ func (f *FileStorer) List(ctx context.Context, prefix string) ([]string, error) // ListStream returns a channel of ObjectInfo for large result sets. func (f *FileStorer) ListStream(ctx context.Context, prefix string) <-chan ObjectInfo { ch := make(chan ObjectInfo) + go func() { defer close(ch) diff --git a/internal/storage/s3.go b/internal/storage/s3.go index 580ac80..1f4f7c5 100644 --- a/internal/storage/s3.go +++ b/internal/storage/s3.go @@ -68,6 +68,7 @@ func (s *S3Storer) List(ctx context.Context, prefix string) ([]string, error) { // ListStream returns a channel of ObjectInfo for large result sets. func (s *S3Storer) ListStream(ctx context.Context, prefix string) <-chan ObjectInfo { ch := make(chan ObjectInfo) + go func() { defer close(ch) diff --git a/internal/vaultik/blobcache.go b/internal/vaultik/blobcache.go index f6a3bd6..2e811d7 100644 --- a/internal/vaultik/blobcache.go +++ b/internal/vaultik/blobcache.go @@ -238,6 +238,7 @@ func (c *blobDiskCache) ReadAt(key string, offset, length int64) ([]byte, error) if err != nil { return nil, err } + defer func() { _ = f.Close() }() buf := make([]byte, length) diff --git a/internal/vaultik/blobcache_test.go b/internal/vaultik/blobcache_test.go index 5d61872..f71f689 100644 --- a/internal/vaultik/blobcache_test.go +++ b/internal/vaultik/blobcache_test.go @@ -14,6 +14,7 @@ func TestBlobDiskCache_BasicGetPut(t *testing.T) { if err != nil { t.Fatal(err) } + defer func() { _ = cache.Close() }() data := []byte("hello world") @@ -79,6 +80,7 @@ func TestBlobDiskCache_OversizedEntryRejected(t *testing.T) { if err != nil { t.Fatal(err) } + defer func() { _ = cache.Close() }() data := make([]byte, 200) @@ -100,6 +102,7 @@ func TestBlobDiskCache_UpdateInPlace(t *testing.T) { if err != nil { t.Fatal(err) } + defer func() { _ = cache.Close() }() err = cache.Put("key1", []byte("v1")) @@ -137,6 +140,7 @@ func TestBlobDiskCache_ReadAt(t *testing.T) { if err != nil { t.Fatal(err) } + defer func() { _ = cache.Close() }() data := make([]byte, 1024) @@ -197,6 +201,7 @@ func TestBlobDiskCache_LRUOrder(t *testing.T) { if err != nil { t.Fatal(err) } + defer func() { _ = cache.Close() }() d := make([]byte, 100) diff --git a/internal/vaultik/integration_test.go b/internal/vaultik/integration_test.go index 0e51afd..bf74e60 100644 --- a/internal/vaultik/integration_test.go +++ b/internal/vaultik/integration_test.go @@ -134,6 +134,7 @@ func (m *MockStorer) ListStream( _ context.Context, prefix string, ) <-chan storage.ObjectInfo { ch := make(chan storage.ObjectInfo) + go func() { defer close(ch) @@ -326,6 +327,7 @@ func TestEndToEndBackup(t *testing.T) { db, err := database.New(ctx, ":memory:") require.NoError(t, err) + defer func() { err := db.Close() if err != nil { @@ -411,6 +413,7 @@ func TestBackupAndVerify(t *testing.T) { db, err := database.New(ctx, ":memory:") require.NoError(t, err) + defer func() { err := db.Close() if err != nil { @@ -968,6 +971,7 @@ func TestDedupOnlySnapshotRestores(t *testing.T) { env := setupDedupBackupEnv( ctx, t, fs, storeDir, dbPath, chunkSize, maxBlobSize) + defer func() { _ = env.db.Close() }() cfg, storer, repos, sm := env.cfg, env.storer, env.repos, env.sm diff --git a/internal/vaultik/restore.go b/internal/vaultik/restore.go index 36a632e..c26c19e 100644 --- a/internal/vaultik/restore.go +++ b/internal/vaultik/restore.go @@ -590,6 +590,7 @@ func (v *Vaultik) downloadSnapshotDB( if err != nil { return nil, fmt.Errorf("downloading %s: %w", dbKey, err) } + defer func() { _ = reader.Close() }() // Read all data @@ -606,6 +607,7 @@ func (v *Vaultik) downloadSnapshotDB( if err != nil { return nil, fmt.Errorf("creating decryption reader: %w", err) } + defer func() { _ = blobReader.Close() }() // Read the binary SQLite database @@ -1115,6 +1117,7 @@ func (v *Vaultik) verifyFile( if err != nil { return 0, fmt.Errorf("opening file: %w", err) } + defer func() { _ = f.Close() }() // Verify each chunk diff --git a/internal/vaultik/restore_sweeper.go b/internal/vaultik/restore_sweeper.go index e363d60..5659f6b 100644 --- a/internal/vaultik/restore_sweeper.go +++ b/internal/vaultik/restore_sweeper.go @@ -110,6 +110,7 @@ func (s *restoreSweeper) blobStillNeeded(blobHash string) (bool, error) { if err != nil { return true, fmt.Errorf("querying referencing files: %w", err) } + defer func() { _ = rows.Close() }() for rows.Next() { diff --git a/internal/vaultik/snapshot.go b/internal/vaultik/snapshot.go index 4e6b98c..80efb3a 100644 --- a/internal/vaultik/snapshot.go +++ b/internal/vaultik/snapshot.go @@ -1130,6 +1130,7 @@ func (v *Vaultik) downloadManifestByKey(remoteKey string) (*snapshot.Manifest, e if err != nil { return nil, err } + defer func() { _ = reader.Close() }() manifest, err := snapshot.DecodeManifest(reader) diff --git a/internal/vaultik/verify.go b/internal/vaultik/verify.go index f5a7365..07d1e01 100644 --- a/internal/vaultik/verify.go +++ b/internal/vaultik/verify.go @@ -96,6 +96,7 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error { if err != nil { return err } + defer func() { if tempDB != nil { _ = tempDB.Close() @@ -343,6 +344,7 @@ func (v *Vaultik) verifyBlob(blobInfo snapshot.BlobInfo, db *sql.DB) error { if err != nil { return fmt.Errorf("failed to download: %w", err) } + defer func() { _ = reader.Close() }() // Get decryptor -- 2.49.1 From 7a37a66d88827f2dc768f71a7d37f30473e94c69 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 9 Aug 2026 01:48:44 +0000 Subject: [PATCH 2/4] Close sql.Rows inline so sqlclosecheck can see it (refs #61) The ten sqlclosecheck findings were not leaks: every one of these queries already deferred a close through the package-local CloseRows helper. sqlclosecheck only recognises a Close call on the rows value in the function that produced it (directly deferred, or inside a deferred closure), so a call that hands rows to a helper reads as unhandled. Rather than keep a helper the linter cannot see through, drop CloseRows and defer a closure that calls rows.Close() directly at each of the eighteen call sites, keeping the existing fatal-on-close-error behaviour byte for byte. The close still runs exactly once, on function exit, after the rows have been read. Fatalf stays; it is still used by the transaction helpers. --- internal/database/blob_chunks.go | 8 +++++++- internal/database/blobs.go | 8 +++++++- internal/database/chunk_files.go | 24 +++++++++++++++++++++--- internal/database/chunks.go | 16 ++++++++++++++-- internal/database/chunks_ext.go | 8 +++++++- internal/database/errors.go | 9 --------- internal/database/file_chunks.go | 24 +++++++++++++++++++++--- internal/database/files.go | 24 +++++++++++++++++++++--- internal/database/snapshots.go | 32 ++++++++++++++++++++++++++++---- 9 files changed, 126 insertions(+), 27 deletions(-) diff --git a/internal/database/blob_chunks.go b/internal/database/blob_chunks.go index 0a009c9..64d99b0 100644 --- a/internal/database/blob_chunks.go +++ b/internal/database/blob_chunks.go @@ -57,7 +57,13 @@ func (r *BlobChunkRepository) GetByBlobID( if err != nil { return nil, fmt.Errorf("querying blob chunks: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var blobChunks []*BlobChunk diff --git a/internal/database/blobs.go b/internal/database/blobs.go index 0acd4cd..50c9862 100644 --- a/internal/database/blobs.go +++ b/internal/database/blobs.go @@ -82,7 +82,13 @@ func (r *BlobRepository) GetAll(ctx context.Context) (map[string]*Blob, error) { if err != nil { return nil, fmt.Errorf("querying blobs: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() out := make(map[string]*Blob) diff --git a/internal/database/chunk_files.go b/internal/database/chunk_files.go index f6b00bd..308af59 100644 --- a/internal/database/chunk_files.go +++ b/internal/database/chunk_files.go @@ -60,7 +60,13 @@ func (r *ChunkFileRepository) GetByChunkHash( if err != nil { return nil, fmt.Errorf("querying chunk files: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() return r.scanChunkFiles(rows) } @@ -80,7 +86,13 @@ func (r *ChunkFileRepository) GetByFilePath( if err != nil { return nil, fmt.Errorf("querying chunk files: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() return r.scanChunkFiles(rows) } @@ -99,7 +111,13 @@ func (r *ChunkFileRepository) GetByFileID( if err != nil { return nil, fmt.Errorf("querying chunk files: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() return r.scanChunkFiles(rows) } diff --git a/internal/database/chunks.go b/internal/database/chunks.go index dbd0c74..833c1aa 100644 --- a/internal/database/chunks.go +++ b/internal/database/chunks.go @@ -106,7 +106,13 @@ func (r *ChunkRepository) GetByHashes( if err != nil { return nil, fmt.Errorf("querying chunks: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var chunks []*Chunk @@ -145,7 +151,13 @@ func (r *ChunkRepository) ListUnpacked( if err != nil { return nil, fmt.Errorf("querying unpacked chunks: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var chunks []*Chunk diff --git a/internal/database/chunks_ext.go b/internal/database/chunks_ext.go index fee4030..4724acc 100644 --- a/internal/database/chunks_ext.go +++ b/internal/database/chunks_ext.go @@ -17,7 +17,13 @@ func (r *ChunkRepository) List(ctx context.Context) ([]*Chunk, error) { if err != nil { return nil, fmt.Errorf("querying chunks: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var chunks []*Chunk diff --git a/internal/database/errors.go b/internal/database/errors.go index 860c23f..49492bd 100644 --- a/internal/database/errors.go +++ b/internal/database/errors.go @@ -1,7 +1,6 @@ package database import ( - "database/sql" "fmt" "os" ) @@ -11,11 +10,3 @@ func Fatalf(format string, args ...any) { fmt.Fprintf(os.Stderr, "FATAL: "+format+"\n", args...) os.Exit(1) } - -// CloseRows closes rows and exits on error -func CloseRows(rows *sql.Rows) { - err := rows.Close() - if err != nil { - Fatalf("failed to close rows: %v", err) - } -} diff --git a/internal/database/file_chunks.go b/internal/database/file_chunks.go index 11711f7..04388d7 100644 --- a/internal/database/file_chunks.go +++ b/internal/database/file_chunks.go @@ -61,7 +61,13 @@ func (r *FileChunkRepository) GetByPath( if err != nil { return nil, fmt.Errorf("querying file chunks: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() return r.scanFileChunks(rows) } @@ -81,7 +87,13 @@ func (r *FileChunkRepository) GetByFileID( if err != nil { return nil, fmt.Errorf("querying file chunks: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() return r.scanFileChunks(rows) } @@ -104,7 +116,13 @@ func (r *FileChunkRepository) GetByPathTx( if err != nil { return nil, fmt.Errorf("querying file chunks: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() fileChunks, err := r.scanFileChunks(rows) LogSQL("GetByPathTx", "Complete", path, "count", len(fileChunks)) diff --git a/internal/database/files.go b/internal/database/files.go index 4beb5d6..8fd69e7 100644 --- a/internal/database/files.go +++ b/internal/database/files.go @@ -168,7 +168,13 @@ func (r *FileRepository) ListModifiedSince( if err != nil { return nil, fmt.Errorf("querying files: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var files []*File @@ -238,7 +244,13 @@ func (r *FileRepository) ListByPrefix( if err != nil { return nil, fmt.Errorf("querying files: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var files []*File @@ -266,7 +278,13 @@ func (r *FileRepository) ListAll(ctx context.Context) ([]*File, error) { if err != nil { return nil, fmt.Errorf("querying files: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var files []*File diff --git a/internal/database/snapshots.go b/internal/database/snapshots.go index 7bc550a..715ab78 100644 --- a/internal/database/snapshots.go +++ b/internal/database/snapshots.go @@ -223,7 +223,13 @@ func (r *SnapshotRepository) ListRecent( if err != nil { return nil, fmt.Errorf("querying snapshots: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() return r.scanSnapshotRows(rows) } @@ -437,7 +443,13 @@ func (r *SnapshotRepository) GetBlobHashes( if err != nil { return nil, fmt.Errorf("querying blob hashes: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var blobs []string @@ -561,7 +573,13 @@ func (r *SnapshotRepository) GetIncompleteSnapshots( if err != nil { return nil, fmt.Errorf("querying incomplete snapshots: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() return r.scanSnapshotRows(rows) } @@ -583,7 +601,13 @@ func (r *SnapshotRepository) GetIncompleteByHostname( if err != nil { return nil, fmt.Errorf("querying incomplete snapshots: %w", err) } - defer CloseRows(rows) + + defer func() { + err := rows.Close() + if err != nil { + Fatalf("failed to close rows: %v", err) + } + }() var snapshots []*Snapshot -- 2.49.1 From cb25b01e70c9ecfe75ac38e66ad977a08621a145 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 9 Aug 2026 01:48:44 +0000 Subject: [PATCH 3/4] Preallocate append targets flagged by prealloc (refs #61) collectBatchFlushData now sizes the file-chunk and chunk-file slices to the number of pending files, a safe lower bound since every file contributes at least one mapping of each kind. The chunker test sizes its reconstruction buffer to the input length, which is exactly what it ends up holding. Append semantics and results are unchanged. --- internal/chunker/chunker_test.go | 2 +- internal/snapshot/scanner.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/chunker/chunker_test.go b/internal/chunker/chunker_test.go index f44919f..520bcd6 100644 --- a/internal/chunker/chunker_test.go +++ b/internal/chunker/chunker_test.go @@ -53,7 +53,7 @@ func TestChunkerLargeFileMultipleChunks(t *testing.T) { } // Verify chunks reconstruct original data - var reconstructed []byte + reconstructed := make([]byte, 0, len(data)) for _, chunk := range chunks { reconstructed = append(reconstructed, chunk.Data...) } diff --git a/internal/snapshot/scanner.go b/internal/snapshot/scanner.go index 8e4b5cd..fc00251 100644 --- a/internal/snapshot/scanner.go +++ b/internal/snapshot/scanner.go @@ -624,11 +624,11 @@ func (s *Scanner) collectBatchFlushData( collectStart := time.Now() - var ( - allFileChunks []database.FileChunk - allChunkFiles []database.ChunkFile - ) - + // Every pending file contributes at least one file-chunk and one + // chunk-file mapping, so the file count is a safe lower bound for the + // initial capacity of both slices. + allFileChunks := make([]database.FileChunk, 0, len(canFlush)) + allChunkFiles := make([]database.ChunkFile, 0, len(canFlush)) allFileIDs := make([]types.FileID, 0, len(canFlush)) allFiles := make([]*database.File, 0, len(canFlush)) -- 2.49.1 From efb0cea1c2b6d6f7ff2a07a15eaaff6ffc0fd8a1 Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 9 Aug 2026 01:48:57 +0000 Subject: [PATCH 4/4] Suppress the revive package-name findings with no fix (closes #61) Three findings remain that cannot be fixed without making a repo-wide naming decision, so each carries a per-site //nolint directive with its justification. revive var-naming (internal/log, internal/crypto, internal/types): fixing these means renaming packages across the whole codebase, which is the repo owner's call, not a lint fix. Neither stdlib log nor stdlib crypto is imported anywhere in the repo, so nothing is actually shadowed today. The rename decision is tracked in issue #76. revive reports a package-name failure only once per package directory, on whichever file it happens to lint first, so every file of the affected packages carries the directive and lists nolintlint alongside revive so the ones that lose the race are not reported as unused. No gosec directives are needed: the pinned golangci-lint v2.12.2 that CI and the Dockerfile use reports nothing at the term.IsTerminal conversions in internal/log and internal/ui or at the os.Remove calls in internal/vaultik/verify.go, so suppressing there would itself fail nolintlint as an unused directive. Verification is script/cibuild, which builds the hash-pinned lint image: it exits 0, with make lint reporting "0 issues" under the canonical .golangci.yml (sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb, unmodified), plus make fmt-check, make test and the release build. That also unblocks issue #59. make check is not a valid gate here: script/lint runs whatever golangci-lint is on PATH rather than the pinned version, which is tracked in issue #78. Also corrects the capacity comment in collectBatchFlushData: an empty file contributes no chunk mappings, so the pending-file count is a rough starting capacity, not a lower bound. TODO.md: record this work, note that the previous next step (reconciling uncommitted ARCHITECTURE.md edits) needed no work because the tree is clean, and move the next step on to the stale-branch triage. --- TODO.md | 30 ++++++++++++++++++++++++------ internal/crypto/encryption.go | 2 +- internal/log/log.go | 4 ++-- internal/log/module.go | 2 +- internal/log/tty_handler.go | 2 +- internal/snapshot/scanner.go | 7 ++++--- internal/types/types.go | 2 +- 7 files changed, 34 insertions(+), 15 deletions(-) diff --git a/TODO.md b/TODO.md index 17d8258..e4374f0 100644 --- a/TODO.md +++ b/TODO.md @@ -14,16 +14,36 @@ pre-1.0 # Next Step -Reconcile the uncommitted ARCHITECTURE.md edits on main: finish and -commit, or revert. +Triage the stale remote branches (issue #71): for each, merge the work +or delete the branch. # Completed Steps +- 2026-08-09: Finished the lint remediation under the canonical + `.golangci.yml` (issue #61, which also unblocks issue #59). The + remaining findings were fixed behavior-preservingly: `wsl_v5` + whitespace, `sqlclosecheck`, and `prealloc`. The `sqlclosecheck` sites + now close `sql.Rows` in a deferred closure instead of via the + `CloseRows` helper, which the linter could not see through. Only the + `revive` package-name findings remain suppressed, with per-site + `//nolint` directives; the package-rename question behind them is + tracked in issue #76. Verified with `script/cibuild`, which exits 0 — + that is the only trustworthy gate, because `script/lint` runs whatever + `golangci-lint` happens to be on `PATH` rather than the pinned + v2.12.2 that CI and the `Dockerfile` use, so `make check` can report + green on findings CI still fails. That tooling gap is tracked in issue + #78. +- 2026-08-09: The earlier next step "reconcile the uncommitted + `ARCHITECTURE.md` edits on `main`" needed no work: the working tree is + clean and `ARCHITECTURE.md` is committed on `main`. - 2026-08-07: Updated golangci-lint to v2.12.2 everywhere it is pinned (`Dockerfile` lint stage, `Makefile` deps target), replaced `.golangci.yml` with the canonical config (v2 schema, `default: all`), - and remediated all lint findings it surfaced (issue #61): - behavior-preserving fixes across every package, `make check` green. + and remediated the bulk of the lint findings it surfaced (issue #61): + behavior-preserving fixes across every package, 2,990 findings down to + 80. `make test` and `make fmt-check` were green at that point but + `make lint` was still red; the commit message claiming `make check` + was green was wrong. - 2026-08-07: Added the standard `.golangci.yml` and `.editorconfig` (issue #59); lint findings under the new config are tracked in issue #61. `script/bootstrap` now installs sqlite3 (needed by tests). @@ -49,6 +69,4 @@ commit, or revert. # Future Steps -- Review stale local branches (add-godoc-to-cli-package, - feature/pluggable-storage-backend) and merge or delete them. - Define remaining scope for a first tagged release and cut v0.1.0. diff --git a/internal/crypto/encryption.go b/internal/crypto/encryption.go index 36c2564..04f36dd 100644 --- a/internal/crypto/encryption.go +++ b/internal/crypto/encryption.go @@ -1,6 +1,6 @@ // Package crypto provides thread-safe age encryption and decryption // helpers used to protect blob and metadata content. -package crypto +package crypto //nolint:revive,nolintlint // stdlib crypto unused; see #76 import ( "bytes" diff --git a/internal/log/log.go b/internal/log/log.go index 17025ca..2806017 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,6 +1,6 @@ // Package log provides the application-wide structured logger: slog // with a colorized TTY handler on terminals and JSON output otherwise. -package log +package log //nolint:revive,nolintlint // stdlib log unused here; see #76 import ( "context" @@ -69,7 +69,7 @@ func Initialize(cfg Config) { Level: level, } - // Check if stdout is a TTY + // Check if stdout is a TTY. if term.IsTerminal(int(os.Stdout.Fd())) { // Use colorized TTY handler logger = slog.New(NewTTYHandler(os.Stdout, opts)) diff --git a/internal/log/module.go b/internal/log/module.go index 525f969..f428604 100644 --- a/internal/log/module.go +++ b/internal/log/module.go @@ -1,4 +1,4 @@ -package log +package log //nolint:revive,nolintlint // stdlib log unused here; see #76 import ( "go.uber.org/fx" diff --git a/internal/log/tty_handler.go b/internal/log/tty_handler.go index e787de8..cfdf4f9 100644 --- a/internal/log/tty_handler.go +++ b/internal/log/tty_handler.go @@ -1,4 +1,4 @@ -package log +package log //nolint:revive,nolintlint // stdlib log unused here; see #76 import ( "context" diff --git a/internal/snapshot/scanner.go b/internal/snapshot/scanner.go index fc00251..86dda30 100644 --- a/internal/snapshot/scanner.go +++ b/internal/snapshot/scanner.go @@ -624,9 +624,10 @@ func (s *Scanner) collectBatchFlushData( collectStart := time.Now() - // Every pending file contributes at least one file-chunk and one - // chunk-file mapping, so the file count is a safe lower bound for the - // initial capacity of both slices. + // A pending file contributes one mapping of each kind per chunk, and + // an empty file contributes none, so the file count is only a rough + // starting capacity for the mapping slices; append grows them as + // needed. It is exact for the file and file-ID slices. allFileChunks := make([]database.FileChunk, 0, len(canFlush)) allChunkFiles := make([]database.ChunkFile, 0, len(canFlush)) allFileIDs := make([]types.FileID, 0, len(canFlush)) diff --git a/internal/types/types.go b/internal/types/types.go index c076f41..5310179 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -2,7 +2,7 @@ // vaultik codebase. Using distinct types for IDs, hashes, paths, and // credentials prevents accidental mixing of semantically different values // that happen to share the same underlying type. -package types +package types //nolint:revive,nolintlint // rename decision tracked in #76 import ( "database/sql/driver" -- 2.49.1