Filter remote object listings by prefix (#129)

`GET /api/v2/remotes/{name}/objects` dropped the `prefix` query parameter, so a filtered listing returned the first page of every artifact in the remote.

- add a `prefix` argument to `ListArtifacts` that matches path prefixes
- pass the `prefix` query parameter through from the objects list handler

Reviewed-on: #129
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #129.
This commit is contained in:
2026-10-04 22:07:21 +11:00
committed by BenVincent
parent 6a08539a78
commit a71d126239
4 changed files with 123 additions and 8 deletions
+1 -1
View File
@@ -62,7 +62,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
+68
View File
@@ -0,0 +1,68 @@
package v2
import (
"context"
"encoding/json"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"git.unkin.net/unkin/artifactapi/internal/database"
"git.unkin.net/unkin/artifactapi/pkg/models"
)
// TestObjectsListPrefix verifies the remote objects listing passes ?prefix=
// through to the database filter.
func TestObjectsListPrefix(t *testing.T) {
if testDSN == "" {
t.Skip("Docker unavailable")
}
ctx := context.Background()
db, err := database.New(testDSN)
if err != nil {
t.Fatal(err)
}
defer db.Close()
const remote = "generic-objs-prefix"
if err := db.CreateRemote(ctx, &models.Remote{
Name: remote, PackageType: models.PackageGeneric, RepoType: models.RepoTypeRemote,
BaseURL: "https://example.com", MutableTTL: 3600,
}); err != nil {
t.Fatal(err)
}
const hash = "sha256:bb22"
if err := db.UpsertBlob(ctx, hash, "blobs/bb/22", 10, "text/plain"); err != nil {
t.Fatal(err)
}
for _, p := range []string{"a/one.txt", "b/two.txt"} {
if err := db.UpsertArtifact(ctx, remote, p, hash, ""); err != nil {
t.Fatal(err)
}
}
router := chi.NewRouter()
router.Mount("/remotes/{name}/objects", NewObjectsHandler(db).Routes())
list := func(query string) []models.Artifact {
t.Helper()
w := httptest.NewRecorder()
router.ServeHTTP(w, httptest.NewRequest("GET", "/remotes/"+remote+"/objects"+query, nil))
if w.Code != 200 {
t.Fatalf("list%s = %d, want 200", query, w.Code)
}
var got []models.Artifact
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
t.Fatalf("decode: %v", err)
}
return got
}
if got := list(""); len(got) != 2 {
t.Fatalf("unfiltered listing returned %d objects, want 2", len(got))
}
if got := list("?prefix=b/"); len(got) != 1 || got[0].Path != "b/two.txt" {
t.Fatalf("prefix=b/ listing = %+v, want only b/two.txt", got)
}
}