Files
stash/pkg/fsutil/file.go
puc9 61c0098ae6 Close input file so SafeMove can delete it (#3714)
* Close input file so SafeMove can delete it

This is happening on Windows and over the network but at the end of SafeMove it fails the move with an error that it can't remove the input because it is in use.
It turns out it is in use by the SafeMove itself :)

* Copy the src file mod time
2023-05-10 11:16:49 +10:00

166 lines
3.9 KiB
Go

package fsutil
import (
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/stashapp/stash/pkg/logger"
)
// 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_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.
func SafeMove(src, dst string) error {
err := os.Rename(src, dst)
if err != nil {
err = CopyFile(src, dst)
if err != nil {
return err
}
err = os.Remove(src)
if err != nil {
logger.Errorf("error removing old file %s during SafeMove: %v", src, 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)
}