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