github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/blobs/local_storage.go (about)

     1  // Copyright 2019 The Cockroach Authors.
     2  //
     3  // Use of this software is governed by the Business Source License
     4  // included in the file licenses/BSL.txt.
     5  //
     6  // As of the Change Date specified in that file, in accordance with
     7  // the Business Source License, use of this software will be governed
     8  // by the Apache License, Version 2.0, included in the file
     9  // licenses/APL.txt.
    10  
    11  package blobs
    12  
    13  import (
    14  	"io"
    15  	"io/ioutil"
    16  	"os"
    17  	"path/filepath"
    18  	"strings"
    19  
    20  	"github.com/cockroachdb/cockroach/pkg/blobs/blobspb"
    21  	"github.com/cockroachdb/cockroach/pkg/util/fileutil"
    22  	"github.com/cockroachdb/errors"
    23  )
    24  
    25  // LocalStorage wraps all operations with the local file system
    26  // that the blob service makes.
    27  type LocalStorage struct {
    28  	externalIODir string
    29  }
    30  
    31  // NewLocalStorage creates a new LocalStorage object and returns
    32  // an error when we cannot take the absolute path of `externalIODir`.
    33  func NewLocalStorage(externalIODir string) (*LocalStorage, error) {
    34  	// An empty externalIODir indicates external IO is completely disabled.
    35  	// Returning a nil *LocalStorage in this case and then hanldling `nil` in the
    36  	// prependExternalIODir helper ensures that that is respected throughout the
    37  	// implementation (as a failure to do so would likely fail loudly with a
    38  	// nil-pointer dereference).
    39  	if externalIODir == "" {
    40  		return nil, nil
    41  	}
    42  	absPath, err := filepath.Abs(externalIODir)
    43  	if err != nil {
    44  		return nil, errors.Wrap(err, "creating LocalStorage object")
    45  	}
    46  	return &LocalStorage{externalIODir: absPath}, nil
    47  }
    48  
    49  // prependExternalIODir makes `path` relative to the configured external I/O directory.
    50  //
    51  // Note that we purposefully only rely on the simplified cleanup
    52  // performed by filepath.Join() - which is limited to stripping out
    53  // occurrences of "../" - because we intendedly want to allow
    54  // operators to "open up" their I/O directory via symlinks. Therefore,
    55  // a full check via filepath.Abs() would be inadequate.
    56  func (l *LocalStorage) prependExternalIODir(path string) (string, error) {
    57  	if l == nil {
    58  		return "", errors.Errorf("local file access is disabled")
    59  	}
    60  	localBase := filepath.Join(l.externalIODir, path)
    61  	if !strings.HasPrefix(localBase, l.externalIODir) {
    62  		return "", errors.Errorf("local file access to paths outside of external-io-dir is not allowed: %s", path)
    63  	}
    64  	return localBase, nil
    65  }
    66  
    67  // WriteFile prepends IO dir to filename and writes the content to that local file.
    68  func (l *LocalStorage) WriteFile(filename string, content io.Reader) (err error) {
    69  	fullPath, err := l.prependExternalIODir(filename)
    70  	if err != nil {
    71  		return err
    72  	}
    73  
    74  	targetDir := filepath.Dir(fullPath)
    75  	if err = os.MkdirAll(targetDir, 0755); err != nil {
    76  		return errors.Wrapf(err, "creating target local directory %q", targetDir)
    77  	}
    78  
    79  	// We generate the temporary file in the desired target directory.
    80  	// This has two purposes:
    81  	// - it avoids relying on the system-wide temporary directory, which
    82  	//   may not be large enough to receive the file.
    83  	// - it avoids a cross-filesystem rename in the common case.
    84  	//   (There can still be cross-filesystem renames in very
    85  	//   exotic edge cases, hence the use fileutil.Move below.)
    86  	// See the explanatory comment for ioutil.TempFile to understand
    87  	// what the "*" in the suffix means.
    88  	tmpFile, err := ioutil.TempFile(targetDir, filepath.Base(fullPath)+"*.tmp")
    89  	if err != nil {
    90  		return errors.Wrap(err, "creating temporary file")
    91  	}
    92  	tmpFileFullName := tmpFile.Name()
    93  	defer func() {
    94  		if err != nil {
    95  			// When an error occurs, we need to clean up the newly created
    96  			// temporary file.
    97  			_ = os.Remove(tmpFileFullName)
    98  			//
    99  			// TODO(someone): in the special case where an attempt is made
   100  			// to upload to a sub-directory of the ext i/o dir for the first
   101  			// time (MkdirAll above did create the sub-directory), and the
   102  			// copy/rename fails, we're now left with a newly created but empty
   103  			// sub-directory.
   104  			//
   105  			// We cannot safely remove that target directory here, because
   106  			// perhaps there is another concurrent operation that is also
   107  			// targeting it. A more principled approach could be to use a
   108  			// mutex lock on directory accesses, and/or occasionally prune
   109  			// empty sub-directories upon node start-ups.
   110  		}
   111  	}()
   112  
   113  	// Copy the data into the temp file. We use a closure here to
   114  	// ensure the temp file is closed after the copy is done.
   115  	if err = func() error {
   116  		defer tmpFile.Close()
   117  		if _, err := io.Copy(tmpFile, content); err != nil {
   118  			return errors.Wrapf(err, "writing to temporary file %q", tmpFileFullName)
   119  		}
   120  		return errors.Wrapf(tmpFile.Sync(), "flushing temporary file %q", tmpFileFullName)
   121  	}(); err != nil {
   122  		return err
   123  	}
   124  
   125  	// Finally put the file to its final location.
   126  	return errors.Wrapf(
   127  		fileutil.Move(tmpFileFullName, fullPath),
   128  		"moving temporary file to final location %q", fullPath)
   129  }
   130  
   131  // ReadFile prepends IO dir to filename and reads the content of that local file.
   132  func (l *LocalStorage) ReadFile(filename string) (res io.ReadCloser, err error) {
   133  	fullPath, err := l.prependExternalIODir(filename)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	f, err := os.Open(fullPath)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  	defer func() {
   142  		if err != nil {
   143  			_ = f.Close()
   144  		}
   145  	}()
   146  	fi, err := f.Stat()
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  	if fi.IsDir() {
   151  		return nil, errors.Errorf("expected a file but %q is a directory", fi.Name())
   152  	}
   153  	return f, nil
   154  }
   155  
   156  // List prepends IO dir to pattern and glob matches all local files against that pattern.
   157  func (l *LocalStorage) List(pattern string) ([]string, error) {
   158  	if pattern == "" {
   159  		return nil, errors.New("pattern cannot be empty")
   160  	}
   161  	fullPath, err := l.prependExternalIODir(pattern)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  	matches, err := filepath.Glob(fullPath)
   166  	if err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	var fileList []string
   171  	for _, file := range matches {
   172  		fileList = append(fileList, strings.TrimPrefix(file, l.externalIODir))
   173  	}
   174  	return fileList, nil
   175  }
   176  
   177  // Delete prepends IO dir to filename and deletes that local file.
   178  func (l *LocalStorage) Delete(filename string) error {
   179  	fullPath, err := l.prependExternalIODir(filename)
   180  	if err != nil {
   181  		return errors.Wrap(err, "deleting file")
   182  	}
   183  	return os.Remove(fullPath)
   184  }
   185  
   186  // Stat prepends IO dir to filename and gets the Stat() of that local file.
   187  func (l *LocalStorage) Stat(filename string) (*blobspb.BlobStat, error) {
   188  	fullPath, err := l.prependExternalIODir(filename)
   189  	if err != nil {
   190  		return nil, errors.Wrap(err, "getting stat of file")
   191  	}
   192  	fi, err := os.Stat(fullPath)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	if fi.IsDir() {
   197  		return nil, errors.Errorf("expected a file but %q is a directory", fi.Name())
   198  	}
   199  	return &blobspb.BlobStat{Filesize: fi.Size()}, nil
   200  }