github.com/maruel/nin@v0.0.0-20220112143044-f35891e3ce7e/disk_interface.go (about)

     1  // Copyright 2011 Google Inc. All Rights Reserved.
     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 nin
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os"
    22  	"path/filepath"
    23  	"runtime"
    24  	"strings"
    25  	"syscall"
    26  )
    27  
    28  // FileReader is an interface for reading files from disk.
    29  //
    30  // See DiskInterface for details. This base offers the minimum interface needed
    31  // just to read files.
    32  type FileReader interface {
    33  	// ReadFile reads a file and returns its content.
    34  	//
    35  	// Unlike os.ReadFile(), if the content is not empty, it appends a zero byte
    36  	// at the end of the slice.
    37  	ReadFile(path string) ([]byte, error)
    38  }
    39  
    40  // DiskInterface is an interface for accessing the disk.
    41  //
    42  // Abstract so it can be mocked out for tests. The real implementation is
    43  // RealDiskInterface.
    44  type DiskInterface interface {
    45  	FileReader
    46  	// Stat stat()'s a file, returning the mtime, or 0 if missing and -1 on
    47  	// other errors.
    48  	Stat(path string) (TimeStamp, error)
    49  
    50  	// MakeDir creates a directory, returning false on failure.
    51  	MakeDir(path string) error
    52  
    53  	// WriteFile creates a file, with the specified name and contents
    54  	WriteFile(path, contents string) error
    55  
    56  	// RemoveFile removes the file named path.
    57  	//
    58  	// It should return an error that matches os.IsNotExist() if the file was not
    59  	// present.
    60  	RemoveFile(path string) error
    61  }
    62  
    63  type dirCache map[string]TimeStamp
    64  type cache map[string]dirCache
    65  
    66  func dirName(path string) string {
    67  	return filepath.Dir(path)
    68  	/*
    69  		pathSeparators := "\\/"
    70  		end := pathSeparators + len(pathSeparators) - 1
    71  
    72  		slashPos := path.findLastOf(pathSeparators)
    73  		if slashPos == -1 {
    74  			return "" // Nothing to do.
    75  		}
    76  		for slashPos > 0 && find(pathSeparators, end, path[slashPos-1]) != end {
    77  			slashPos--
    78  		}
    79  		return path[0:slashPos]
    80  	*/
    81  }
    82  
    83  func statSingleFile(path string) (TimeStamp, error) {
    84  	s, err := os.Stat(path)
    85  	if err != nil {
    86  		// See TestDiskInterfaceTest_StatMissingFile for rationale for ENOTDIR
    87  		// check.
    88  		if os.IsNotExist(err) || errors.Unwrap(err) == syscall.ENOTDIR {
    89  			return 0, nil
    90  		}
    91  		return -1, err
    92  	}
    93  	return TimeStamp(s.ModTime().UnixMicro()), nil
    94  }
    95  
    96  func statAllFilesInDir(dir string, stamps map[string]TimeStamp) error {
    97  	f, err := os.Open(dir)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	d, err := f.Readdir(0)
   102  	if err != nil {
   103  		_ = f.Close()
   104  		return err
   105  	}
   106  	for _, i := range d {
   107  		if !i.IsDir() {
   108  			stamps[i.Name()] = TimeStamp(i.ModTime().UnixMicro())
   109  		}
   110  	}
   111  	return f.Close()
   112  }
   113  
   114  // MakeDirs create all the parent directories for path; like mkdir -p
   115  // `basename path`.
   116  func MakeDirs(d DiskInterface, path string) error {
   117  	dir := dirName(path)
   118  	if dir == path || dir == "." || dir == "" {
   119  		return nil // Reached root; assume it's there.
   120  	}
   121  	mtime, err := d.Stat(dir)
   122  	if mtime < 0 {
   123  		return err
   124  	}
   125  	if mtime > 0 {
   126  		return nil // Exists already; we're done.
   127  	}
   128  
   129  	// Directory doesn't exist.  Try creating its parent first.
   130  	if err := MakeDirs(d, dir); err != nil {
   131  		return err
   132  	}
   133  	return d.MakeDir(dir)
   134  }
   135  
   136  //
   137  
   138  // RealDiskInterface is the implementation of DiskInterface that actually hits
   139  // the disk.
   140  type RealDiskInterface struct {
   141  	// Whether stat information can be cached.
   142  	useCache bool
   143  
   144  	// TODO: Neither a map nor a hashmap seems ideal here.  If the statcache
   145  	// works out, come up with a better data structure.
   146  	cache cache
   147  }
   148  
   149  // MSDN: "Naming Files, Paths, and Namespaces"
   150  // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx
   151  const maxPath = 260
   152  
   153  // Stat implements DiskInterface.
   154  func (r *RealDiskInterface) Stat(path string) (TimeStamp, error) {
   155  	defer metricRecord("node stat")()
   156  	if runtime.GOOS == "windows" {
   157  		if path != "" && path[0] != '\\' && len(path) >= maxPath {
   158  			return -1, fmt.Errorf("Stat(%s): Filename longer than %d characters", path, maxPath)
   159  		}
   160  		if !r.useCache {
   161  			return statSingleFile(path)
   162  		}
   163  
   164  		dir := dirName(path)
   165  		o := 0
   166  		if dir != "" {
   167  			o = len(dir) + 1
   168  		}
   169  		base := path[o:]
   170  		if base == ".." {
   171  			// statAllFilesInDir does not report any information for base = "..".
   172  			base = "."
   173  			dir = path
   174  		}
   175  
   176  		dir = strings.ToLower(dir)
   177  		base = strings.ToLower(base)
   178  
   179  		ci, ok := r.cache[dir]
   180  		if !ok {
   181  			ci = dirCache{}
   182  			r.cache[dir] = ci
   183  			s := "."
   184  			if dir != "" {
   185  				s = dir
   186  			}
   187  			if err := statAllFilesInDir(s, ci); err != nil {
   188  				delete(r.cache, dir)
   189  				return -1, err
   190  			}
   191  		}
   192  		return ci[base], nil
   193  	}
   194  	return statSingleFile(path)
   195  }
   196  
   197  // WriteFile implements DiskInterface.
   198  func (r *RealDiskInterface) WriteFile(path string, contents string) error {
   199  	return ioutil.WriteFile(path, unsafeByteSlice(contents), 0o666)
   200  }
   201  
   202  // MakeDir implements DiskInterface.
   203  func (r *RealDiskInterface) MakeDir(path string) error {
   204  	return os.Mkdir(path, 0o777)
   205  }
   206  
   207  // ReadFile implements DiskInterface.
   208  func (r *RealDiskInterface) ReadFile(path string) ([]byte, error) {
   209  	c, err := ioutil.ReadFile(path)
   210  	if err == nil {
   211  		if len(c) != 0 {
   212  			// ioutil.ReadFile() is guaranteed to have an extra byte in the slice,
   213  			// (ab)use it.
   214  			c = c[:len(c)+1]
   215  		}
   216  		return c, nil
   217  	}
   218  	return nil, err
   219  }
   220  
   221  // RemoveFile implements DiskInterface.
   222  func (r *RealDiskInterface) RemoveFile(path string) error {
   223  	return os.Remove(path)
   224  }
   225  
   226  // AllowStatCache sets whether stat information can be cached.
   227  //
   228  // Only has an effect on Windows.
   229  func (r *RealDiskInterface) AllowStatCache(allow bool) {
   230  	if runtime.GOOS == "windows" {
   231  		r.useCache = allow
   232  		if !r.useCache {
   233  			r.cache = nil
   234  		} else if r.cache == nil {
   235  			r.cache = cache{}
   236  		}
   237  	}
   238  }