stash/pkg/fsutil/file.go

177 lines
4.5 KiB
Go

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
}