This commit is contained in:
eric
2025-08-28 13:06:53 +08:00
parent 620ba326a9
commit 1b98ac6e72
350 changed files with 154034 additions and 0 deletions

40
fileutils/copy.go Normal file
View File

@@ -0,0 +1,40 @@
package fileutils
import (
"io/fs"
"os"
"path"
"github.com/spf13/afero"
)
// Copy copies a file or folder from one place to another.
func Copy(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if src = path.Clean("/" + src); src == "" {
return os.ErrNotExist
}
if dst = path.Clean("/" + dst); dst == "" {
return os.ErrNotExist
}
if src == "/" || dst == "/" {
// Prohibit copying from or to the virtual root directory.
return os.ErrInvalid
}
if dst == src {
return os.ErrInvalid
}
info, err := afs.Stat(src)
if err != nil {
return err
}
if info.IsDir() {
return CopyDir(afs, src, dst, fileMode, dirMode)
}
return CopyFile(afs, src, dst, fileMode, dirMode)
}

63
fileutils/dir.go Normal file
View File

@@ -0,0 +1,63 @@
package fileutils
import (
"errors"
"io/fs"
"github.com/spf13/afero"
)
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Get properties of source.
srcinfo, err := afs.Stat(source)
if err != nil {
return err
}
// Create the destination directory.
err = afs.MkdirAll(dest, srcinfo.Mode())
if err != nil {
return err
}
dir, _ := afs.Open(source)
obs, err := dir.Readdir(-1)
if err != nil {
return err
}
var errs []error
for _, obj := range obs {
fsource := source + "/" + obj.Name()
fdest := dest + "/" + obj.Name()
if obj.IsDir() {
// Create sub-directories, recursively.
err = CopyDir(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}
} else {
// Perform the file copy.
err = CopyFile(afs, fsource, fdest, fileMode, dirMode)
if err != nil {
errs = append(errs, err)
}
}
}
var errString string
for _, err := range errs {
errString += err.Error() + "\n"
}
if errString != "" {
return errors.New(errString)
}
return nil
}

129
fileutils/file.go Normal file
View File

@@ -0,0 +1,129 @@
package fileutils
import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"github.com/spf13/afero"
)
// MoveFile moves file from src to dst.
// By default the rename filesystem system call is used. If src and dst point to different volumes
// the file copy is used as a fallback
func MoveFile(afs afero.Fs, src, dst string, fileMode, dirMode fs.FileMode) error {
if afs.Rename(src, dst) == nil {
return nil
}
// fallback
err := Copy(afs, src, dst, fileMode, dirMode)
if err != nil {
_ = afs.Remove(dst)
return err
}
if err := afs.RemoveAll(src); err != nil {
return err
}
return nil
}
// CopyFile copies a file from source to dest and returns
// an error if any.
func CopyFile(afs afero.Fs, source, dest string, fileMode, dirMode fs.FileMode) error {
// Open the source file.
src, err := afs.Open(source)
if err != nil {
return err
}
defer src.Close()
// Makes the directory needed to create the dst
// file.
err = afs.MkdirAll(filepath.Dir(dest), dirMode)
if err != nil {
return err
}
// Create the destination file.
dst, err := afs.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode)
if err != nil {
return err
}
defer dst.Close()
// Copy the contents of the file.
_, err = io.Copy(dst, src)
if err != nil {
return err
}
// Copy the mode
info, err := afs.Stat(source)
if err != nil {
return err
}
err = afs.Chmod(dest, info.Mode())
if err != nil {
return err
}
return nil
}
// CommonPrefix returns common directory path of provided files
func CommonPrefix(sep byte, paths ...string) string {
// Handle special cases.
switch len(paths) {
case 0:
return ""
case 1:
return path.Clean(paths[0])
}
// Note, we treat string as []byte, not []rune as is often
// done in Go. (And sep as byte, not rune). This is because
// most/all supported OS' treat paths as string of non-zero
// bytes. A filename may be displayed as a sequence of Unicode
// runes (typically encoded as UTF-8) but paths are
// not required to be valid UTF-8 or in any normalized form
// (e.g. "é" (U+00C9) and "é" (U+0065,U+0301) are different
// file names.
c := []byte(path.Clean(paths[0]))
// We add a trailing sep to handle the case where the
// common prefix directory is included in the path list
// (e.g. /home/user1, /home/user1/foo, /home/user1/bar).
// path.Clean will have cleaned off trailing / separators with
// the exception of the root directory, "/" (in which case we
// make it "//", but this will get fixed up to "/" below).
c = append(c, sep)
// Ignore the first path since it's already in c
for _, v := range paths[1:] {
// Clean up each path before testing it
v = path.Clean(v) + string(sep)
// Find the first non-common byte and truncate c
if len(v) < len(c) {
c = c[:len(v)]
}
for i := 0; i < len(c); i++ {
if v[i] != c[i] {
c = c[:i]
break
}
}
}
// Remove trailing non-separator characters and the final separator
for i := len(c) - 1; i >= 0; i-- {
if c[i] == sep {
c = c[:i]
break
}
}
return string(c)
}

46
fileutils/file_test.go Normal file
View File

@@ -0,0 +1,46 @@
package fileutils
import "testing"
func TestCommonPrefix(t *testing.T) {
testCases := map[string]struct {
paths []string
want string
}{
"same lvl": {
paths: []string{
"/home/user/file1",
"/home/user/file2",
},
want: "/home/user",
},
"sub folder": {
paths: []string{
"/home/user/folder",
"/home/user/folder/file",
},
want: "/home/user/folder",
},
"relative path": {
paths: []string{
"/home/user/folder",
"/home/user/folder/../folder2",
},
want: "/home/user",
},
"no common path": {
paths: []string{
"/home/user/folder",
"/etc/file",
},
want: "",
},
}
for name, tt := range testCases {
t.Run(name, func(t *testing.T) {
if got := CommonPrefix('/', tt.paths...); got != tt.want {
t.Errorf("CommonPrefix() = %v, want %v", got, tt.want)
}
})
}
}