/* * Copyright 2026 Oleg Borodin * * 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" "encoding/json" "errors" "fmt" "io" "net/http" "strconv" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) 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 if params.ContentType != oimMimeType && params.ContentType != ddmMimeType { 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 } inManData := buffer.Bytes() if int64(len(inManData)) != contentLength { err = fmt.Errorf("Mismatch Content-Length and received manifest size: %d vs %d", contentLength, len(inManData)) code := http.StatusInternalServerError return res, code, err } if len(inManData) > (4 * 1024 * 1024) { err = fmt.Errorf("Payload more 4M: %d bytes", len(inManData)) code := http.StatusRequestEntityTooLarge return res, code, err } inMan := &ocispec.Manifest{} inMan.Subject = &ocispec.Descriptor{} inMan.Subject.Platform = &ocispec.Platform{} err = json.Unmarshal(inManData, inMan) if err != nil { err = fmt.Errorf("Manifest parsing error: %v", err) return res, http.StatusInternalServerError, err } if inMan.MediaType == "" { inMan.MediaType = params.ContentType } name := params.Name reference := params.Reference arch := inMan.Subject.Platform.Architecture os := inMan.Subject.Platform.OS variant := inMan.Subject.Platform.Variant manexist, exMandescr, err := oper.mdb.GetManifestsByReferenceArchitecture(ctx, name, reference, arch, os, variant) if err != nil { return res, http.StatusInternalServerError, err } inManDescr, inlayerdescrs, err := descrsFromManifest(name, reference, inMan, inManData) // Always check layer files for availability var blobError error for _, blobDescr := range inlayerdescrs { 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 !manexist { // Store manifest and layesrs data err = oper.mdb.InsertManifestWithLayers(ctx, &inManDescr, inlayerdescrs) if err != nil { return res, http.StatusInternalServerError, err } } else { /* TODO: only update descr if bytes.Equal(exManData, inManData) { return res, http.StatusCreated, err } */ exManData := []byte(exMandescr.Payload) exMan := &ocispec.Manifest{} err := json.Unmarshal(exManData, exMan) if err != nil { return res, http.StatusInternalServerError, err } addedBlobDescrs, uselessBlobDescrs, err := layersDiff(name, reference, exMan, inMan, inManData) if err != nil { return res, http.StatusInternalServerError, err } // Starting manifest and blobs transaction err = oper.mdb.UpdateManifestWithBlobs(ctx, &inManDescr, 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 inlayerdescrs { // 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 }