From f0c27d4e0791a2a48e4a396be188c698a1ed39ef Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sun, 4 Oct 2026 15:47:47 +1100 Subject: [PATCH] Filter remote object listings by prefix --- internal/api/v2/objects.go | 2 +- internal/database/artifacts.go | 9 +++++---- internal/database/database_test.go | 13 ++++++++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/api/v2/objects.go b/internal/api/v2/objects.go index 4505c3e..2fad2e0 100644 --- a/internal/api/v2/objects.go +++ b/internal/api/v2/objects.go @@ -51,7 +51,7 @@ func (h *ObjectsHandler) list(w http.ResponseWriter, r *http.Request) { remoteName := chi.URLParam(r, "name") limit, offset := pageBounds(r) - artifacts, err := h.db.ListArtifacts(r.Context(), remoteName, limit, offset) + artifacts, err := h.db.ListArtifacts(r.Context(), remoteName, r.URL.Query().Get("prefix"), limit, offset) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/database/artifacts.go b/internal/database/artifacts.go index ab1046f..4e823e2 100644 --- a/internal/database/artifacts.go +++ b/internal/database/artifacts.go @@ -65,7 +65,8 @@ func (db *DB) TouchArtifactAccess(ctx context.Context, remoteName, path string) return err } -func (db *DB) ListArtifacts(ctx context.Context, remoteName string, limit, offset int) ([]models.Artifact, error) { +// ListArtifacts pages a remote's artifacts whose path starts with prefix ("" lists all). +func (db *DB) ListArtifacts(ctx context.Context, remoteName, prefix string, limit, offset int) ([]models.Artifact, error) { rows, err := db.Pool.Query(ctx, ` SELECT a.id, a.remote_name, a.path, a.content_hash, a.upstream_etag, a.upstream_last_modified, a.first_seen_at, a.last_fetched_at, @@ -73,10 +74,10 @@ func (db *DB) ListArtifacts(ctx context.Context, remoteName string, limit, offse b.size_bytes, b.content_type FROM artifacts a JOIN blobs b ON a.content_hash = b.content_hash - WHERE a.remote_name = $1 + WHERE a.remote_name = $1 AND left(a.path, length($2)) = $2 ORDER BY a.path - LIMIT $2 OFFSET $3 - `, remoteName, limit, offset) + LIMIT $3 OFFSET $4 + `, remoteName, prefix, limit, offset) if err != nil { return nil, err } diff --git a/internal/database/database_test.go b/internal/database/database_test.go index ad1fcf7..dd30e76 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -168,10 +168,17 @@ func TestArtifactsAndBlobs(t *testing.T) { if err := testDB.TouchArtifactAccess(ctx(), "r-art", "path/a.txt"); err != nil { t.Fatal(err) } - arts, err := testDB.ListArtifacts(ctx(), "r-art", 10, 0) - if err != nil || len(arts) != 1 { + if err := testDB.UpsertArtifact(ctx(), "r-art", "other/b.txt", hash, ""); err != nil { + t.Fatal(err) + } + arts, err := testDB.ListArtifacts(ctx(), "r-art", "", 10, 0) + if err != nil || len(arts) != 2 { t.Fatalf("list artifacts: %v %v", len(arts), err) } + arts, err = testDB.ListArtifacts(ctx(), "r-art", "path/", 10, 0) + if err != nil || len(arts) != 1 || arts[0].Path != "path/a.txt" { + t.Fatalf("list artifacts with prefix: %+v %v", arts, err) + } if err := testDB.InsertAccessLog(ctx(), "r-art", "path/a.txt", true, 10, 5, "1.2.3.4"); err != nil { t.Fatal(err) } @@ -320,7 +327,7 @@ func TestDatabaseErrorPaths(t *testing.T) { if _, err := bad.ListVirtuals(ctx); err == nil { t.Error("ListVirtuals should error") } - if _, err := bad.ListArtifacts(ctx, "r", 10, 0); err == nil { + if _, err := bad.ListArtifacts(ctx, "r", "", 10, 0); err == nil { t.Error("ListArtifacts should error") } if _, err := bad.ListLocalFiles(ctx, "r", 10, 0); err == nil {