diff --git a/internal/api/v2/local.go b/internal/api/v2/local.go index f1e7ed0..52f2849 100644 --- a/internal/api/v2/local.go +++ b/internal/api/v2/local.go @@ -185,13 +185,35 @@ func (h *LocalHandler) remove(w http.ResponseWriter, r *http.Request) { repoName := chi.URLParam(r, "name") filePath := chi.URLParam(r, "*") - if err := h.db.DeleteLocalFile(r.Context(), repoName, filePath); err != nil { + if err := deleteLocalFile(r.Context(), h.db, repoName, filePath); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } +// deleteLocalFile removes a local file and runs the provider's post-delete hook, +// so provider-derived state (e.g. RPM metadata that feeds generated repodata) +// stops referencing a package that no longer exists. +func deleteLocalFile(ctx context.Context, db *database.DB, repoName, filePath string) error { + if err := db.DeleteLocalFile(ctx, repoName, filePath); err != nil { + return err + } + + remote, err := db.GetRemote(ctx, repoName) + if err != nil { + return nil // file is gone; no repo left to resolve a cleanup hook from + } + prov, err := provider.Get(remote.PackageType) + if err != nil { + return nil + } + if hook, ok := prov.(provider.PostDeleteHook); ok { + return hook.AfterDelete(ctx, repoName, filePath, db) + } + return nil +} + func (h *LocalHandler) DB() *database.DB { return h.db } diff --git a/internal/api/v2/local_evict_cleanup_test.go b/internal/api/v2/local_evict_cleanup_test.go new file mode 100644 index 0000000..a6d493d --- /dev/null +++ b/internal/api/v2/local_evict_cleanup_test.go @@ -0,0 +1,75 @@ +package v2 + +import ( + "context" + "net/http/httptest" + "testing" + + "github.com/go-chi/chi/v5" + + "git.unkin.net/unkin/artifactapi/internal/database" + "git.unkin.net/unkin/artifactapi/internal/provider" + _ "git.unkin.net/unkin/artifactapi/internal/provider/rpm" // register the rpm provider so its PostDeleteHook runs + "git.unkin.net/unkin/artifactapi/pkg/models" +) + +// TestLocalEvictCleansRPMMetadata verifies that evicting an RPM from a local +// repo also removes the derived rpm_metadata row, so generated repodata stops +// listing the deleted package. +func TestLocalEvictCleansRPMMetadata(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 repo = "rpm-evict-cleanup" + if err := db.CreateRemote(ctx, &models.Remote{Name: repo, PackageType: models.PackageRPM, RepoType: models.RepoTypeLocal}); err != nil { + t.Fatal(err) + } + + const hash = "sha256:bb22" + const path = "Packages/example-0.1.0-1.x86_64.rpm" + if err := db.UpsertBlob(ctx, hash, "blobs/bb/22", 2048, "application/x-rpm"); err != nil { + t.Fatal(err) + } + if err := db.CreateLocalFile(ctx, repo, path, hash); err != nil { + t.Fatal(err) + } + if err := db.InsertRPMMetadata(ctx, &provider.RPMMetadata{ + RepoName: repo, FilePath: path, ContentHash: hash, + Name: "example", Version: "0.1.0", Release: "1", Arch: "x86_64", + Requires: []provider.RPMDep{}, Provides: []provider.RPMDep{}, + Files: []provider.RPMFile{}, Changelogs: []provider.RPMChangelog{}, + }); err != nil { + t.Fatal(err) + } + + h := NewObjectsHandler(db) + router := chi.NewRouter() + router.Route("/locals/{name}/objects", func(r chi.Router) { + r.Delete("/*", h.LocalRoutes().ServeHTTP) + }) + + del := httptest.NewRequest("DELETE", "/locals/"+repo+"/objects/"+path, nil) + dw := httptest.NewRecorder() + router.ServeHTTP(dw, del) + if dw.Code != 204 { + t.Fatalf("evict = %d, want 204", dw.Code) + } + + if f, _ := db.GetLocalFile(ctx, repo, path); f != nil { + t.Fatalf("local file still present after evict: %+v", f) + } + entries, err := db.ListRPMMetadataEntries(ctx, repo) + if err != nil { + t.Fatal(err) + } + if len(entries) != 0 { + t.Fatalf("rpm_metadata still present after evict: %+v", entries) + } +} diff --git a/internal/api/v2/objects.go b/internal/api/v2/objects.go index 4851409..4505c3e 100644 --- a/internal/api/v2/objects.go +++ b/internal/api/v2/objects.go @@ -75,7 +75,7 @@ func (h *ObjectsHandler) evictLocal(w http.ResponseWriter, r *http.Request) { repoName := chi.URLParam(r, "name") path := chi.URLParam(r, "*") - if err := h.db.DeleteLocalFile(r.Context(), repoName, path); err != nil { + if err := deleteLocalFile(r.Context(), h.db, repoName, path); err != nil { http.Error(w, fmt.Sprintf("evict failed: %v", err), http.StatusInternalServerError) return } diff --git a/internal/database/rpm_metadata.go b/internal/database/rpm_metadata.go index cfcb84c..e0fc38a 100644 --- a/internal/database/rpm_metadata.go +++ b/internal/database/rpm_metadata.go @@ -32,6 +32,11 @@ func (db *DB) InsertRPMMetadata(ctx context.Context, meta *provider.RPMMetadata) return err } +func (db *DB) DeleteRPMMetadata(ctx context.Context, repoName, filePath string) error { + _, err := db.Pool.Exec(ctx, `DELETE FROM rpm_metadata WHERE repo_name = $1 AND file_path = $2`, repoName, filePath) + return err +} + type RPMMetadataRow struct { RepoName string FilePath string diff --git a/internal/provider/provider.go b/internal/provider/provider.go index fc628b6..eb698ae 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -53,10 +53,20 @@ type PostUploadHook interface { AfterUpload(ctx context.Context, repoName, storagePath, contentHash string, blobs BlobReader, db MetadataStore) } +// PostDeleteHook lets a provider clean up derived state (e.g. RPM metadata that +// feeds generated repodata) after a local file is removed. +type PostDeleteHook interface { + AfterDelete(ctx context.Context, repoName, storagePath string, db MetadataDeleter) error +} + type MetadataStore interface { InsertRPMMetadata(ctx context.Context, meta *RPMMetadata) error } +type MetadataDeleter interface { + DeleteRPMMetadata(ctx context.Context, repoName, filePath string) error +} + type RPMMetadataReader interface { ListRPMMetadataEntries(ctx context.Context, repoName string) ([]RPMMetadata, error) } diff --git a/internal/provider/rpm/rpm.go b/internal/provider/rpm/rpm.go index 3860ac1..e3ce8d6 100644 --- a/internal/provider/rpm/rpm.go +++ b/internal/provider/rpm/rpm.go @@ -151,6 +151,15 @@ func (p *Provider) AfterUpload(ctx context.Context, repoName, storagePath, conte slog.Info("rpm metadata: parsed", "repo", repoName, "name", meta.Name, "version", meta.Version, "arch", meta.Arch) } +func (p *Provider) AfterDelete(ctx context.Context, repoName, storagePath string, db provider.MetadataDeleter) error { + if err := db.DeleteRPMMetadata(ctx, repoName, storagePath); err != nil { + slog.Error("rpm metadata: delete failed", "repo", repoName, "path", storagePath, "error", err) + return err + } + slog.Info("rpm metadata: deleted", "repo", repoName, "path", storagePath) + return nil +} + func rpmDepFromEntry(e rpmlib.Dependency) provider.RPMDep { dep := provider.RPMDep{Name: e.Name()} if e.Flags() != 0 {