github.com/nak3/source-to-image@v1.1.10-0.20180319140719-2ed55639898d/pkg/util/fs/fs.go (about) 1 package fs 2 3 import ( 4 "fmt" 5 "io" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path" 10 "path/filepath" 11 "runtime" 12 "sync" 13 "time" 14 15 "github.com/openshift/source-to-image/pkg/util/cmd" 16 utilglog "github.com/openshift/source-to-image/pkg/util/glog" 17 18 s2ierr "github.com/openshift/source-to-image/pkg/errors" 19 ) 20 21 var glog = utilglog.StderrLog 22 23 // FileSystem allows STI to work with the file system and 24 // perform tasks such as creating and deleting directories 25 type FileSystem interface { 26 Chmod(file string, mode os.FileMode) error 27 Rename(from, to string) error 28 MkdirAll(dirname string) error 29 MkdirAllWithPermissions(dirname string, perm os.FileMode) error 30 Mkdir(dirname string) error 31 Exists(file string) bool 32 Copy(sourcePath, targetPath string) error 33 CopyContents(sourcePath, targetPath string) error 34 RemoveDirectory(dir string) error 35 CreateWorkingDirectory() (string, error) 36 Open(file string) (io.ReadCloser, error) 37 Create(file string) (io.WriteCloser, error) 38 WriteFile(file string, data []byte) error 39 ReadDir(string) ([]os.FileInfo, error) 40 Stat(string) (os.FileInfo, error) 41 Lstat(string) (os.FileInfo, error) 42 Walk(string, filepath.WalkFunc) error 43 Readlink(string) (string, error) 44 Symlink(string, string) error 45 } 46 47 // NewFileSystem creates a new instance of the default FileSystem 48 // implementation 49 func NewFileSystem() FileSystem { 50 return &fs{ 51 runner: cmd.NewCommandRunner(), 52 fileModes: make(map[string]os.FileMode), 53 } 54 } 55 56 type fs struct { 57 runner cmd.CommandRunner 58 59 // on Windows, fileModes is used to track the UNIX file mode of every file we 60 // work with; m is used to synchronize access to fileModes. 61 fileModes map[string]os.FileMode 62 m sync.Mutex 63 } 64 65 // FileInfo is a struct which implements os.FileInfo. We use it (a) for test 66 // purposes, and (b) because we enrich the FileMode on Windows systems 67 type FileInfo struct { 68 FileName string 69 FileSize int64 70 FileMode os.FileMode 71 FileModTime time.Time 72 FileIsDir bool 73 FileSys interface{} 74 } 75 76 // Name retuns the filename of fi 77 func (fi *FileInfo) Name() string { 78 return fi.FileName 79 } 80 81 // Size returns the file size of fi 82 func (fi *FileInfo) Size() int64 { 83 return fi.FileSize 84 } 85 86 // Mode returns the file mode of fi 87 func (fi *FileInfo) Mode() os.FileMode { 88 return fi.FileMode 89 } 90 91 // ModTime returns the file modification time of fi 92 func (fi *FileInfo) ModTime() time.Time { 93 return fi.FileModTime 94 } 95 96 // IsDir returns true if fi refers to a directory 97 func (fi *FileInfo) IsDir() bool { 98 return fi.FileIsDir 99 } 100 101 // Sys returns the sys interface of fi 102 func (fi *FileInfo) Sys() interface{} { 103 return fi.FileSys 104 } 105 106 func copyFileInfo(src os.FileInfo) *FileInfo { 107 return &FileInfo{ 108 FileName: src.Name(), 109 FileSize: src.Size(), 110 FileMode: src.Mode(), 111 FileModTime: src.ModTime(), 112 FileIsDir: src.IsDir(), 113 FileSys: src.Sys(), 114 } 115 } 116 117 // Stat returns a FileInfo describing the named file. 118 func (h *fs) Stat(path string) (os.FileInfo, error) { 119 fi, err := os.Stat(path) 120 if runtime.GOOS == "windows" && err == nil { 121 fi = h.enrichFileInfo(path, fi) 122 } 123 return fi, err 124 } 125 126 // Lstat returns a FileInfo describing the named file (not following symlinks). 127 func (h *fs) Lstat(path string) (os.FileInfo, error) { 128 fi, err := os.Lstat(path) 129 if runtime.GOOS == "windows" && err == nil { 130 fi = h.enrichFileInfo(path, fi) 131 } 132 return fi, err 133 } 134 135 // ReadDir reads the directory named by dirname and returns a list of directory 136 // entries sorted by filename. 137 func (h *fs) ReadDir(path string) ([]os.FileInfo, error) { 138 fis, err := ioutil.ReadDir(path) 139 if runtime.GOOS == "windows" && err == nil { 140 h.enrichFileInfos(path, fis) 141 } 142 return fis, err 143 } 144 145 // Chmod sets the file mode 146 func (h *fs) Chmod(file string, mode os.FileMode) error { 147 err := os.Chmod(file, mode) 148 if runtime.GOOS == "windows" && err == nil { 149 h.m.Lock() 150 h.fileModes[file] = mode 151 h.m.Unlock() 152 return nil 153 } 154 return err 155 } 156 157 // Rename renames or moves a file 158 func (h *fs) Rename(from, to string) error { 159 return os.Rename(from, to) 160 } 161 162 // MkdirAll creates the directory and all its parents 163 func (h *fs) MkdirAll(dirname string) error { 164 return os.MkdirAll(dirname, 0700) 165 } 166 167 // MkdirAllWithPermissions creates the directory and all its parents with the provided permissions 168 func (h *fs) MkdirAllWithPermissions(dirname string, perm os.FileMode) error { 169 return os.MkdirAll(dirname, perm) 170 } 171 172 // Mkdir creates the specified directory 173 func (h *fs) Mkdir(dirname string) error { 174 return os.Mkdir(dirname, 0700) 175 } 176 177 // Exists determines whether the given file exists 178 func (h *fs) Exists(file string) bool { 179 _, err := h.Stat(file) 180 return err == nil 181 } 182 183 // Copy copies the source to a destination. 184 // If the source is a file, then the destination has to be a file as well, 185 // otherwise you will get an error. 186 // If the source is a directory, then the destination has to be a directory and 187 // we copy the content of the source directory to destination directory 188 // recursively. 189 func (h *fs) Copy(source string, dest string) (err error) { 190 return doCopy(h, source, dest) 191 } 192 193 func doCopy(h FileSystem, source, dest string) error { 194 // Our current symlink behaviour is broken. We follow symlinks and copy the 195 // referenced file, and fail when a symlink is broken. In all cases we 196 // should actually copy the symlink as is. For now, at least catch the 197 // broken symlink case and copy the symlink rather than failing. 198 lstatinfo, lstaterr := h.Lstat(source) 199 _, staterr := h.Stat(source) 200 if lstaterr == nil && 201 lstatinfo.Mode()&os.ModeSymlink != 0 && 202 os.IsNotExist(staterr) { 203 glog.V(5).Infof("(broken) L %q -> %q", source, dest) 204 linkdest, err := h.Readlink(source) 205 if err != nil { 206 return err 207 } 208 209 return h.Symlink(linkdest, dest) 210 } 211 212 sourcefile, err := h.Open(source) 213 if err != nil { 214 return err 215 } 216 defer sourcefile.Close() 217 sourceinfo, err := h.Stat(source) 218 if err != nil { 219 return err 220 } 221 222 if sourceinfo.IsDir() { 223 glog.V(5).Infof("D %q -> %q", source, dest) 224 return h.CopyContents(source, dest) 225 } 226 227 destinfo, _ := h.Stat(dest) 228 if destinfo != nil && destinfo.IsDir() { 229 return fmt.Errorf("destination must be full path to a file, not directory") 230 } 231 destfile, err := h.Create(dest) 232 if err != nil { 233 return err 234 } 235 defer destfile.Close() 236 glog.V(5).Infof("F %q -> %q", source, dest) 237 if _, err := io.Copy(destfile, sourcefile); err != nil { 238 return err 239 } 240 241 return h.Chmod(dest, sourceinfo.Mode()) 242 } 243 244 // CopyContents copies the content of the source directory to a destination 245 // directory. 246 // If the destination directory does not exists, it will be created. 247 // The source directory itself will not be copied, only its content. If you 248 // want this behavior, the destination must include the source directory name. 249 func (h *fs) CopyContents(src string, dest string) (err error) { 250 sourceinfo, err := h.Stat(src) 251 if err != nil { 252 return err 253 } 254 if err = os.MkdirAll(dest, sourceinfo.Mode()); err != nil { 255 return err 256 } 257 directory, err := os.Open(src) 258 if err != nil { 259 return err 260 } 261 defer directory.Close() 262 objects, err := directory.Readdir(-1) 263 if err != nil { 264 return err 265 } 266 for _, obj := range objects { 267 source := path.Join(src, obj.Name()) 268 destination := path.Join(dest, obj.Name()) 269 if err := h.Copy(source, destination); err != nil { 270 return err 271 } 272 } 273 return 274 } 275 276 // RemoveDirectory removes the specified directory and all its contents 277 func (h *fs) RemoveDirectory(dir string) error { 278 glog.V(2).Infof("Removing directory '%s'", dir) 279 280 // HACK: If deleting a directory in windows, call out to the system to do the deletion 281 // TODO: Remove this workaround when we switch to go 1.7 -- os.RemoveAll should 282 // be fixed for Windows in that release. https://github.com/golang/go/issues/9606 283 if runtime.GOOS == "windows" { 284 command := exec.Command("cmd.exe", "/c", fmt.Sprintf("rd /s /q %s", dir)) 285 output, err := command.Output() 286 if err != nil { 287 glog.Errorf("Error removing directory %q: %v %s", dir, err, string(output)) 288 return err 289 } 290 return nil 291 } 292 293 err := os.RemoveAll(dir) 294 if err != nil { 295 glog.Errorf("Error removing directory '%s': %v", dir, err) 296 } 297 return err 298 } 299 300 // CreateWorkingDirectory creates a directory to be used for STI 301 func (h *fs) CreateWorkingDirectory() (directory string, err error) { 302 directory, err = ioutil.TempDir("", "s2i") 303 if err != nil { 304 return "", s2ierr.NewWorkDirError(directory, err) 305 } 306 307 return directory, err 308 } 309 310 // Open opens a file and returns a ReadCloser interface to that file 311 func (h *fs) Open(filename string) (io.ReadCloser, error) { 312 return os.Open(filename) 313 } 314 315 // Create creates a file and returns a WriteCloser interface to that file 316 func (h *fs) Create(filename string) (io.WriteCloser, error) { 317 return os.Create(filename) 318 } 319 320 // WriteFile opens a file and writes data to it, returning error if such 321 // occurred 322 func (h *fs) WriteFile(filename string, data []byte) error { 323 return ioutil.WriteFile(filename, data, 0700) 324 } 325 326 // Walk walks the file tree rooted at root, calling walkFn for each file or 327 // directory in the tree, including root. 328 func (h *fs) Walk(root string, walkFn filepath.WalkFunc) error { 329 wrapper := func(path string, info os.FileInfo, err error) error { 330 if runtime.GOOS == "windows" && err == nil { 331 info = h.enrichFileInfo(path, info) 332 } 333 return walkFn(path, info, err) 334 } 335 return filepath.Walk(root, wrapper) 336 } 337 338 // enrichFileInfo is used on Windows. It takes an os.FileInfo object, e.g. as 339 // returned by os.Stat, and enriches the OS-returned file mode with the "real" 340 // UNIX file mode, if we know what it is. 341 func (h *fs) enrichFileInfo(path string, fi os.FileInfo) os.FileInfo { 342 h.m.Lock() 343 if mode, ok := h.fileModes[path]; ok { 344 fi = copyFileInfo(fi) 345 fi.(*FileInfo).FileMode = mode 346 } 347 h.m.Unlock() 348 return fi 349 } 350 351 // enrichFileInfos is used on Windows. It takes an array of os.FileInfo 352 // objects, e.g. as returned by os.ReadDir, and for each file enriches the OS- 353 // returned file mode with the "real" UNIX file mode, if we know what it is. 354 func (h *fs) enrichFileInfos(root string, fis []os.FileInfo) { 355 h.m.Lock() 356 for i := range fis { 357 if mode, ok := h.fileModes[filepath.Join(root, fis[i].Name())]; ok { 358 fis[i] = copyFileInfo(fis[i]) 359 fis[i].(*FileInfo).FileMode = mode 360 } 361 } 362 h.m.Unlock() 363 } 364 365 // Readlink reads the destination of a symlink 366 func (h *fs) Readlink(name string) (string, error) { 367 return os.Readlink(name) 368 } 369 370 // Symlink creates a symlink at newname, pointing to oldname 371 func (h *fs) Symlink(oldname, newname string) error { 372 return os.Symlink(oldname, newname) 373 }