vcom/internal/fs/ops.go

326 lines
6.9 KiB
Go

package vfs
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"syscall"
"time"
)
type TransferStats struct {
FilesTotal int
BytesTotal int64
}
type CopyProgress struct {
FilesDone int
FilesTotal int
BytesDone int64
BytesTotal int64
CurrentPath string
}
type copyProgressState struct {
filesDone int
bytesDone int64
stats TransferStats
callback func(CopyProgress)
lastEmit time.Time
}
func CopyPath(srcPath string, dstDir string, overwrite bool) (string, error) {
return CopyPathWithProgress(srcPath, dstDir, overwrite, TransferStats{}, nil)
}
func CopyStats(srcPath string) (TransferStats, error) {
srcInfo, err := os.Lstat(srcPath)
if err != nil {
return TransferStats{}, fmt.Errorf("stat %s: %w", srcPath, err)
}
if srcInfo.Mode()&os.ModeSymlink != 0 {
return TransferStats{FilesTotal: 1, BytesTotal: 0}, nil
}
if !srcInfo.IsDir() {
return TransferStats{FilesTotal: 1, BytesTotal: srcInfo.Size()}, nil
}
stats := TransferStats{}
err = filepath.WalkDir(srcPath, func(current string, d fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if d.IsDir() {
return nil
}
info, err := os.Lstat(current)
if err != nil {
return err
}
stats.FilesTotal++
if info.Mode()&os.ModeSymlink == 0 {
stats.BytesTotal += info.Size()
}
return nil
})
if err != nil {
return TransferStats{}, err
}
return stats, nil
}
func CopyPathWithProgress(srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
srcInfo, err := os.Lstat(srcPath)
if err != nil {
return "", fmt.Errorf("stat %s: %w", srcPath, err)
}
targetPath := filepath.Join(dstDir, filepath.Base(srcPath))
if same, err := samePath(srcPath, targetPath); err != nil {
return "", err
} else if same {
return "", fmt.Errorf("source and target are the same: %s", targetPath)
}
if exists, err := PathExists(targetPath); err != nil {
return "", err
} else if exists {
if !overwrite {
return "", ErrOverwrite(targetPath)
}
if err := os.RemoveAll(targetPath); err != nil {
return "", err
}
}
if progress == nil {
progress = func(CopyProgress) {}
}
if stats.FilesTotal == 0 && stats.BytesTotal == 0 {
resolved, err := CopyStats(srcPath)
if err != nil {
return "", err
}
stats = resolved
}
tracker := copyProgressState{stats: stats, callback: progress}
tracker.emit(srcPath, true)
if srcInfo.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(srcPath)
if err != nil {
return "", err
}
if err := os.Symlink(target, targetPath); err != nil {
return "", err
}
tracker.finishFile(srcPath)
return targetPath, nil
}
if srcInfo.IsDir() {
if err := copyDir(srcPath, targetPath, &tracker); err != nil {
return "", err
}
tracker.emit(srcPath, true)
return targetPath, nil
}
if err := copyFile(srcPath, targetPath, srcInfo.Mode(), &tracker); err != nil {
return "", err
}
tracker.emit(srcPath, true)
return targetPath, nil
}
func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
targetPath := filepath.Join(dstDir, filepath.Base(srcPath))
if same, err := samePath(srcPath, targetPath); err != nil {
return "", err
} else if same {
return "", fmt.Errorf("source and target are the same: %s", targetPath)
}
if exists, err := PathExists(targetPath); err != nil {
return "", err
} else if exists {
if !overwrite {
return "", ErrOverwrite(targetPath)
}
if err := os.RemoveAll(targetPath); err != nil {
return "", err
}
}
if err := os.Rename(srcPath, targetPath); err == nil {
return targetPath, nil
} else if !errors.Is(err, syscall.EXDEV) {
return "", err
}
targetPath, err := CopyPath(srcPath, dstDir, overwrite)
if err != nil {
return "", err
}
if err := DeletePath(srcPath); err != nil {
return "", err
}
return targetPath, nil
}
func PathExists(path string) (bool, error) {
if _, err := os.Lstat(path); err == nil {
return true, nil
} else if errors.Is(err, os.ErrNotExist) {
return false, nil
} else {
return false, err
}
}
func DeletePath(path string) error {
return os.RemoveAll(path)
}
func MakeDir(parent string, name string) (string, error) {
target := filepath.Join(parent, name)
if err := os.MkdirAll(target, 0o755); err != nil {
return "", err
}
return target, nil
}
func copyDir(srcDir string, dstDir string, tracker *copyProgressState) error {
info, err := os.Lstat(srcDir)
if err != nil {
return err
}
if err := os.MkdirAll(dstDir, info.Mode().Perm()); err != nil {
return err
}
entries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
srcPath := filepath.Join(srcDir, entry.Name())
dstPath := filepath.Join(dstDir, entry.Name())
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
switch {
case info.Mode()&os.ModeSymlink != 0:
target, err := os.Readlink(srcPath)
if err != nil {
return err
}
if err := os.Symlink(target, dstPath); err != nil {
return err
}
if tracker != nil {
tracker.finishFile(srcPath)
}
case info.IsDir():
if err := copyDir(srcPath, dstPath, tracker); err != nil {
return err
}
default:
if err := copyFile(srcPath, dstPath, info.Mode(), tracker); err != nil {
return err
}
}
}
return nil
}
func copyFile(srcPath string, dstPath string, mode os.FileMode, tracker *copyProgressState) error {
srcFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode.Perm())
if err != nil {
return err
}
defer dstFile.Close()
writer := io.Writer(dstFile)
if tracker != nil {
writer = &progressWriter{base: dstFile, tracker: tracker, path: srcPath}
}
if _, err := io.Copy(writer, srcFile); err != nil {
return err
}
if tracker != nil {
tracker.finishFile(srcPath)
}
return nil
}
type progressWriter struct {
base io.Writer
tracker *copyProgressState
path string
}
func (w *progressWriter) Write(data []byte) (int, error) {
n, err := w.base.Write(data)
if n > 0 {
w.tracker.addBytes(int64(n), w.path)
}
return n, err
}
func (s *copyProgressState) addBytes(delta int64, currentPath string) {
s.bytesDone += delta
s.emit(currentPath, false)
}
func (s *copyProgressState) finishFile(currentPath string) {
s.filesDone++
s.emit(currentPath, true)
}
func (s *copyProgressState) emit(currentPath string, force bool) {
if s.callback == nil {
return
}
if !force && time.Since(s.lastEmit) < 75*time.Millisecond {
return
}
s.lastEmit = time.Now()
s.callback(CopyProgress{
FilesDone: s.filesDone,
FilesTotal: s.stats.FilesTotal,
BytesDone: s.bytesDone,
BytesTotal: s.stats.BytesTotal,
CurrentPath: currentPath,
})
}
func samePath(left string, right string) (bool, error) {
leftAbs, err := filepath.Abs(left)
if err != nil {
return false, err
}
rightAbs, err := filepath.Abs(right)
if err != nil {
return false, err
}
return leftAbs == rightAbs, nil
}