working commit

This commit is contained in:
2026-02-06 09:27:45 +02:00
parent 4f01100473
commit 88bfe00d61
11 changed files with 246 additions and 302 deletions
+5
View File
@@ -12,3 +12,8 @@ type Manifest struct {
CreatedBy string `db:"created_by" json:"createdBy,omitempty"`
UpdatedBy string `db:"updated_by" json:"updatedBy,omitempty"`
}
type Tags struct {
Name string `json:"name" yaml:"name"`
Tags []string `json:"tags" yaml:"tags"`
}
+17 -10
View File
@@ -29,9 +29,12 @@ func (hand *Handler) BlobExists(rctx *router.Context) {
res, code, err := hand.oper.BlobExists(ctx, params)
if err != nil {
hand.logg.Errorf("BlobExist error: %v", err)
} else if res.Exists {
}
if code == http.StatusOK {
rctx.SetHeader("Docker-Content-Digest", res.DockerContentDigest)
rctx.SetHeader("Content-Length", res.ContentLength)
rctx.SetHeader("Content-Type", res.ContentType)
}
rctx.SetStatus(code)
}
@@ -139,18 +142,22 @@ func (hand *Handler) GetBlob(rctx *router.Context) {
hand.logg.Errorf("GetBlob error: %v", err)
}
rctx.SetHeader("Content-Length", res.ContentLength)
rctx.SetHeader("Content-Type", res.ContentType)
rctx.SetHeader("Docker-Content-Digest", res.DockerContentDigest)
rctx.SetStatus(code)
if code == http.StatusOK {
rctx.SetHeader("Content-Length", res.ContentLength)
rctx.SetHeader("Content-Type", res.ContentType)
rctx.SetHeader("Docker-Content-Digest", res.DockerContentDigest)
rctx.SetStatus(code)
defer res.ReadCloser.Close()
_, err = io.Copy(rctx.Writer, res.ReadCloser)
if err != nil {
hand.logg.Errorf("GetFile error: %v", err)
rctx.SetStatus(http.StatusInternalServerError)
defer res.ReadCloser.Close()
_, err = io.Copy(rctx.Writer, res.ReadCloser)
if err != nil {
hand.logg.Errorf("GetFile error: %v", err)
rctx.SetStatus(http.StatusInternalServerError)
return
}
return
}
rctx.SetStatus(code)
}
// DELETE /v2/<name>/blobs/<digest> 202 404/405
+1 -2
View File
@@ -110,6 +110,5 @@ func (hand *Handler) ListFiles(rctx *router.Context) {
rctx.SetStatus(code)
return
}
rctx.SetStatus(code)
rctx.SendJSON(res)
rctx.SendJSON(code, res)
}
+42 -9
View File
@@ -1,7 +1,8 @@
package handler
import (
//"mstore/app/descr"
"net/http"
"mstore/app/operator"
"mstore/app/router"
)
@@ -18,7 +19,7 @@ func (hand *Handler) ManifestExists(rctx *router.Context) {
res, code, err := hand.oper.ManifestExists(ctx, params)
if err != nil {
hand.logg.Errorf("ManifestExist error: %v", err)
} else if res.Exists {
} else if code == http.StatusOK {
rctx.SetHeader("Content-Length", res.ContentLength)
rctx.SetHeader("Content-Type", res.ContentType)
rctx.SetHeader("Docker-Content-Digest", res.DockerContentDigest)
@@ -70,13 +71,14 @@ func (hand *Handler) GetManifest(rctx *router.Context) {
rctx.SetStatus(code)
return
}
rctx.SetHeader("Content-Length", res.ContentLength)
rctx.SetHeader("Content-Type", res.ContentType)
rctx.SetHeader("Docker-Content-Digest", res.DockerContentDigest)
if code == http.StatusOK {
rctx.SetHeader("Content-Length", res.ContentLength)
rctx.SetHeader("Content-Type", res.ContentType)
rctx.SetHeader("Docker-Content-Digest", res.DockerContentDigest)
rctx.SendBytes(code, []byte(res.Payload))
return
}
rctx.SetStatus(code)
rctx.SendBytes([]byte(res.Payload))
}
// DELETE /v2/<name>/manifests/<reference> 200 404
@@ -89,9 +91,40 @@ func (hand *Handler) DeleteManifest(rctx *router.Context) {
Reference: reference,
}
ctx := rctx.GetContext()
res, code, err := hand.oper.DeleteManifest(ctx, params)
_, code, err := hand.oper.DeleteManifest(ctx, params)
if err != nil {
hand.logg.Errorf("DeleteManifest error: %v", err)
}
rctx.SetStatus(code)
}
// GET /v2/<name>/referrers/<digest> 200 404/400
// GET /v2/<name>/referrers/<digest>?artifactType=<artifactType> 200 404/400
func (hand *Handler) GetReferer(rctx *router.Context) {
name, _ := rctx.GetSubpath("name")
digest, _ := rctx.GetSubpath("digest")
params := &operator.GetRefererParams{
Name: name,
Digest: digest,
}
res, code, err := hand.oper.GetReferer(rctx.Ctx, params)
if err != nil {
hand.logg.Errorf("GetReferer error: %v", err)
}
rctx.SendText(code, res.Reference)
}
// GET /v2/<name>/tags/list 200 404
// GET /v2/<name>/tags/list?n=<integer>&last=<integer>
func (hand *Handler) GetTags(rctx *router.Context) {
name, _ := rctx.GetSubpath("name")
params := &operator.GetTagsParams{
Name: name,
}
ctx := rctx.GetContext()
res, code, err := hand.oper.GetTags(ctx, params)
if err != nil {
hand.logg.Errorf("GetTags error: %v", err)
}
rctx.SendJSON(code, res.TagDescr)
}
+4 -2
View File
@@ -1,6 +1,8 @@
package handler
import (
"net/http"
"mstore/app/router"
)
@@ -15,7 +17,7 @@ func (hand *Handler) SendResult(rctx *router.Context, result any) {
Error: false,
Result: result,
}
rctx.SendJSON(response)
rctx.SendJSON(http.StatusOK, response)
}
func (hand *Handler) SendError(rctx *router.Context, err error) {
@@ -23,5 +25,5 @@ func (hand *Handler) SendError(rctx *router.Context, err error) {
Error: true,
Message: err.Error(),
}
rctx.SendJSON(response)
rctx.SendJSON(http.StatusOK, response)
}
+8 -6
View File
@@ -9,18 +9,20 @@ import (
func (db *Database) InsertBlob(ctx context.Context, layer *descr.Blob) error {
var err error
request := `
INSERT INTO blobs(id, name, reference, mediaType, digest, size, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
INSERT INTO blobs(id, name, reference, mediaType, digest, size,
created_at, updated_at, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`
_, err = db.db.Exec(request,
layer.ID, layer.Name, layer.Reference, layer.MediaType,
layer.Digest, layer.Size, layer.CreatedAt, layer.UpdatedAt)
layer.ID, layer.Name, layer.Reference, layer.MediaType, layer.Digest, layer.Size,
layer.CreatedAt, layer.UpdatedAt,
layer.CreatedBy, layer.UpdatedBy)
if err != nil {
return err
}
return err
}
func (db *Database) GetBlobByDigest(ctx context.Context, digest string) (bool, descr.Blob, error) {
func (db *Database) xxxGetBlobByDigest(ctx context.Context, digest string) (bool, descr.Blob, error) {
var err error
blobs := make([]descr.Blob, 0)
res := descr.Blob{}
@@ -42,7 +44,7 @@ func (db *Database) GetBlobByNameDigest(ctx context.Context, name, digest string
blobs := make([]descr.Blob, 0)
res := descr.Blob{}
exists := false
request := `SELECT * FROM blobs WHERE name = $1 AND digest = $1 LIMIT 1`
request := `SELECT * FROM blobs WHERE name = $1 AND digest = $2 LIMIT 1`
err = db.db.Select(&blobs, request, name, digest)
if err != nil {
return exists, res, err
+32 -7
View File
@@ -9,12 +9,39 @@ import (
func (db *Database) InsertManifest(ctx context.Context, manifest *descr.Manifest) error {
var err error
var request string
request = `
INSERT INTO manifests(id, name, reference, contentType, payload, digest, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8);`
request = `INSERT INTO manifests(id, name, reference, contentType, payload, digest,
created_at, updated_at, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`
_, err = db.db.Exec(request, manifest.ID, manifest.Name, manifest.Reference,
manifest.ContentType, manifest.Payload, manifest.Digest,
manifest.CreatedAt, manifest.UpdatedAt)
manifest.CreatedAt, manifest.UpdatedAt,
manifest.CreatedBy, manifest.UpdatedBy)
if err != nil {
return err
}
return err
}
func (db *Database) InsertManifests(ctx context.Context, manifests []*descr.Manifest) error {
var err error
// Begin Tx
tx, err := db.db.BeginTx(ctx, nil)
for _, manifest := range manifests {
var request string
request = `INSERT INTO manifests(id, name, reference, contentType, payload, digest,
created_at, updated_at, created_by, updated_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`
_, err = tx.Exec(request, manifest.ID, manifest.Name, manifest.Reference,
manifest.ContentType, manifest.Payload, manifest.Digest,
manifest.CreatedAt, manifest.UpdatedAt,
manifest.CreatedBy, manifest.UpdatedBy)
if err != nil {
return err
}
}
// Commit
err = tx.Commit()
if err != nil {
return err
}
@@ -25,9 +52,7 @@ func (db *Database) UpdateManifest(ctx context.Context, manifest *descr.Manifest
var err error
var request string
// Manifest
request = `
UPDATE manifests
SET contentType = $1, payload = $2, digest = $3, updated_at = $4
request = `UPDATE manifests SET contentType = $1, payload = $2, digest = $3, updated_at = $4
WHERE name = $5 AND reference = $6`
_, err = db.db.Exec(request, manifest.ContentType, manifest.Payload, manifest.Digest,
manifest.UpdatedAt, manifest.Name, manifest.Reference)
+30 -30
View File
@@ -17,7 +17,8 @@ type BlobExistsParams struct {
type BlobExistsResult struct {
DockerContentDigest string
ContentLength string
Exists bool
ContentType string
//Exists bool
}
func (oper *Operator) BlobExists(ctx context.Context, params *BlobExistsParams) (*BlobExistsResult, int, error) {
@@ -32,37 +33,30 @@ func (oper *Operator) BlobExists(ctx context.Context, params *BlobExistsParams)
err = fmt.Errorf("Empty name")
return res, http.StatusBadRequest, err
}
exists, blobDescr, err := oper.mdb.GetBlobByDigest(ctx, params.Digest)
// Check blob descriptor
descrExists, blobDescr, err := oper.mdb.GetBlobByNameDigest(ctx, params.Digest, params.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if !exists {
if !descrExists {
return res, http.StatusNotFound, err
}
// Check blob file
blobExists, _, err := oper.store.BlobExists(params.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if !blobExists {
return res, http.StatusNotFound, err
}
res.ContentLength = strconv.FormatInt(blobDescr.Size, 10)
res.DockerContentDigest = params.Digest
res.Exists = exists
res.DockerContentDigest = blobDescr.Digest
res.ContentType = blobDescr.MediaType
return res, http.StatusOK, err
}
// https://github.com/opencontainers/distribution-spec/blob/v1.1.1/spec.md
//
// PostUpload
//
// POST then PUT
//
// To push a blob monolithically by using a POST request followed by a PUT request, there are two steps:
//
// - Obtain a session id (upload URL)
// - Upload the blob to said URL
//
// A chunked blob upload is accomplished in three phases:
//
// - Obtain a session ID (upload URL) (POST)
// - Upload the chunks (PATCH)
// Close the session (PUT)
type PostUploadParams struct {
Name string
Digest string
@@ -106,11 +100,6 @@ type PatchUploadResult struct {
Range string
}
// The response for each successful chunk upload MUST be 202 Accepted, and MUST have the following headers:
//
// Location: <location>
// Range: 0-<end-of-range>
func (oper *Operator) PatchUpload(ctx context.Context, params *PatchUploadParams) (*PatchUploadResult, int, error) {
var err error
res := &PatchUploadResult{}
@@ -250,7 +239,11 @@ func (oper *Operator) GetBlob(ctx context.Context, params *GetBlobParams) (*GetB
if err != nil {
return res, http.StatusInternalServerError, err
}
if !blobExists {
descrExist, blobDescr, err := oper.mdb.GetBlobByNameDigest(ctx, params.Name, params.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if !blobExists || !descrExist {
return res, http.StatusNotFound, err
}
@@ -259,6 +252,7 @@ func (oper *Operator) GetBlob(ctx context.Context, params *GetBlobParams) (*GetB
return res, http.StatusInternalServerError, err
}
res.ContentType = blobDescr.MediaType
res.ContentLength = strconv.FormatInt(blobSize, 10)
res.DockerContentDigest = params.Digest
res.ReadCloser = readCloser
@@ -284,19 +278,25 @@ func (oper *Operator) DeleteBlob(ctx context.Context, params *DeleteBlobParams)
return res, http.StatusBadRequest, err
}
// Check namespace record
exists, _, err := oper.mdb.GetBlobByNameDigest(ctx, params.Name, params.Digest)
descrExists, _, err := oper.mdb.GetBlobByNameDigest(ctx, params.Name, params.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if !exists {
oper.logg.Debugf("Blob %s:%s descr exists %v", params.Name, params.Digest, descrExists)
if !descrExists {
return res, http.StatusNotFound, err
}
err = oper.mdb.DeleteBlobByNameDigest(ctx, params.Name, params.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
// Removing blob file if usage == 0
blobUsage, err := oper.mdb.GetBlobUsage(ctx, params.Digest)
oper.logg.Debugf("Blob %s have usage %d", params.Digest, blobUsage)
if err != nil {
return res, http.StatusInternalServerError, err
}
+96 -6
View File
@@ -154,18 +154,20 @@ func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams
incomingManifestDescr, incomingLayerDescrs, err := descrsFromManifest(name, reference, incomingManifest, incomingManifestBytes)
// Always check layer files for availability
var blobError error
for _, layer := range incomingLayerDescrs {
layerExists, _, err := oper.store.BlobExists(layer.Digest)
for _, blobDescr := range incomingLayerDescrs {
blobExists, _, err := oper.store.BlobExists(blobDescr.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if !layerExists {
err := fmt.Errorf("Layer %s not found.", layer.Digest)
if !blobExists {
oper.logg.Warningf("Found incomleted blob binary for %s:%s", blobDescr.Name, blobDescr.Digest)
err := fmt.Errorf("Layer %s not found.", blobDescr.Digest)
blobError = errors.Join(blobError, err)
}
}
if blobError != nil {
return res, http.StatusInternalServerError, blobError
// TODO: more relevant code?
return res, http.StatusFailedDependency, blobError
}
if !manifestExists {
// Store manifest and layesrs data
@@ -213,6 +215,21 @@ func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams
}
}
for _, blobDescr := range incomingLayerDescrs {
// TODO: move the requests to db layer transaction
blobDescrExists, _, err := oper.mdb.GetBlobByNameDigest(ctx, blobDescr.Name, blobDescr.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if !blobDescrExists {
oper.logg.Warningf("Save incomleted blob descriptor for %s:%s", blobDescr.Name, blobDescr.Digest)
err = oper.mdb.InsertBlob(ctx, &blobDescr)
if err != nil {
return res, http.StatusInternalServerError, err
}
}
}
res.Location = fmt.Sprintf(`/v2/%s/manifests/%s`, params.Name, params.Reference)
return res, http.StatusCreated, err
@@ -282,9 +299,21 @@ type DeleteManifestResult struct{}
func (oper *Operator) DeleteManifest(ctx context.Context, params *DeleteManifestParams) (*DeleteManifestResult, int, error) {
var err error
res := &DeleteManifestResult{}
manifestDescr := descr.Manifest{}
if params.Name == "" {
err = fmt.Errorf("Empty name")
return res, http.StatusBadRequest, err
}
if params.Reference == "" {
err = fmt.Errorf("Empty reference")
return res, http.StatusBadRequest, err
}
var exists bool
var reference string
manifestDescr := descr.Manifest{}
// Check manifest by digest as name
if stringLikeSHADigest(params.Reference) {
digest := normalizeSHADigest(params.Reference)
@@ -354,3 +383,64 @@ func (oper *Operator) deleteManifestObjects(ctx context.Context, name, reference
}
return err
}
type GetTagsParams struct {
Name string
}
type GetTagsResult struct {
TagDescr descr.Tags
}
func (oper *Operator) GetTags(ctx context.Context, params *GetTagsParams) (*GetTagsResult, int, error) {
var err error
res := &GetTagsResult{
TagDescr: descr.Tags{
Name: params.Name,
Tags: make([]string, 0),
},
}
if params.Name == "" {
err = fmt.Errorf("Empty name")
return res, http.StatusBadRequest, err
}
manifestDescrs, err := oper.mdb.ListManifestsByName(ctx, params.Name)
if err != nil {
return res, http.StatusInternalServerError, err
}
for _, manifestDescr := range manifestDescrs {
res.TagDescr.Tags = append(res.TagDescr.Tags, manifestDescr.Reference)
}
return res, http.StatusOK, err
}
type GetRefererParams struct {
Name string
Digest string
}
type GetRefererResult struct {
Reference string
}
func (oper *Operator) GetReferer(ctx context.Context, params *GetRefererParams) (*GetRefererResult, int, error) {
var err error
res := &GetRefererResult{}
if params.Name == "" {
err = fmt.Errorf("Empty name")
return res, http.StatusBadRequest, err
}
if params.Digest == "" {
err = fmt.Errorf("Empty digest")
return res, http.StatusBadRequest, err
}
manifests, err := oper.mdb.GetReferer(ctx, params.Name, params.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if len(manifests) == 0 {
return res, http.StatusNotFound, err
}
res.Reference = manifests[0].Reference
return res, http.StatusOK, err
}
+6 -3
View File
@@ -57,16 +57,19 @@ func (rctx *Context) SetStatus(httpStatus int) {
rctx.Writer.WriteHeader(httpStatus)
}
func (rctx *Context) SendJSON(payload any) {
func (rctx *Context) SendJSON(statusCode int, payload any) {
rctx.Writer.Header().Set("Content-Type", "application/json")
rctx.Writer.WriteHeader(statusCode)
json.NewEncoder(rctx.Writer).Encode(payload)
}
func (rctx *Context) SendText(payload string) {
func (rctx *Context) SendText(statusCode int, payload string) {
rctx.Writer.Header().Set("Content-Type", "text/plain")
rctx.Writer.WriteHeader(statusCode)
rctx.Writer.Write([]byte(payload))
}
func (rctx *Context) SendBytes(payload []byte) {
func (rctx *Context) SendBytes(statusCode int, payload []byte) {
rctx.Writer.WriteHeader(statusCode)
rctx.Writer.Write(payload)
}
+5 -227
View File
@@ -67,248 +67,26 @@ func (svc *Service) Build() error {
svc.rout.Get("/v3/api/file/{filepath}", svc.hand.GetFile)
svc.rout.Delete("/v3/api/file/{filepath}", svc.hand.DeleteFile)
svc.rout.Get("/v3/api/files/{filepath}", svc.hand.ListFiles)
svc.rout.Get("/v2/", svc.hand.GetVersion)
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md
//
// Pulling manifests
//
// To pull a manifest, perform a GET request to a URL in the following form:
// /v2/<name>/manifests/<reference> end-3
//
// <name> refers to the namespace of the repository.
// <reference> MUST be either (a) the digest of the manifest or (b) a tag.
// The <reference> MUST NOT be in any other format.
//
// Throughout this document, <name> MUST match the following regular expression:
//
// [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*
//
// Throughout this document, <reference> as a tag MUST be at most 128 characters
// in length and MUST match the following regular expression:
//
// [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}
// Pushing Manifests
//
// To push a manifest, perform a PUT request to a path in the following format,
// and with the following headers and body: /v2/<name>/manifests/<reference>
const reference = `{reference:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}}`
svc.rout.Head(`/v2/{name}/manifests/{reference}`, svc.hand.ManifestExists)
svc.rout.Put(`/v2/{name}/manifests/{reference}`, svc.hand.PutManifest)
// Pulling manifests
//
// To pull a manifest, perform a GET request to a URL in the following form:
// /v2/<name>/manifests/<reference> end-3
//
// <name> refers to the namespace of the repository. <reference> MUST be either
// (a) the digest of the manifest or (b) a tag. The <reference> MUST NOT be in
// any other format. Throughout this document, <name> MUST match the following
// regular expression:
//
// [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*
//
// Implementers note: Many clients impose a limit of 255 characters on the length of
// the concatenation of the registry hostname (and optional port), /, and <name> value.
// If the registry name is registry.example.org:5000, those clients would be limited
// to a <name> of 229 characters (255 minus 25 for the registry hostname and port
// and minus 1 for a / separator). For compatibility with those clients, registries should
// avoid values of <name> that would cause this limit to be exceeded.
//
// Throughout this document, <reference> as a tag MUST be at most 128 characters
// in length and MUST match the following regular expression:
//
// [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}
//
// The client SHOULD include an Accept header indicating which manifest content types
// it supports. In a successful response, the Content-Type header will indicate
// the type of the returned manifest. The registry SHOULD NOT include parameters
// on the Content-Type header. The client SHOULD ignore parameters on the Content-Type
// header.
// The Content-Type header SHOULD match what the client pushed as the manifest's
// Content-Type. If the manifest has a mediaType field, clients SHOULD reject
// unless the mediaType field's value matches the type specified by the Content-Type header.
//
// For more information on the use of Accept headers and content negotiation,
// please see Content Negotiation and RFC7231.
//
// A GET request to an existing manifest URL MUST provide the expected manifest,
// with a response code that MUST be 200 OK. A successful response SHOULD contain
// the digest of the uploaded blob in the header Docker-Content-Digest.
//
// The Docker-Content-Digest header, if present on the response,
// returns the canonical digest of the uploaded blob which MAY differ
// from the provided digest. If the digest does differ,
// it MAY be the case that the hashing algorithms used do not match.
//
// See Content Digests apdx-3 for information on how to detect the hashing
// algorithm in use. Most clients MAY ignore the value, but if it is used,
// the client MUST verify the value matches the returned manifest.
//
// If the <reference> part of a manifest request is a digest, clients SHOULD verify
// the returned manifest matches this digest.
//
// If the manifest is not found in the repository, the response code
// MUST be 404 Not Found.
svc.rout.Get(`/v2/{name}/manifests/{reference}`, svc.hand.GetManifest)
// Pulling blobs
//
// To pull a blob, perform a GET request to a URL in the following form:
// /v2/<name>/blobs/<digest>
//
// <name> is the namespace of the repository, and <digest> is the blob's digest.
//
// A GET request to an existing blob URL MUST provide the expected blob,
// with a response code that MUST be 200 OK. A successful response SHOULD contain
// the digest of the uploaded blob in the header Docker-Content-Digest. //
// If present, the value of this header MUST be a digest matching that of the
// response body. Most clients MAY ignore the value, but if it is used,
// the client MUST verify the value matches the returned response body.
// Clients SHOULD verify that the response body matches the requested digest.
//
// If the blob is not found in the repository, the response code MUST be 404 Not Found.
svc.rout.Delete(`/v2/{name}/manifests/{reference}`, svc.hand.DeleteManifest)
svc.rout.Head(`/v2/{name}/blobs/{digest}`, svc.hand.BlobExists)
// Single POST
//
// Registries MAY support pushing blobs using a single POST request.
//
// To push a blob monolithically by using a single POST request, perform a POST request
// to a URL in the following form, and with the following headers and body:
//
// /v2/<name>/blobs/uploads/?digest=<digest>
//
// Content-Length: <length>
// Content-Type: application/octet-stream
//
// <upload byte stream>
//
// Here, <name> is the repository's namespace, <digest> is the blob's digest,
// and <length> is the size (in bytes) of the blob.
//
// The Content-Length header MUST match the blob's actual content length.
// Likewise, the <digest> MUST match the blob's digest.
//
// Registries that do not support single request monolithic uploads SHOULD
// return a 202 Accepted status code and Location header and clients SHOULD
// proceed with a subsequent PUT request, as described by the POST then PUT upload method.
//
// Successful completion of the request MUST return a 201 Created and MUST include the following header:
//
// Location: <blob-location>
//
// Here, <blob-location> is a pullable blob URL. This location does not necessarily
// have to be served by your registry, for example, in the case of a signed URL from
// some cloud storage provider that your registry generates.
svc.rout.Post(`/v2/{name}/blobs/uploads/`, svc.hand.PostUpload)
// Pushing a blob in chunks
//
// A chunked blob upload is accomplished in three phases:
//
// - Obtain a session ID (upload URL) (POST)
// - Upload the chunks (PATCH)
// - Close the session (PUT)
//
// For information on obtaining a session ID, reference the above section on pushing
// a blob monolithically via the POST/PUT method. The process remains unchanged for
// chunked upload, except that the post request MUST include the following header:
//
// Content-Length: 0
//
// If the registry has a minimum chunk size, the POST response SHOULD include
// the following header, where <size> is the size in bytes
// (see the blob PATCH definition for usage):
//
// OCI-Chunk-Min-Length: <size>
//
// Please reference the above section for restrictions on the <location>.
//
// To upload a chunk, issue a PATCH request to a URL path in the following format,
// and with the following headers and body:
//
// URL path: <location>
//
// Content-Type: application/octet-stream
// Content-Range: <range>
// Content-Length: <length>
//
// <upload byte stream of chunk>
//
// The <location> refers to the URL obtained from the preceding POST request.
svc.rout.Patch(`/v2/{name}/blobs/uploads/{reference}`, svc.hand.PatchUpload)
// To close the session, issue a PUT request to a url in the following format,
// and with the following headers (and optional body, depending on whether or not
// the final chunk was uploaded already via a PATCH request):
//
// <location>?digest=<digest>
//
// Content-Length: <length of chunk, if present>
// Content-Range: <range of chunk, if present>
// Content-Type: application/octet-stream <if chunk provided>
//
// OPTIONAL: <final chunk byte stream>
//
// The closing PUT request MUST include the <digest> of the whole blob
// (not the final chunk) as a query parameter.
//
// The response to a successful closing of the session MUST be 201 Created,
// and MUST contain the following header:
//
// Location: <blob-location>
//
// Here, <blob-location> is a pullable blob URL.
svc.rout.Put(`/v2/{name}/uploads/{reference}`, svc.hand.PutUpload)
// Pulling blobs
//
// To pull a blob, perform a GET request to a URL in the following form:
//
// /v2/<name>/blobs/<digest> end-2
//
// <name> is the namespace of the repository, and <digest> is the blob's digest.
//
// A GET request to an existing blob URL MUST provide the expected blob,
// with a response code that MUST be 200 OK.
// A successful response SHOULD contain the digest of the uploaded blob in the header Docker-Content-Digest.
// If present, the value of this header MUST be a digest matching that of the response body.
// Most clients MAY ignore the value, but if it is used, the client MUST verify the value
// matches the returned response body. Clients SHOULD verify that the response body
// matches the requested digest.
//
// If the blob is not found in the repository, the response code MUST be 404 Not Found.
//
// A registry SHOULD support the Range request header in accordance with RFC 9110.
svc.rout.Get(`/v2/{name}/blobs/{digest}`, svc.hand.GetBlob)
// Deleting Blobs
//
// To delete a blob, perform a DELETE request to a path in the following format:
//
// /v2/<name>/blobs/<digest>
//
// <name> is the namespace of the repository, and <digest> is the digest
// of the blob to be deleted.
//
// Upon success, the registry MUST respond with code 202 Accepted.
// If the blob is not found, a 404 Not Found code MUST be returned.
// If blob deletion is disabled, the registry MUST respond with either
// a 400 Bad Request or a 405 Method Not Allowed.
svc.rout.Delete(`/v2/{name}/blobs/{digest}`, svc.hand.DeleteBlob)
svc.rout.Get(`/v2/{name}/tags/list`, svc.hand.GetTags)
svc.rout.Get(`/v2/{name}/referrers/{digest}`, svc.hand.GetReferer)
svc.rout.NotFound(svc.hand.NotFound)
selector := svc.rout.Selector()