diff --git a/app/descr/manifest.go b/app/descr/manifest.go index 871505d..ccfb941 100644 --- a/app/descr/manifest.go +++ b/app/descr/manifest.go @@ -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"` +} diff --git a/app/handler/blob.go b/app/handler/blob.go index aaabdb5..a274a46 100644 --- a/app/handler/blob.go +++ b/app/handler/blob.go @@ -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//blobs/ 202 404/405 diff --git a/app/handler/file.go b/app/handler/file.go index ba323bb..15bc792 100644 --- a/app/handler/file.go +++ b/app/handler/file.go @@ -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) } diff --git a/app/handler/manifest.go b/app/handler/manifest.go index 3152948..377e872 100644 --- a/app/handler/manifest.go +++ b/app/handler/manifest.go @@ -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//manifests/ 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//referrers/ 200 404/400 +// GET /v2//referrers/?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//tags/list 200 404 +// GET /v2//tags/list?n=&last= +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) +} diff --git a/app/handler/response.go b/app/handler/response.go index 5b91dd1..b531bbf 100644 --- a/app/handler/response.go +++ b/app/handler/response.go @@ -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) } diff --git a/app/maindb/blob.go b/app/maindb/blob.go index 908f575..4ef26f7 100644 --- a/app/maindb/blob.go +++ b/app/maindb/blob.go @@ -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 diff --git a/app/maindb/manifest.go b/app/maindb/manifest.go index c63ae21..55fad9e 100644 --- a/app/maindb/manifest.go +++ b/app/maindb/manifest.go @@ -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) diff --git a/app/operator/blob.go b/app/operator/blob.go index 65b0fa0..2754b65 100644 --- a/app/operator/blob.go +++ b/app/operator/blob.go @@ -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: -// Range: 0- - 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 } diff --git a/app/operator/manifest.go b/app/operator/manifest.go index dd30d8b..33f9931 100644 --- a/app/operator/manifest.go +++ b/app/operator/manifest.go @@ -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 +} diff --git a/app/router/context.go b/app/router/context.go index 0fcb42c..711f01e 100644 --- a/app/router/context.go +++ b/app/router/context.go @@ -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) } diff --git a/app/service/service.go b/app/service/service.go index 94de53c..3e96237 100644 --- a/app/service/service.go +++ b/app/service/service.go @@ -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//manifests/ end-3 - // - // refers to the namespace of the repository. - // MUST be either (a) the digest of the manifest or (b) a tag. - // The MUST NOT be in any other format. - // - // Throughout this document, MUST match the following regular expression: - // - // [a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)* - // - // Throughout this document, 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//manifests/ - - 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//manifests/ end-3 - // - // refers to the namespace of the repository. MUST be either - // (a) the digest of the manifest or (b) a tag. The MUST NOT be in - // any other format. Throughout this document, 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 value. - // If the registry name is registry.example.org:5000, those clients would be limited - // to a 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 that would cause this limit to be exceeded. - // - // Throughout this document, 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 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//blobs/ - // - // is the namespace of the repository, and 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//blobs/uploads/?digest= - // - // Content-Length: - // Content-Type: application/octet-stream - // - // - // - // Here, is the repository's namespace, is the blob's digest, - // and is the size (in bytes) of the blob. - // - // The Content-Length header MUST match the blob's actual content length. - // Likewise, the 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: - // - // Here, 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 is the size in bytes - // (see the blob PATCH definition for usage): - // - // OCI-Chunk-Min-Length: - // - // Please reference the above section for restrictions on the . - // - // 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: - // - // Content-Type: application/octet-stream - // Content-Range: - // Content-Length: - // - // - // - // The 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): - // - // ?digest= - // - // Content-Length: - // Content-Range: - // Content-Type: application/octet-stream - // - // OPTIONAL: - // - // The closing PUT request MUST include the 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: - // - // Here, 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//blobs/ end-2 - // - // is the namespace of the repository, and 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//blobs/ - // - // is the namespace of the repository, and 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()