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 }