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,4 +1,4 @@
|
||||
package snapshot
|
||||
package snapshot_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -19,6 +19,12 @@ import (
|
||||
"sneak.berlin/go/vaultik/internal/types"
|
||||
)
|
||||
|
||||
// errBlobNotFound is returned by the mock S3 client for unknown hashes.
|
||||
var errBlobNotFound = errors.New("blob not found")
|
||||
|
||||
// testFile1Name is the shared fixture filename used across backup tests.
|
||||
const testFile1Name = "file1.txt"
|
||||
|
||||
// MockS3Client is a mock implementation of S3 operations for testing
|
||||
type MockS3Client struct {
|
||||
storage map[string][]byte
|
||||
@@ -30,39 +36,149 @@ func NewMockS3Client() *MockS3Client {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockS3Client) PutBlob(ctx context.Context, hash string, data []byte) error {
|
||||
func (m *MockS3Client) PutBlob(_ context.Context, hash string, data []byte) error {
|
||||
m.storage[hash] = data
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockS3Client) GetBlob(ctx context.Context, hash string) ([]byte, error) {
|
||||
func (m *MockS3Client) GetBlob(_ context.Context, hash string) ([]byte, error) {
|
||||
data, ok := m.storage[hash]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("blob not found: %s", hash)
|
||||
return nil, fmt.Errorf("%w: %s", errBlobNotFound, hash)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (m *MockS3Client) BlobExists(ctx context.Context, hash string) (bool, error) {
|
||||
func (m *MockS3Client) BlobExists(_ context.Context, hash string) (bool, error) {
|
||||
_, ok := m.storage[hash]
|
||||
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (m *MockS3Client) CreateBucket(ctx context.Context, bucket string) error {
|
||||
func (m *MockS3Client) CreateBucket(_ context.Context, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifyBackupFiles checks the file records created by a backup against
|
||||
// the fixture filesystem.
|
||||
func verifyBackupFiles(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
repos *database.Repositories,
|
||||
testFS fstest.MapFS,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
files, err := repos.Files.ListByPrefix(ctx, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list files: %v", err)
|
||||
}
|
||||
|
||||
expectedFiles := map[string]bool{
|
||||
testFile1Name: true,
|
||||
"dir1/file2.txt": true,
|
||||
"dir1/subdir/file3.txt": true,
|
||||
"largefile.bin": true,
|
||||
}
|
||||
|
||||
if len(files) != len(expectedFiles) {
|
||||
t.Errorf("Expected %d files, got %d", len(expectedFiles), len(files))
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !expectedFiles[file.Path.String()] {
|
||||
t.Errorf("Unexpected file in database: %s", file.Path)
|
||||
}
|
||||
|
||||
delete(expectedFiles, file.Path.String())
|
||||
|
||||
// Verify file metadata
|
||||
fsFile := testFS[file.Path.String()]
|
||||
if fsFile == nil {
|
||||
t.Errorf("File %s not found in test filesystem", file.Path)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if file.Size != int64(len(fsFile.Data)) {
|
||||
t.Errorf("File %s: expected size %d, got %d",
|
||||
file.Path, len(fsFile.Data), file.Size)
|
||||
}
|
||||
|
||||
if file.Mode != uint32(fsFile.Mode) {
|
||||
t.Errorf("File %s: expected mode %o, got %o",
|
||||
file.Path, fsFile.Mode, file.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
if len(expectedFiles) > 0 {
|
||||
t.Errorf("Files not found in database: %v", expectedFiles)
|
||||
}
|
||||
}
|
||||
|
||||
// verifyBackupChunksAndBlobs checks that chunking produced the expected
|
||||
// records and every referenced blob exists in the mock S3 store.
|
||||
func verifyBackupChunksAndBlobs(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
repos *database.Repositories,
|
||||
s3Client *MockS3Client,
|
||||
snapshotID string,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
chunks, err := repos.Chunks.List(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list chunks: %v", err)
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
t.Error("No chunks found in database")
|
||||
}
|
||||
|
||||
// The large file should create 10 chunks (10MB / 1MB chunk size)
|
||||
// Plus the small files
|
||||
minExpectedChunks := 10 + 3
|
||||
if len(chunks) < minExpectedChunks {
|
||||
t.Errorf("Expected at least %d chunks, got %d", minExpectedChunks, len(chunks))
|
||||
}
|
||||
|
||||
// Verify at least one blob was created and uploaded
|
||||
// We can't list blobs directly, but we can check via snapshot blobs
|
||||
blobHashes, err := repos.Snapshots.GetBlobHashes(ctx, snapshotID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get blob hashes: %v", err)
|
||||
}
|
||||
|
||||
if len(blobHashes) == 0 {
|
||||
t.Error("Expected at least one blob to be created")
|
||||
}
|
||||
|
||||
for _, blobHash := range blobHashes {
|
||||
// Check blob exists in mock S3
|
||||
exists, err := s3Client.BlobExists(ctx, blobHash)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to check blob %s: %v", blobHash, err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
t.Errorf("Blob %s not found in S3", blobHash)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupWithInMemoryFS(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary directory for the database
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "test.db")
|
||||
|
||||
// Create test filesystem
|
||||
testFS := fstest.MapFS{
|
||||
"file1.txt": &fstest.MapFile{
|
||||
testFile1Name: &fstest.MapFile{
|
||||
Data: []byte("Hello, World!"),
|
||||
Mode: 0644,
|
||||
ModTime: time.Now(),
|
||||
@@ -129,100 +245,21 @@ func TestBackupWithInMemoryFS(t *testing.T) {
|
||||
t.Error("Expected snapshot to have files")
|
||||
}
|
||||
|
||||
// Verify files in database
|
||||
files, err := repos.Files.ListByPrefix(ctx, "")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list files: %v", err)
|
||||
}
|
||||
|
||||
expectedFiles := map[string]bool{
|
||||
"file1.txt": true,
|
||||
"dir1/file2.txt": true,
|
||||
"dir1/subdir/file3.txt": true,
|
||||
"largefile.bin": true,
|
||||
}
|
||||
|
||||
if len(files) != len(expectedFiles) {
|
||||
t.Errorf("Expected %d files, got %d", len(expectedFiles), len(files))
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !expectedFiles[file.Path.String()] {
|
||||
t.Errorf("Unexpected file in database: %s", file.Path)
|
||||
}
|
||||
|
||||
delete(expectedFiles, file.Path.String())
|
||||
|
||||
// Verify file metadata
|
||||
fsFile := testFS[file.Path.String()]
|
||||
if fsFile == nil {
|
||||
t.Errorf("File %s not found in test filesystem", file.Path)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if file.Size != int64(len(fsFile.Data)) {
|
||||
t.Errorf("File %s: expected size %d, got %d", file.Path, len(fsFile.Data), file.Size)
|
||||
}
|
||||
|
||||
if file.Mode != uint32(fsFile.Mode) {
|
||||
t.Errorf("File %s: expected mode %o, got %o", file.Path, fsFile.Mode, file.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
if len(expectedFiles) > 0 {
|
||||
t.Errorf("Files not found in database: %v", expectedFiles)
|
||||
}
|
||||
|
||||
// Verify chunks
|
||||
chunks, err := repos.Chunks.List(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list chunks: %v", err)
|
||||
}
|
||||
|
||||
if len(chunks) == 0 {
|
||||
t.Error("No chunks found in database")
|
||||
}
|
||||
|
||||
// The large file should create 10 chunks (10MB / 1MB chunk size)
|
||||
// Plus the small files
|
||||
minExpectedChunks := 10 + 3
|
||||
if len(chunks) < minExpectedChunks {
|
||||
t.Errorf("Expected at least %d chunks, got %d", minExpectedChunks, len(chunks))
|
||||
}
|
||||
|
||||
// Verify at least one blob was created and uploaded
|
||||
// We can't list blobs directly, but we can check via snapshot blobs
|
||||
blobHashes, err := repos.Snapshots.GetBlobHashes(ctx, snapshotID)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get blob hashes: %v", err)
|
||||
}
|
||||
|
||||
if len(blobHashes) == 0 {
|
||||
t.Error("Expected at least one blob to be created")
|
||||
}
|
||||
|
||||
for _, blobHash := range blobHashes {
|
||||
// Check blob exists in mock S3
|
||||
exists, err := s3Client.BlobExists(ctx, blobHash)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to check blob %s: %v", blobHash, err)
|
||||
}
|
||||
|
||||
if !exists {
|
||||
t.Errorf("Blob %s not found in S3", blobHash)
|
||||
}
|
||||
}
|
||||
// Verify files, chunks, and blob records
|
||||
verifyBackupFiles(ctx, t, repos, testFS)
|
||||
verifyBackupChunksAndBlobs(ctx, t, repos, s3Client, snapshotID)
|
||||
}
|
||||
|
||||
func TestBackupDeduplication(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a temporary directory for the database
|
||||
tempDir := t.TempDir()
|
||||
dbPath := filepath.Join(tempDir, "test.db")
|
||||
|
||||
// Create test filesystem with duplicate content
|
||||
testFS := fstest.MapFS{
|
||||
"file1.txt": &fstest.MapFile{
|
||||
testFile1Name: &fstest.MapFile{
|
||||
Data: []byte("Duplicate content"),
|
||||
Mode: 0644,
|
||||
ModTime: time.Now(),
|
||||
@@ -290,7 +327,8 @@ func TestBackupDeduplication(t *testing.T) {
|
||||
|
||||
// The duplicate content chunk should be referenced by 2 files
|
||||
if chunk.Size == int64(len("Duplicate content")) && len(files) != 2 {
|
||||
t.Errorf("Expected duplicate chunk to be referenced by 2 files, got %d", len(files))
|
||||
t.Errorf("Expected duplicate chunk to be referenced by 2 files, got %d",
|
||||
len(files))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,8 +342,19 @@ type BackupEngine struct {
|
||||
}
|
||||
}
|
||||
|
||||
// backupCounters accumulates statistics across a test backup run.
|
||||
type backupCounters struct {
|
||||
fileCount int64
|
||||
chunkCount int64
|
||||
blobCount int64
|
||||
totalSize int64
|
||||
blobSize int64
|
||||
}
|
||||
|
||||
// Backup performs a backup of the given filesystem
|
||||
func (b *BackupEngine) Backup(ctx context.Context, fsys fs.FS, root string) (string, error) {
|
||||
func (b *BackupEngine) Backup(
|
||||
ctx context.Context, fsys fs.FS, root string,
|
||||
) (string, error) {
|
||||
// Create a new snapshot
|
||||
hostname, _ := os.Hostname()
|
||||
snapshotID := time.Now().Format(time.RFC3339)
|
||||
@@ -325,8 +374,7 @@ func (b *BackupEngine) Backup(ctx context.Context, fsys fs.FS, root string) (str
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Track counters
|
||||
var fileCount, chunkCount, blobCount, totalSize, blobSize int64
|
||||
counters := &backupCounters{}
|
||||
|
||||
// Track which chunks we've seen to handle deduplication
|
||||
processedChunks := make(map[string]bool)
|
||||
@@ -354,122 +402,170 @@ func (b *BackupEngine) Backup(ctx context.Context, fsys fs.FS, root string) (str
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create file record in a short transaction
|
||||
file := &database.File{
|
||||
Path: types.FilePath(path),
|
||||
Size: info.Size(),
|
||||
Mode: uint32(info.Mode()),
|
||||
MTime: info.ModTime(),
|
||||
UID: 1000, // Default UID for test
|
||||
GID: 1000, // Default GID for test
|
||||
}
|
||||
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
return b.repos.Files.Create(ctx, tx, file)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fileCount++
|
||||
totalSize += info.Size()
|
||||
|
||||
// Read and process file in chunks
|
||||
f, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
// Log but don't fail since we're already in an error path potentially
|
||||
fmt.Fprintf(os.Stderr, "Failed to close file: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Process file in chunks
|
||||
chunkIndex := 0
|
||||
buffer := make([]byte, defaultChunkSize)
|
||||
|
||||
for {
|
||||
n, err := f.Read(buffer)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
chunkData := buffer[:n]
|
||||
chunkHash := calculateHash(chunkData)
|
||||
|
||||
// Check if chunk already exists (outside of transaction)
|
||||
existingChunk, _ := b.repos.Chunks.GetByHash(ctx, chunkHash)
|
||||
if existingChunk == nil {
|
||||
// Create new chunk in a short transaction
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
chunk := &database.Chunk{
|
||||
ChunkHash: types.ChunkHash(chunkHash),
|
||||
Size: int64(n),
|
||||
}
|
||||
|
||||
return b.repos.Chunks.Create(ctx, tx, chunk)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
processedChunks[chunkHash] = true
|
||||
}
|
||||
|
||||
// Create file-chunk mapping in a short transaction
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
fileChunk := &database.FileChunk{
|
||||
FileID: file.ID,
|
||||
Idx: chunkIndex,
|
||||
ChunkHash: types.ChunkHash(chunkHash),
|
||||
}
|
||||
|
||||
return b.repos.FileChunks.Create(ctx, tx, fileChunk)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create chunk-file mapping in a short transaction
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
chunkFile := &database.ChunkFile{
|
||||
ChunkHash: types.ChunkHash(chunkHash),
|
||||
FileID: file.ID,
|
||||
FileOffset: int64(chunkIndex * defaultChunkSize),
|
||||
Length: int64(n),
|
||||
}
|
||||
|
||||
return b.repos.ChunkFiles.Create(ctx, tx, chunkFile)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chunkIndex++
|
||||
}
|
||||
|
||||
return nil
|
||||
return b.backupOneFile(ctx, fsys, path, info, processedChunks, counters)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// After all files are processed, create blobs for new chunks
|
||||
err = b.createBlobsForChunks(ctx, snapshotID, processedChunks, counters)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Update snapshot with final counts
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
return b.repos.Snapshots.UpdateCounts(ctx, tx, snapshotID,
|
||||
counters.fileCount, counters.chunkCount, counters.blobCount,
|
||||
counters.totalSize, counters.blobSize)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return snapshotID, nil
|
||||
}
|
||||
|
||||
// backupOneFile records a single regular file and its chunks.
|
||||
func (b *BackupEngine) backupOneFile(
|
||||
ctx context.Context,
|
||||
fsys fs.FS,
|
||||
path string,
|
||||
info fs.FileInfo,
|
||||
processedChunks map[string]bool,
|
||||
counters *backupCounters,
|
||||
) error {
|
||||
// Create file record in a short transaction
|
||||
file := &database.File{
|
||||
Path: types.FilePath(path),
|
||||
Size: info.Size(),
|
||||
Mode: uint32(info.Mode()),
|
||||
MTime: info.ModTime(),
|
||||
UID: 1000, // Default UID for test
|
||||
GID: 1000, // Default GID for test
|
||||
}
|
||||
|
||||
err := b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
return b.repos.Files.Create(ctx, tx, file)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
counters.fileCount++
|
||||
counters.totalSize += info.Size()
|
||||
|
||||
// Read and process file in chunks
|
||||
f, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
// Log but don't fail since we're already in an error path potentially
|
||||
fmt.Fprintf(os.Stderr, "Failed to close file: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Process file in chunks
|
||||
chunkIndex := 0
|
||||
buffer := make([]byte, defaultChunkSize)
|
||||
|
||||
for {
|
||||
n, err := f.Read(buffer)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return err
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
err = b.recordChunk(ctx, file, chunkIndex, buffer[:n], processedChunks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chunkIndex++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// recordChunk creates the chunk record (if new) and its file associations.
|
||||
func (b *BackupEngine) recordChunk(
|
||||
ctx context.Context,
|
||||
file *database.File,
|
||||
chunkIndex int,
|
||||
chunkData []byte,
|
||||
processedChunks map[string]bool,
|
||||
) error {
|
||||
chunkHash := calculateHash(chunkData)
|
||||
|
||||
// Check if chunk already exists (outside of transaction)
|
||||
existingChunk, _ := b.repos.Chunks.GetByHash(ctx, chunkHash)
|
||||
if existingChunk == nil {
|
||||
// Create new chunk in a short transaction
|
||||
err := b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
chunk := &database.Chunk{
|
||||
ChunkHash: types.ChunkHash(chunkHash),
|
||||
Size: int64(len(chunkData)),
|
||||
}
|
||||
|
||||
return b.repos.Chunks.Create(ctx, tx, chunk)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
processedChunks[chunkHash] = true
|
||||
}
|
||||
|
||||
// Create file-chunk mapping in a short transaction
|
||||
err := b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
fileChunk := &database.FileChunk{
|
||||
FileID: file.ID,
|
||||
Idx: chunkIndex,
|
||||
ChunkHash: types.ChunkHash(chunkHash),
|
||||
}
|
||||
|
||||
return b.repos.FileChunks.Create(ctx, tx, fileChunk)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create chunk-file mapping in a short transaction
|
||||
return b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
chunkFile := &database.ChunkFile{
|
||||
ChunkHash: types.ChunkHash(chunkHash),
|
||||
FileID: file.ID,
|
||||
FileOffset: int64(chunkIndex * defaultChunkSize),
|
||||
Length: int64(len(chunkData)),
|
||||
}
|
||||
|
||||
return b.repos.ChunkFiles.Create(ctx, tx, chunkFile)
|
||||
})
|
||||
}
|
||||
|
||||
// createBlobsForChunks uploads one blob per new chunk and records the blob
|
||||
// metadata and snapshot association.
|
||||
func (b *BackupEngine) createBlobsForChunks(
|
||||
ctx context.Context,
|
||||
snapshotID string,
|
||||
processedChunks map[string]bool,
|
||||
counters *backupCounters,
|
||||
) error {
|
||||
for chunkHash := range processedChunks {
|
||||
// Get chunk data (outside of transaction)
|
||||
chunk, err := b.repos.Chunks.GetByHash(ctx, chunkHash)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
chunkCount++
|
||||
counters.chunkCount++
|
||||
|
||||
// In a real system, blobs would contain multiple chunks and be encrypted
|
||||
// For testing, we'll create a blob with a "blob-" prefix to differentiate
|
||||
@@ -479,8 +575,9 @@ func (b *BackupEngine) Backup(ctx context.Context, fsys fs.FS, root string) (str
|
||||
dummyData := []byte(chunkHash)
|
||||
|
||||
// Upload to S3 as a blob
|
||||
if err := b.s3Client.PutBlob(ctx, blobHash, dummyData); err != nil {
|
||||
return "", err
|
||||
err = b.s3Client.PutBlob(ctx, blobHash, dummyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create blob entry in a short transaction
|
||||
@@ -496,11 +593,11 @@ func (b *BackupEngine) Backup(ctx context.Context, fsys fs.FS, root string) (str
|
||||
return b.repos.Blobs.Create(ctx, tx, blob)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
blobCount++
|
||||
blobSize += chunk.Size
|
||||
counters.blobCount++
|
||||
counters.blobSize += chunk.Size
|
||||
|
||||
// Create blob-chunk mapping in a short transaction
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
@@ -514,27 +611,20 @@ func (b *BackupEngine) Backup(ctx context.Context, fsys fs.FS, root string) (str
|
||||
return b.repos.BlobChunks.Create(ctx, tx, blobChunk)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
// Add blob to snapshot in a short transaction
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
return b.repos.Snapshots.AddBlob(ctx, tx, snapshotID, blobID, types.BlobHash(blobHash))
|
||||
return b.repos.Snapshots.AddBlob(ctx, tx, snapshotID, blobID,
|
||||
types.BlobHash(blobHash))
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update snapshot with final counts
|
||||
err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
||||
return b.repos.Snapshots.UpdateCounts(ctx, tx, snapshotID, fileCount, chunkCount, blobCount, totalSize, blobSize)
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return snapshotID, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func calculateHash(data []byte) string {
|
||||
|
||||
Reference in New Issue
Block a user