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  }