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 }