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  }