github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/fileutils/fileutils.go (about)

     1  package fileutils
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"runtime"
    15  	"strings"
    16  	"time"
    17  	"unicode"
    18  
    19  	"github.com/gofrs/flock"
    20  	"github.com/thoas/go-funk"
    21  
    22  	"github.com/ActiveState/cli/internal/assets"
    23  	"github.com/ActiveState/cli/internal/errs"
    24  	"github.com/ActiveState/cli/internal/locale"
    25  	"github.com/ActiveState/cli/internal/logging"
    26  	"github.com/ActiveState/cli/internal/multilog"
    27  	"github.com/ActiveState/cli/internal/rollbar"
    28  )
    29  
    30  // nullByte represents the null-terminator byte
    31  const nullByte byte = 0
    32  
    33  // FileMode is the mode used for created files
    34  const FileMode = 0o644
    35  
    36  // DirMode is the mode used for created dirs
    37  const DirMode = os.ModePerm
    38  
    39  // AmendOptions used to specify write actions for WriteFile
    40  type AmendOptions uint8
    41  
    42  const (
    43  	// AmendByAppend content to end of file
    44  	AmendByAppend AmendOptions = iota
    45  	// WriteOverwrite file with contents
    46  	WriteOverwrite
    47  	// AmendByPrepend - add content start of file
    48  	AmendByPrepend
    49  )
    50  
    51  var ErrorFileNotFound = errs.New("File could not be found")
    52  
    53  // ReplaceAll replaces all instances of search text with replacement text in a
    54  // file, which may be a binary file.
    55  func ReplaceAll(filename, find, replace string) error {
    56  	// Read the file's bytes and create find and replace byte arrays for search
    57  	// and replace.
    58  	fileBytes, err := os.ReadFile(filename)
    59  	if err != nil {
    60  		return err
    61  	}
    62  
    63  	changed, byts, err := replaceInFile(fileBytes, find, replace)
    64  	if err != nil {
    65  		return err
    66  	}
    67  
    68  	// skip writing file, if we did not change anything
    69  	if !changed {
    70  		return nil
    71  	}
    72  
    73  	if err := WriteFile(filename, byts); err != nil {
    74  		return errs.Wrap(err, "WriteFile %s failed", filename)
    75  	}
    76  
    77  	return nil
    78  }
    79  
    80  // replaceInFile replaces all occurrences of oldpath with newpath
    81  // For binary files with nul-terminated strings, it ensures that the replaces strings are still valid nul-terminated strings and the returned buffer has the same size as the input buffer buf.
    82  // The first return argument denotes whether at least one file has been replaced
    83  func replaceInFile(buf []byte, oldpath, newpath string) (bool, []byte, error) {
    84  	findBytes := []byte(oldpath)
    85  	replaceBytes := []byte(newpath)
    86  	replaceBytesLen := len(replaceBytes)
    87  
    88  	// Check if the file is a binary file. If so, the search and replace byte
    89  	// arrays must be of equal length (replacement being NUL-padded as necessary).
    90  	var replaceRegex *regexp.Regexp
    91  	quoteEscapeFind := regexp.QuoteMeta(oldpath)
    92  
    93  	// Ensure we replace both types of backslashes on Windows
    94  	if runtime.GOOS == "windows" {
    95  		quoteEscapeFind = strings.ReplaceAll(quoteEscapeFind, `\\`, `(\\|\\\\)`)
    96  	}
    97  	if IsBinary(buf) {
    98  		// logging.Debug("Assuming file '%s' is a binary file", filename)
    99  
   100  		regexExpandBytes := []byte("${1}")
   101  		// Must account for the expand characters (ie. '${1}') in the
   102  		// replacement bytes in order for the binary paddding to be correct
   103  		replaceBytes = append(replaceBytes, regexExpandBytes...)
   104  
   105  		// Replacement regex for binary files must account for null characters
   106  		replaceRegex = regexp.MustCompile(fmt.Sprintf(`%s([^\x00]*)`, quoteEscapeFind))
   107  		if replaceBytesLen > len(findBytes) {
   108  			multilog.Log(logging.ErrorNoStacktrace, rollbar.Error)("Replacement text too long: %s, original text: %s", string(replaceBytes), string(findBytes))
   109  			return false, nil, errors.New("replacement text cannot be longer than search text in a binary file")
   110  		} else if len(findBytes) > replaceBytesLen {
   111  			// Pad replacement with NUL bytes.
   112  			// logging.Debug("Padding replacement text by %d byte(s)", len(findBytes)-len(replaceBytes))
   113  			paddedReplaceBytes := make([]byte, len(findBytes)+len(regexExpandBytes))
   114  			copy(paddedReplaceBytes, replaceBytes)
   115  			replaceBytes = paddedReplaceBytes
   116  		}
   117  	} else {
   118  		replaceRegex = regexp.MustCompile(quoteEscapeFind)
   119  		// logging.Debug("Assuming file '%s' is a text file", filename)
   120  	}
   121  
   122  	replaced := replaceRegex.ReplaceAll(buf, replaceBytes)
   123  
   124  	return !bytes.Equal(replaced, buf), replaced, nil
   125  }
   126  
   127  // IsSymlink checks if a path is a symlink
   128  func IsSymlink(path string) bool {
   129  	fi, err := os.Lstat(path)
   130  	if err != nil {
   131  		return false
   132  	}
   133  	return (fi.Mode() & os.ModeSymlink) == os.ModeSymlink
   134  }
   135  
   136  // IsBinary checks if the given bytes are for a binary file
   137  func IsBinary(fileBytes []byte) bool {
   138  	return bytes.IndexByte(fileBytes, nullByte) != -1
   139  }
   140  
   141  // TargetExists checks if the given file or folder exists
   142  func TargetExists(path string) bool {
   143  	if FileExists(path) || DirExists(path) {
   144  		return true
   145  	}
   146  
   147  	_, err1 := os.Stat(path)
   148  	_, err2 := os.Readlink(path) // os.Stat returns false on Symlinks that don't point to a valid file
   149  	_, err3 := os.Lstat(path)    // for links where os.Stat and os.Readlink fail (e.g. Windows socket files)
   150  	return err1 == nil || err2 == nil || err3 == nil
   151  }
   152  
   153  // FileExists checks if the given file (not folder) exists
   154  func FileExists(path string) bool {
   155  	fi, err := os.Stat(path)
   156  	if err != nil {
   157  		return false
   158  	}
   159  
   160  	mode := fi.Mode()
   161  	return mode.IsRegular()
   162  }
   163  
   164  // DirExists checks if the given directory exists
   165  func DirExists(path string) bool {
   166  	fi, err := os.Stat(path)
   167  	if err != nil {
   168  		return false
   169  	}
   170  
   171  	mode := fi.Mode()
   172  	return mode.IsDir()
   173  }
   174  
   175  // Sha256Hash will sha256 hash the given file
   176  func Sha256Hash(path string) (string, error) {
   177  	hasher := sha256.New()
   178  	b, err := os.ReadFile(path)
   179  	if err != nil {
   180  		return "", errs.Wrap(err, fmt.Sprintf("Cannot read file: %s", path))
   181  	}
   182  	hasher.Write(b)
   183  	return hex.EncodeToString(hasher.Sum(nil)), nil
   184  }
   185  
   186  // HashDirectory will sha256 hash the given directory
   187  func HashDirectory(path string) (string, error) {
   188  	hasher := sha256.New()
   189  	err := filepath.Walk(path, func(path string, f os.FileInfo, err error) error {
   190  		if err != nil {
   191  			return err
   192  		}
   193  
   194  		if f.IsDir() {
   195  			return nil
   196  		}
   197  
   198  		b, err := os.ReadFile(path)
   199  		if err != nil {
   200  			return err
   201  		}
   202  		hasher.Write(b)
   203  
   204  		return nil
   205  	})
   206  	if err != nil {
   207  		return "", errs.Wrap(err, fmt.Sprintf("Cannot hash directory: %s", path))
   208  	}
   209  
   210  	return hex.EncodeToString(hasher.Sum(nil)), nil
   211  }
   212  
   213  // Mkdir is a small helper function to create a directory if it doesnt already exist
   214  func Mkdir(path string, subpath ...string) error {
   215  	if len(subpath) > 0 {
   216  		subpathStr := filepath.Join(subpath...)
   217  		path = filepath.Join(path, subpathStr)
   218  	}
   219  	if _, err := os.Stat(path); os.IsNotExist(err) {
   220  		err = os.MkdirAll(path, DirMode)
   221  		if err != nil {
   222  			return errs.Wrap(err, fmt.Sprintf("MkdirAll failed for path: %s", path))
   223  		}
   224  	}
   225  	return nil
   226  }
   227  
   228  // MkdirUnlessExists will make the directory structure if it doesn't already exists
   229  func MkdirUnlessExists(path string) error {
   230  	if DirExists(path) {
   231  		return nil
   232  	}
   233  	return Mkdir(path)
   234  }
   235  
   236  // CopyFile copies a file from one location to another
   237  func CopyFile(src, target string) error {
   238  	in, err := os.Open(src)
   239  	if err != nil {
   240  		return errs.Wrap(err, "os.Open %s failed", src)
   241  	}
   242  	defer in.Close()
   243  
   244  	inInfo, err := in.Stat()
   245  	if err != nil {
   246  		return errs.Wrap(err, "get file info failed")
   247  	}
   248  
   249  	// Create target directory if it doesn't exist
   250  	dir := filepath.Dir(target)
   251  	err = MkdirUnlessExists(dir)
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	// Create target file
   257  	out, err := os.Create(target)
   258  	if err != nil {
   259  		return errs.Wrap(err, "os.Create %s failed", target)
   260  	}
   261  	defer out.Close()
   262  
   263  	// Copy bytes to target file
   264  	_, err = io.Copy(out, in)
   265  	if err != nil {
   266  		return errs.Wrap(err, "io.Copy failed")
   267  	}
   268  	err = out.Close()
   269  	if err != nil {
   270  		return errs.Wrap(err, "out.Close failed")
   271  	}
   272  
   273  	if err := os.Chmod(out.Name(), inInfo.Mode().Perm()); err != nil {
   274  		return errs.Wrap(err, "chmod failed")
   275  	}
   276  
   277  	return nil
   278  }
   279  
   280  func CopyAsset(assetName, dest string) error {
   281  	asset, err := assets.ReadFileBytes(assetName)
   282  	if err != nil {
   283  		return errs.Wrap(err, "Asset %s failed", assetName)
   284  	}
   285  
   286  	err = os.WriteFile(dest, asset, 0o644)
   287  	if err != nil {
   288  		return errs.Wrap(err, "os.WriteFile %s failed", dest)
   289  	}
   290  
   291  	return nil
   292  }
   293  
   294  func CopyMultipleFiles(files map[string]string) error {
   295  	for src, target := range files {
   296  		err := CopyFile(src, target)
   297  		if err != nil {
   298  			return err
   299  		}
   300  	}
   301  	return nil
   302  }
   303  
   304  // ReadFileUnsafe is an unsafe version of os.ReadFile, DO NOT USE THIS OUTSIDE OF TESTS
   305  func ReadFileUnsafe(src string) []byte {
   306  	b, err := os.ReadFile(src)
   307  	if err != nil {
   308  		panic(fmt.Sprintf("Cannot read file: %s, error: %s", src, err.Error()))
   309  	}
   310  	return b
   311  }
   312  
   313  // ReadFile reads the content of a file
   314  func ReadFile(filePath string) ([]byte, error) {
   315  	b, err := os.ReadFile(filePath)
   316  	if err != nil {
   317  		return nil, errs.Wrap(err, "os.ReadFile %s failed", filePath)
   318  	}
   319  	return b, nil
   320  }
   321  
   322  // WriteFile writes data to a file, if it exists it is overwritten, if it doesn't exist it is created and data is written
   323  func WriteFile(filePath string, data []byte) (rerr error) {
   324  	err := MkdirUnlessExists(filepath.Dir(filePath))
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	// make the target file temporarily writable
   330  	fileExists := FileExists(filePath)
   331  	if fileExists {
   332  		stat, _ := os.Stat(filePath)
   333  		if err := os.Chmod(filePath, FileMode); err != nil {
   334  			return errs.Wrap(err, "os.Chmod %s failed", filePath)
   335  		}
   336  		defer func() {
   337  			err = os.Chmod(filePath, stat.Mode().Perm())
   338  			if err != nil {
   339  				rerr = errs.Pack(rerr, errs.Wrap(err, "os.Chmod %s failed", filePath))
   340  			}
   341  		}()
   342  	}
   343  
   344  	f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, FileMode)
   345  	if err != nil {
   346  		if !fileExists {
   347  			target := filepath.Dir(filePath)
   348  			err = errs.Pack(err, fmt.Errorf("access to target %q is denied", target))
   349  		}
   350  		return errs.Wrap(err, "os.OpenFile %s failed", filePath)
   351  	}
   352  	defer f.Close()
   353  
   354  	_, err = f.Write(data)
   355  	if err != nil {
   356  		return errs.Wrap(err, "file.Write %s failed", filePath)
   357  	}
   358  	return nil
   359  }
   360  
   361  func AmendFileLocked(filePath string, data []byte, flag AmendOptions) error {
   362  	locker := flock.New(filePath + ".lock")
   363  
   364  	if err := locker.Lock(); err != nil {
   365  		return errs.Wrap(err, "Could not acquire file lock")
   366  	}
   367  
   368  	if err := AmendFile(filePath, data, flag); err != nil {
   369  		return errs.Wrap(err, "Could not write to file")
   370  	}
   371  
   372  	return locker.Unlock()
   373  }
   374  
   375  // AppendToFile appends the data to the file (if it exists) with the given data, if the file doesn't exist
   376  // it is created and the data is written
   377  func AppendToFile(filepath string, data []byte) error {
   378  	return AmendFile(filepath, data, AmendByAppend)
   379  }
   380  
   381  // PrependToFile prepends the data to the file (if it exists) with the given data, if the file doesn't exist
   382  // it is created and the data is written
   383  func PrependToFile(filepath string, data []byte) error {
   384  	return AmendFile(filepath, data, AmendByPrepend)
   385  }
   386  
   387  // AmendFile amends data to a file, supports append, or prepend
   388  func AmendFile(filePath string, data []byte, flag AmendOptions) error {
   389  	switch flag {
   390  	case AmendByAppend, AmendByPrepend:
   391  
   392  	default:
   393  		return locale.NewInputError("fileutils_err_amend_file", "", filePath)
   394  	}
   395  
   396  	err := Touch(filePath)
   397  	if err != nil {
   398  		return errs.Wrap(err, "Touch %s failed", filePath)
   399  	}
   400  
   401  	b, err := ReadFile(filePath)
   402  	if err != nil {
   403  		return errs.Wrap(err, "ReadFile %s failed", filePath)
   404  	}
   405  
   406  	if flag == AmendByPrepend {
   407  		data = append(data, b...)
   408  	} else if flag == AmendByAppend {
   409  		data = append(b, data...)
   410  	}
   411  
   412  	f, err := os.OpenFile(filePath, os.O_WRONLY, FileMode)
   413  	if err != nil {
   414  		return errs.Wrap(err, "os.OpenFile %s failed", filePath)
   415  	}
   416  	defer f.Close()
   417  
   418  	_, err = f.Write(data)
   419  	if err != nil {
   420  		return errs.Wrap(err, "file.Write %s failed", filePath)
   421  	}
   422  	return nil
   423  }
   424  
   425  // FindFileInPath will find a file by the given file-name in the directory provided or in
   426  // one of the parent directories of that path by walking up the tree. If the file is found,
   427  // the path to that file is returned, otherwise an failure is returned.
   428  func FindFileInPath(dir, filename string) (string, error) {
   429  	absDir, err := filepath.Abs(dir)
   430  	if err != nil {
   431  		return "", errs.Wrap(err, "filepath.Abs %s failed", dir)
   432  	} else if filepath := walkPathAndFindFile(absDir, filename); filepath != "" {
   433  		return filepath, nil
   434  	}
   435  	return "", locale.WrapError(ErrorFileNotFound, "err_file_not_found_in_path", "", filename, absDir)
   436  }
   437  
   438  // walkPathAndFindFile finds a file in the provided directory or one of its parent directories.
   439  // walkPathAndFindFile prefers an absolute directory path.
   440  func walkPathAndFindFile(dir, filename string) string {
   441  	if file := filepath.Join(dir, filename); FileExists(file) {
   442  		return file
   443  	} else if parentDir := filepath.Dir(dir); parentDir != dir {
   444  		return walkPathAndFindFile(parentDir, filename)
   445  	}
   446  	return ""
   447  }
   448  
   449  // Touch will attempt to "touch" a given filename by trying to open it read-only or create
   450  // the file with 0644 perms if it does not exist.
   451  func Touch(path string) error {
   452  	err := MkdirUnlessExists(filepath.Dir(path))
   453  	if err != nil {
   454  		return err
   455  	}
   456  	file, err := os.OpenFile(path, os.O_CREATE, FileMode)
   457  	if err != nil {
   458  		return errs.Wrap(err, "os.OpenFile %s failed", path)
   459  	}
   460  	if err := file.Close(); err != nil {
   461  		return errs.Wrap(err, "file.Close %s failed", path)
   462  	}
   463  	return nil
   464  }
   465  
   466  // TouchFileUnlessExists will attempt to "touch" a given filename if it doesn't already exists
   467  func TouchFileUnlessExists(path string) error {
   468  	if TargetExists(path) {
   469  		return nil
   470  	}
   471  	return Touch(path)
   472  }
   473  
   474  // IsEmptyDir returns true if the directory at the provided path has no files (including dirs) within it.
   475  func IsEmptyDir(path string) (bool, error) {
   476  	dir, err := os.Open(path)
   477  	if err != nil {
   478  		return false, errs.Wrap(err, "os.Open %s failed", path)
   479  	}
   480  
   481  	files, err := dir.Readdir(1)
   482  	dir.Close()
   483  	if err != nil && err != io.EOF {
   484  		return false, errs.Wrap(err, "dir.Readdir %s failed", path)
   485  	}
   486  
   487  	return (len(files) == 0), nil
   488  }
   489  
   490  // MoveAllFilesCallback is invoked for every file that we move
   491  type MoveAllFilesCallback func(fromPath, toPath string)
   492  
   493  // MoveAllFilesRecursively moves files and directories from one directory to another.
   494  // Unlike in MoveAllFiles, the destination directory does not need to be empty, and
   495  // may include directories that are moved from the source directory.
   496  // It also counts the moved files for use in a progress bar.
   497  // Warnings are printed if
   498  // - a source file overwrites an existing destination file
   499  // - a sub-directory exists in both the source and and the destination and their permissions do not match
   500  func MoveAllFilesRecursively(fromPath, toPath string, cb MoveAllFilesCallback) error {
   501  	if !DirExists(fromPath) {
   502  		return locale.NewError("err_os_not_a_directory", "", fromPath)
   503  	} else if !DirExists(toPath) {
   504  		return locale.NewError("err_os_not_a_directory", "", toPath)
   505  	}
   506  
   507  	// read all child files and dirs
   508  	dir, err := os.Open(fromPath)
   509  	if err != nil {
   510  		return errs.Wrap(err, "os.Open %s failed", fromPath)
   511  	}
   512  	fileInfos, err := dir.Readdir(-1)
   513  	dir.Close()
   514  	if err != nil {
   515  		return errs.Wrap(err, "dir.Readdir %s failed", fromPath)
   516  	}
   517  
   518  	// any found files and dirs
   519  	for _, fileInfo := range fileInfos {
   520  		subFromPath := filepath.Join(fromPath, fileInfo.Name())
   521  		subToPath := filepath.Join(toPath, fileInfo.Name())
   522  		toInfo, err := os.Lstat(subToPath)
   523  		// if stat returns, the destination path exists (either file or directory)
   524  		toPathExists := toInfo != nil && err == nil
   525  		// handle case where destination exists
   526  		if toPathExists {
   527  			if fileInfo.IsDir() != toInfo.IsDir() {
   528  				return locale.NewError("err_incompatible_move_file_dir", "", subFromPath, subToPath)
   529  			}
   530  			if fileInfo.Mode() != toInfo.Mode() {
   531  				logging.Warning(locale.Tr("warn_move_incompatible_modes", "", subFromPath, subToPath))
   532  			}
   533  
   534  			if !toInfo.IsDir() {
   535  				// If the subToPath file exists, we remove it first - in order to ensure compatibility between platforms:
   536  				// On Windows, the following renaming step can otherwise fail if subToPath is read-only (file removal is allowed)
   537  				err = os.Remove(subToPath)
   538  				if err != nil {
   539  					multilog.Error("Failed to remove file scheduled to be overwritten: %s (file mode: %#o): %v", subToPath, toInfo.Mode(), err)
   540  				}
   541  			}
   542  		}
   543  
   544  		// If we are moving to a directory, call function recursively to overwrite and add files in that directory
   545  		if fileInfo.IsDir() {
   546  			// create target directories that don't exist yet
   547  			if !toPathExists {
   548  				err = Mkdir(subToPath)
   549  				if err != nil {
   550  					return locale.WrapError(err, "err_move_create_directory", "Failed to create directory {{.V0}}", subToPath)
   551  				}
   552  				err = os.Chmod(subToPath, fileInfo.Mode())
   553  				if err != nil {
   554  					return locale.WrapError(err, "err_move_set_dir_permissions", "Failed to set file mode for directory {{.V0}}", subToPath)
   555  				}
   556  			}
   557  			err := MoveAllFilesRecursively(subFromPath, subToPath, cb)
   558  			if err != nil {
   559  				return err
   560  			}
   561  			// source path should be empty now
   562  			err = os.Remove(subFromPath)
   563  			if err != nil {
   564  				return errs.Wrap(err, "os.Remove %s failed", subFromPath)
   565  			}
   566  
   567  			cb(subFromPath, subToPath)
   568  			continue
   569  		}
   570  
   571  		err = os.Rename(subFromPath, subToPath)
   572  		if err != nil {
   573  			var mode fs.FileMode
   574  			if toPathExists {
   575  				mode = toInfo.Mode()
   576  			}
   577  			return errs.Wrap(err, "os.Rename %s:%s failed (file mode: %#o)", subFromPath, subToPath, mode)
   578  		}
   579  		cb(subFromPath, subToPath)
   580  	}
   581  	return nil
   582  }
   583  
   584  // CopyAndRenameFiles copies files from fromDir to toDir.
   585  // If the target file exists already, the source file is first copied next to the target file, and then overwrites the target by renaming the source.
   586  // This method is more robust and than copying directly, in case the target file is opened or executed.
   587  func CopyAndRenameFiles(fromPath, toPath string, exclude ...string) error {
   588  	logging.Debug("Copying files from %s to %s", fromPath, toPath)
   589  
   590  	if !DirExists(fromPath) {
   591  		return locale.NewError("err_os_not_a_directory", "", fromPath)
   592  	} else if !DirExists(toPath) {
   593  		return locale.NewError("err_os_not_a_directory", "", toPath)
   594  	}
   595  
   596  	// read all child files and dirs
   597  	files, err := ListDir(fromPath, true)
   598  	if err != nil {
   599  		return errs.Wrap(err, "Could not ListDir %s", fromPath)
   600  	}
   601  
   602  	// any found files and dirs
   603  	for _, file := range files {
   604  		if funk.Contains(exclude, file.Name()) {
   605  			continue
   606  		}
   607  
   608  		rpath := file.RelativePath()
   609  		fromPath := filepath.Join(fromPath, rpath)
   610  		toPath := filepath.Join(toPath, rpath)
   611  
   612  		if file.IsDir() {
   613  			if err := MkdirUnlessExists(toPath); err != nil {
   614  				return errs.Wrap(err, "Could not create dir: %s", toPath)
   615  			}
   616  			continue
   617  		}
   618  
   619  		finfo, err := file.Info()
   620  		if err != nil {
   621  			return errs.Wrap(err, "Could not get file info for %s", file.RelativePath())
   622  		}
   623  
   624  		if TargetExists(toPath) {
   625  			tmpToPath := fmt.Sprintf("%s.new", toPath)
   626  			err := CopyFile(fromPath, tmpToPath)
   627  			if err != nil {
   628  				return errs.Wrap(err, "failed to copy %s -> %s", fromPath, tmpToPath)
   629  			}
   630  			err = os.Chmod(tmpToPath, finfo.Mode())
   631  			if err != nil {
   632  				return errs.Wrap(err, "failed to set file permissions for %s", tmpToPath)
   633  			}
   634  			err = os.Rename(tmpToPath, toPath)
   635  			if err != nil {
   636  				// cleanup
   637  				_ = os.Remove(tmpToPath)
   638  				return errs.Wrap(err, "os.Rename %s -> %s failed", tmpToPath, toPath)
   639  			}
   640  		} else {
   641  			err := CopyFile(fromPath, toPath)
   642  			if err != nil {
   643  				return errs.Wrap(err, "Copy %s -> %s failed", fromPath, toPath)
   644  			}
   645  			err = os.Chmod(toPath, finfo.Mode())
   646  			if err != nil {
   647  				return errs.Wrap(err, "failed to set file permissions for %s", toPath)
   648  			}
   649  		}
   650  	}
   651  	return nil
   652  }
   653  
   654  // MoveAllFiles will move all of the files/dirs within one directory to another directory. Both directories
   655  // must already exist.
   656  func MoveAllFiles(fromPath, toPath string) error {
   657  	if !DirExists(fromPath) {
   658  		return locale.NewError("err_os_not_a_directory", "", fromPath)
   659  	} else if !DirExists(toPath) {
   660  		return locale.NewError("err_os_not_a_directory", "", toPath)
   661  	}
   662  
   663  	// read all child files and dirs
   664  	dir, err := os.Open(fromPath)
   665  	if err != nil {
   666  		return errs.Wrap(err, "os.Open %s failed", fromPath)
   667  	}
   668  	fileInfos, err := dir.Readdir(-1)
   669  	dir.Close()
   670  	if err != nil {
   671  		return errs.Wrap(err, "dir.Readdir %s failed", fromPath)
   672  	}
   673  
   674  	// any found files and dirs
   675  	for _, fileInfo := range fileInfos {
   676  		fromPath := filepath.Join(fromPath, fileInfo.Name())
   677  		toPath := filepath.Join(toPath, fileInfo.Name())
   678  		err := os.Rename(fromPath, toPath)
   679  		if err != nil {
   680  			return errs.Wrap(err, "os.Rename %s:%s failed", fromPath, toPath)
   681  		}
   682  	}
   683  	return nil
   684  }
   685  
   686  // WriteTempFile writes data to a temp file.
   687  func WriteTempFile(pattern string, data []byte) (string, error) {
   688  	tempDir := os.TempDir()
   689  	return WriteTempFileToDir(tempDir, pattern, data, os.ModePerm)
   690  }
   691  
   692  // WriteTempFileToDir writes data to a temp file in the given dir
   693  func WriteTempFileToDir(dir, pattern string, data []byte, perm os.FileMode) (string, error) {
   694  	f, err := os.CreateTemp(dir, pattern)
   695  	if err != nil {
   696  		return "", errs.Wrap(err, "os.CreateTemp %s (%s) failed", dir, pattern)
   697  	}
   698  
   699  	if _, err = f.Write(data); err != nil {
   700  		os.Remove(f.Name())
   701  		return "", errs.Wrap(err, "f.Write %s failed", f.Name())
   702  	}
   703  
   704  	if err = f.Close(); err != nil {
   705  		os.Remove(f.Name())
   706  		return "", errs.Wrap(err, "f.Close %s failed", f.Name())
   707  	}
   708  
   709  	if err := os.Chmod(f.Name(), perm); err != nil {
   710  		os.Remove(f.Name())
   711  		return "", errs.Wrap(err, "os.Chmod %s failed", f.Name())
   712  	}
   713  
   714  	return f.Name(), nil
   715  }
   716  
   717  type DirReader interface {
   718  	ReadDir(string) ([]os.DirEntry, error)
   719  }
   720  
   721  func CopyFilesDirReader(reader DirReader, src, dst, placeholderFileName string) error {
   722  	entries, err := reader.ReadDir(src)
   723  	if err != nil {
   724  		return errs.Wrap(err, "reader.ReadDir %s failed", src)
   725  	}
   726  
   727  	for _, entry := range entries {
   728  		srcPath := filepath.Join(src, entry.Name())
   729  		destPath := filepath.Join(dst, entry.Name())
   730  
   731  		switch entry.Type() & os.ModeType {
   732  		case os.ModeDir:
   733  			err := MkdirUnlessExists(destPath)
   734  			if err != nil {
   735  				return errs.Wrap(err, "MkdirUnlessExists %s failed", destPath)
   736  			}
   737  
   738  			err = CopyFilesDirReader(reader, srcPath, destPath, placeholderFileName)
   739  			if err != nil {
   740  				return errs.Wrap(err, "CopyFiles %s:%s failed", srcPath, destPath)
   741  			}
   742  		case os.ModeSymlink:
   743  			err := CopySymlink(srcPath, destPath)
   744  			if err != nil {
   745  				return errs.Wrap(err, "CopySymlink %s:%s failed", srcPath, destPath)
   746  			}
   747  		default:
   748  			if entry.Name() == placeholderFileName {
   749  				continue
   750  			}
   751  
   752  			err := CopyAsset(srcPath, destPath)
   753  			if err != nil {
   754  				return errs.Wrap(err, "CopyFile %s:%s failed", srcPath, destPath)
   755  			}
   756  		}
   757  	}
   758  
   759  	return nil
   760  }
   761  
   762  // CopyFiles will copy all of the files/dirs within one directory to another.
   763  // Both directories must already exist
   764  func CopyFiles(src, dst string) error {
   765  	return copyFiles(src, dst, false)
   766  }
   767  
   768  func copyFiles(src, dest string, remove bool) error {
   769  	if !DirExists(src) {
   770  		return locale.NewError("err_os_not_a_directory", "", src)
   771  	}
   772  	if !DirExists(dest) {
   773  		return locale.NewError("err_os_not_a_directory", "", dest)
   774  	}
   775  
   776  	entries, err := os.ReadDir(src)
   777  	if err != nil {
   778  		return errs.Wrap(err, "os.ReadDir %s failed", src)
   779  	}
   780  
   781  	for _, entry := range entries {
   782  		srcPath := filepath.Join(src, entry.Name())
   783  		destPath := filepath.Join(dest, entry.Name())
   784  
   785  		fileInfo, err := os.Lstat(srcPath)
   786  		if err != nil {
   787  			return errs.Wrap(err, "os.Lstat %s failed", srcPath)
   788  		}
   789  
   790  		switch fileInfo.Mode() & os.ModeType {
   791  		case os.ModeDir:
   792  			err := MkdirUnlessExists(destPath)
   793  			if err != nil {
   794  				return errs.Wrap(err, "MkdirUnlessExists %s failed", destPath)
   795  			}
   796  			err = CopyFiles(srcPath, destPath)
   797  			if err != nil {
   798  				return errs.Wrap(err, "CopyFiles %s:%s failed", srcPath, destPath)
   799  			}
   800  		case os.ModeSymlink:
   801  			err := CopySymlink(srcPath, destPath)
   802  			if err != nil {
   803  				return errs.Wrap(err, "CopySymlink %s:%s failed", srcPath, destPath)
   804  			}
   805  		default:
   806  			err := CopyFile(srcPath, destPath)
   807  			if err != nil {
   808  				return errs.Wrap(err, "CopyFile %s:%s failed", srcPath, destPath)
   809  			}
   810  		}
   811  	}
   812  
   813  	if remove {
   814  		if err := os.RemoveAll(src); err != nil {
   815  			return errs.Wrap(err, "os.RemovaAll %s failed", src)
   816  		}
   817  	}
   818  
   819  	return nil
   820  }
   821  
   822  // CopySymlink reads the symlink at src and creates a new
   823  // link at dest
   824  func CopySymlink(src, dest string) error {
   825  	link, err := os.Readlink(src)
   826  	if err != nil {
   827  		return errs.Wrap(err, "os.Readlink %s failed", src)
   828  	}
   829  
   830  	err = os.Symlink(link, dest)
   831  	if err != nil {
   832  		return errs.Wrap(err, "os.Symlink %s:%s failed", link, dest)
   833  	}
   834  
   835  	return nil
   836  }
   837  
   838  // TempFileUnsafe returns a tempfile handler or panics if it cannot be created
   839  // This is for use in tests, do not use it outside tests!
   840  func TempFileUnsafe(dir, pattern string) *os.File {
   841  	f, err := os.CreateTemp(dir, pattern)
   842  	if err != nil {
   843  		panic(fmt.Sprintf("Could not create tempFile: %v", err))
   844  	}
   845  	return f
   846  }
   847  
   848  func TempFilePathUnsafe(dir, pattern string) string {
   849  	f := TempFileUnsafe(dir, pattern)
   850  	defer f.Close()
   851  	return f.Name()
   852  }
   853  
   854  // TempDirUnsafe returns a temp path or panics if it cannot be created
   855  // This is for use in tests, do not use it outside tests!
   856  func TempDirUnsafe() string {
   857  	f, err := os.MkdirTemp("", "")
   858  	if err != nil {
   859  		panic(fmt.Sprintf("Could not create tempDir: %v", err))
   860  	}
   861  	return f
   862  }
   863  
   864  func TempDirFromBaseDirUnsafe(baseDir string) string {
   865  	f, err := os.MkdirTemp(baseDir, "")
   866  	if err != nil {
   867  		panic(fmt.Sprintf("Could not create tempDir: %v", err))
   868  	}
   869  	return f
   870  }
   871  
   872  // MoveAllFilesCrossDisk will move all of the files/dirs within one directory
   873  // to another directory even across disks. Both directories must already exist.
   874  func MoveAllFilesCrossDisk(src, dst string) error {
   875  	err := MoveAllFiles(src, dst)
   876  	if err != nil {
   877  		multilog.Error("Move all files failed with error: %s. Falling back to copy files", err)
   878  	}
   879  
   880  	return copyFiles(src, dst, true)
   881  }
   882  
   883  // Join is identical to filepath.Join except that it doesn't clean the input, allowing for
   884  // more consistent behavior
   885  func Join(elem ...string) string {
   886  	for i, e := range elem {
   887  		if e != "" {
   888  			return strings.Join(elem[i:], string(filepath.Separator))
   889  		}
   890  	}
   891  	return ""
   892  }
   893  
   894  // PrepareDir prepares a path by ensuring it exists and the path is consistent
   895  func PrepareDir(path string) (string, error) {
   896  	err := MkdirUnlessExists(path)
   897  	if err != nil {
   898  		return "", err
   899  	}
   900  
   901  	path, err = filepath.Abs(path)
   902  	if err != nil {
   903  		return "", err
   904  	}
   905  
   906  	path, err = filepath.EvalSymlinks(path)
   907  	if err != nil {
   908  		return "", err
   909  	}
   910  
   911  	return path, nil
   912  }
   913  
   914  // LogPath will walk the given file path and log the name, permissions, mod
   915  // time, and file size of all files it encounters
   916  func LogPath(path string) error {
   917  	return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
   918  		if err != nil {
   919  			multilog.Error("Error walking filepath at: %s", path)
   920  			return err
   921  		}
   922  
   923  		logging.Debug(strings.Join([]string{
   924  			fmt.Sprintf("File name: %s", info.Name()),
   925  			fmt.Sprintf("File permissions: %s", info.Mode()),
   926  			fmt.Sprintf("File mod time: %s", info.ModTime()),
   927  			fmt.Sprintf("File size: %d", info.Size()),
   928  		}, "\n"))
   929  		return nil
   930  	})
   931  }
   932  
   933  // IsDir returns true if the given path is a directory
   934  func IsDir(path string) bool {
   935  	info, err := os.Stat(path)
   936  	if err != nil {
   937  		return false
   938  	}
   939  	return info.IsDir()
   940  }
   941  
   942  // ResolvePath gets the absolute location of the provided path and
   943  // fully evaluates the result if it is a symlink.
   944  func ResolvePath(path string) (string, error) {
   945  	absPath, err := filepath.Abs(path)
   946  	if err != nil {
   947  		return path, errs.Wrap(err, "cannot get absolute filepath of %q", path)
   948  	}
   949  
   950  	if !TargetExists(path) {
   951  		return absPath, nil
   952  	}
   953  
   954  	evalPath, err := filepath.EvalSymlinks(absPath)
   955  	if err != nil {
   956  		return absPath, errs.Wrap(err, "cannot evaluate symlink %q", absPath)
   957  	}
   958  
   959  	return evalPath, nil
   960  }
   961  
   962  // PathsEqual checks whether the paths given all resolve to the same path
   963  func PathsEqual(paths ...string) (bool, error) {
   964  	if len(paths) < 2 {
   965  		return false, errs.New("Must supply at least two paths")
   966  	}
   967  
   968  	var equalTo string
   969  	for _, path := range paths {
   970  		resolvedPath, err := ResolvePath(path)
   971  		if err != nil {
   972  			return false, errs.Wrap(err, "Could not resolve path: %s", path)
   973  		}
   974  		if equalTo == "" {
   975  			equalTo = resolvedPath
   976  			continue
   977  		}
   978  		if resolvedPath != equalTo {
   979  			return false, nil
   980  		}
   981  	}
   982  
   983  	return true, nil
   984  }
   985  
   986  // PathContainsParent checks if the directory path is equal to or a child directory
   987  // of the targeted directory. Symlinks are evaluated for this comparison.
   988  func PathContainsParent(path, parentPath string) (bool, error) {
   989  	if path == parentPath {
   990  		return true, nil
   991  	}
   992  
   993  	efmt := "cannot resolve %q"
   994  
   995  	resPath, err := ResolvePath(path)
   996  	if err != nil {
   997  		return false, errs.Wrap(err, efmt, path)
   998  	}
   999  
  1000  	resParent, err := ResolvePath(parentPath)
  1001  	if err != nil {
  1002  		return false, errs.Wrap(err, efmt, parentPath)
  1003  	}
  1004  
  1005  	return resolvedPathContainsParent(resPath, resParent), nil
  1006  }
  1007  
  1008  func resolvedPathContainsParent(path, parentPath string) bool {
  1009  	if !strings.HasSuffix(path, string(os.PathSeparator)) {
  1010  		path += string(os.PathSeparator)
  1011  	}
  1012  
  1013  	if !strings.HasSuffix(parentPath, string(os.PathSeparator)) {
  1014  		parentPath += string(os.PathSeparator)
  1015  	}
  1016  
  1017  	return path == parentPath || strings.HasPrefix(path, parentPath)
  1018  }
  1019  
  1020  // SymlinkTarget retrieves the target of the given symlink
  1021  func SymlinkTarget(symlink string) (string, error) {
  1022  	fileInfo, err := os.Lstat(symlink)
  1023  	if err != nil {
  1024  		return "", errs.Wrap(err, "Could not lstat symlink")
  1025  	}
  1026  
  1027  	if fileInfo.Mode()&os.ModeSymlink != os.ModeSymlink {
  1028  		return "", errs.New("%s is not a symlink", symlink)
  1029  	}
  1030  
  1031  	evalDest, err := os.Readlink(symlink)
  1032  	if err != nil {
  1033  		return "", errs.Wrap(err, "Could not resolve symlink: %s", symlink)
  1034  	}
  1035  
  1036  	return evalDest, nil
  1037  }
  1038  
  1039  // ListDirSimple recursively lists filepaths under the given sourcePath
  1040  // This does not follow symlinks
  1041  func ListDirSimple(sourcePath string, includeDirs bool) ([]string, error) {
  1042  	result := []string{}
  1043  	err := filepath.WalkDir(sourcePath, func(path string, f fs.DirEntry, err error) error {
  1044  		if err != nil {
  1045  			return errs.Wrap(err, "Could not walk path: %s", path)
  1046  		}
  1047  		if !includeDirs && f.IsDir() {
  1048  			return nil
  1049  		}
  1050  		result = append(result, path)
  1051  		return nil
  1052  	})
  1053  	if err != nil {
  1054  		return result, errs.Wrap(err, "Could not walk dir: %s", sourcePath)
  1055  	}
  1056  	return result, nil
  1057  }
  1058  
  1059  // ListFilesUnsafe lists filepaths under the given sourcePath non-recursively
  1060  func ListFilesUnsafe(sourcePath string) []string {
  1061  	result := []string{}
  1062  	files, err := os.ReadDir(sourcePath)
  1063  	if err != nil {
  1064  		panic(fmt.Sprintf("Could not read dir: %s, error: %s", sourcePath, errs.JoinMessage(err)))
  1065  	}
  1066  	for _, file := range files {
  1067  		result = append(result, filepath.Join(sourcePath, file.Name()))
  1068  	}
  1069  	return result
  1070  }
  1071  
  1072  type DirEntry struct {
  1073  	fs.DirEntry
  1074  	absolutePath string
  1075  	rootPath     string
  1076  }
  1077  
  1078  func (d DirEntry) Path() string {
  1079  	return d.absolutePath
  1080  }
  1081  
  1082  func (d DirEntry) RelativePath() string {
  1083  	// This is a bit awkward, but fs.DirEntry does not give us a relative path to the originally queried dir
  1084  	return strings.TrimPrefix(d.absolutePath, d.rootPath)
  1085  }
  1086  
  1087  // ListDir recursively lists filepaths under the given sourcePath
  1088  // This does not follow symlinks
  1089  func ListDir(sourcePath string, includeDirs bool) ([]DirEntry, error) {
  1090  	result := []DirEntry{}
  1091  	sourcePath = filepath.Clean(sourcePath)
  1092  	if err := filepath.WalkDir(sourcePath, func(path string, f fs.DirEntry, err error) error {
  1093  		if path == sourcePath {
  1094  			return nil // I don't know why WalkDir feels the need to include the very dir I queried..
  1095  		}
  1096  		if err != nil {
  1097  			return errs.Wrap(err, "Could not walk path: %s", path)
  1098  		}
  1099  		if !includeDirs && f.IsDir() {
  1100  			return nil
  1101  		}
  1102  		result = append(result, DirEntry{f, path, sourcePath + string(filepath.Separator)})
  1103  		return nil
  1104  	}); err != nil {
  1105  		return result, errs.Wrap(err, "Could not walk dir: %s", sourcePath)
  1106  	}
  1107  	return result, nil
  1108  }
  1109  
  1110  // PathInList returns whether the provided path list contains the provided
  1111  // path.
  1112  func PathInList(listSep, pathList, path string) (bool, error) {
  1113  	paths := strings.Split(pathList, listSep)
  1114  	for _, p := range paths {
  1115  		equal, err := PathsEqual(p, path)
  1116  		if err != nil {
  1117  			return false, err
  1118  		}
  1119  		if equal {
  1120  			return true, nil
  1121  		}
  1122  	}
  1123  	return false, nil
  1124  }
  1125  
  1126  func FileContains(path string, searchText []byte) (bool, error) {
  1127  	if !TargetExists(path) {
  1128  		return false, nil
  1129  	}
  1130  	b, err := ReadFile(path)
  1131  	if err != nil {
  1132  		return false, errs.Wrap(err, "Could not read file")
  1133  	}
  1134  	return bytes.Contains(b, searchText), nil
  1135  }
  1136  
  1137  func ModTime(path string) (time.Time, error) {
  1138  	stat, err := os.Stat(path)
  1139  	if err != nil {
  1140  		return time.Now(), errs.Wrap(err, "Could not stat file %s", path)
  1141  	}
  1142  	return stat.ModTime(), nil
  1143  }
  1144  
  1145  func CaseSensitivePath(path string) (string, error) {
  1146  	// On Windows Glob may not work with the short path (ie., DOS 8.3 notation)
  1147  	path, err := GetLongPathName(path)
  1148  	if err != nil {
  1149  		return "", errs.Wrap(err, "Failed to get long path name")
  1150  	}
  1151  
  1152  	var searchPath string
  1153  	if runtime.GOOS != "windows" {
  1154  		searchPath = globPath(path)
  1155  	} else {
  1156  		volume := filepath.VolumeName(path)
  1157  		remainder := strings.TrimLeft(path, volume)
  1158  		searchPath = filepath.Join(volume, globPath(remainder))
  1159  	}
  1160  
  1161  	matches, err := filepath.Glob(searchPath)
  1162  	if err != nil {
  1163  		return "", errs.Wrap(err, "Failed to search for path")
  1164  	}
  1165  
  1166  	if len(matches) == 0 {
  1167  		return "", errs.New("Could not find path: %s", path)
  1168  	}
  1169  
  1170  	return matches[0], nil
  1171  }
  1172  
  1173  // PathsMatch checks if all the given paths resolve to the same value
  1174  func PathsMatch(paths ...string) (bool, error) {
  1175  	for _, path := range paths[1:] {
  1176  		p1, err := ResolvePath(path)
  1177  		if err != nil {
  1178  			return false, errs.Wrap(err, "Could not resolve path %s", path)
  1179  		}
  1180  		p2, err := ResolvePath(paths[0])
  1181  		if err != nil {
  1182  			return false, errs.Wrap(err, "Could not resolve path %s", paths[0])
  1183  		}
  1184  		if p1 != p2 {
  1185  			logging.Debug("Path %s does not match %s", p1, p2)
  1186  			return false, nil
  1187  		}
  1188  	}
  1189  	return true, nil
  1190  }
  1191  
  1192  func globPath(path string) string {
  1193  	var result string
  1194  	for _, r := range path {
  1195  		if unicode.IsLetter(r) {
  1196  			result += fmt.Sprintf("[%c%c]", unicode.ToUpper(r), unicode.ToLower(r))
  1197  		} else {
  1198  			result += string(r)
  1199  		}
  1200  	}
  1201  	return result
  1202  }