working commit
This commit is contained in:
@@ -99,7 +99,6 @@ EXTRA_mstored_SOURCES += \
|
|||||||
app/imageoper/getman.go \
|
app/imageoper/getman.go \
|
||||||
app/imageoper/getrefer.go \
|
app/imageoper/getrefer.go \
|
||||||
app/imageoper/gettags.go \
|
app/imageoper/gettags.go \
|
||||||
app/imageoper/imgaux.go \
|
|
||||||
app/imageoper/listman.go \
|
app/imageoper/listman.go \
|
||||||
app/imageoper/manexist.go \
|
app/imageoper/manexist.go \
|
||||||
app/imageoper/ociaux.go \
|
app/imageoper/ociaux.go \
|
||||||
@@ -146,7 +145,6 @@ EXTRA_mstored_SOURCES += \
|
|||||||
pkg/accntcli/updgrant.go \
|
pkg/accntcli/updgrant.go \
|
||||||
pkg/auxhttp/basic.go \
|
pkg/auxhttp/basic.go \
|
||||||
pkg/auxhttp/crange.go \
|
pkg/auxhttp/crange.go \
|
||||||
pkg/auxoci/ociaux.go \
|
|
||||||
pkg/auxpwd/passwd.go \
|
pkg/auxpwd/passwd.go \
|
||||||
pkg/auxtool/cleandir.go \
|
pkg/auxtool/cleandir.go \
|
||||||
pkg/auxtool/fileex.go \
|
pkg/auxtool/fileex.go \
|
||||||
|
|||||||
+22
-23
@@ -411,29 +411,28 @@ EXTRA_mstored_SOURCES = cmd/mstored/starter/starter.go \
|
|||||||
app/imageoper/delblob.go app/imageoper/delman.go \
|
app/imageoper/delblob.go app/imageoper/delman.go \
|
||||||
app/imageoper/getblob.go app/imageoper/getman.go \
|
app/imageoper/getblob.go app/imageoper/getman.go \
|
||||||
app/imageoper/getrefer.go app/imageoper/gettags.go \
|
app/imageoper/getrefer.go app/imageoper/gettags.go \
|
||||||
app/imageoper/imgaux.go app/imageoper/listman.go \
|
app/imageoper/listman.go app/imageoper/manexist.go \
|
||||||
app/imageoper/manexist.go app/imageoper/ociaux.go \
|
app/imageoper/ociaux.go app/imageoper/operator.go \
|
||||||
app/imageoper/operator.go app/imageoper/patchupload.go \
|
app/imageoper/patchupload.go app/imageoper/postupload.go \
|
||||||
app/imageoper/postupload.go app/imageoper/putman.go \
|
app/imageoper/putman.go app/imageoper/putupload.go \
|
||||||
app/imageoper/putupload.go app/imageoper/service.go \
|
app/imageoper/service.go app/imageoper/version.go \
|
||||||
app/imageoper/version.go app/locker/locker.go \
|
app/locker/locker.go app/logger/logger.go \
|
||||||
app/logger/logger.go app/maindb/account.go app/maindb/blob.go \
|
app/maindb/account.go app/maindb/blob.go app/maindb/file.go \
|
||||||
app/maindb/file.go app/maindb/grant.go app/maindb/init.go \
|
app/maindb/grant.go app/maindb/init.go app/maindb/maindb.go \
|
||||||
app/maindb/maindb.go app/maindb/manifest.go \
|
app/maindb/manifest.go app/maindb/scheme.go \
|
||||||
app/maindb/scheme.go app/router/bindobj.go \
|
app/router/bindobj.go app/router/context.go \
|
||||||
app/router/context.go app/router/corsmw.go \
|
app/router/corsmw.go app/router/loggingmw.go \
|
||||||
app/router/loggingmw.go app/router/pathc.go \
|
app/router/pathc.go app/router/recovermw.go \
|
||||||
app/router/recovermw.go app/router/router.go \
|
app/router/router.go app/server/server.go \
|
||||||
app/server/server.go app/service/service.go \
|
app/service/service.go app/storage/storage.go \
|
||||||
app/storage/storage.go pkg/accntcli/client.go \
|
pkg/accntcli/client.go pkg/accntcli/createacc.go \
|
||||||
pkg/accntcli/createacc.go pkg/accntcli/creategrant.go \
|
pkg/accntcli/creategrant.go pkg/accntcli/delacc.go \
|
||||||
pkg/accntcli/delacc.go pkg/accntcli/delgrant.go \
|
pkg/accntcli/delgrant.go pkg/accntcli/getacc.go \
|
||||||
pkg/accntcli/getacc.go pkg/accntcli/getgrant.go \
|
pkg/accntcli/getgrant.go pkg/accntcli/httpcall.go \
|
||||||
pkg/accntcli/httpcall.go pkg/accntcli/listacc.go \
|
pkg/accntcli/listacc.go pkg/accntcli/listgrants.go \
|
||||||
pkg/accntcli/listgrants.go pkg/accntcli/referer.go \
|
pkg/accntcli/referer.go pkg/accntcli/servhello.go \
|
||||||
pkg/accntcli/servhello.go pkg/accntcli/updateacc.go \
|
pkg/accntcli/updateacc.go pkg/accntcli/updgrant.go \
|
||||||
pkg/accntcli/updgrant.go pkg/auxhttp/basic.go \
|
pkg/auxhttp/basic.go pkg/auxhttp/crange.go \
|
||||||
pkg/auxhttp/crange.go pkg/auxoci/ociaux.go \
|
|
||||||
pkg/auxpwd/passwd.go pkg/auxtool/cleandir.go \
|
pkg/auxpwd/passwd.go pkg/auxtool/cleandir.go \
|
||||||
pkg/auxtool/fileex.go pkg/auxtool/randstr.go \
|
pkg/auxtool/fileex.go pkg/auxtool/randstr.go \
|
||||||
pkg/auxtool/tmpfile.go pkg/auxtool/unixnow.go \
|
pkg/auxtool/tmpfile.go pkg/auxtool/unixnow.go \
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ const (
|
|||||||
logdir = "/var/log/mstore"
|
logdir = "/var/log/mstore"
|
||||||
datadir = "/var/data/mstore"
|
datadir = "/var/data/mstore"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
srvname = "mstored"
|
srvname = "mstored"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type Operator struct {
|
|||||||
store *storage.Storage
|
store *storage.Storage
|
||||||
logg *logger.Logger
|
logg *logger.Logger
|
||||||
iLock *locker.Locker
|
iLock *locker.Locker
|
||||||
fLock *locker.Locker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOperator(params *OperatorParams) (*Operator, error) {
|
func NewOperator(params *OperatorParams) (*Operator, error) {
|
||||||
@@ -36,7 +35,6 @@ func NewOperator(params *OperatorParams) (*Operator, error) {
|
|||||||
store: params.Store,
|
store: params.Store,
|
||||||
}
|
}
|
||||||
oper.iLock = locker.NewLocker()
|
oper.iLock = locker.NewLocker()
|
||||||
oper.fLock = locker.NewLocker()
|
|
||||||
oper.logg = logger.NewLoggerWithSubject("fileoper")
|
oper.logg = logger.NewLoggerWithSubject("fileoper")
|
||||||
return oper, err
|
return oper, err
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-8
@@ -15,6 +15,8 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"mstore/pkg/descr"
|
"mstore/pkg/descr"
|
||||||
|
|
||||||
|
ocidigest "github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeleteManifestParams struct {
|
type DeleteManifestParams struct {
|
||||||
@@ -43,26 +45,27 @@ func (oper *Operator) DeleteManifest(ctx context.Context, params *DeleteManifest
|
|||||||
var exists bool
|
var exists bool
|
||||||
var reference string
|
var reference string
|
||||||
|
|
||||||
manifestDescr := descr.Manifest{}
|
mandescr := descr.Manifest{}
|
||||||
|
|
||||||
// Check manifest by digest as name
|
// Check manifest by digest as name
|
||||||
if stringLikeSHADigest(params.Reference) {
|
|
||||||
digest := normalizeSHADigest(params.Reference)
|
digobj, err := ocidigest.Parse(params.Reference)
|
||||||
exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest)
|
if err == nil {
|
||||||
|
exists, mandescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digobj.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
return res, http.StatusNotFound, err
|
return res, http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
reference = manifestDescr.Reference
|
reference = mandescr.Reference
|
||||||
err = oper.deleteManifestObjects(ctx, params.Name, reference)
|
err = oper.deleteManifestObjects(ctx, params.Name, reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check manifest by name and reference
|
// Check manifest by name and reference
|
||||||
exists, manifestDescrs, err := oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference)
|
exists, mandescrs, err := oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -70,8 +73,8 @@ func (oper *Operator) DeleteManifest(ctx context.Context, params *DeleteManifest
|
|||||||
return res, http.StatusNotFound, err
|
return res, http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
reference = params.Reference
|
reference = params.Reference
|
||||||
for _, manifestDescr := range manifestDescrs {
|
for _, mandescr := range mandescrs {
|
||||||
reference = manifestDescr.Reference
|
reference = mandescr.Reference
|
||||||
err = oper.deleteManifestObjects(ctx, params.Name, reference)
|
err = oper.deleteManifestObjects(ctx, params.Name, reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
|
|||||||
+22
-35
@@ -15,8 +15,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"mstore/pkg/auxoci"
|
|
||||||
"mstore/pkg/descr"
|
"mstore/pkg/descr"
|
||||||
|
|
||||||
|
ocidigest "github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GetManifestParams struct {
|
type GetManifestParams struct {
|
||||||
@@ -47,56 +48,42 @@ func (oper *Operator) GetManifest(ctx context.Context, params *GetManifestParams
|
|||||||
oper.iLock.WaitAndLock(resName)
|
oper.iLock.WaitAndLock(resName)
|
||||||
defer oper.iLock.Done(resName)
|
defer oper.iLock.Done(resName)
|
||||||
|
|
||||||
manifestDescr := descr.Manifest{}
|
manDescr := descr.Manifest{}
|
||||||
var exists bool
|
var exists bool
|
||||||
// TODO: checking layers?
|
digobj, err := ocidigest.Parse(params.Reference)
|
||||||
if stringLikeSHADigest(params.Reference) {
|
if err == nil {
|
||||||
digest := normalizeSHADigest(params.Reference)
|
exists, manDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digobj.String())
|
||||||
exists, manifestDescr, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
return res, http.StatusNotFound, err
|
return res, http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
|
res.DockerContentDigest = manDescr.Digest
|
||||||
manifestDigest := auxoci.SHA256DigestFromString(manifestDescr.Payload)
|
size := int64(len(manDescr.Payload))
|
||||||
res.DockerContentDigest = manifestDigest.String()
|
res.ContentLength = strconv.FormatInt(size, 10)
|
||||||
|
res.ContentType = manDescr.ContentType
|
||||||
res.ContentLength = strconv.FormatInt(int64(len(manifestDescr.Payload)), 10)
|
res.Payload = manDescr.Payload
|
||||||
res.ContentType = manifestDescr.ContentType
|
|
||||||
res.Payload = manifestDescr.Payload
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// Create index of manifests
|
// Create index of manifests
|
||||||
exists, manifestDescrs, err := oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference)
|
exists, manDescrs, err := oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
return res, http.StatusNotFound, err
|
return res, http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
/*
|
_, indexdata, err := indexFromManigestDescrs(manDescrs)
|
||||||
index, indexBytes, err := indexFromManigestDescrs(manifestDescrs)
|
if err != nil {
|
||||||
if err != nil {
|
return res, http.StatusInternalServerError, err
|
||||||
return res, http.StatusInternalServerError, err
|
}
|
||||||
}
|
digobj := ocidigest.SHA256.FromBytes(indexdata)
|
||||||
indexDigest := auxoci.SHA256DigestFromString(indexBytes)
|
res.DockerContentDigest = digobj.String()
|
||||||
res.DockerContentDigest = indexDigest.String()
|
size := int64(len(indexdata))
|
||||||
|
res.ContentLength = strconv.FormatInt(size, 10)
|
||||||
res.ContentLength = strconv.FormatInt(int64(len(indexBytes)), 10)
|
res.ContentType = oiiMediaType
|
||||||
res.ContentType = index.MediaType
|
res.Payload = string(indexdata)
|
||||||
res.Payload = string(indexBytes)
|
|
||||||
*/
|
|
||||||
manifestDescr = manifestDescrs[0]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
}
|
}
|
||||||
return res, http.StatusOK, err
|
return res, http.StatusOK, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 (
|
|
||||||
"encoding/hex"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const sha256prefix = "sha256:"
|
|
||||||
const sha512prefix = "sha512:"
|
|
||||||
|
|
||||||
func normalizeSHADigest(digest string) string {
|
|
||||||
if stringLikeSHA256Digest(digest) && !strings.HasPrefix(digest, sha256prefix) {
|
|
||||||
digest = sha256prefix + digest
|
|
||||||
} else if stringLikeSHA512Digest(digest) && !strings.HasPrefix(digest, sha512prefix) {
|
|
||||||
digest = sha512prefix + digest
|
|
||||||
}
|
|
||||||
return digest
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringLikeSHADigest(some string) bool {
|
|
||||||
return stringLikeSHA256Digest(some) || stringLikeSHA512Digest(some)
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringLikeSHA256Digest(some string) bool {
|
|
||||||
if strings.HasPrefix(some, sha256prefix) {
|
|
||||||
some = strings.TrimPrefix(some, sha256prefix)
|
|
||||||
}
|
|
||||||
_, err := hex.DecodeString(some)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(some) == 64 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringLikeSHA512Digest(some string) bool {
|
|
||||||
if strings.HasPrefix(some, sha512prefix) {
|
|
||||||
some = strings.TrimPrefix(some, sha512prefix)
|
|
||||||
}
|
|
||||||
_, err := hex.DecodeString(some)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(some) == 128 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
+27
-18
@@ -15,8 +15,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"mstore/pkg/auxoci"
|
//"mstore/pkg/auxoci"
|
||||||
"mstore/pkg/descr"
|
"mstore/pkg/descr"
|
||||||
|
|
||||||
|
ocidigest "github.com/opencontainers/go-digest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ManifestExistsParams struct {
|
type ManifestExistsParams struct {
|
||||||
@@ -43,35 +45,42 @@ func (oper *Operator) ManifestExists(ctx context.Context, params *ManifestExists
|
|||||||
return res, http.StatusBadRequest, err
|
return res, http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifest descr.Manifest
|
var man descr.Manifest
|
||||||
manifests := make([]descr.Manifest, 0)
|
var exist bool
|
||||||
var exists bool
|
|
||||||
if stringLikeSHA256Digest(params.Reference) {
|
digobj, err := ocidigest.Parse(params.Reference)
|
||||||
digest := fmt.Sprintf("%s:%s", sha256prefix, params.Reference)
|
if err == nil {
|
||||||
exists, manifest, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digest)
|
exist, man, err = oper.mdb.GetManifestByDigest(ctx, params.Name, digobj.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exist {
|
||||||
return res, http.StatusNotFound, err
|
return res, http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
|
size := int64(len(man.Payload))
|
||||||
|
res.ContentLength = strconv.FormatInt(size, 10)
|
||||||
|
res.ContentType = man.ContentType
|
||||||
|
res.DockerContentDigest = man.Digest
|
||||||
|
res.Exists = exist
|
||||||
} else {
|
} else {
|
||||||
exists, manifests, err = oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference)
|
// Create index of manifests
|
||||||
|
exists, manDescrs, err := oper.mdb.GetManifestsByReference(ctx, params.Name, params.Reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
if !exists {
|
if !exists {
|
||||||
return res, http.StatusNotFound, err
|
return res, http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
manifest = manifests[0] // TODO: tmp
|
_, indexdata, err := indexFromManigestDescrs(manDescrs)
|
||||||
|
if err != nil {
|
||||||
|
return res, http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
digobj := ocidigest.SHA256.FromBytes(indexdata)
|
||||||
|
res.DockerContentDigest = digobj.String()
|
||||||
|
size := int64(len(indexdata))
|
||||||
|
res.ContentLength = strconv.FormatInt(size, 10)
|
||||||
|
res.ContentType = oiiMediaType
|
||||||
|
res.Exists = true
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
return res, http.StatusOK, err
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-22
@@ -12,7 +12,7 @@ package imageoper
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"mstore/pkg/auxoci"
|
//"mstore/pkg/auxoci"
|
||||||
"mstore/pkg/auxtool"
|
"mstore/pkg/auxtool"
|
||||||
"mstore/pkg/auxuuid"
|
"mstore/pkg/auxuuid"
|
||||||
"mstore/pkg/descr"
|
"mstore/pkg/descr"
|
||||||
@@ -26,45 +26,50 @@ const (
|
|||||||
oimMediaType = "application/vnd.oci.image.manifest.v1+json"
|
oimMediaType = "application/vnd.oci.image.manifest.v1+json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func indexFromManigestDescrs(manifestDescrs []descr.Manifest) (ocispec.Index, string, error) {
|
func indexFromManigestDescrs(mandescrs []descr.Manifest) (ocispec.Index, []byte, error) {
|
||||||
var err error
|
var err error
|
||||||
var payload string
|
var indexdata []byte
|
||||||
index := ocispec.Index{
|
index := ocispec.Index{
|
||||||
MediaType: oiiMediaType,
|
MediaType: oiiMediaType,
|
||||||
Manifests: make([]ocispec.Descriptor, 0),
|
Manifests: make([]ocispec.Descriptor, 0),
|
||||||
}
|
}
|
||||||
index.Versioned.SchemaVersion = 2
|
index.Versioned.SchemaVersion = 2
|
||||||
for _, manifestDescr := range manifestDescrs {
|
for _, mandescr := range mandescrs {
|
||||||
var ociManifest ocispec.Manifest
|
var man ocispec.Manifest
|
||||||
|
|
||||||
ociManifest.Subject = &ocispec.Descriptor{}
|
man.Subject = &ocispec.Descriptor{}
|
||||||
ociManifest.Subject.Platform = &ocispec.Platform{}
|
man.Subject.Platform = &ocispec.Platform{}
|
||||||
ociManifest.Config = ocispec.Descriptor{}
|
man.Config = ocispec.Descriptor{}
|
||||||
ociManifest.Config.Platform = &ocispec.Platform{}
|
man.Config.Platform = &ocispec.Platform{}
|
||||||
|
|
||||||
err = json.Unmarshal([]byte(manifestDescr.Payload), &ociManifest)
|
err = json.Unmarshal([]byte(mandescr.Payload), &man)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return index, payload, err
|
return index, indexdata, err
|
||||||
}
|
}
|
||||||
|
digobj, err := ocidigest.Parse(mandescr.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return index, indexdata, err
|
||||||
|
}
|
||||||
|
size := int64(len(mandescr.Payload))
|
||||||
descriptor := ocispec.Descriptor{
|
descriptor := ocispec.Descriptor{
|
||||||
MediaType: oimMediaType,
|
MediaType: mandescr.ContentType,
|
||||||
Digest: auxoci.SHA256DigestFromString(manifestDescr.Payload),
|
Digest: digobj,
|
||||||
Size: int64(len(manifestDescr.Payload)),
|
Size: size,
|
||||||
Platform: ociManifest.Subject.Platform,
|
Platform: man.Subject.Platform,
|
||||||
}
|
}
|
||||||
index.Manifests = append(index.Manifests, descriptor)
|
index.Manifests = append(index.Manifests, descriptor)
|
||||||
}
|
}
|
||||||
indexBytes, err := json.Marshal(index)
|
indexdata, err = json.Marshal(index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return index, payload, err
|
return index, indexdata, err
|
||||||
}
|
}
|
||||||
payload = string(indexBytes)
|
indexdata = indexdata
|
||||||
return index, payload, err
|
return index, indexdata, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func descrsFromManifest(name, reference string, manifest *ocispec.Manifest, rawManifest []byte) (descr.Manifest, []descr.Blob, error) {
|
func descrsFromManifest(name, reference string, manifest *ocispec.Manifest, rawManifest []byte) (descr.Manifest, []descr.Blob, error) {
|
||||||
var err error
|
var err error
|
||||||
manifestDescr := descr.Manifest{}
|
mandescr := descr.Manifest{}
|
||||||
//configDescr := descr.Blob{}
|
//configDescr := descr.Blob{}
|
||||||
blobDescrs := make([]descr.Blob, 0)
|
blobDescrs := make([]descr.Blob, 0)
|
||||||
|
|
||||||
@@ -72,7 +77,7 @@ func descrsFromManifest(name, reference string, manifest *ocispec.Manifest, rawM
|
|||||||
|
|
||||||
// Make manifest descriptor
|
// Make manifest descriptor
|
||||||
manifestDigest := ocidigest.SHA256.FromBytes(rawManifest).String()
|
manifestDigest := ocidigest.SHA256.FromBytes(rawManifest).String()
|
||||||
manifestDescr = descr.Manifest{
|
mandescr = descr.Manifest{
|
||||||
ID: auxuuid.NewUUID(),
|
ID: auxuuid.NewUUID(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Reference: reference,
|
Reference: reference,
|
||||||
@@ -114,7 +119,7 @@ func descrsFromManifest(name, reference string, manifest *ocispec.Manifest, rawM
|
|||||||
layerMap[string(layer.Digest)] = true
|
layerMap[string(layer.Digest)] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return manifestDescr, blobDescrs, err
|
return mandescr, blobDescrs, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func layersDiff(name, reference string, existingManifest, incomingManifest *ocispec.Manifest, rawManifest []byte) ([]descr.Blob, []descr.Blob, error) {
|
func layersDiff(name, reference string, existingManifest, incomingManifest *ocispec.Manifest, rawManifest []byte) ([]descr.Blob, []descr.Blob, error) {
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type Operator struct {
|
|||||||
store *storage.Storage
|
store *storage.Storage
|
||||||
logg *logger.Logger
|
logg *logger.Logger
|
||||||
iLock *locker.Locker
|
iLock *locker.Locker
|
||||||
fLock *locker.Locker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOperator(params *OperatorParams) (*Operator, error) {
|
func NewOperator(params *OperatorParams) (*Operator, error) {
|
||||||
@@ -36,7 +35,6 @@ func NewOperator(params *OperatorParams) (*Operator, error) {
|
|||||||
store: params.Store,
|
store: params.Store,
|
||||||
}
|
}
|
||||||
oper.iLock = locker.NewLocker()
|
oper.iLock = locker.NewLocker()
|
||||||
oper.fLock = locker.NewLocker()
|
|
||||||
oper.logg = logger.NewLoggerWithSubject("imageoper")
|
oper.logg = logger.NewLoggerWithSubject("imageoper")
|
||||||
return oper, err
|
return oper, err
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-34
@@ -12,13 +12,14 @@ package imageoper
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"mstore/pkg/auxoci"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PutManifestParams struct {
|
type PutManifestParams struct {
|
||||||
@@ -53,14 +54,8 @@ func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams
|
|||||||
err = fmt.Errorf("Empty name")
|
err = fmt.Errorf("Empty name")
|
||||||
return res, http.StatusBadRequest, err
|
return res, http.StatusBadRequest, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check Content-Type
|
// Check Content-Type
|
||||||
var mimeIsAcceptably bool
|
if params.ContentType != oimMimeType && params.ContentType != ddmMimeType {
|
||||||
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)
|
err = fmt.Errorf("Unknown or empty Content-Type: %s", params.ContentType)
|
||||||
return res, http.StatusNotFound, err
|
return res, http.StatusNotFound, err
|
||||||
}
|
}
|
||||||
@@ -86,47 +81,48 @@ func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
inManData := buffer.Bytes()
|
||||||
incomingManifestBytes := buffer.Bytes()
|
if int64(len(inManData)) != contentLength {
|
||||||
|
|
||||||
if int64(len(incomingManifestBytes)) != contentLength {
|
|
||||||
err = fmt.Errorf("Mismatch Content-Length and received manifest size: %d vs %d",
|
err = fmt.Errorf("Mismatch Content-Length and received manifest size: %d vs %d",
|
||||||
contentLength, len(incomingManifestBytes))
|
contentLength, len(inManData))
|
||||||
code := http.StatusInternalServerError
|
code := http.StatusInternalServerError
|
||||||
return res, code, err
|
return res, code, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(incomingManifestBytes) > (4 * 1024 * 1024) {
|
if len(inManData) > (4 * 1024 * 1024) {
|
||||||
err = fmt.Errorf("Payload more 4M: %d bytes", len(incomingManifestBytes))
|
err = fmt.Errorf("Payload more 4M: %d bytes", len(inManData))
|
||||||
code := http.StatusRequestEntityTooLarge
|
code := http.StatusRequestEntityTooLarge
|
||||||
return res, code, err
|
return res, code, err
|
||||||
}
|
}
|
||||||
|
inMan := &ocispec.Manifest{}
|
||||||
incomingManifest, err := auxoci.ParseOCIManifest(incomingManifestBytes)
|
err = json.Unmarshal(inManData, inMan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = fmt.Errorf("Parsing OCI manifest error: %v", err)
|
err = fmt.Errorf("Manifest parsing error: %v", err)
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
if incomingManifest.MediaType == "" {
|
if inMan.MediaType == "" {
|
||||||
incomingManifest.MediaType = params.ContentType
|
inMan.MediaType = params.ContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
name := params.Name
|
name := params.Name
|
||||||
reference := params.Reference
|
reference := params.Reference
|
||||||
|
|
||||||
arch := incomingManifest.Subject.Platform.Architecture
|
if inMan.Subject == nil {
|
||||||
os := incomingManifest.Subject.Platform.OS
|
}
|
||||||
variant := incomingManifest.Subject.Platform.Variant
|
|
||||||
|
|
||||||
manifestExists, existingManifestDescr, err := oper.mdb.GetManifestsByReferenceArchitecture(ctx, name, reference, arch, os, variant)
|
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 {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
incomingManifestDescr, incomingLayerDescrs, err := descrsFromManifest(name, reference, incomingManifest, incomingManifestBytes)
|
inManDescr, inlayerdescrs, err := descrsFromManifest(name, reference, inMan, inManData)
|
||||||
// Always check layer files for availability
|
// Always check layer files for availability
|
||||||
var blobError error
|
var blobError error
|
||||||
for _, blobDescr := range incomingLayerDescrs {
|
for _, blobDescr := range inlayerdescrs {
|
||||||
blobExists, _, err := oper.store.BlobExists(blobDescr.Digest)
|
blobExists, _, err := oper.store.BlobExists(blobDescr.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
@@ -141,32 +137,33 @@ func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams
|
|||||||
// TODO: more relevant code?
|
// TODO: more relevant code?
|
||||||
return res, http.StatusFailedDependency, blobError
|
return res, http.StatusFailedDependency, blobError
|
||||||
}
|
}
|
||||||
if !manifestExists {
|
if !manexist {
|
||||||
// Store manifest and layesrs data
|
// Store manifest and layesrs data
|
||||||
err = oper.mdb.InsertManifestWithLayers(ctx, &incomingManifestDescr, incomingLayerDescrs)
|
err = oper.mdb.InsertManifestWithLayers(ctx, &inManDescr, inlayerdescrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
/* TODO: only update descr
|
/* TODO: only update descr
|
||||||
if bytes.Equal(existingManifestBytes, incomingManifestBytes) {
|
if bytes.Equal(exManData, inManData) {
|
||||||
return res, http.StatusCreated, err
|
return res, http.StatusCreated, err
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
existingManifestBytes := []byte(existingManifestDescr.Payload)
|
exManData := []byte(exMandescr.Payload)
|
||||||
existingManifest, err := auxoci.ParseOCIManifest(existingManifestBytes)
|
exMan := &ocispec.Manifest{}
|
||||||
|
err := json.Unmarshal(exManData, exMan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
addedBlobDescrs, uselessBlobDescrs, err := layersDiff(name, reference,
|
addedBlobDescrs, uselessBlobDescrs, err := layersDiff(name, reference,
|
||||||
existingManifest, incomingManifest, incomingManifestBytes)
|
exMan, inMan, inManData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Starting manifest and blobs transaction
|
// Starting manifest and blobs transaction
|
||||||
err = oper.mdb.UpdateManifestWithBlobs(ctx, &incomingManifestDescr, addedBlobDescrs, uselessBlobDescrs)
|
err = oper.mdb.UpdateManifestWithBlobs(ctx, &inManDescr, addedBlobDescrs, uselessBlobDescrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, http.StatusInternalServerError, err
|
return res, http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
@@ -190,7 +187,7 @@ func (oper *Operator) PutManifest(ctx context.Context, params *PutManifestParams
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, blobDescr := range incomingLayerDescrs {
|
for _, blobDescr := range inlayerdescrs {
|
||||||
// TODO: move the requests to db layer transaction
|
// TODO: move the requests to db layer transaction
|
||||||
blobDescrExists, _, err := oper.mdb.GetBlobByNameDigest(ctx, blobDescr.Name, blobDescr.Digest)
|
blobDescrExists, _, err := oper.mdb.GetBlobByNameDigest(ctx, blobDescr.Name, blobDescr.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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 auxoci
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
ocidigest "github.com/opencontainers/go-digest"
|
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ParseOCIManifest(rawManifest []byte) (*ocispec.Manifest, error) {
|
|
||||||
manifest := &ocispec.Manifest{}
|
|
||||||
manifest.Subject = &ocispec.Descriptor{}
|
|
||||||
manifest.Subject.Platform = &ocispec.Platform{}
|
|
||||||
manifest.Config = ocispec.Descriptor{}
|
|
||||||
manifest.Config.Platform = &ocispec.Platform{}
|
|
||||||
|
|
||||||
err := json.Unmarshal(rawManifest, &manifest)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("Manifest parsing error: %v", err)
|
|
||||||
return manifest, err
|
|
||||||
}
|
|
||||||
return manifest, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func SHA256DigestFromString(payload string) ocidigest.Digest {
|
|
||||||
return ocidigest.SHA256.FromString(payload)
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 digest
|
package digest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 filecli
|
package filecli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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 repocli
|
package repocli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
Reference in New Issue
Block a user