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  }