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 }