package fsutil import ( "fmt" "io" "os" "path/filepath" "regexp" "runtime" "strings" ) // CopyFile copies the contents of the file at srcpath to a regular file at dstpath. // It will copy the last modified timestamp // If dstpath already exists the function will fail. func CopyFile(srcpath, dstpath string) (err error) { r, err := os.Open(srcpath) if err != nil { return err } w, err := os.OpenFile(dstpath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0666) if err != nil { r.Close() // We need to close the input file as the defer below would not be called. return err } defer func() { r.Close() // ok to ignore error: file was opened read-only. e := w.Close() // Report the error from w.Close, if any. // But do so only if there isn't already an outgoing error. if e != nil && err == nil { err = e } // Copy modified time if err == nil { // io.Copy succeeded, we should fix the dstpath timestamp srcFileInfo, e := os.Stat(srcpath) if e != nil { err = e return } e = os.Chtimes(dstpath, srcFileInfo.ModTime(), srcFileInfo.ModTime()) if e != nil { err = e } } }() _, err = io.Copy(w, r) return err } // SafeMove attempts to move the file with path src to dest using os.Rename. If this fails, then it copies src to dest, then deletes src. // If the copy fails, or the delete fails, the function will return an error. func SafeMove(src, dst string) error { err := os.Rename(src, dst) if err != nil { copyErr := CopyFile(src, dst) if copyErr != nil { return fmt.Errorf("copying file during SaveMove failed with: '%w'; renaming file failed previously with: '%v'", copyErr, err) } removeErr := os.Remove(src) if removeErr != nil { // if we can't remove the old file, remove the new one and fail _ = os.Remove(dst) return fmt.Errorf("removing old file during SafeMove failed with: '%w'; renaming file failed previously with: '%v'", removeErr, err) } } return nil } // MatchExtension returns true if the extension of the provided path // matches any of the provided extensions. func MatchExtension(path string, extensions []string) bool { ext := filepath.Ext(path) for _, e := range extensions { if strings.EqualFold(ext, "."+e) { return true } } return false } // FindInPaths returns the path to baseName in the first path where it exists from paths. func FindInPaths(paths []string, baseName string) string { for _, p := range paths { filePath := filepath.Join(p, baseName) if exists, _ := FileExists(filePath); exists { return filePath } } return "" } // FileExists returns true if the given path exists and is a file. // This function returns false and the error encountered if the call to os.Stat fails. func FileExists(path string) (bool, error) { info, err := os.Stat(path) if err == nil { return !info.IsDir(), nil } return false, err } // WriteFile writes file to path creating parent directories if needed func WriteFile(path string, file []byte) error { pathErr := EnsureDirAll(filepath.Dir(path)) if pathErr != nil { return fmt.Errorf("cannot ensure path exists: %w", pathErr) } return os.WriteFile(path, file, 0755) } // GetNameFromPath returns the name of a file from its path // if stripExtension is true the extension is omitted from the name func GetNameFromPath(path string, stripExtension bool) string { fn := filepath.Base(path) if stripExtension { ext := filepath.Ext(fn) fn = strings.TrimSuffix(fn, ext) } return fn } // Touch creates an empty file at the given path if it doesn't already exist func Touch(path string) error { var _, err = os.Stat(path) if os.IsNotExist(err) { var file, err = os.Create(path) if err != nil { return err } defer file.Close() } return nil } var ( replaceCharsRE = regexp.MustCompile(`[&=\\/:*"?_ ]`) removeCharsRE = regexp.MustCompile(`[^[:alnum:]-.]`) multiHyphenRE = regexp.MustCompile(`\-+`) ) // SanitiseBasename returns a file basename removing any characters that are illegal or problematic to use in the filesystem. func SanitiseBasename(v string) string { v = strings.TrimSpace(v) // replace illegal filename characters with - v = replaceCharsRE.ReplaceAllString(v, "-") // remove other characters v = removeCharsRE.ReplaceAllString(v, "") // remove multiple hyphens v = multiHyphenRE.ReplaceAllString(v, "-") return strings.TrimSpace(v) } // GetExeName returns the name of the given executable for the current platform. // One windows it returns the name with the .exe extension. func GetExeName(base string) string { if runtime.GOOS == "windows" { return base + ".exe" } return base }