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>
85 lines
2.4 KiB
Go
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)
|
|
}
|