github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/store/blobstore/local.go (about) 1 // Copyright 2019 Dolthub, Inc. 2 // 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 package blobstore 16 17 import ( 18 "context" 19 "errors" 20 "io" 21 "os" 22 "path/filepath" 23 "time" 24 25 "github.com/dolthub/fslock" 26 "github.com/google/uuid" 27 28 "github.com/dolthub/dolt/go/libraries/utils/file" 29 "github.com/dolthub/dolt/go/store/util/tempfiles" 30 ) 31 32 const ( 33 bsExt = ".bs" 34 lockExt = ".lock" 35 ) 36 37 type localBlobRangeReadCloser struct { 38 br BlobRange 39 rc io.ReadCloser 40 pos int64 41 } 42 43 func (lbrrc *localBlobRangeReadCloser) Read(p []byte) (int, error) { 44 remaining := lbrrc.br.length - lbrrc.pos 45 46 if remaining == 0 { 47 return 0, io.EOF 48 } else if int64(len(p)) > remaining { 49 partial := p[:remaining] 50 n, err := lbrrc.rc.Read(partial) 51 lbrrc.pos += int64(n) 52 53 return n, err 54 } 55 56 n, err := lbrrc.rc.Read(p) 57 lbrrc.pos += int64(n) 58 59 return n, err 60 } 61 62 func (lbrrc *localBlobRangeReadCloser) Close() error { 63 return lbrrc.rc.Close() 64 } 65 66 // LocalBlobstore is a Blobstore implementation that uses the local filesystem 67 type LocalBlobstore struct { 68 RootDir string 69 } 70 71 var _ Blobstore = &LocalBlobstore{} 72 73 // NewLocalBlobstore returns a new LocalBlobstore instance 74 func NewLocalBlobstore(dir string) *LocalBlobstore { 75 return &LocalBlobstore{dir} 76 } 77 78 func (bs *LocalBlobstore) Path() string { 79 return bs.RootDir 80 } 81 82 // Get retrieves an io.reader for the portion of a blob specified by br along with 83 // its version 84 func (bs *LocalBlobstore) Get(ctx context.Context, key string, br BlobRange) (io.ReadCloser, string, error) { 85 path := filepath.Join(bs.RootDir, key) + bsExt 86 f, err := os.Open(path) 87 88 if err != nil { 89 if os.IsNotExist(err) { 90 return nil, "", NotFound{key} 91 } 92 return nil, "", err 93 } 94 95 info, err := f.Stat() 96 if err != nil { 97 return nil, "", err 98 } 99 ver := info.ModTime().String() 100 101 rc, err := readCloserForFileRange(f, br) 102 if err != nil { 103 _ = f.Close() 104 return nil, "", err 105 } 106 return rc, ver, nil 107 } 108 109 func readCloserForFileRange(f *os.File, br BlobRange) (io.ReadCloser, error) { 110 seekType := 1 111 if br.offset < 0 { 112 info, err := f.Stat() 113 if err != nil { 114 return nil, err 115 } 116 seekType = 0 117 br = br.positiveRange(info.Size()) 118 } 119 120 _, err := f.Seek(br.offset, seekType) 121 122 if err != nil { 123 return nil, err 124 } 125 126 if br.length != 0 { 127 return &localBlobRangeReadCloser{br, f, 0}, nil 128 } 129 130 return f, nil 131 } 132 133 // Put sets the blob and the version for a key 134 func (bs *LocalBlobstore) Put(ctx context.Context, key string, totalSize int64, reader io.Reader) (string, error) { 135 // written as temp file and renamed so the file corresponding to this key 136 // never exists in a partially written state 137 tempFile, err := func() (string, error) { 138 temp, err := tempfiles.MovableTempFileProvider.NewFile("", uuid.New().String()) 139 if err != nil { 140 return "", err 141 } 142 defer temp.Close() 143 144 if _, err = io.Copy(temp, reader); err != nil { 145 return "", err 146 } 147 return temp.Name(), nil 148 }() 149 150 if err != nil { 151 return "", err 152 } 153 154 time.Sleep(time.Millisecond * 10) // mtime resolution 155 path := filepath.Join(bs.RootDir, key) + bsExt 156 if err = file.Rename(tempFile, path); err != nil { 157 return "", err 158 } 159 160 info, err := os.Stat(path) 161 if err != nil { 162 return "", err 163 } 164 return info.ModTime().String(), nil 165 } 166 167 func fLock(lockFilePath string) (*fslock.Lock, error) { 168 lck := fslock.New(lockFilePath) 169 err := lck.Lock() 170 171 if err != nil { 172 return nil, err 173 } 174 175 return lck, nil 176 } 177 178 // CheckAndPut will check the current version of a blob against an expectedVersion, and if the 179 // versions match it will update the data and version associated with the key 180 func (bs *LocalBlobstore) CheckAndPut(ctx context.Context, expectedVersion, key string, totalSize int64, reader io.Reader) (string, error) { 181 path := filepath.Join(bs.RootDir, key) + bsExt 182 lockFilePath := path + lockExt 183 lck, err := fLock(lockFilePath) 184 185 if err != nil { 186 return "", errors.New("Could not acquire lock of " + lockFilePath) 187 } 188 189 defer lck.Unlock() 190 191 rc, ver, err := bs.Get(ctx, key, BlobRange{}) 192 193 if err != nil { 194 if !IsNotFoundError(err) { 195 return "", errors.New("Unable to read current version of " + path) 196 } 197 } else { 198 rc.Close() 199 } 200 201 if expectedVersion != ver { 202 return "", CheckAndPutError{key, expectedVersion, ver} 203 } 204 205 return bs.Put(ctx, key, totalSize, reader) 206 } 207 208 // Exists returns true if a blob exists for the given key, and false if it does not. 209 // error may be returned if there are errors accessing the filesystem data. 210 func (bs *LocalBlobstore) Exists(ctx context.Context, key string) (bool, error) { 211 path := filepath.Join(bs.RootDir, key) + bsExt 212 _, err := os.Stat(path) 213 214 if os.IsNotExist(err) { 215 return false, nil 216 } 217 218 return err == nil, err 219 } 220 221 func (bs *LocalBlobstore) Concatenate(ctx context.Context, key string, sources []string) (ver string, err error) { 222 totalSize := int64(0) 223 readers := make([]io.Reader, len(sources)) 224 for i := range readers { 225 path := filepath.Join(bs.RootDir, sources[i]) + bsExt 226 f, err := os.Open(path) 227 if err != nil { 228 return "", err 229 } 230 info, err := f.Stat() 231 if err != nil { 232 return "", err 233 } 234 totalSize += info.Size() 235 readers[i] = f 236 } 237 238 ver, err = bs.Put(ctx, key, totalSize, io.MultiReader(readers...)) 239 240 for i := range readers { 241 if cerr := readers[i].(io.Closer).Close(); err != nil { 242 err = cerr 243 } 244 } 245 return 246 }