Files
mcr/internal/oci/delete.go
Kyle Isom c01e7ffa30 Phase 7: OCI delete path for manifests and blobs
Manifest delete (DELETE /v2/<name>/manifests/<digest>): rejects tag
references with 405 UNSUPPORTED per OCI spec, cascades to tags and
manifest_blobs via ON DELETE CASCADE, returns 202 Accepted.

Blob delete (DELETE /v2/<name>/blobs/<digest>): removes manifest_blobs
associations only — blob row and file are preserved for GC to handle,
since other repos may reference the same content-addressed blob.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 20:23:47 -07:00

85 lines
2.4 KiB
Go

package oci
import (
"errors"
"fmt"
"net/http"
"git.wntrmute.dev/kyle/mcr/internal/db"
"git.wntrmute.dev/kyle/mcr/internal/policy"
)
// handleManifestDelete handles DELETE /v2/<name>/manifests/<digest>.
// Per OCI spec, deletion by tag is not supported — only by digest.
func (h *Handler) handleManifestDelete(w http.ResponseWriter, r *http.Request, repo, reference string) {
if !h.checkPolicy(w, r, policy.ActionDelete, repo) {
return
}
// Reference must be a digest, not a tag.
if !isDigest(reference) {
writeOCIError(w, "UNSUPPORTED", http.StatusMethodNotAllowed,
"manifest deletion by tag is not supported; use digest")
return
}
repoID, err := h.db.GetRepositoryByName(repo)
if err != nil {
if errors.Is(err, db.ErrRepoNotFound) {
writeOCIError(w, "NAME_UNKNOWN", http.StatusNotFound,
fmt.Sprintf("repository %q not found", repo))
return
}
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
if err := h.db.DeleteManifest(repoID, reference); err != nil {
if errors.Is(err, db.ErrManifestNotFound) {
writeOCIError(w, "MANIFEST_UNKNOWN", http.StatusNotFound,
fmt.Sprintf("manifest %q not found", reference))
return
}
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
h.audit(r, "manifest_deleted", repo, reference)
w.WriteHeader(http.StatusAccepted)
}
// handleBlobDelete handles DELETE /v2/<name>/blobs/<digest>.
// Removes manifest_blobs associations for this repo only. Does not delete
// the blob row or file — that is GC's responsibility.
func (h *Handler) handleBlobDelete(w http.ResponseWriter, r *http.Request, repo, digest string) {
if !h.checkPolicy(w, r, policy.ActionDelete, repo) {
return
}
repoID, err := h.db.GetRepositoryByName(repo)
if err != nil {
if errors.Is(err, db.ErrRepoNotFound) {
writeOCIError(w, "NAME_UNKNOWN", http.StatusNotFound,
fmt.Sprintf("repository %q not found", repo))
return
}
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
if err := h.db.DeleteBlobFromRepo(repoID, digest); err != nil {
if errors.Is(err, db.ErrBlobNotFound) {
writeOCIError(w, "BLOB_UNKNOWN", http.StatusNotFound,
fmt.Sprintf("blob %q not found in repository", digest))
return
}
writeOCIError(w, "UNKNOWN", http.StatusInternalServerError, "internal error")
return
}
h.audit(r, "blob_deleted", repo, digest)
w.WriteHeader(http.StatusAccepted)
}