github.com/drud/ddev@v1.21.5-alpha1.0.20230226034409-94fcc4b94453/pkg/fileutil/files.go (about)

     1  package fileutil
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rand"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"text/template"
    15  
    16  	"runtime"
    17  
    18  	"github.com/drud/ddev/pkg/output"
    19  	"github.com/drud/ddev/pkg/util"
    20  )
    21  
    22  // CopyFile copies the contents of the file named src to the file named
    23  // by dst. The file will be created if it does not already exist. If the
    24  // destination file exists, all its contents will be replaced by the contents
    25  // of the source file. The file mode will be copied from the source and
    26  // the copied data is synced/flushed to stable storage. Credit @m4ng0squ4sh https://gist.github.com/m4ng0squ4sh/92462b38df26839a3ca324697c8cba04
    27  func CopyFile(src string, dst string) error {
    28  	in, err := os.Open(src)
    29  	if err != nil {
    30  		return err
    31  	}
    32  	defer util.CheckClose(in)
    33  	out, err := os.Create(dst)
    34  	if err != nil {
    35  		return fmt.Errorf("Failed to create file %v, err: %v", src, err)
    36  	}
    37  	defer util.CheckClose(out)
    38  	_, err = io.Copy(out, in)
    39  	if err != nil {
    40  		return fmt.Errorf("Failed to copy file from %v to %v err: %v", src, dst, err)
    41  	}
    42  
    43  	err = out.Sync()
    44  	if err != nil {
    45  		return err
    46  	}
    47  
    48  	// os.Chmod fails on long path (> 256 characters) on windows.
    49  	// A description of this problem with golang is at https://github.com/golang/dep/issues/774#issuecomment-311560825
    50  	// It could end up fixed in a future version of golang.
    51  	if runtime.GOOS != "windows" {
    52  		si, err := os.Stat(src)
    53  		if err != nil {
    54  			return err
    55  		}
    56  
    57  		err = os.Chmod(dst, si.Mode())
    58  		if err != nil {
    59  			return fmt.Errorf("Failed to chmod file %v to mode %v, err=%v", dst, si.Mode(), err)
    60  		}
    61  	}
    62  
    63  	return nil
    64  }
    65  
    66  // CopyDir recursively copies a directory tree, attempting to preserve permissions.
    67  // Source directory must exist, destination directory must *not* exist.
    68  // Symlinks are ignored and skipped. Credit @r0l1 https://gist.github.com/r0l1/92462b38df26839a3ca324697c8cba04
    69  func CopyDir(src string, dst string) error {
    70  	src = filepath.Clean(src)
    71  	dst = filepath.Clean(dst)
    72  
    73  	si, err := os.Stat(src)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	if !si.IsDir() {
    78  		return fmt.Errorf("CopyDir: source directory %s is not a directory", src)
    79  	}
    80  
    81  	_, err = os.Stat(dst)
    82  	if err != nil && !os.IsNotExist(err) {
    83  		return err
    84  	}
    85  	if err == nil {
    86  		return fmt.Errorf("CopyDir: destination %s already exists", dst)
    87  	}
    88  
    89  	err = os.MkdirAll(dst, si.Mode())
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	dirEntrySlice, err := os.ReadDir(src)
    95  	if err != nil {
    96  		return err
    97  	}
    98  
    99  	for _, de := range dirEntrySlice {
   100  
   101  		srcPath := filepath.Join(src, de.Name())
   102  		dstPath := filepath.Join(dst, de.Name())
   103  
   104  		if de.IsDir() {
   105  			err = CopyDir(srcPath, dstPath)
   106  			if err != nil {
   107  				return err
   108  			}
   109  		} else {
   110  			deInfo, err := de.Info()
   111  			if err != nil {
   112  				return err
   113  			}
   114  			err = CopyFile(srcPath, dstPath)
   115  			if err != nil && deInfo.Mode()&os.ModeSymlink != 0 {
   116  				output.UserOut.Warnf("failed to copy symlink %s, skipping...\n", srcPath)
   117  				continue
   118  			}
   119  			if err != nil {
   120  				return err
   121  			}
   122  		}
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  // IsDirectory returns true if path is a dir, false on error or not directory
   129  func IsDirectory(path string) bool {
   130  	fileInfo, err := os.Stat(path)
   131  	if err != nil {
   132  		return false
   133  	}
   134  	return fileInfo.IsDir()
   135  }
   136  
   137  // FileIsReadable checks to make sure a file exists and is readable
   138  func FileIsReadable(name string) bool {
   139  	file, err := os.OpenFile(name, os.O_RDONLY, 0666)
   140  	if err != nil {
   141  		return false
   142  	}
   143  	file.Close()
   144  	return true
   145  }
   146  
   147  // PurgeDirectory removes all of the contents of a given
   148  // directory, leaving the directory itself intact.
   149  func PurgeDirectory(path string) error {
   150  	dir, err := os.Open(path)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	defer util.CheckClose(dir)
   156  
   157  	files, err := dir.Readdirnames(-1)
   158  	if err != nil {
   159  		return err
   160  	}
   161  
   162  	for _, file := range files {
   163  		err = os.Chmod(filepath.Join(path, file), 0777)
   164  		if err != nil {
   165  			return err
   166  		}
   167  		err = os.RemoveAll(filepath.Join(path, file))
   168  		if err != nil {
   169  			return err
   170  		}
   171  	}
   172  	return nil
   173  }
   174  
   175  // FgrepStringInFile is a small hammer for looking for a literal string in a file.
   176  // It should only be used against very modest sized files, as the entire file is read
   177  // into a string.
   178  func FgrepStringInFile(fullPath string, needle string) (bool, error) {
   179  	fullFileBytes, err := os.ReadFile(fullPath)
   180  	if err != nil {
   181  		return false, err
   182  	}
   183  	fullFileString := string(fullFileBytes)
   184  	return strings.Contains(fullFileString, needle), nil
   185  }
   186  
   187  // GrepStringInFile is a small hammer for looking for a regex in a file.
   188  // It should only be used against very modest sized files, as the entire file is read
   189  // into a string.
   190  func GrepStringInFile(fullPath string, needle string) (bool, error) {
   191  	fullFileBytes, err := os.ReadFile(fullPath)
   192  	if err != nil {
   193  		return false, fmt.Errorf("failed to open file %s, err:%v ", fullPath, err)
   194  	}
   195  	fullFileString := string(fullFileBytes)
   196  	re := regexp.MustCompile(needle)
   197  	matches := re.FindStringSubmatch(fullFileString)
   198  	return len(matches) > 0, nil
   199  }
   200  
   201  // ListFilesInDir returns an array of files or directories found in a directory
   202  func ListFilesInDir(path string) ([]string, error) {
   203  	var fileList []string
   204  	dirEntrySlice, err := os.ReadDir(path)
   205  	if err != nil {
   206  		return fileList, err
   207  	}
   208  
   209  	for _, de := range dirEntrySlice {
   210  		fileList = append(fileList, de.Name())
   211  	}
   212  	return fileList, nil
   213  }
   214  
   215  // ListFilesInDirFullPath returns an array of full path of files found in a directory
   216  func ListFilesInDirFullPath(path string) ([]string, error) {
   217  	var fileList []string
   218  	dirEntrySlice, err := os.ReadDir(path)
   219  	if err != nil {
   220  		return fileList, err
   221  	}
   222  
   223  	for _, de := range dirEntrySlice {
   224  		fileList = append(fileList, filepath.Join(path, de.Name()))
   225  	}
   226  	return fileList, nil
   227  }
   228  
   229  // RandomFilenameBase generates a temporary filename for use in testing or whatever.
   230  // From https://stackoverflow.com/a/28005931/215713
   231  func RandomFilenameBase() string {
   232  	randBytes := make([]byte, 16)
   233  	_, _ = rand.Read(randBytes)
   234  	return hex.EncodeToString(randBytes)
   235  }
   236  
   237  // ReplaceStringInFile takes search and replace strings, an original path, and a dest path, returns error
   238  func ReplaceStringInFile(searchString string, replaceString string, origPath string, destPath string) error {
   239  	input, err := os.ReadFile(origPath)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	output := bytes.Replace(input, []byte(searchString), []byte(replaceString), -1)
   245  
   246  	// nolint: revive
   247  	if err = os.WriteFile(destPath, output, 0666); err != nil {
   248  		return err
   249  	}
   250  	return nil
   251  }
   252  
   253  // IsSameFile determines whether two paths refer to the same file/dir
   254  func IsSameFile(path1 string, path2 string) (bool, error) {
   255  	path1fi, err := os.Stat(path1)
   256  	if err != nil {
   257  		return false, err
   258  	}
   259  	path2fi, err := os.Stat(path2)
   260  	if err != nil {
   261  		return false, err
   262  	}
   263  	return os.SameFile(path1fi, path2fi), nil
   264  }
   265  
   266  // ReadFileIntoString just gets the contents of file into string
   267  func ReadFileIntoString(path string) (string, error) {
   268  	bytes, err := os.ReadFile(path)
   269  	if err != nil {
   270  		return "", err
   271  	}
   272  	return string(bytes), err
   273  }
   274  
   275  // AppendStringToFile takes a path to a file and a string to append
   276  // and it appends it, returning err
   277  func AppendStringToFile(path string, appendString string) error {
   278  	f, err := os.OpenFile(path,
   279  		os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
   280  	if err != nil {
   281  		return err
   282  	}
   283  	defer f.Close()
   284  	if _, err := f.WriteString(appendString); err != nil {
   285  		return err
   286  	}
   287  	return nil
   288  }
   289  
   290  type XSymContents struct {
   291  	LinkLocation string
   292  	LinkTarget   string
   293  }
   294  
   295  // FindSimulatedXsymSymlinks searches the basePath provided for files
   296  // whose first line is XSym, which is used in cifs filesystem for simulated
   297  // symlinks.
   298  func FindSimulatedXsymSymlinks(basePath string) ([]XSymContents, error) {
   299  	symLinks := make([]XSymContents, 0)
   300  	err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
   301  		if err != nil {
   302  			return err
   303  		}
   304  		//TODO: Skip a directory named .git? Skip other arbitrary dirs or files?
   305  		if !info.IsDir() {
   306  			if info.Size() == 1067 {
   307  				contents, err := os.ReadFile(path)
   308  				if err != nil {
   309  					return err
   310  				}
   311  				lines := strings.Split(string(contents), "\n")
   312  				if lines[0] != "XSym" {
   313  					return nil
   314  				}
   315  				if len(lines) < 4 {
   316  					return fmt.Errorf("Apparent XSym doesn't have enough lines: %s", path)
   317  				}
   318  				// target is 4th line
   319  				linkTarget := filepath.Clean(lines[3])
   320  				symLinks = append(symLinks, XSymContents{LinkLocation: path, LinkTarget: linkTarget})
   321  			}
   322  		}
   323  		return nil
   324  	})
   325  	return symLinks, err
   326  }
   327  
   328  // ReplaceSimulatedXsymSymlinks walks a list of XSymContents and makes real symlinks
   329  // in their place. This is only valid on Windows host, only works with Docker for Windows
   330  // (cifs filesystem)
   331  func ReplaceSimulatedXsymSymlinks(links []XSymContents) error {
   332  	for _, item := range links {
   333  		err := os.Remove(item.LinkLocation)
   334  		if err != nil {
   335  			return err
   336  		}
   337  		err = os.Symlink(item.LinkTarget, item.LinkLocation)
   338  		if err != nil {
   339  			return err
   340  		}
   341  	}
   342  	return nil
   343  }
   344  
   345  // CanCreateSymlinks tests to see if it's possible to create a symlink
   346  func CanCreateSymlinks() bool {
   347  	tmpdir := os.TempDir()
   348  	linkPath := filepath.Join(tmpdir, RandomFilenameBase())
   349  	// This doesn't attempt to create the real file; we don't need it.
   350  	err := os.Symlink(filepath.Join(tmpdir, "realfile.txt"), linkPath)
   351  	//nolint: errcheck
   352  	defer os.Remove(linkPath)
   353  	if err != nil {
   354  		return false
   355  	}
   356  	return true
   357  }
   358  
   359  // ReplaceSimulatedLinks walks the path provided and tries to replace XSym links with real ones.
   360  func ReplaceSimulatedLinks(path string) {
   361  	links, err := FindSimulatedXsymSymlinks(path)
   362  	if err != nil {
   363  		util.Warning("Error finding XSym Symlinks: %v", err)
   364  	}
   365  	if len(links) == 0 {
   366  		return
   367  	}
   368  
   369  	if !CanCreateSymlinks() {
   370  		util.Warning("This host computer is unable to create real symlinks, please see the docs to enable developer mode:\n%s\nNote that the simulated symlinks created inside the container will work fine for most projects.", "https://ddev.readthedocs.io/en/stable/users/basics/developer-tools/#windows-os-and-ddev-composer")
   371  		return
   372  	}
   373  
   374  	err = ReplaceSimulatedXsymSymlinks(links)
   375  	if err != nil {
   376  		util.Warning("Failed replacing simulated symlinks: %v", err)
   377  	}
   378  	replacedLinks := make([]string, 0)
   379  	for _, l := range links {
   380  		replacedLinks = append(replacedLinks, l.LinkLocation)
   381  	}
   382  	util.Success("Replaced these simulated symlinks with real symlinks: %v", replacedLinks)
   383  	return
   384  }
   385  
   386  // RemoveContents removes contents of passed directory
   387  // From https://stackoverflow.com/questions/33450980/how-to-remove-all-contents-of-a-directory-using-golang
   388  func RemoveContents(dir string) error {
   389  	d, err := os.Open(dir)
   390  	if err != nil {
   391  		return err
   392  	}
   393  	defer d.Close()
   394  	names, err := d.Readdirnames(-1)
   395  	if err != nil {
   396  		return err
   397  	}
   398  	for _, name := range names {
   399  		err = os.RemoveAll(filepath.Join(dir, name))
   400  		if err != nil {
   401  			return err
   402  		}
   403  	}
   404  	return nil
   405  }
   406  
   407  // TemplateStringToFile takes a template string, runs templ.Execute on it, and writes it out to file
   408  func TemplateStringToFile(content string, vars map[string]interface{}, targetFilePath string) error {
   409  
   410  	templ := template.New("templateStringToFile:" + targetFilePath)
   411  	templ, err := templ.Parse(content)
   412  	if err != nil {
   413  		return err
   414  	}
   415  
   416  	var doc bytes.Buffer
   417  	err = templ.Execute(&doc, vars)
   418  	if err != nil {
   419  		return err
   420  	}
   421  
   422  	f, err := os.Create(targetFilePath)
   423  	if err != nil {
   424  		return err
   425  	}
   426  	defer util.CheckClose(f)
   427  
   428  	_, err = f.WriteString(doc.String())
   429  	if err != nil {
   430  		return nil
   431  	}
   432  	return nil
   433  }
   434  
   435  // CheckSignatureOrNoFile checks to make sure that a file or directory either doesn't exist
   436  // or has #ddev-generated in its contents (so it can be overwritten)
   437  // returns nil if overwrite is OK (if sig found or no file existing)
   438  func CheckSignatureOrNoFile(path string, signature string) error {
   439  	var err error
   440  	switch {
   441  	case !FileExists(path):
   442  		return nil
   443  
   444  	case FileExists(path) && !IsDirectory(path):
   445  		found, err := FgrepStringInFile(path, signature)
   446  		// It's unlikely that we'll get an error, but report it if we do.
   447  		if err != nil {
   448  			return err
   449  		}
   450  		// We found the file and it has the signature in it.
   451  		if !found {
   452  			return fmt.Errorf("signature was not found in file %s", path)
   453  		}
   454  		return nil
   455  
   456  	case IsDirectory(path):
   457  		err = filepath.WalkDir(path, func(path string, info fs.DirEntry, err error) error {
   458  			if err != nil {
   459  				return err
   460  			}
   461  			// If a directory, nothing to do, continue traversing
   462  			if info.IsDir() {
   463  				return nil
   464  			}
   465  			// If file doesn't exist, nothing to do, continue traversing
   466  			if !FileExists(path) {
   467  				return nil
   468  			}
   469  			// Now check to see if file has signature.
   470  			found, err := FgrepStringInFile(path, signature)
   471  			// It's unlikely that we'll get an error, but report it if we do.
   472  			if err != nil {
   473  				return err
   474  			}
   475  			// We have the file and it does not have the signature in it.
   476  			// that means it's not safe to overwrite it.
   477  			if !found {
   478  				return fmt.Errorf("signature was not found in file %s", path)
   479  			}
   480  			return nil
   481  		})
   482  	}
   483  	return err
   484  }