diff --git a/internal/globals/globals.go b/internal/globals/globals.go index 80c538b..21edab6 100644 --- a/internal/globals/globals.go +++ b/internal/globals/globals.go @@ -50,5 +50,6 @@ func (g *Globals) ShortCommit() string { if len(g.Commit) > 12 { return g.Commit[:12] } + return g.Commit } diff --git a/internal/log/log.go b/internal/log/log.go index 2843d25..c9867ee 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -84,6 +84,7 @@ func getCaller(skip int) string { if !ok { return "unknown" } + return fmt.Sprintf("%s:%d", filepath.Base(file), line) } @@ -94,6 +95,7 @@ func Fatal(msg string, args ...any) { args = append(args, "caller", getCaller(2)) logger.Error(msg, args...) } + os.Exit(1) } @@ -172,6 +174,7 @@ func With(args ...any) *slog.Logger { if logger != nil { return logger.With(args...) } + return slog.Default() } diff --git a/internal/log/tty_handler.go b/internal/log/tty_handler.go index 91ab803..81c1fe6 100644 --- a/internal/log/tty_handler.go +++ b/internal/log/tty_handler.go @@ -33,6 +33,7 @@ func NewTTYHandler(out io.Writer, opts *slog.HandlerOptions) *TTYHandler { if opts == nil { opts = &slog.HandlerOptions{} } + return &TTYHandler{ out: out, opts: *opts, @@ -54,7 +55,9 @@ func (h *TTYHandler) Handle(_ context.Context, r slog.Record) error { // Level and color level := r.Level.String() + var levelColor string + switch r.Level { case slog.LevelDebug: levelColor = colorGray @@ -96,10 +99,12 @@ func (h *TTYHandler) Handle(_ context.Context, r slog.Record) error { _, _ = fmt.Fprintf(h.out, " %s%s%s=%s%s%s", colorCyan, a.Key, colorReset, colorBlue, value, colorReset) + return true }) _, _ = fmt.Fprintln(h.out) + return nil } @@ -122,6 +127,7 @@ func formatDuration(d time.Duration) string { } else if d < time.Minute { return fmt.Sprintf("%.1fs", d.Seconds()) } + return d.String() } @@ -131,10 +137,12 @@ func formatBytes(b int64) string { if b < unit { return fmt.Sprintf("%d B", b) } + div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } + return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) } diff --git a/internal/pidlock/pidlock.go b/internal/pidlock/pidlock.go index dfe0306..e576970 100644 --- a/internal/pidlock/pidlock.go +++ b/internal/pidlock/pidlock.go @@ -77,6 +77,7 @@ func (l *Lock) Release() error { } l.path = "" // Prevent double-release + return nil } @@ -104,5 +105,6 @@ func isProcessRunning(pid int) bool { // On Unix, FindProcess always succeeds. We need to send signal 0 to check. err = process.Signal(syscall.Signal(0)) + return err == nil } diff --git a/internal/pidlock/pidlock_test.go b/internal/pidlock/pidlock_test.go index d256ee1..dce224d 100644 --- a/internal/pidlock/pidlock_test.go +++ b/internal/pidlock/pidlock_test.go @@ -40,6 +40,7 @@ func TestAcquireBlocksSecondInstance(t *testing.T) { // Acquire first lock lock1, err := Acquire(tmpDir) require.NoError(t, err) + require.NotNil(t, lock1) defer func() { _ = lock1.Release() }() @@ -61,6 +62,7 @@ func TestAcquireWithStaleLock(t *testing.T) { // Should be able to acquire lock (stale lock is cleaned up) lock, err := Acquire(tmpDir) require.NoError(t, err) + require.NotNil(t, lock) defer func() { _ = lock.Release() }() @@ -88,6 +90,7 @@ func TestReleaseIsIdempotent(t *testing.T) { func TestReleaseNilLock(t *testing.T) { var lock *Lock + err := lock.Release() assert.NoError(t, err) } @@ -98,6 +101,7 @@ func TestAcquireCreatesDirectory(t *testing.T) { lock, err := Acquire(nestedDir) require.NoError(t, err) + require.NotNil(t, lock) defer func() { _ = lock.Release() }() diff --git a/internal/s3/client.go b/internal/s3/client.go index 2861be7..1079b8f 100644 --- a/internal/s3/client.go +++ b/internal/s3/client.go @@ -42,7 +42,7 @@ type Config struct { // Used to suppress SDK warnings about checksums. type nopLogger struct{} -func (nopLogger) Logf(classification logging.Classification, format string, v ...interface{}) {} +func (nopLogger) Logf(classification logging.Classification, format string, v ...any) {} // NewClient creates a new S3 client with the provided configuration. // It establishes a connection to the S3-compatible storage service and @@ -92,6 +92,7 @@ func (c *Client) PutObject(ctx context.Context, key string, data io.Reader) erro Key: aws.String(fullKey), Body: data, }) + return err } @@ -137,6 +138,7 @@ func (c *Client) PutObjectWithProgress(ctx context.Context, key string, data io. // close the returned reader when done to avoid resource leaks. func (c *Client) GetObject(ctx context.Context, key string) (io.ReadCloser, error) { fullKey := c.prefix + key + result, err := c.s3Client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(fullKey), @@ -144,6 +146,7 @@ func (c *Client) GetObject(ctx context.Context, key string) (io.ReadCloser, erro if err != nil { return nil, err } + return result.Body, nil } @@ -156,6 +159,7 @@ func (c *Client) DeleteObject(ctx context.Context, key string) error { Bucket: aws.String(c.bucket), Key: aws.String(fullKey), }) + return err } @@ -168,6 +172,7 @@ func (c *Client) ListObjects(ctx context.Context, prefix string) ([]string, erro fullPrefix := c.prefix + prefix var keys []string + paginator := s3.NewListObjectsV2Paginator(c.s3Client, &s3.ListObjectsV2Input{ Bucket: aws.String(c.bucket), Prefix: aws.String(fullPrefix), @@ -186,6 +191,7 @@ func (c *Client) ListObjects(ctx context.Context, prefix string) ([]string, erro if len(key) > len(c.prefix) { key = key[len(c.prefix):] } + keys = append(keys, key) } } @@ -200,18 +206,23 @@ func (c *Client) ListObjects(ctx context.Context, prefix string) ([]string, erro // Note: This method returns false for any error, not just "not found". func (c *Client) HeadObject(ctx context.Context, key string) (bool, error) { fullKey := c.prefix + key + _, err := c.s3Client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(fullKey), }) if err != nil { - var notFound *s3types.NotFound - var noSuchKey *s3types.NoSuchKey + var ( + notFound *s3types.NotFound + noSuchKey *s3types.NoSuchKey + ) if errors.As(err, ¬Found) || errors.As(err, &noSuchKey) { return false, nil } + return false, err } + return true, nil } @@ -247,6 +258,7 @@ func (c *Client) ListObjectsStream(ctx context.Context, prefix string, recursive page, err := paginator.NextPage(ctx) if err != nil { ch <- ObjectInfo{Err: err} + return } @@ -257,6 +269,7 @@ func (c *Client) ListObjectsStream(ctx context.Context, prefix string, recursive if len(key) > len(c.prefix) { key = key[len(c.prefix):] } + ch <- ObjectInfo{ Key: key, Size: *obj.Size, @@ -275,6 +288,7 @@ func (c *Client) ListObjectsStream(ctx context.Context, prefix string, recursive // Returns an error if the object doesn't exist or if the operation fails. func (c *Client) StatObject(ctx context.Context, key string) (*ObjectInfo, error) { fullKey := c.prefix + key + result, err := c.s3Client.HeadObject(ctx, &s3.HeadObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(fullKey), @@ -313,6 +327,7 @@ func (c *Client) Endpoint() string { if c.endpoint == "" { return "s3.amazonaws.com" } + return c.endpoint } @@ -329,11 +344,14 @@ func (pr *progressReader) Read(p []byte) (int, error) { n, err := pr.reader.Read(p) if n > 0 { atomic.AddInt64(&pr.read, int64(n)) + if pr.callback != nil { - if callbackErr := pr.callback(atomic.LoadInt64(&pr.read)); callbackErr != nil { + callbackErr := pr.callback(atomic.LoadInt64(&pr.read)) + if callbackErr != nil { return n, callbackErr } } } + return n, err } diff --git a/internal/s3/client_test.go b/internal/s3/client_test.go index 1bb267e..7ebab27 100644 --- a/internal/s3/client_test.go +++ b/internal/s3/client_test.go @@ -12,7 +12,8 @@ import ( func TestClient(t *testing.T) { ts := NewTestServer(t) defer func() { - if err := ts.Cleanup(); err != nil { + err := ts.Cleanup() + if err != nil { t.Errorf("cleanup failed: %v", err) } }() @@ -35,6 +36,7 @@ func TestClient(t *testing.T) { // Test PutObject testKey := "foo/bar.txt" testData := []byte("test data") + err = client.PutObject(ctx, testKey, bytes.NewReader(testData)) if err != nil { t.Fatalf("failed to put object: %v", err) @@ -46,7 +48,8 @@ func TestClient(t *testing.T) { t.Fatalf("failed to get object: %v", err) } defer func() { - if err := reader.Close(); err != nil { + err := reader.Close() + if err != nil { t.Errorf("failed to close reader: %v", err) } }() @@ -65,6 +68,7 @@ func TestClient(t *testing.T) { if err != nil { t.Fatalf("failed to head object: %v", err) } + if !exists { t.Error("expected object to exist") } @@ -74,9 +78,11 @@ func TestClient(t *testing.T) { if err != nil { t.Fatalf("failed to list objects: %v", err) } + if len(keys) != 1 { t.Errorf("expected 1 key, got %d", len(keys)) } + if keys[0] != testKey { t.Errorf("unexpected key: got %s, want %s", keys[0], testKey) } @@ -92,6 +98,7 @@ func TestClient(t *testing.T) { if err != nil { t.Fatalf("failed to head object after deletion: %v", err) } + if exists { t.Error("expected object to not exist after deletion") } diff --git a/internal/s3/s3_test.go b/internal/s3/s3_test.go index bc359f0..d21e86a 100644 --- a/internal/s3/s3_test.go +++ b/internal/s3/s3_test.go @@ -3,6 +3,7 @@ package s3_test import ( "bytes" "context" + "errors" "fmt" "io" "net/http" @@ -57,7 +58,8 @@ func NewTestServer(t *testing.T) *TestServer { // Start server in background go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + err := server.ListenAndServe() + if err != nil && !errors.Is(err, http.ErrServerClosed) { t.Logf("test server error: %v", err) } }() @@ -77,7 +79,7 @@ func NewTestServer(t *testing.T) *TestServer { "", )), config.WithClientLogMode(aws.LogRetries|aws.LogRequestWithBody|aws.LogResponseWithBody), - config.WithLogger(logging.LoggerFunc(func(classification logging.Classification, format string, v ...interface{}) { + config.WithLogger(logging.LoggerFunc(func(classification logging.Classification, format string, v ...any) { // Capture logs to buffer instead of stdout fmt.Fprintf(logBuf, "SDK %s %s %s\n", time.Now().Format("2006/01/02 15:04:05"), @@ -125,7 +127,8 @@ func (ts *TestServer) Cleanup() error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if err := ts.server.Shutdown(ctx); err != nil { + err := ts.server.Shutdown(ctx) + if err != nil { return err } @@ -141,7 +144,8 @@ func (ts *TestServer) Client() *s3.Client { func TestBasicS3Operations(t *testing.T) { ts := NewTestServer(t) defer func() { - if err := ts.Cleanup(); err != nil { + err := ts.Cleanup() + if err != nil { t.Errorf("cleanup failed: %v", err) } }() @@ -172,7 +176,8 @@ func TestBasicS3Operations(t *testing.T) { t.Fatalf("failed to get object: %v", err) } defer func() { - if err := result.Body.Close(); err != nil { + err := result.Body.Close() + if err != nil { t.Errorf("failed to close body: %v", err) } }() @@ -192,7 +197,8 @@ func TestBasicS3Operations(t *testing.T) { func TestBlobOperations(t *testing.T) { ts := NewTestServer(t) defer func() { - if err := ts.Cleanup(); err != nil { + err := ts.Cleanup() + if err != nil { t.Errorf("cleanup failed: %v", err) } }() @@ -255,7 +261,8 @@ func TestBlobOperations(t *testing.T) { func TestMetadataOperations(t *testing.T) { ts := NewTestServer(t) defer func() { - if err := ts.Cleanup(); err != nil { + err := ts.Cleanup() + if err != nil { t.Errorf("cleanup failed: %v", err) } }()