1
0
mirror of https://github.com/regclient/regclient.git synced 2025-07-29 09:01:11 +03:00

Refactoring the blob package

The main goal was to remove unnecessary interfaces. To avoid breaking
users, type aliases were used on the old interface names.
Comments were updated to better align with the godoc style.

Signed-off-by: Brandon Mitchell <git@bmitch.net>
This commit is contained in:
Brandon Mitchell
2023-09-26 20:18:19 -04:00
parent 934e52602a
commit cf47d837b7
6 changed files with 273 additions and 223 deletions

View File

@ -1,19 +1,39 @@
// Package blob is the underlying type for pushing and pulling blobs
// Package blob is the underlying type for pushing and pulling blobs.
package blob
import (
"io"
"net/http"
"github.com/opencontainers/go-digest"
"github.com/regclient/regclient/types"
v1 "github.com/regclient/regclient/types/oci/v1"
"github.com/regclient/regclient/types/ref"
)
// Blob interface is used for returning blobs
// Blob interface is used for returning blobs.
type Blob interface {
Common
// GetDescriptor returns the descriptor associated with the blob.
GetDescriptor() types.Descriptor
// RawBody returns the raw content of the blob.
RawBody() ([]byte, error)
// RawHeaders returns the headers received from the registry.
RawHeaders() http.Header
// Response returns the response associated with the blob.
Response() *http.Response
// Digest returns the provided or calculated digest of the blob.
//
// Deprecated: Digest should be replaced by GetDescriptor().Digest.
Digest() digest.Digest
// Length returns the provided or calculated length of the blob.
//
// Deprecated: Length should be replaced by GetDescriptor().Size.
Length() int64
// MediaType returns the Content-Type header received from the registry.
//
// Deprecated: MediaType should be replaced by GetDescriptor().MediaType.
MediaType() string
}
type blobConfig struct {
@ -26,51 +46,52 @@ type blobConfig struct {
rawBody []byte
}
// Opts is used for options to create a new blob.
type Opts func(*blobConfig)
// WithDesc specifies the descriptor associated with the blob
// WithDesc specifies the descriptor associated with the blob.
func WithDesc(d types.Descriptor) Opts {
return func(bc *blobConfig) {
bc.desc = d
}
}
// WithHeader defines the headers received when pulling a blob
// WithHeader defines the headers received when pulling a blob.
func WithHeader(header http.Header) Opts {
return func(bc *blobConfig) {
bc.header = header
}
}
// WithImage provides the OCI Image config needed for config blobs
// WithImage provides the OCI Image config needed for config blobs.
func WithImage(image v1.Image) Opts {
return func(bc *blobConfig) {
bc.image = &image
}
}
// WithRawBody defines the raw blob contents for OCIConfig
// WithRawBody defines the raw blob contents for OCIConfig.
func WithRawBody(raw []byte) Opts {
return func(bc *blobConfig) {
bc.rawBody = raw
}
}
// WithReader defines the reader for a new blob
// WithReader defines the reader for a new blob.
func WithReader(rc io.Reader) Opts {
return func(bc *blobConfig) {
bc.rdr = rc
}
}
// WithRef specifies the reference where the blob was pulled from
// WithRef specifies the reference where the blob was pulled from.
func WithRef(r ref.Ref) Opts {
return func(bc *blobConfig) {
bc.r = r
}
}
// WithResp includes the http response, which is used to extract the headers and reader
// WithResp includes the http response, which is used to extract the headers and reader.
func WithResp(resp *http.Response) Opts {
return func(bc *blobConfig) {
bc.resp = resp

View File

@ -257,16 +257,11 @@ func TestReader(t *testing.T) {
if i != bl {
t.Errorf("read length, expected %d, received %d", bl, i)
}
bSeek, ok := b.(io.Seeker)
if !ok {
t.Errorf("seek interface missing")
return
}
_, err = bSeek.Seek(5, io.SeekStart)
_, err = b.Seek(5, io.SeekStart)
if err == nil {
t.Errorf("seek to non-zero position did not fail")
}
pos, err := bSeek.Seek(0, io.SeekStart)
pos, err := b.Seek(0, io.SeekStart)
if err != nil {
t.Errorf("seek err: %v", err)
return
@ -298,12 +293,7 @@ func TestReader(t *testing.T) {
if i != bl {
t.Errorf("read length, expected %d, received %d", bl, i)
}
bSeek, ok = b.(io.Seeker)
if !ok {
t.Errorf("seek interface missing")
return
}
_, err = bSeek.Seek(0, io.SeekStart)
_, err = b.Seek(0, io.SeekStart)
if err != nil {
t.Errorf("seek err: %v", err)
return
@ -372,10 +362,12 @@ func TestOCI(t *testing.T) {
t.Errorf("failed to unmarshal exBlob: %v", err)
return
}
tests := []struct {
tt := []struct {
name string
opts []Opts
fromJSON []byte
wantRaw []byte
wantJSON []byte
wantDesc types.Descriptor
}{
{
@ -386,6 +378,16 @@ func TestOCI(t *testing.T) {
},
wantDesc: exDesc,
wantRaw: exBlob,
wantJSON: exBlob,
},
{
name: "JSONMarshal",
opts: []Opts{
WithDesc(exDesc),
},
fromJSON: exBlob,
wantDesc: exDesc,
wantJSON: exBlob,
},
{
name: "Config with Default Desc",
@ -404,27 +406,43 @@ func TestOCI(t *testing.T) {
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
oc := NewOCIConfig(tt.opts...)
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
oc := NewOCIConfig(tc.opts...)
if tt.wantDesc.Digest != "" && tt.wantDesc.Digest != oc.GetDescriptor().Digest {
t.Errorf("digest, expected %s, received %s", tt.wantDesc.Digest, oc.GetDescriptor().Digest)
if len(tc.fromJSON) > 0 {
err := oc.UnmarshalJSON(tc.fromJSON)
if err != nil {
t.Errorf("failed to unmarshal json: %v", err)
}
if tt.wantDesc.MediaType != "" && tt.wantDesc.MediaType != oc.GetDescriptor().MediaType {
t.Errorf("media type, expected %s, received %s", tt.wantDesc.MediaType, oc.GetDescriptor().MediaType)
}
if tt.wantDesc.Size > 0 && tt.wantDesc.Size != oc.GetDescriptor().Size {
t.Errorf("size, expected %d, received %d", tt.wantDesc.Size, oc.GetDescriptor().Size)
if tc.wantDesc.Digest != "" && tc.wantDesc.Digest != oc.GetDescriptor().Digest {
t.Errorf("digest, expected %s, received %s", tc.wantDesc.Digest, oc.GetDescriptor().Digest)
}
if len(tt.wantRaw) > 0 {
if tc.wantDesc.MediaType != "" && tc.wantDesc.MediaType != oc.GetDescriptor().MediaType {
t.Errorf("media type, expected %s, received %s", tc.wantDesc.MediaType, oc.GetDescriptor().MediaType)
}
if tc.wantDesc.Size > 0 && tc.wantDesc.Size != oc.GetDescriptor().Size {
t.Errorf("size, expected %d, received %d", tc.wantDesc.Size, oc.GetDescriptor().Size)
}
if len(tc.wantRaw) > 0 {
raw, err := oc.RawBody()
if err != nil {
t.Errorf("config rawbody: %v", err)
return
}
if !bytes.Equal(tt.wantRaw, raw) {
t.Errorf("config bytes, expected %s, received %s", string(tt.wantRaw), string(raw))
if !bytes.Equal(tc.wantRaw, raw) {
t.Errorf("config bytes, expected %s, received %s", string(tc.wantRaw), string(raw))
}
}
if len(tc.wantJSON) > 0 {
ocJSON, err := oc.MarshalJSON()
if err != nil {
t.Errorf("json marshal: %v", err)
return
}
if !bytes.Equal(tc.wantJSON, ocJSON) {
t.Errorf("json marshal, expected %s, received %s", string(tc.wantJSON), string(ocJSON))
}
}
})
@ -476,7 +494,7 @@ func TestTarReader(t *testing.T) {
fh.Close()
dig := digger.Digest()
tests := []struct {
tt := []struct {
name string
opts []Opts
errClose bool
@ -507,14 +525,14 @@ func TestTarReader(t *testing.T) {
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
fh, err := os.Open(fileLayer)
if err != nil {
t.Errorf("failed to open test data: %v", err)
return
}
opts := append(tt.opts, WithReader(fh))
opts := append(tc.opts, WithReader(fh))
btr := NewTarReader(opts...)
tr, err := btr.GetTarReader()
if err != nil {
@ -542,9 +560,9 @@ func TestTarReader(t *testing.T) {
}
}
err = btr.Close()
if !tt.errClose && err != nil {
if !tc.errClose && err != nil {
t.Errorf("failed to close tar reader: %v", err)
} else if tt.errClose && err == nil {
} else if tc.errClose && err == nil {
t.Errorf("close did not fail")
}
})
@ -552,7 +570,7 @@ func TestTarReader(t *testing.T) {
}
func TestReadFile(t *testing.T) {
tests := []struct {
tt := []struct {
name string
filename string
content string
@ -600,20 +618,20 @@ func TestReadFile(t *testing.T) {
return
}
blobDigest := digest.FromBytes(fileBytes)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
fh, err := os.Open(fileLayerWH)
if err != nil {
t.Errorf("failed to open test data: %v", err)
return
}
btr := NewTarReader(WithReader(fh), WithDesc(types.Descriptor{Size: int64(len(fileBytes)), Digest: blobDigest, MediaType: types.MediaTypeOCI1Layer}))
th, rdr, err := btr.ReadFile(tt.filename)
if tt.expectErr != nil {
th, rdr, err := btr.ReadFile(tc.filename)
if tc.expectErr != nil {
if err == nil {
t.Errorf("ReadFile did not fail")
} else if !errors.Is(err, tt.expectErr) && err.Error() != tt.expectErr.Error() {
t.Errorf("unexpected error, expected %v, received %v", tt.expectErr, err)
} else if !errors.Is(err, tc.expectErr) && err.Error() != tc.expectErr.Error() {
t.Errorf("unexpected error, expected %v, received %v", tc.expectErr, err)
}
err = btr.Close()
if err != nil {
@ -640,8 +658,8 @@ func TestReadFile(t *testing.T) {
if err != nil {
t.Errorf("failed reading file: %v", err)
}
if tt.content != string(content) {
t.Errorf("file content mismatch: expected %s, received %s", tt.content, string(content))
if tc.content != string(content) {
t.Errorf("file content mismatch: expected %s, received %s", tc.content, string(content))
}
err = btr.Close()
if err != nil {

View File

@ -12,21 +12,11 @@ import (
"github.com/regclient/regclient/types/ref"
)
// Common interface is provided by all Blob implementations
type Common interface {
GetDescriptor() types.Descriptor
Response() *http.Response
RawHeaders() http.Header
// Common was previously an interface. A type alias is provided for upgrades.
type Common = *BCommon
// Deprecated: Digest should be replaced by GetDescriptor().Digest
Digest() digest.Digest
// Deprecated: Length should be replaced by GetDescriptor().Size
Length() int64
// Deprecated: MediaType should be replaced by GetDescriptor().MediaType
MediaType() string
}
type common struct {
// BCommon is a common struct for all blobs which includes various shared methods.
type BCommon struct {
r ref.Ref
desc types.Descriptor
blobSet bool
@ -34,32 +24,38 @@ type common struct {
resp *http.Response
}
// GetDescriptor returns the descriptor associated with the blob
func (b *common) GetDescriptor() types.Descriptor {
return b.desc
// GetDescriptor returns the descriptor associated with the blob.
func (c *BCommon) GetDescriptor() types.Descriptor {
return c.desc
}
// Digest returns the provided or calculated digest of the blob
func (b *common) Digest() digest.Digest {
return b.desc.Digest
// Digest returns the provided or calculated digest of the blob.
//
// Deprecated: Digest should be replaced by GetDescriptor().Digest.
func (c *BCommon) Digest() digest.Digest {
return c.desc.Digest
}
// Length returns the provided or calculated length of the blob
func (b *common) Length() int64 {
return b.desc.Size
// Length returns the provided or calculated length of the blob.
//
// Deprecated: Length should be replaced by GetDescriptor().Size.
func (c *BCommon) Length() int64 {
return c.desc.Size
}
// MediaType returns the Content-Type header received from the registry
func (b *common) MediaType() string {
return b.desc.MediaType
// MediaType returns the Content-Type header received from the registry.
//
// Deprecated: MediaType should be replaced by GetDescriptor().MediaType.
func (c *BCommon) MediaType() string {
return c.desc.MediaType
}
// RawHeaders returns the headers received from the registry
func (b *common) RawHeaders() http.Header {
return b.rawHeader
// RawHeaders returns the headers received from the registry.
func (c *BCommon) RawHeaders() http.Header {
return c.rawHeader
}
// Response returns the response associated with the blob
func (b *common) Response() *http.Response {
return b.resp
// Response returns the response associated with the blob.
func (c *BCommon) Response() *http.Response {
return c.resp
}

View File

@ -13,23 +13,19 @@ import (
v1 "github.com/regclient/regclient/types/oci/v1"
)
// OCIConfig wraps an OCI Config struct extracted from a Blob
type OCIConfig interface {
Blob
GetConfig() v1.Image
SetConfig(v1.Image)
}
// OCIConfig was previously an interface. A type alias is provided for upgrading.
type OCIConfig = *BOCIConfig
// ociConfig includes an OCI Config struct extracted from a Blob
// Image is included as an anonymous field to facilitate json and templating calls transparently
type ociConfig struct {
common
// BOCIConfig includes an OCI Image Config struct that may be extracted from or pushed to a blob.
type BOCIConfig struct {
BCommon
rawBody []byte
v1.Image
image v1.Image
}
// NewOCIConfig creates a new BlobOCIConfig from an OCI Image
func NewOCIConfig(opts ...Opts) OCIConfig {
// NewOCIConfig creates a new BOCIConfig.
// When created from an existing blob, a BOCIConfig will be created using BReader.ToOCIConfig().
func NewOCIConfig(opts ...Opts) *BOCIConfig {
bc := blobConfig{}
for _, opt := range opts {
opt(&bc)
@ -56,49 +52,76 @@ func NewOCIConfig(opts ...Opts) OCIConfig {
bc.desc.MediaType = types.MediaTypeOCI1ImageConfig
}
}
c := common{
b := BOCIConfig{
BCommon: BCommon{
desc: bc.desc,
r: bc.r,
rawHeader: bc.header,
resp: bc.resp,
}
b := ociConfig{
common: c,
},
rawBody: bc.rawBody,
}
if bc.image != nil {
b.Image = *bc.image
b.image = *bc.image
b.blobSet = true
}
return &b
}
// GetConfig returns OCI config
func (b *ociConfig) GetConfig() v1.Image {
return b.Image
// GetConfig returns OCI config.
func (oc *BOCIConfig) GetConfig() v1.Image {
return oc.image
}
// RawBody returns the original body from the request
func (b *ociConfig) RawBody() ([]byte, error) {
// RawBody returns the original body from the request.
func (oc *BOCIConfig) RawBody() ([]byte, error) {
var err error
if !b.blobSet {
if !oc.blobSet {
return []byte{}, fmt.Errorf("Blob is not defined")
}
if len(b.rawBody) == 0 {
b.rawBody, err = json.Marshal(b.Image)
if len(oc.rawBody) == 0 {
oc.rawBody, err = json.Marshal(oc.image)
}
return b.rawBody, err
return oc.rawBody, err
}
// SetConfig updates the config, including raw body and descriptor
func (b *ociConfig) SetConfig(c v1.Image) {
b.Image = c
b.rawBody, _ = json.Marshal(b.Image)
if b.desc.MediaType == "" {
b.desc.MediaType = types.MediaTypeOCI1ImageConfig
// SetConfig updates the config, including raw body and descriptor.
func (oc *BOCIConfig) SetConfig(image v1.Image) {
oc.image = image
oc.rawBody, _ = json.Marshal(oc.image)
if oc.desc.MediaType == "" {
oc.desc.MediaType = types.MediaTypeOCI1ImageConfig
}
b.desc.Digest = digest.FromBytes(b.rawBody)
b.desc.Size = int64(len(b.rawBody))
b.blobSet = true
oc.desc.Digest = digest.FromBytes(oc.rawBody)
oc.desc.Size = int64(len(oc.rawBody))
oc.blobSet = true
}
// MarshalJSON passes through the marshalling to the underlying image if rawBody is not available.
func (oc *BOCIConfig) MarshalJSON() ([]byte, error) {
if !oc.blobSet {
return []byte{}, fmt.Errorf("Blob is not defined")
}
if len(oc.rawBody) > 0 {
return oc.rawBody, nil
}
return json.Marshal(oc.image)
}
// UnmarshalJSON extracts json content and populates the content.
func (oc *BOCIConfig) UnmarshalJSON(data []byte) error {
image := v1.Image{}
err := json.Unmarshal(data, &image)
if err != nil {
return err
}
oc.rawBody = make([]byte, len(data))
copy(oc.rawBody, data)
if oc.desc.MediaType == "" {
oc.desc.MediaType = types.MediaTypeOCI1ImageConfig
}
oc.desc.Digest = digest.FromBytes(oc.rawBody)
oc.desc.Size = int64(len(oc.rawBody))
oc.blobSet = true
return nil
}

View File

@ -14,25 +14,20 @@ import (
"github.com/regclient/regclient/types"
)
// Reader is an unprocessed Blob with an available ReadCloser for reading the Blob
type Reader interface {
Blob
io.ReadSeekCloser
ToOCIConfig() (OCIConfig, error)
ToTarReader() (TarReader, error)
}
// Reader was previously an interface. A type alias is provided for upgrading.
type Reader = *BReader
// reader is the internal struct implementing BlobReader
type reader struct {
common
// BReader is used to read blobs.
type BReader struct {
BCommon
readBytes int64
reader io.Reader
origRdr io.Reader
digester digest.Digester
}
// NewReader creates a new reader
func NewReader(opts ...Opts) Reader {
// NewReader creates a new BReader.
func NewReader(opts ...Opts) *BReader {
bc := blobConfig{}
for _, opt := range opts {
opt(&bc)
@ -59,14 +54,13 @@ func NewReader(opts ...Opts) Reader {
bc.desc.Digest, _ = digest.Parse(bc.header.Get("Docker-Content-Digest"))
}
}
c := common{
br := BReader{
BCommon: BCommon{
r: bc.r,
desc: bc.desc,
rawHeader: bc.header,
resp: bc.resp,
}
br := reader{
common: c,
},
origRdr: bc.rdr,
}
if bc.rdr != nil {
@ -84,119 +78,121 @@ func NewReader(opts ...Opts) Reader {
return &br
}
func (b *reader) Close() error {
if b.origRdr == nil {
// Close attempts to close the reader and populates/validates the digest.
func (r *BReader) Close() error {
if r.origRdr == nil {
return nil
}
// attempt to close if available in original reader
bc, ok := b.origRdr.(io.Closer)
bc, ok := r.origRdr.(io.Closer)
if !ok {
return nil
}
return bc.Close()
}
// RawBody returns the original body from the request
func (b *reader) RawBody() ([]byte, error) {
return io.ReadAll(b)
// RawBody returns the original body from the request.
func (r *BReader) RawBody() ([]byte, error) {
return io.ReadAll(r)
}
// Read passes through the read operation while computing the digest and tracking the size
func (b *reader) Read(p []byte) (int, error) {
if b.reader == nil {
// Read passes through the read operation while computing the digest and tracking the size.
func (r *BReader) Read(p []byte) (int, error) {
if r.reader == nil {
return 0, fmt.Errorf("blob has no reader: %w", io.ErrUnexpectedEOF)
}
size, err := b.reader.Read(p)
b.readBytes = b.readBytes + int64(size)
size, err := r.reader.Read(p)
r.readBytes = r.readBytes + int64(size)
if err == io.EOF {
// check/save size
if b.desc.Size == 0 {
b.desc.Size = b.readBytes
} else if b.readBytes < b.desc.Size {
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, b.desc.Size, b.readBytes, err)
} else if b.readBytes > b.desc.Size {
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, b.desc.Size, b.readBytes, err)
if r.desc.Size == 0 {
r.desc.Size = r.readBytes
} else if r.readBytes < r.desc.Size {
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrShortRead, r.desc.Size, r.readBytes, err)
} else if r.readBytes > r.desc.Size {
err = fmt.Errorf("%w [expected %d, received %d]: %v", types.ErrSizeLimitExceeded, r.desc.Size, r.readBytes, err)
}
// check/save digest
if b.desc.Digest == "" {
b.desc.Digest = b.digester.Digest()
} else if b.desc.Digest != b.digester.Digest() {
err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, b.desc.Digest.String(), b.digester.Digest().String(), err)
if r.desc.Digest == "" {
r.desc.Digest = r.digester.Digest()
} else if r.desc.Digest != r.digester.Digest() {
err = fmt.Errorf("%w [expected %s, calculated %s]: %v", types.ErrDigestMismatch, r.desc.Digest.String(), r.digester.Digest().String(), err)
}
}
return size, err
}
// Seek passes through the seek operation, reseting or invalidating the digest
func (b *reader) Seek(offset int64, whence int) (int64, error) {
func (r *BReader) Seek(offset int64, whence int) (int64, error) {
if offset == 0 && whence == io.SeekCurrent {
return b.readBytes, nil
return r.readBytes, nil
}
// cannot do an arbitrary seek and still digest without a lot more complication
if offset != 0 || whence != io.SeekStart {
return b.readBytes, fmt.Errorf("unable to seek to arbitrary position")
return r.readBytes, fmt.Errorf("unable to seek to arbitrary position")
}
rdrSeek, ok := b.origRdr.(io.Seeker)
rdrSeek, ok := r.origRdr.(io.Seeker)
if !ok {
return b.readBytes, fmt.Errorf("Seek unsupported")
return r.readBytes, fmt.Errorf("Seek unsupported")
}
o, err := rdrSeek.Seek(offset, whence)
if err != nil || o != 0 {
return b.readBytes, err
return r.readBytes, err
}
// reset internal offset and digest calculation
rdr := b.origRdr
if b.desc.Size > 0 {
rdr := r.origRdr
if r.desc.Size > 0 {
rdr = &limitread.LimitRead{
Reader: rdr,
Limit: b.desc.Size,
Limit: r.desc.Size,
}
}
digester := digest.Canonical.Digester()
b.reader = io.TeeReader(rdr, digester.Hash())
b.digester = digester
b.readBytes = 0
r.reader = io.TeeReader(rdr, digester.Hash())
r.digester = digester
r.readBytes = 0
return 0, nil
}
// ToOCIConfig converts a blobReader to a BlobOCIConfig
func (b *reader) ToOCIConfig() (OCIConfig, error) {
if !b.blobSet {
// ToOCIConfig converts a BReader to a BOCIConfig.
func (r *BReader) ToOCIConfig() (*BOCIConfig, error) {
if !r.blobSet {
return nil, fmt.Errorf("blob is not defined")
}
if b.readBytes != 0 {
if r.readBytes != 0 {
return nil, fmt.Errorf("unable to convert after read has been performed")
}
blobBody, err := io.ReadAll(b)
errC := b.Close()
blobBody, err := io.ReadAll(r)
errC := r.Close()
if err != nil {
return nil, fmt.Errorf("error reading image config for %s: %w", b.r.CommonName(), err)
return nil, fmt.Errorf("error reading image config for %s: %w", r.r.CommonName(), err)
}
if errC != nil {
return nil, fmt.Errorf("error closing blob reader: %w", err)
}
return NewOCIConfig(
WithDesc(b.desc),
WithHeader(b.rawHeader),
WithDesc(r.desc),
WithHeader(r.rawHeader),
WithRawBody(blobBody),
WithRef(b.r),
WithResp(b.resp),
WithRef(r.r),
WithResp(r.resp),
), nil
}
func (b *reader) ToTarReader() (TarReader, error) {
if !b.blobSet {
// ToTarReader converts a BReader to a BTarReader
func (r *BReader) ToTarReader() (*BTarReader, error) {
if !r.blobSet {
return nil, fmt.Errorf("blob is not defined")
}
if b.readBytes != 0 {
if r.readBytes != 0 {
return nil, fmt.Errorf("unable to convert after read has been performed")
}
return NewTarReader(
WithDesc(b.desc),
WithHeader(b.rawHeader),
WithRef(b.r),
WithResp(b.resp),
WithReader(b.reader),
WithDesc(r.desc),
WithHeader(r.rawHeader),
WithRef(r.r),
WithResp(r.resp),
WithReader(r.reader),
), nil
}

View File

@ -14,36 +14,32 @@ import (
"github.com/regclient/regclient/types"
)
// TarReader reads or writes to a blob with tar contents and optional compression
type TarReader interface {
Blob
io.Closer
GetTarReader() (*tar.Reader, error)
ReadFile(filename string) (*tar.Header, io.Reader, error)
}
// TarReader was previously an interface. A type alias is provided for upgrading.
type TarReader = *BTarReader
type tarReader struct {
common
// BTarReader is used to read individual files from an image layer.
type BTarReader struct {
BCommon
origRdr io.Reader
reader io.Reader
digester digest.Digester
tr *tar.Reader
}
// NewTarReader creates a TarReader
func NewTarReader(opts ...Opts) TarReader {
// NewTarReader creates a BTarReader.
// Typically a BTarReader will be created using BReader.ToTarReader().
func NewTarReader(opts ...Opts) *BTarReader {
bc := blobConfig{}
for _, opt := range opts {
opt(&bc)
}
c := common{
tr := BTarReader{
BCommon: BCommon{
desc: bc.desc,
r: bc.r,
rawHeader: bc.header,
resp: bc.resp,
}
tr := tarReader{
common: c,
},
origRdr: bc.rdr,
}
if bc.rdr != nil {
@ -61,8 +57,8 @@ func NewTarReader(opts ...Opts) TarReader {
return &tr
}
// Close attempts to close the reader and populates/validates the digest
func (tr *tarReader) Close() error {
// Close attempts to close the reader and populates/validates the digest.
func (tr *BTarReader) Close() error {
// attempt to close if available in original reader
if trc, ok := tr.origRdr.(io.Closer); ok && trc != nil {
return trc.Close()
@ -70,8 +66,8 @@ func (tr *tarReader) Close() error {
return nil
}
// GetTarReader returns the tar.Reader for the blob
func (tr *tarReader) GetTarReader() (*tar.Reader, error) {
// GetTarReader returns the tar.Reader for the blob.
func (tr *BTarReader) GetTarReader() (*tar.Reader, error) {
if tr.reader == nil {
return nil, fmt.Errorf("blob has no reader defined")
}
@ -85,8 +81,8 @@ func (tr *tarReader) GetTarReader() (*tar.Reader, error) {
return tr.tr, nil
}
// RawBody returns the original body from the request
func (tr *tarReader) RawBody() ([]byte, error) {
// RawBody returns the original body from the request.
func (tr *BTarReader) RawBody() ([]byte, error) {
if !tr.blobSet {
return []byte{}, fmt.Errorf("Blob is not defined")
}
@ -109,8 +105,8 @@ func (tr *tarReader) RawBody() ([]byte, error) {
return b, err
}
// ReadFile parses the tar to find a file
func (tr *tarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) {
// ReadFile parses the tar to find a file.
func (tr *BTarReader) ReadFile(filename string) (*tar.Header, io.Reader, error) {
if strings.HasPrefix(filename, ".wh.") {
return nil, nil, fmt.Errorf(".wh. prefix is reserved for whiteout files")
}