github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/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  	"strconv"
    27  	"sync"
    28  
    29  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    30  	"oras.land/oras-go/v2/content"
    31  	"oras.land/oras-go/v2/errdef"
    32  	"oras.land/oras-go/v2/internal/ioutil"
    33  	"oras.land/oras-go/v2/internal/spec"
    34  )
    35  
    36  // bufPool is a pool of byte buffers that can be reused for copying content
    37  // between files.
    38  var bufPool = sync.Pool{
    39  	New: func() interface{} {
    40  		// the buffer size should be larger than or equal to 128 KiB
    41  		// for performance considerations.
    42  		// we choose 1 MiB here so there will be less disk I/O.
    43  		buffer := make([]byte, 1<<20) // buffer size = 1 MiB
    44  		return &buffer
    45  	},
    46  }
    47  
    48  // Storage is a CAS based on file system with the OCI-Image layout.
    49  // Reference: https://github.com/opencontainers/image-spec/blob/v1.1.0/image-layout.md
    50  type Storage struct {
    51  	*ReadOnlyStorage
    52  	// root is the root directory of the OCI layout.
    53  	root string
    54  	// ingestRoot is the root directory of the temporary ingest files.
    55  	ingestRoot string
    56  }
    57  
    58  // NewStorage creates a new CAS based on file system with the OCI-Image layout.
    59  func NewStorage(root string) (*Storage, error) {
    60  	rootAbs, err := filepath.Abs(root)
    61  	if err != nil {
    62  		return nil, fmt.Errorf("failed to resolve absolute path for %s: %w", root, err)
    63  	}
    64  
    65  	return &Storage{
    66  		ReadOnlyStorage: NewStorageFromFS(os.DirFS(rootAbs)),
    67  		root:            rootAbs,
    68  		ingestRoot:      filepath.Join(rootAbs, "ingest"),
    69  	}, nil
    70  }
    71  
    72  // Push pushes the content, matching the expected descriptor.
    73  func (s *Storage) Push(_ context.Context, expected ocispec.Descriptor, content io.Reader) error {
    74  	path, err := blobPath(expected.Digest)
    75  	if err != nil {
    76  		return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrInvalidDigest)
    77  	}
    78  	target := filepath.Join(s.root, path)
    79  
    80  	// check if the target content already exists in the blob directory.
    81  	if _, err := os.Stat(target); err == nil {
    82  		return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrAlreadyExists)
    83  	} else if !os.IsNotExist(err) {
    84  		return err
    85  	}
    86  
    87  	if err := ensureDir(filepath.Dir(target)); err != nil {
    88  		return err
    89  	}
    90  
    91  	// write the content to a temporary ingest file.
    92  	ingest, err := s.ingest(expected, content)
    93  	if err != nil {
    94  		return err
    95  	}
    96  
    97  	// move the content from the temporary ingest file to the target path.
    98  	// since blobs are read-only once stored, if the target blob already exists,
    99  	// Rename() will fail for permission denied when trying to overwrite it.
   100  	if err := os.Rename(ingest, target); err != nil {
   101  		// remove the ingest file in case of error
   102  		os.Remove(ingest)
   103  		if errors.Is(err, os.ErrPermission) {
   104  			return fmt.Errorf("%s: %s: %w", expected.Digest, expected.MediaType, errdef.ErrAlreadyExists)
   105  		}
   106  
   107  		return err
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  // Return the ingest file matching name
   114  func (s *Storage) IngestFile(name string) string {
   115  	ingestFiles, err := filepath.Glob(filepath.Join(s.ingestRoot, name+"_*"))
   116  	if err != nil || len(ingestFiles) == 0 {
   117  		// Error or no files found
   118  		return ""
   119  	}
   120  	// Found at least one file, return up the first one
   121  	// TODO: Look for the largest matching file?
   122  	return ingestFiles[0]
   123  }
   124  
   125  // Delete removes the target from the system.
   126  func (s *Storage) Delete(ctx context.Context, target ocispec.Descriptor) error {
   127  	path, err := blobPath(target.Digest)
   128  	if err != nil {
   129  		return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrInvalidDigest)
   130  	}
   131  	targetPath := filepath.Join(s.root, path)
   132  	err = os.Remove(targetPath)
   133  	if err != nil {
   134  		if errors.Is(err, fs.ErrNotExist) {
   135  			return fmt.Errorf("%s: %s: %w", target.Digest, target.MediaType, errdef.ErrNotFound)
   136  		}
   137  		return err
   138  	}
   139  	return nil
   140  }
   141  
   142  // ingest write the content into a temporary ingest file.
   143  func (s *Storage) ingest(expected ocispec.Descriptor, contentReader io.Reader) (path string, ingestErr error) {
   144  	if err := ensureDir(s.ingestRoot); err != nil {
   145  		return "", fmt.Errorf("failed to ensure ingest dir: %w", err)
   146  	}
   147  
   148  	// Resume Download
   149  	resume := expected.Annotations[spec.AnnotationResumeDownload]
   150  	ingestFile := expected.Annotations[spec.AnnotationResumeFilename]
   151  	ingestSize, err := strconv.ParseInt(expected.Annotations[spec.AnnotationResumeOffset], 10, 64)
   152  	if err != nil {
   153  		ingestSize = 0
   154  	}
   155  
   156  	// See if a partial ingest file with this hash already exists
   157  	var fp *os.File
   158  	if resume == "true" && ingestFile != "" && ingestSize > 0 && ingestSize < expected.Size {
   159  		// Found a suitable ingest file
   160  		fp, err = os.OpenFile(ingestFile, os.O_RDWR|os.O_APPEND, 0o600)
   161  		if err != nil {
   162  			return "", fmt.Errorf("failed to open partial ingest file: %w", err)
   163  		}
   164  
   165  		// Rewind file to re-verify current contents on disk
   166  		fp.Seek(0, io.SeekStart)
   167  
   168  		// Make a new Hash and update for current contents on disk
   169  		newHash := expected.Digest.Algorithm().Hash()
   170  		if n, err := io.Copy(newHash, fp); err != nil || n != ingestSize {
   171  			// If error, assume we can't use what is there
   172  			ingestSize = 0
   173  		} else {
   174  			eh, err := content.EncodeHash(newHash)
   175  			if err != nil {
   176  				// oops, still can't resume...
   177  				ingestSize = 0
   178  			} else {
   179  				expected.Annotations[spec.AnnotationResumeHash] = eh
   180  			}
   181  		}
   182  		if ingestSize == 0 {
   183  			// Reset file pointer
   184  			fp.Seek(0, io.SeekStart)
   185  		}
   186  	} else {
   187  		// No partial ingest files found
   188  		// create a temp file with the file name format "blobDigest_randomString"
   189  		// in the ingest directory.
   190  		// Go ensures that multiple programs or goroutines calling CreateTemp
   191  		// simultaneously will not choose the same file.
   192  		fp, err = os.CreateTemp(s.ingestRoot, expected.Digest.Encoded()+"_*")
   193  		if err != nil {
   194  			return "", fmt.Errorf("failed to create ingest file: %w", err)
   195  		}
   196  	}
   197  
   198  	path = fp.Name()
   199  	defer func() {
   200  		// remove the temp file in case of error.
   201  		// this executes after the file is closed.
   202  		if ingestErr != nil {
   203  			os.Remove(path)
   204  		}
   205  	}()
   206  	defer fp.Close()
   207  
   208  	// Copy downloaded bits to ingest file only if we do not already have it all
   209  	if ingestSize >= 0 && ingestSize < expected.Size {
   210  		buf := bufPool.Get().(*[]byte)
   211  		defer bufPool.Put(buf)
   212  		if err := ioutil.CopyBuffer(fp, contentReader, *buf, expected); err != nil {
   213  			return "", fmt.Errorf("failed to ingest: %w", err)
   214  		}
   215  	}
   216  
   217  	// change to readonly
   218  	if err := os.Chmod(path, 0444); err != nil {
   219  		return "", fmt.Errorf("failed to make readonly: %w", err)
   220  	}
   221  
   222  	return
   223  }
   224  
   225  // ensureDir ensures the directories of the path exists.
   226  func ensureDir(path string) error {
   227  	return os.MkdirAll(path, 0777)
   228  }