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 }