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{} }