oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/content/oci/readonlystorage.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package oci
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"io"
    23  	"io/fs"
    24  	"path"
    25  
    26  	"github.com/opencontainers/go-digest"
    27  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    28  	"oras.land/oras-go/v2/errdef"
    29  	"oras.land/oras-go/v2/internal/fs/tarfs"
    30  )
    31  
    32  // ReadOnlyStorage is a read-only CAS based on file system with the OCI-Image
    33  // layout.
    34  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
    35  type ReadOnlyStorage struct {
    36  	fsys fs.FS
    37  }
    38  
    39  // NewStorageFromFS creates a new read-only CAS from fsys.
    40  func NewStorageFromFS(fsys fs.FS) *ReadOnlyStorage {
    41  	return &ReadOnlyStorage{
    42  		fsys: fsys,
    43  	}
    44  }
    45  
    46  // NewStorageFromTar creates a new read-only CAS from a tar archive located at
    47  // path.
    48  func NewStorageFromTar(path string) (*ReadOnlyStorage, error) {
    49  	tfs, err := tarfs.New(path)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  	return NewStorageFromFS(tfs), nil
    54  }
    55  
    56  // Fetch fetches the content identified by the descriptor.
    57  func (s *ReadOnlyStorage) Fetch(_ context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
    58  	path, err := blobPath(target.Digest)
    59  	if err != nil {
    60  		return nil, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest)
    61  	}
    62  
    63  	fp, err := s.fsys.Open(path)
    64  	if err != nil {
    65  		if errors.Is(err, fs.ErrNotExist) {
    66  			return nil, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound)
    67  		}
    68  		return nil, err
    69  	}
    70  
    71  	return fp, nil
    72  }
    73  
    74  // Exists returns true if the described content Exists.
    75  func (s *ReadOnlyStorage) Exists(_ context.Context, target ocispec.Descriptor) (bool, error) {
    76  	path, err := blobPath(target.Digest)
    77  	if err != nil {
    78  		return false, fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest)
    79  	}
    80  
    81  	_, err = fs.Stat(s.fsys, path)
    82  	if err != nil {
    83  		if errors.Is(err, fs.ErrNotExist) {
    84  			return false, nil
    85  		}
    86  		return false, err
    87  	}
    88  
    89  	return true, nil
    90  }
    91  
    92  // blobPath calculates blob path from the given digest.
    93  func blobPath(dgst digest.Digest) (string, error) {
    94  	if err := dgst.Validate(); err != nil {
    95  		return "", fmt.Errorf("cannot calculate blob path from invalid digest %s: %w: %v",
    96  			dgst.String(), errdef.ErrInvalidDigest, err)
    97  	}
    98  	return path.Join(ocispec.ImageBlobsDir, dgst.Algorithm().String(), dgst.Encoded()), nil
    99  }