/* * 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 operator import ( "bytes" "context" "errors" "fmt" "io" "net/http" "slices" "strconv" "mstore/pkg/auxoci" "mstore/pkg/descr" ) type ManifestExistsParams struct { Name string Reference string } type ManifestExistsResult struct { ContentLength string ContentType string DockerContentDigest string Exists bool } func (oper *Operator) ManifestExists(ctx context.Context, params *ManifestExistsParams) (*ManifestExistsResult, int, error) { var err error res := &ManifestExistsResult{} 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 manifest descr.Manifest manifests := make([]descr.Manifest, 0) var exists bool if stringLikeSHA256Digest(params.Reference) { digest := fmt.Sprintf("%s:%s", sha256prefix, params.Reference) exists, manifest, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } } else { exists, manifests, err = oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } manifest = manifests[0] // TODO: tmp } digest := auxoci.SHA256DigestFromString(manifest.Payload) payloadSize := len(manifest.Payload) res.ContentLength = strconv.FormatInt(int64(payloadSize), 10) res.ContentType = manifest.ContentType res.DockerContentDigest = digest.String() res.Exists = exists return res, http.StatusOK, err } 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" oicMimeType = "application/vnd.oci.image.config.v1+json" dciMimeType = "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 } // 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 } // Check point oper.logg.Debugf("=== Incoming manifest type: %s", params.ContentType) oper.logg.Debugf("=== Incoming manifest body: %s", string(incomingManifestBytes)) if params.ContentType == oicMimeType || params.ContentType == dciMimeType { return res, http.StatusOK, 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, existengManifestDescr, 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(existengManifestDescr.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 } 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 } } } res.Location = fmt.Sprintf(`/v2/%s/manifests/%s`, params.Name, params.Reference) return res, http.StatusCreated, err } type GetManifestParams struct { Name string Reference string } type GetManifestResult struct { ContentLength string ContentType string DockerContentDigest string Payload string } func (oper *Operator) GetManifest(ctx context.Context, params *GetManifestParams) (*GetManifestResult, int, error) { var err error res := &GetManifestResult{} 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 } manifestDescr := descr.Manifest{} var exists bool // TODO: checking layers? if stringLikeSHADigest(params.Reference) { digest := normalizeSHADigest(params.Reference) exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } manifestDigest := auxoci.SHA256DigestFromString(manifestDescr.Payload) res.DockerContentDigest = manifestDigest.String() res.ContentLength = strconv.FormatInt(int64(len(manifestDescr.Payload)), 10) res.ContentType = manifestDescr.ContentType res.Payload = manifestDescr.Payload } else { exists, manifestDescrs, err := oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } index, indexBytes, err := indexFromManigestDescrs(manifestDescrs) if err != nil { return res, http.StatusInternalServerError, err } indexDigest := auxoci.SHA256DigestFromString(indexBytes) res.DockerContentDigest = indexDigest.String() res.ContentLength = strconv.FormatInt(int64(len(indexBytes)), 10) res.ContentType = index.MediaType res.Payload = string(indexBytes) } return res, http.StatusOK, err } type DeleteManifestParams struct { Name string Reference string } type DeleteManifestResult struct{} func (oper *Operator) DeleteManifest(ctx context.Context, params *DeleteManifestParams) (*DeleteManifestResult, int, error) { var err error res := &DeleteManifestResult{} 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) exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } reference = manifestDescr.Reference err = oper.deleteManifestObjects(ctx, params.Name, reference) if err != nil { return res, http.StatusInternalServerError, err } } else { // Check manifest by name and reference exists, manifestDescrs, err := oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference) if err != nil { return res, http.StatusInternalServerError, err } if !exists { return res, http.StatusNotFound, err } reference = params.Reference for _, manifestDescr := range manifestDescrs { reference = manifestDescr.Reference err = oper.deleteManifestObjects(ctx, params.Name, reference) if err != nil { return res, http.StatusInternalServerError, err } } } // Get blobs associated with the name return res, http.StatusAccepted, err } func (oper *Operator) deleteManifestObjects(ctx context.Context, name, reference string) error { var err error // Get blobs associated with the name layers, err := oper.mdb.GetBlobsByReferense(ctx, name, reference) if err != nil { return err } for _, blob := range layers { // Delete descr record err = oper.mdb.DeleteBlobByNameDigest(ctx, blob.Name, blob.Digest) if err != nil { return err } // Check blob file exists, _, err := oper.store.BlobExists(blob.Digest) if err != nil { return err } if exists { // Check blob usage blobUsage, err := oper.mdb.GetBlobUsage(ctx, blob.Digest) if err != nil { return err } // Delete if blob useless if blobUsage == 0 { err = oper.store.DeleteBlob(blob.Digest) if err != nil { return err } } } } err = oper.mdb.DeleteManifest(ctx, name, reference) if err != nil { return err } 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 } type ListManifestsParams struct { N int64 } type ListManifestsResult struct { Repositories []string `json:"repositories"` } func (oper *Operator) ListManifests(ctx context.Context, params *ListManifestsParams) (*ListManifestsResult, int, error) { var err error res := &ListManifestsResult{ Repositories: make([]string, 0), } refMap := make(map[string]bool) descrs, err := oper.mdb.ListAllManifests(ctx) if err != nil { return res, http.StatusInternalServerError, err } for _, descr := range descrs { _, already := refMap[descr.Name] if !already { refMap[descr.Name] = true } } for key, _ := range refMap { res.Repositories = append(res.Repositories, key) } slices.Sort(res.Repositories) return res, http.StatusOK, err }