Files
mstore/app/imageoper/putman.go
T

213 lines
6.4 KiB
Go

/*
* Copyright 2026 Oleg Borodin <onborodin@gmail.com>
*
* This work is published and licensed under a Creative Commons
* Attribution-NonCommercial-NoDerivatives 4.0 International License.
*
* Distribution of this work is permitted, but commercial use and
* modifications are strictly prohibited.
*/
package imageoper
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"mstore/pkg/auxoci"
)
type PutManifestParams struct {
ContentType string
ContentLength string
Name string
Reference string
Reader io.Reader
}
type PutManifestResult struct {
Location string
}
const (
ddmMimeType = "application/vnd.docker.distribution.manifest.v2+json"
oimMimeType = "application/vnd.oci.image.manifest.v1+json"
XXXoicMimeType = "application/vnd.oci.image.config.v1+json"
XXXdciMimeType = "application/vnd.docker.container.image.v1+json"
)
// TODO: lock for the name-reference or simular?
func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams) (*PutManifestResult, int, error) {
var err error
res := &PutManifestResult{}
if params.Reference == "" {
err = fmt.Errorf("Empty reference")
return res, http.StatusBadRequest, err
}
if params.Name == "" {
err = fmt.Errorf("Empty name")
return res, http.StatusBadRequest, err
}
// Check Content-Type
var mimeIsAcceptably bool
mimeIsAcceptably = mimeIsAcceptably || params.ContentType == oimMimeType
mimeIsAcceptably = mimeIsAcceptably || params.ContentType == ddmMimeType
//mimeIsAcceptably = mimeIsAcceptably || params.ContentType == oicMimeType
//mimeIsAcceptably = mimeIsAcceptably || params.ContentType == dciMimeType
if !mimeIsAcceptably {
err = fmt.Errorf("Unknown or empty Content-Type: %s", params.ContentType)
return res, http.StatusNotFound, err
}
if params.ContentLength == "" {
code := http.StatusLengthRequired
err = fmt.Errorf("Content-Length is empty")
return res, code, err
}
contentLength, err := strconv.ParseInt(params.ContentLength, 10, 64)
if err != nil {
err = fmt.Errorf("Cannot parse Content-Length value [%s]: %v", params.ContentLength, err)
code := http.StatusLengthRequired
return res, code, err
}
resName := params.Name
oper.iLock.WaitAndLock(resName)
defer oper.iLock.Done(resName)
// Copy manifest data
buffer := bytes.NewBuffer(nil)
_, err = io.Copy(buffer, params.Reader)
if err != nil {
return res, http.StatusInternalServerError, err
}
incomingManifestBytes := buffer.Bytes()
if int64(len(incomingManifestBytes)) != contentLength {
err = fmt.Errorf("Mismatch Content-Length and received manifest size: %d vs %d",
contentLength, len(incomingManifestBytes))
code := http.StatusInternalServerError
return res, code, err
}
if len(incomingManifestBytes) > (4 * 1024 * 1024) {
err = fmt.Errorf("Payload more 4M: %d bytes", len(incomingManifestBytes))
code := http.StatusRequestEntityTooLarge
return res, code, err
}
incomingManifest, err := auxoci.ParseOCIManifest(incomingManifestBytes)
if err != nil {
err = fmt.Errorf("Parsing OCI manifest error: %v", err)
return res, http.StatusInternalServerError, err
}
if incomingManifest.MediaType == "" {
incomingManifest.MediaType = params.ContentType
}
name := params.Name
reference := params.Reference
arch := incomingManifest.Subject.Platform.Architecture
os := incomingManifest.Subject.Platform.OS
variant := incomingManifest.Subject.Platform.Variant
manifestExists, existingManifestDescr, err := oper.mdb.GetManifestsByReferenceArchitecture(ctx, name, reference, arch, os, variant)
if err != nil {
return res, http.StatusInternalServerError, err
}
incomingManifestDescr, incomingLayerDescrs, err := descrsFromManifest(name, reference, incomingManifest, incomingManifestBytes)
// Always check layer files for availability
var blobError error
for _, blobDescr := range incomingLayerDescrs {
blobExists, _, err := oper.store.BlobExists(blobDescr.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
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 {
// TODO: more relevant code?
return res, http.StatusFailedDependency, blobError
}
if !manifestExists {
// Store manifest and layesrs data
err = oper.mdb.InsertManifestWithLayers(ctx, &incomingManifestDescr, incomingLayerDescrs)
if err != nil {
return res, http.StatusInternalServerError, err
}
} else {
/* TODO: only update descr
if bytes.Equal(existingManifestBytes, incomingManifestBytes) {
return res, http.StatusCreated, err
}
*/
existingManifestBytes := []byte(existingManifestDescr.Payload)
existingManifest, err := auxoci.ParseOCIManifest(existingManifestBytes)
if err != nil {
return res, http.StatusInternalServerError, err
}
addedBlobDescrs, uselessBlobDescrs, err := layersDiff(name, reference,
existingManifest, incomingManifest, incomingManifestBytes)
if err != nil {
return res, http.StatusInternalServerError, err
}
// Starting manifest and blobs transaction
err = oper.mdb.UpdateManifestWithBlobs(ctx, &incomingManifestDescr, addedBlobDescrs, uselessBlobDescrs)
if err != nil {
return res, http.StatusInternalServerError, err
}
//goto end
for _, blob := range uselessBlobDescrs {
exists, _, err := oper.store.BlobExists(blob.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
blobUsage, err := oper.mdb.GetBlobUsage(ctx, blob.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
if exists && blobUsage == 0 {
err = oper.store.DeleteBlob(blob.Digest)
if err != nil {
return res, http.StatusInternalServerError, err
}
}
}
}
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
}
}
}
//end:
res.Location = fmt.Sprintf(`/v2/%s/manifests/%s`, params.Name, params.Reference)
return res, http.StatusCreated, err
}