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