mirror of
https://github.com/project-zot/zot.git
synced 2024-12-16 21:56:37 -05:00
fix(storage): do not open/download blobs when validating manifests (#1566)
when pushing manifests, zot will validate blobs (layers + config blob) are present in repo, currently it opens(in case of filesystem storage) or download( in case of cloud storage) each blob. fixed that by adding a new method ImageStore.CheckBlobPresence() on storage to check blobs presence without checking the cache like ImageStore.CheckBlob() method does. Signed-off-by: Petu Eusebiu <peusebiu@cisco.com>
This commit is contained in:
parent
f3aa855405
commit
5494a1b8d6
9 changed files with 136 additions and 18 deletions
|
@ -3439,7 +3439,12 @@ func TestCrossRepoMount(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
ctlr := makeController(conf, dir, "../../test/data")
|
err := os.MkdirAll(path.Join(dir, "zot-cve-test"), storageConstants.DefaultDirPerms)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ctlr := makeController(conf, path.Join(dir, "zot-cve-test"), "../../test/data/zot-cve-test")
|
||||||
|
|
||||||
|
ctlr.Config.Storage.RootDirectory = dir
|
||||||
ctlr.Config.Storage.RemoteCache = false
|
ctlr.Config.Storage.RemoteCache = false
|
||||||
ctlr.Config.Storage.Dedupe = false
|
ctlr.Config.Storage.Dedupe = false
|
||||||
|
|
||||||
|
@ -3551,7 +3556,7 @@ func TestCrossRepoMount(t *testing.T) {
|
||||||
cm.StartAndWait(port)
|
cm.StartAndWait(port)
|
||||||
|
|
||||||
// wait for dedupe task to run
|
// wait for dedupe task to run
|
||||||
time.Sleep(15 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
|
|
||||||
params["mount"] = string(manifestDigest)
|
params["mount"] = string(manifestDigest)
|
||||||
postResponse, err = client.R().
|
postResponse, err = client.R().
|
||||||
|
|
|
@ -138,8 +138,8 @@ func validateOCIManifest(imgStore storageTypes.ImageStore, repo, reference strin
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := imgStore.GetBlobContent(repo, layer.Digest)
|
ok, _, err := imgStore.StatBlob(repo, layer.Digest)
|
||||||
if err != nil {
|
if err != nil || !ok {
|
||||||
return layer.Digest, zerr.ErrBlobNotFound
|
return layer.Digest, zerr.ErrBlobNotFound
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1087,7 +1087,11 @@ func (is *ImageStoreLocal) BlobPath(repo string, digest godigest.Digest) string
|
||||||
return path.Join(is.rootDir, repo, "blobs", digest.Algorithm().String(), digest.Encoded())
|
return path.Join(is.rootDir, repo, "blobs", digest.Algorithm().String(), digest.Encoded())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckBlob verifies a blob and returns true if the blob is correct.
|
/*
|
||||||
|
CheckBlob verifies a blob and returns true if the blob is correct
|
||||||
|
|
||||||
|
If the blob is not found but it's found in cache then it will be copied over.
|
||||||
|
*/
|
||||||
func (is *ImageStoreLocal) CheckBlob(repo string, digest godigest.Digest) (bool, int64, error) {
|
func (is *ImageStoreLocal) CheckBlob(repo string, digest godigest.Digest) (bool, int64, error) {
|
||||||
var lockLatency time.Time
|
var lockLatency time.Time
|
||||||
|
|
||||||
|
@ -1095,8 +1099,6 @@ func (is *ImageStoreLocal) CheckBlob(repo string, digest godigest.Digest) (bool,
|
||||||
return false, -1, err
|
return false, -1, err
|
||||||
}
|
}
|
||||||
|
|
||||||
blobPath := is.BlobPath(repo, digest)
|
|
||||||
|
|
||||||
if is.dedupe && fmt.Sprintf("%v", is.cache) != fmt.Sprintf("%v", nil) {
|
if is.dedupe && fmt.Sprintf("%v", is.cache) != fmt.Sprintf("%v", nil) {
|
||||||
is.Lock(&lockLatency)
|
is.Lock(&lockLatency)
|
||||||
defer is.Unlock(&lockLatency)
|
defer is.Unlock(&lockLatency)
|
||||||
|
@ -1105,14 +1107,13 @@ func (is *ImageStoreLocal) CheckBlob(repo string, digest godigest.Digest) (bool,
|
||||||
defer is.RUnlock(&lockLatency)
|
defer is.RUnlock(&lockLatency)
|
||||||
}
|
}
|
||||||
|
|
||||||
binfo, err := os.Stat(blobPath)
|
if ok, size, err := is.StatBlob(repo, digest); err == nil || ok {
|
||||||
if err == nil {
|
return true, size, nil
|
||||||
is.log.Debug().Str("blob path", blobPath).Msg("blob path found")
|
|
||||||
|
|
||||||
return true, binfo.Size(), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is.log.Debug().Err(err).Str("blob", blobPath).Msg("failed to find blob, searching it in cache")
|
blobPath := is.BlobPath(repo, digest)
|
||||||
|
|
||||||
|
is.log.Debug().Str("blob", blobPath).Msg("failed to find blob, searching it in cache")
|
||||||
|
|
||||||
// Check blobs in cache
|
// Check blobs in cache
|
||||||
dstRecord, err := is.checkCacheBlob(digest)
|
dstRecord, err := is.checkCacheBlob(digest)
|
||||||
|
@ -1135,6 +1136,24 @@ func (is *ImageStoreLocal) CheckBlob(repo string, digest godigest.Digest) (bool,
|
||||||
return true, blobSize, nil
|
return true, blobSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StatBlob verifies if a blob is present inside a repository. The caller function SHOULD lock from outside.
|
||||||
|
func (is *ImageStoreLocal) StatBlob(repo string, digest godigest.Digest) (bool, int64, error) {
|
||||||
|
if err := digest.Validate(); err != nil {
|
||||||
|
return false, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blobPath := is.BlobPath(repo, digest)
|
||||||
|
|
||||||
|
binfo, err := os.Stat(blobPath)
|
||||||
|
if err != nil {
|
||||||
|
is.log.Debug().Str("blob path", blobPath).Msg("failed to find blob")
|
||||||
|
|
||||||
|
return false, -1, zerr.ErrBlobNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, binfo.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (is *ImageStoreLocal) checkCacheBlob(digest godigest.Digest) (string, error) {
|
func (is *ImageStoreLocal) checkCacheBlob(digest godigest.Digest) (string, error) {
|
||||||
if err := digest.Validate(); err != nil {
|
if err := digest.Validate(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
|
@ -2191,10 +2191,18 @@ func TestGarbageCollect(t *testing.T) {
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(hasBlob, ShouldEqual, false)
|
So(hasBlob, ShouldEqual, false)
|
||||||
|
|
||||||
|
hasBlob, _, err = imgStore.StatBlob(repoName, odigest)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(hasBlob, ShouldEqual, false)
|
||||||
|
|
||||||
hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
|
hasBlob, _, err = imgStore.CheckBlob(repoName, bdigest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(hasBlob, ShouldEqual, true)
|
So(hasBlob, ShouldEqual, true)
|
||||||
|
|
||||||
|
hasBlob, _, err = imgStore.StatBlob(repoName, bdigest)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(hasBlob, ShouldEqual, true)
|
||||||
|
|
||||||
// sleep so orphan blob can be GC'ed
|
// sleep so orphan blob can be GC'ed
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
|
|
@ -987,7 +987,11 @@ func (is *ObjectStorage) BlobPath(repo string, digest godigest.Digest) string {
|
||||||
return path.Join(is.rootDir, repo, "blobs", digest.Algorithm().String(), digest.Encoded())
|
return path.Join(is.rootDir, repo, "blobs", digest.Algorithm().String(), digest.Encoded())
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckBlob verifies a blob and returns true if the blob is correct.
|
/*
|
||||||
|
CheckBlob verifies a blob and returns true if the blob is correct
|
||||||
|
|
||||||
|
If the blob is not found but it's found in cache then it will be copied over.
|
||||||
|
*/
|
||||||
func (is *ObjectStorage) CheckBlob(repo string, digest godigest.Digest) (bool, int64, error) {
|
func (is *ObjectStorage) CheckBlob(repo string, digest godigest.Digest) (bool, int64, error) {
|
||||||
var lockLatency time.Time
|
var lockLatency time.Time
|
||||||
|
|
||||||
|
@ -1036,6 +1040,47 @@ func (is *ObjectStorage) CheckBlob(repo string, digest godigest.Digest) (bool, i
|
||||||
return true, blobSize, nil
|
return true, blobSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StatBlob verifies if a blob is present inside a repository. The caller function SHOULD lock from outside.
|
||||||
|
func (is *ObjectStorage) StatBlob(repo string, digest godigest.Digest) (bool, int64, error) {
|
||||||
|
if err := digest.Validate(); err != nil {
|
||||||
|
return false, -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
blobPath := is.BlobPath(repo, digest)
|
||||||
|
|
||||||
|
binfo, err := is.store.Stat(context.Background(), blobPath)
|
||||||
|
if err == nil && binfo.Size() > 0 {
|
||||||
|
is.log.Debug().Str("blob path", blobPath).Msg("blob path found")
|
||||||
|
|
||||||
|
return true, binfo.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
is.log.Error().Err(err).Str("blob", blobPath).Msg("failed to stat blob")
|
||||||
|
|
||||||
|
return false, -1, zerr.ErrBlobNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// then it's a 'deduped' blob
|
||||||
|
|
||||||
|
// Check blobs in cache
|
||||||
|
dstRecord, err := is.checkCacheBlob(digest)
|
||||||
|
if err != nil {
|
||||||
|
is.log.Error().Err(err).Str("digest", digest.String()).Msg("cache: not found")
|
||||||
|
|
||||||
|
return false, -1, zerr.ErrBlobNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
binfo, err = is.store.Stat(context.Background(), dstRecord)
|
||||||
|
if err != nil {
|
||||||
|
is.log.Error().Err(err).Str("blob", blobPath).Msg("failed to stat blob")
|
||||||
|
|
||||||
|
return false, -1, zerr.ErrBlobNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, binfo.Size(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (is *ObjectStorage) checkCacheBlob(digest godigest.Digest) (string, error) {
|
func (is *ObjectStorage) checkCacheBlob(digest godigest.Digest) (string, error) {
|
||||||
if err := digest.Validate(); err != nil {
|
if err := digest.Validate(); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -1256,7 +1301,7 @@ func (is *ObjectStorage) GetBlob(repo string, digest godigest.Digest, mediaType
|
||||||
return blobReadCloser, binfo.Size(), nil
|
return blobReadCloser, binfo.Size(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlobContent returns blob contents, SHOULD lock from outside.
|
// GetBlobContent returns blob contents, the caller function SHOULD lock from outside.
|
||||||
func (is *ObjectStorage) GetBlobContent(repo string, digest godigest.Digest) ([]byte, error) {
|
func (is *ObjectStorage) GetBlobContent(repo string, digest godigest.Digest) ([]byte, error) {
|
||||||
if err := digest.Validate(); err != nil {
|
if err := digest.Validate(); err != nil {
|
||||||
return []byte{}, err
|
return []byte{}, err
|
||||||
|
@ -1321,7 +1366,7 @@ func (is *ObjectStorage) GetOrasReferrers(repo string, gdigest godigest.Digest,
|
||||||
return common.GetOrasReferrers(is, repo, gdigest, artifactType, is.log)
|
return common.GetOrasReferrers(is, repo, gdigest, artifactType, is.log)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIndexContent returns index.json contents, SHOULD lock from outside.
|
// GetIndexContent returns index.json contents, the caller function SHOULD lock from outside.
|
||||||
func (is *ObjectStorage) GetIndexContent(repo string) ([]byte, error) {
|
func (is *ObjectStorage) GetIndexContent(repo string) ([]byte, error) {
|
||||||
dir := path.Join(is.rootDir, repo)
|
dir := path.Join(is.rootDir, repo)
|
||||||
|
|
||||||
|
|
|
@ -890,6 +890,9 @@ func TestNegativeCasesObjectsStorage(t *testing.T) {
|
||||||
|
|
||||||
_, _, err = imgStore.CheckBlob(testImage, digest)
|
_, _, err = imgStore.CheckBlob(testImage, digest)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
|
_, _, err = imgStore.StatBlob(testImage, digest)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Test ValidateRepo", func(c C) {
|
Convey("Test ValidateRepo", func(c C) {
|
||||||
|
@ -1270,7 +1273,13 @@ func TestS3Dedupe(t *testing.T) {
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(blob, ShouldEqual, buflen)
|
So(blob, ShouldEqual, buflen)
|
||||||
|
|
||||||
_, checkBlobSize1, err := imgStore.CheckBlob("dedupe1", digest)
|
ok, checkBlobSize1, err := imgStore.CheckBlob("dedupe1", digest)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
So(checkBlobSize1, ShouldBeGreaterThan, 0)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ok, checkBlobSize1, err = imgStore.StatBlob("dedupe1", digest)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
So(checkBlobSize1, ShouldBeGreaterThan, 0)
|
So(checkBlobSize1, ShouldBeGreaterThan, 0)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
@ -3745,6 +3754,9 @@ func TestS3DedupeErr(t *testing.T) {
|
||||||
_, err = imgStore.GetBlobContent("repo2", digest)
|
_, err = imgStore.GetBlobContent("repo2", digest)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
|
_, _, err = imgStore.StatBlob("repo2", digest)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
_, _, _, err = imgStore.GetBlobPartial("repo2", digest, "application/vnd.oci.image.layer.v1.tar+gzip", 0, 1)
|
_, _, _, err = imgStore.GetBlobPartial("repo2", digest, "application/vnd.oci.image.layer.v1.tar+gzip", 0, 1)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
|
@ -3829,6 +3841,9 @@ func TestS3DedupeErr(t *testing.T) {
|
||||||
_, err = imgStore.GetBlobContent("repo2", digest)
|
_, err = imgStore.GetBlobContent("repo2", digest)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
|
_, _, err = imgStore.StatBlob("repo2", digest)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
_, _, _, err = imgStore.GetBlobPartial("repo2", digest, "application/vnd.oci.image.layer.v1.tar+gzip", 0, 1)
|
_, _, _, err = imgStore.GetBlobPartial("repo2", digest, "application/vnd.oci.image.layer.v1.tar+gzip", 0, 1)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
|
|
|
@ -259,6 +259,10 @@ func TestStorageAPIs(t *testing.T) {
|
||||||
_, _, err = imgStore.CheckBlob("test", digest)
|
_, _, err = imgStore.CheckBlob("test", digest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ok, _, err := imgStore.StatBlob("test", digest)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
blob, _, err := imgStore.GetBlob("test", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
|
blob, _, err := imgStore.GetBlob("test", digest, "application/vnd.oci.image.layer.v1.tar+gzip")
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
@ -401,6 +405,10 @@ func TestStorageAPIs(t *testing.T) {
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
So(hasBlob, ShouldEqual, false)
|
So(hasBlob, ShouldEqual, false)
|
||||||
|
|
||||||
|
hasBlob, _, err = imgStore.StatBlob("test", digest)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(hasBlob, ShouldEqual, false)
|
||||||
|
|
||||||
err = imgStore.DeleteBlob("test", "inexistent")
|
err = imgStore.DeleteBlob("test", "inexistent")
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
|
@ -457,7 +465,12 @@ func TestStorageAPIs(t *testing.T) {
|
||||||
err = imgStore.FinishBlobUpload("test", bupload, buf, digest)
|
err = imgStore.FinishBlobUpload("test", bupload, buf, digest)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
_, _, err = imgStore.CheckBlob("test", digest)
|
ok, _, err := imgStore.CheckBlob("test", digest)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
ok, _, err = imgStore.StatBlob("test", digest)
|
||||||
|
So(ok, ShouldBeTrue)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
_, _, err = imgStore.GetBlob("test", "inexistent", "application/vnd.oci.image.layer.v1.tar+gzip")
|
_, _, err = imgStore.GetBlob("test", "inexistent", "application/vnd.oci.image.layer.v1.tar+gzip")
|
||||||
|
@ -486,6 +499,9 @@ func TestStorageAPIs(t *testing.T) {
|
||||||
|
|
||||||
_, _, err = imgStore.CheckBlob("test", "inexistent")
|
_, _, err = imgStore.CheckBlob("test", "inexistent")
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
|
_, _, err = imgStore.StatBlob("test", "inexistent")
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Bad image manifest", func() {
|
Convey("Bad image manifest", func() {
|
||||||
|
|
|
@ -38,6 +38,7 @@ type ImageStore interface { //nolint:interfacebloat
|
||||||
DeleteBlobUpload(repo, uuid string) error
|
DeleteBlobUpload(repo, uuid string) error
|
||||||
BlobPath(repo string, digest godigest.Digest) string
|
BlobPath(repo string, digest godigest.Digest) string
|
||||||
CheckBlob(repo string, digest godigest.Digest) (bool, int64, error)
|
CheckBlob(repo string, digest godigest.Digest) (bool, int64, error)
|
||||||
|
StatBlob(repo string, digest godigest.Digest) (bool, int64, error)
|
||||||
GetBlob(repo string, digest godigest.Digest, mediaType string) (io.ReadCloser, int64, error)
|
GetBlob(repo string, digest godigest.Digest, mediaType string) (io.ReadCloser, int64, error)
|
||||||
GetBlobPartial(repo string, digest godigest.Digest, mediaType string, from, to int64,
|
GetBlobPartial(repo string, digest godigest.Digest, mediaType string, from, to int64,
|
||||||
) (io.ReadCloser, int64, int64, error)
|
) (io.ReadCloser, int64, int64, error)
|
||||||
|
|
|
@ -35,6 +35,7 @@ type MockedImageStore struct {
|
||||||
DeleteBlobUploadFn func(repo string, uuid string) error
|
DeleteBlobUploadFn func(repo string, uuid string) error
|
||||||
BlobPathFn func(repo string, digest godigest.Digest) string
|
BlobPathFn func(repo string, digest godigest.Digest) string
|
||||||
CheckBlobFn func(repo string, digest godigest.Digest) (bool, int64, error)
|
CheckBlobFn func(repo string, digest godigest.Digest) (bool, int64, error)
|
||||||
|
StatBlobFn func(repo string, digest godigest.Digest) (bool, int64, error)
|
||||||
GetBlobPartialFn func(repo string, digest godigest.Digest, mediaType string, from, to int64,
|
GetBlobPartialFn func(repo string, digest godigest.Digest, mediaType string, from, to int64,
|
||||||
) (io.ReadCloser, int64, int64, error)
|
) (io.ReadCloser, int64, int64, error)
|
||||||
GetBlobFn func(repo string, digest godigest.Digest, mediaType string) (io.ReadCloser, int64, error)
|
GetBlobFn func(repo string, digest godigest.Digest, mediaType string) (io.ReadCloser, int64, error)
|
||||||
|
@ -251,6 +252,14 @@ func (is MockedImageStore) CheckBlob(repo string, digest godigest.Digest) (bool,
|
||||||
return true, 0, nil
|
return true, 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (is MockedImageStore) StatBlob(repo string, digest godigest.Digest) (bool, int64, error) {
|
||||||
|
if is.StatBlobFn != nil {
|
||||||
|
return is.StatBlobFn(repo, digest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (is MockedImageStore) GetBlobPartial(repo string, digest godigest.Digest, mediaType string, from, to int64,
|
func (is MockedImageStore) GetBlobPartial(repo string, digest godigest.Digest, mediaType string, from, to int64,
|
||||||
) (io.ReadCloser, int64, int64, error) {
|
) (io.ReadCloser, int64, int64, error) {
|
||||||
if is.GetBlobPartialFn != nil {
|
if is.GetBlobPartialFn != nil {
|
||||||
|
|
Loading…
Reference in a new issue