oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/content/oci/storage.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  	"os"
    25  	"path/filepath"
    26  	"sync"
    27  
    28  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    29  	"oras.land/oras-go/v2/errdef"
    30  	"oras.land/oras-go/v2/internal/ioutil"
    31  )
    32  
    33  // bufPool is a pool of byte buffers that can be reused for copying content
    34  // between files.
    35  var bufPool = sync.Pool{
    36  	New: func() interface{} {
    37  		// the buffer size should be larger than or equal to 128 KiB
    38  		// for performance considerations.
    39  		// we choose 1 MiB here so there will be less disk I/O.
    40  		buffer := make([]byte, 1<<20) // buffer size = 1 MiB
    41  		return &buffer
    42  	},
    43  }
    44  
    45  // Storage is a CAS based on file system with the OCI-Image layout.
    46  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
    47  type Storage struct {
    48  	*ReadOnlyStorage
    49  	// root is the root directory of the OCI layout.
    50  	root string
    51  	// ingestRoot is the root directory of the temporary ingest files.
    52  	ingestRoot string
    53  }
    54  
    55  // NewStorage creates a new CAS based on file system with the OCI-Image layout.
    56  func NewStorage(root string) (*Storage, error) {
    57  	rootAbs, err := filepath.Abs(root)
    58  	if err != nil {
    59  		return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", root, err)
    60  	}
    61  
    62  	return &Storage{
    63  		ReadOnlyStorage: NewStorageFromFS(os.DirFS(rootAbs)),
    64  		root:            rootAbs,
    65  		ingestRoot:      filepath.Join(rootAbs, "ingest"),
    66  	}, nil
    67  }
    68  
    69  // Push pushes the content, matching the expected descriptor.
    70  func (s *Storage) Push(_ context.Context, expected ocispec.Descriptor, content io.Reader) error {
    71  	path, err := blobPath(expected.Digest)
    72  	if err != nil {
    73  		return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrInvalidDigest)
    74  	}
    75  	target := filepath.Join(s.root, path)
    76  
    77  	// check if the target content already exists in the blob directory.
    78  	if _, err := os.Stat(target); err == nil {
    79  		return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrAlreadyExists)
    80  	} else if !os.IsNotExist(err) {
    81  		return err
    82  	}
    83  
    84  	if err := ensureDir(filepath.Dir(target)); err != nil {
    85  		return err
    86  	}
    87  
    88  	// write the content to a temporary ingest file.
    89  	ingest, err := s.ingest(expected, content)
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	// move the content from the temporary ingest file to the target path.
    95  	// since blobs are read-only once stored, if the target blob already exists,
    96  	// Rename() will fail for permission denied when trying to overwrite it.
    97  	if err := os.Rename(ingest, target); err != nil {
    98  		// remove the ingest file in case of error
    99  		os.Remove(ingest)
   100  		if errors.Is(err, os.ErrPermission) {
   101  			return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrAlreadyExists)
   102  		}
   103  
   104  		return err
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  // Delete removes the target from the system.
   111  func (s *Storage) Delete(ctx context.Context, target ocispec.Descriptor) error {
   112  	path, err := blobPath(target.Digest)
   113  	if err != nil {
   114  		return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest)
   115  	}
   116  	targetPath := filepath.Join(s.root, path)
   117  	err = os.Remove(targetPath)
   118  	if err != nil {
   119  		if errors.Is(err, fs.ErrNotExist) {
   120  			return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound)
   121  		}
   122  		return err
   123  	}
   124  	return nil
   125  }
   126  
   127  // ingest write the content into a temporary ingest file.
   128  func (s *Storage) ingest(expected ocispec.Descriptor, content io.Reader) (path string, ingestErr error) {
   129  	if err := ensureDir(s.ingestRoot); err != nil {
   130  		return "", fmt.Errorf("failed to ensure ingest dir: %w", err)
   131  	}
   132  
   133  	// create a temp file with the file name format "blobDigest_randomString"
   134  	// in the ingest directory.
   135  	// Go ensures that multiple programs or goroutines calling CreateTemp
   136  	// simultaneously will not choose the same file.
   137  	fp, err := os.CreateTemp(s.ingestRoot, expected.Digest.Encoded()+"_*")
   138  	if err != nil {
   139  		return "", fmt.Errorf("failed to create ingest file: %w", err)
   140  	}
   141  
   142  	path = fp.Name()
   143  	defer func() {
   144  		// remove the temp file in case of error.
   145  		// this executes after the file is closed.
   146  		if ingestErr != nil {
   147  			os.Remove(path)
   148  		}
   149  	}()
   150  	defer fp.Close()
   151  
   152  	buf := bufPool.Get().(*[]byte)
   153  	defer bufPool.Put(buf)
   154  	if err := ioutil.CopyBuffer(fp, content, *buf, expected); err != nil {
   155  		return "", fmt.Errorf("failed to ingest: %w", err)
   156  	}
   157  
   158  	// change to readonly
   159  	if err := os.Chmod(path, 0444); err != nil {
   160  		return "", fmt.Errorf("failed to make readonly: %w", err)
   161  	}
   162  
   163  	return
   164  }
   165  
   166  // ensureDir ensures the directories of the path exists.
   167  func ensureDir(path string) error {
   168  	return os.MkdirAll(path, 0777)
   169  }