working commit
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user