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 }