github.com/grailbio/base@v0.0.11/file/localfile.go (about) 1 // Copyright 2018 GRAIL, Inc. All rights reserved. 2 // Use of this source code is governed by the Apache-2.0 3 // license that can be found in the LICENSE file. 4 5 package file 6 7 import ( 8 "context" 9 "fmt" 10 "io" 11 "io/ioutil" 12 "os" 13 "path/filepath" 14 "sort" 15 "time" 16 17 "github.com/grailbio/base/errors" 18 "github.com/grailbio/base/ioctx" 19 "github.com/grailbio/base/log" 20 ) 21 22 type localImpl struct{} 23 24 type accessMode int 25 26 const ( 27 readonly accessMode = iota // file opened by Open. 28 writeonlyFile // regular file opened by Create. 29 writeonlyDev // device or socket opened by Create. 30 ) 31 32 type localInfo struct { 33 size int64 34 modTime time.Time 35 } 36 37 type localFile struct { 38 f *os.File 39 mode accessMode 40 path string // User-supplied path. 41 realPath string // Path after symlink resolution. 42 } 43 44 type localLister struct { 45 prefix string 46 err error 47 path string 48 info os.FileInfo 49 todo []string 50 recurse bool 51 } 52 53 func (impl *localImpl) String() string { 54 return "local" 55 } 56 57 // Open implements file.Implementation. 58 func (impl *localImpl) Open(ctx context.Context, path string, _ ...Opts) (File, error) { 59 f, err := os.Open(path) 60 if err != nil { 61 if os.IsNotExist(err) { 62 err = errors.E(err, errors.NotExist) 63 } 64 return nil, err 65 } 66 lf := localFile{f: f, mode: readonly, path: path} 67 return &lf, nil 68 } 69 70 // Create implements file.Implementation. To make writes appear linearizable, 71 // it creates a temporary file with name <path>.tmp, then renames the temp file 72 // to <path> on Close. 73 func (*localImpl) Create(ctx context.Context, path string, _ ...Opts) (File, error) { 74 if path == "" { // Detect common errors quickly. 75 return nil, fmt.Errorf("file.Create: empty pathname") 76 } 77 realPath, err := filepath.EvalSymlinks(path) 78 if err != nil { 79 // This happens when the file doesn't exist, including the case where path 80 // is a symlink and the symlink destination doesn't exist. 81 // 82 // TODO(saito) UNIX open(2), O_CREAT creates the symlink destination in this 83 // case. Instead, here we create a tempfile in Dir(path), then delete the 84 // symlink on close. 85 realPath = path 86 } 87 if stat, err := os.Stat(path); err == nil { 88 if (stat.Mode()&os.ModeDevice != 0) || (stat.Mode()&os.ModeNamedPipe != 0) || (stat.Mode()&os.ModeSocket != 0) { 89 f, err := os.Create(path) 90 if err != nil { 91 return nil, err 92 } 93 return &localFile{f: f, mode: writeonlyDev, path: path, realPath: realPath}, nil 94 } 95 if stat.IsDir() { 96 return nil, fmt.Errorf("file.Create %s: is a directory", path) 97 } 98 } 99 100 // filepath.Dir just strips the last "/" if path ends with "/". Else, it 101 // removes the last component of the path. That's what we want. 102 dir := filepath.Dir(realPath) 103 f, err := ioutil.TempFile(dir, filepath.Base(realPath)+".tmp") 104 if err != nil { 105 if err = os.MkdirAll(dir, 0777); err != nil { 106 log.Error.Printf("mkdir %v: error %v ", dir, err) 107 } 108 f, err = ioutil.TempFile(dir, "localtmp") 109 if err != nil { 110 return nil, err 111 } 112 } 113 return &localFile{f: f, mode: writeonlyFile, path: path, realPath: realPath}, nil 114 } 115 116 // Close implements file.Implementation. 117 func (f *localFile) Close(ctx context.Context) error { 118 return f.close(ctx, true) 119 } 120 121 // CloseNoSync closes the file without an fsync. 122 func (f *localFile) CloseNoSync(ctx context.Context) error { 123 return f.close(ctx, false) 124 } 125 126 func (f *localFile) close(_ context.Context, doSync bool) error { 127 switch f.mode { 128 case readonly, writeonlyDev: 129 return f.f.Close() 130 default: 131 var err error 132 if doSync { 133 err = f.f.Sync() 134 } 135 if e := f.f.Close(); e != nil && err == nil { 136 err = e 137 } 138 if err != nil { 139 _ = os.Remove(f.f.Name()) 140 return err 141 } 142 return os.Rename(f.f.Name(), f.realPath) 143 } 144 } 145 146 // Discard implements file.File. 147 func (f *localFile) Discard(ctx context.Context) { 148 switch f.mode { 149 case readonly, writeonlyDev: 150 return 151 } 152 if err := f.f.Close(); err != nil { 153 log.Printf("discard %s: close: %v", f.Name(), err) 154 } 155 if err := os.Remove(f.f.Name()); err != nil { 156 log.Printf("discard %s: remove: %v", f.Name(), err) 157 } 158 } 159 160 // String implements file.File. 161 func (f *localFile) String() string { 162 return f.path 163 } 164 165 // Name implements file.File. 166 func (f *localFile) Name() string { 167 return f.path 168 } 169 170 // Reader implements file.File 171 func (f *localFile) Reader(context.Context) io.ReadSeeker { 172 if f.mode != readonly { 173 return NewError(fmt.Errorf("reader %v: file is not opened in read mode", f.Name())) 174 } 175 return f.f 176 } 177 178 type localReader struct { 179 f *os.File 180 pos int64 181 } 182 183 func (r *localReader) Read(_ context.Context, p []byte) (int, error) { 184 n, err := r.f.ReadAt(p, r.pos) 185 r.pos += int64(n) 186 return n, err 187 } 188 189 func (r *localReader) Close(context.Context) error { 190 r.f = nil 191 return nil 192 } 193 194 // OffsetReader implements file.File 195 func (f *localFile) OffsetReader(offset int64) ioctx.ReadCloser { 196 if f.mode != readonly { 197 return ioctx.FromStdReadCloser(NewError(fmt.Errorf("reader %v: file is not opened in read mode", f.Name()))) 198 } 199 return &localReader{f: f.f, pos: offset} 200 } 201 202 // Writer implements file.Writer 203 func (f *localFile) Writer(context.Context) io.Writer { 204 if f.mode == readonly { 205 return NewError(fmt.Errorf("writer %v: file is not opened in write mode", f.Name())) 206 } 207 return f.f 208 } 209 210 // List implements file.Implementation 211 func (impl *localImpl) List(ctx context.Context, prefix string, recurse bool) Lister { 212 return &localLister{prefix: prefix, todo: []string{prefix}, recurse: recurse} 213 } 214 215 // Remove implements file.Implementation. 216 func (*localImpl) Remove(ctx context.Context, path string) error { 217 return os.Remove(path) 218 } 219 220 func (*localImpl) Presign(_ context.Context, path, _ string, _ time.Duration) (string, error) { 221 return "", errors.E(errors.NotSupported, 222 fmt.Sprintf("presign %v: local files not supported", path)) 223 } 224 225 // Stat implements file.Implementation 226 func (impl *localImpl) Stat(ctx context.Context, path string, _ ...Opts) (Info, error) { 227 info, err := os.Stat(path) 228 if err != nil { 229 if os.IsNotExist(err) { 230 err = errors.E(err, errors.NotExist) 231 } 232 return nil, err 233 } 234 if info.IsDir() { 235 return nil, fmt.Errorf("stat %v: is a directory", path) 236 } 237 return &localInfo{size: info.Size(), modTime: info.ModTime()}, nil 238 } 239 240 // Stat implements file.File 241 func (f *localFile) Stat(context.Context) (Info, error) { 242 info, err := f.f.Stat() 243 if err != nil { 244 return nil, err 245 } 246 if info.IsDir() { 247 return nil, fmt.Errorf("stat %v: is a directory", f.path) 248 } 249 return &localInfo{size: info.Size(), modTime: info.ModTime()}, nil 250 } 251 252 var _ ioctx.WriterAt = (*localFile)(nil) 253 254 func (f *localFile) WriteAt(_ context.Context, p []byte, off int64) (n int, err error) { 255 return f.f.WriteAt(p, off) 256 } 257 258 func (i *localInfo) Size() int64 { return i.size } 259 func (i *localInfo) ModTime() time.Time { return i.modTime } 260 261 // Scan implements Lister.Scan. 262 func (l *localLister) Scan() bool { 263 264 for { 265 if len(l.todo) == 0 || l.err != nil { 266 return false 267 } 268 l.path, l.todo = l.todo[0], l.todo[1:] 269 l.info, l.err = os.Stat(l.path) 270 if os.IsNotExist(l.err) { 271 l.err = nil 272 continue 273 } 274 if l.err != nil { 275 return false 276 } 277 if !l.info.IsDir() { 278 return true 279 } 280 if l.recurse || l.path == l.prefix { 281 var paths []string 282 paths, l.err = readDirNames(l.path) 283 if l.err != nil { 284 return false 285 } 286 for i := range paths { 287 paths[i] = filepath.Join(l.path, paths[i]) 288 } 289 l.todo = append(paths, l.todo...) 290 } 291 if l.showDirs() && l.path != l.prefix { 292 return true 293 } 294 continue 295 } 296 } 297 298 // Path returns the most recent path that was scanned. 299 func (l *localLister) Path() string { 300 return l.path 301 } 302 303 // Info returns the os.FileInfo for the most recent path scanned, or nil if IsDir() is true 304 func (l *localLister) Info() Info { 305 infoSize := l.info.Size() 306 if l.info.IsDir() { 307 return nil 308 } 309 return &localInfo{size: infoSize, modTime: l.info.ModTime()} 310 } 311 312 // Info returns the os.FileInfo for the most recent path scanned. 313 func (l *localLister) IsDir() bool { 314 return l.info.IsDir() 315 } 316 317 // Err returns the first error that occurred while scanning. 318 func (l *localLister) Err() error { 319 return l.err 320 } 321 322 // showDirs controls whether directories are returned during a scan 323 func (l *localLister) showDirs() bool { 324 return !l.recurse 325 } 326 327 // readDirNames reads the directory named by dirname and returns 328 // a sorted list of directory entries. 329 func readDirNames(dirname string) ([]string, error) { 330 f, err := os.Open(dirname) 331 if err != nil { 332 return nil, err 333 } 334 names, err := f.Readdirnames(-1) 335 if e := f.Close(); e != nil && err == nil { 336 err = e 337 } 338 if err != nil { 339 return nil, err 340 } 341 sort.Strings(names) 342 return names, nil 343 } 344 345 // NewLocalImplementation returns a new file.Implementation for the local file system 346 // that uses Go's native "os" module. This function is only for unittests. 347 // Applications should use functions such as file.Open, file.Create to access 348 // the local file system. 349 func NewLocalImplementation() Implementation { return &localImpl{} }