fix: use Recommends instead of Depends for ueberzugpp in deb, add Ubuntu install guide
This commit is contained in:
parent
915b695e88
commit
1577ee5525
1302 changed files with 402483 additions and 2 deletions
527
src/vcom-0.2.5/internal/fs/ops.go
Normal file
527
src/vcom-0.2.5/internal/fs/ops.go
Normal file
|
|
@ -0,0 +1,527 @@
|
|||
package vfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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
|
||||
Stage string
|
||||
}
|
||||
|
||||
type copyProgressState struct {
|
||||
ctx context.Context
|
||||
filesDone int
|
||||
bytesDone int64
|
||||
stats TransferStats
|
||||
callback func(CopyProgress)
|
||||
lastEmit time.Time
|
||||
stage string
|
||||
discover bool // if true, count files during copy for progress total
|
||||
}
|
||||
|
||||
func (s *copyProgressState) discoverFiles(count int, dirPath string) {
|
||||
if count == 0 {
|
||||
return
|
||||
}
|
||||
s.stats.FilesTotal += count
|
||||
s.emit(dirPath, false)
|
||||
}
|
||||
|
||||
func CopyPath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||
return CopyPathWithProgress(srcPath, dstDir, overwrite, TransferStats{}, nil)
|
||||
}
|
||||
|
||||
func CopyPathWithProgressContext(ctx context.Context, srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
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 err := ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if progress == nil {
|
||||
progress = func(CopyProgress) {}
|
||||
}
|
||||
tracker := copyProgressState{
|
||||
ctx: ctx,
|
||||
stats: stats,
|
||||
callback: progress,
|
||||
stage: "Scanning files...",
|
||||
discover: stats.FilesTotal == 0,
|
||||
}
|
||||
tracker.emit(srcPath, true)
|
||||
tracker.stage = "Copying files..."
|
||||
|
||||
cleanupOnErr := func(copyErr error) (string, error) {
|
||||
if copyErr != nil {
|
||||
_ = os.RemoveAll(targetPath)
|
||||
}
|
||||
return "", copyErr
|
||||
}
|
||||
|
||||
if srcInfo.Mode()&os.ModeSymlink != 0 {
|
||||
target, err := os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ctx.Err(); 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 cleanupOnErr(err)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return cleanupOnErr(err)
|
||||
}
|
||||
tracker.emit(srcPath, true)
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
if err := copyFile(srcPath, targetPath, srcInfo.Mode(), &tracker); err != nil {
|
||||
return cleanupOnErr(err)
|
||||
}
|
||||
tracker.emit(srcPath, true)
|
||||
return targetPath, 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
|
||||
}
|
||||
stats.FilesTotal++
|
||||
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) {
|
||||
return CopyPathWithProgressContext(context.Background(), srcPath, dstDir, overwrite, stats, progress)
|
||||
}
|
||||
|
||||
func MovePath(srcPath string, dstDir string, overwrite bool) (string, error) {
|
||||
return MovePathWithProgress(srcPath, dstDir, overwrite, TransferStats{}, nil)
|
||||
}
|
||||
|
||||
func MovePathWithProgress(srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||
return MovePathWithProgressContext(context.Background(), srcPath, dstDir, overwrite, stats, progress)
|
||||
}
|
||||
|
||||
func MovePathWithProgressContext(ctx context.Context, srcPath string, dstDir string, overwrite bool, stats TransferStats, progress func(CopyProgress)) (string, error) {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
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 err := ctx.Err(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Rename(srcPath, targetPath); err == nil {
|
||||
progress(CopyProgress{
|
||||
FilesDone: stats.FilesTotal,
|
||||
FilesTotal: stats.FilesTotal,
|
||||
BytesDone: stats.BytesTotal,
|
||||
BytesTotal: stats.BytesTotal,
|
||||
CurrentPath: srcPath,
|
||||
Stage: "Move completed",
|
||||
})
|
||||
return targetPath, nil
|
||||
} else if !errors.Is(err, syscall.EXDEV) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
targetPath, err := CopyPathWithProgressContext(ctx, srcPath, dstDir, overwrite, stats, progress)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
_ = os.RemoveAll(targetPath)
|
||||
return "", err
|
||||
}
|
||||
progress(CopyProgress{
|
||||
FilesDone: stats.FilesTotal,
|
||||
FilesTotal: stats.FilesTotal,
|
||||
BytesDone: stats.BytesTotal,
|
||||
BytesTotal: stats.BytesTotal,
|
||||
CurrentPath: srcPath,
|
||||
Stage: "Finalizing move",
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
// MoveToTrash moves a file or directory to the FreeDesktop Trash directory
|
||||
// (~/.local/share/Trash). Follows the FreeDesktop Trash specification:
|
||||
// - The original item is moved to Trash/files/<basename>
|
||||
// - A .trashinfo file is written to Trash/info/<basename>.trashinfo
|
||||
// - If <basename> already exists in Trash/files, a numeric suffix is appended.
|
||||
func MoveToTrash(path string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine home directory: %w", err)
|
||||
}
|
||||
|
||||
trashDir := filepath.Join(home, ".local", "share", "Trash")
|
||||
filesDir := filepath.Join(trashDir, "files")
|
||||
infoDir := filepath.Join(trashDir, "info")
|
||||
|
||||
if err := os.MkdirAll(filesDir, 0o700); err != nil {
|
||||
return fmt.Errorf("cannot create trash files directory: %w", err)
|
||||
}
|
||||
if err := os.MkdirAll(infoDir, 0o700); err != nil {
|
||||
return fmt.Errorf("cannot create trash info directory: %w", err)
|
||||
}
|
||||
|
||||
baseName := filepath.Base(path)
|
||||
|
||||
// Generate a unique name in the trash directory
|
||||
destName := baseName
|
||||
for counter := 1; ; counter++ {
|
||||
destPath := filepath.Join(filesDir, destName)
|
||||
if _, err := os.Stat(destPath); os.IsNotExist(err) {
|
||||
break
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("cannot stat trash path: %w", err)
|
||||
}
|
||||
destName = fmt.Sprintf("%s.%d", baseName, counter)
|
||||
}
|
||||
|
||||
destPath := filepath.Join(filesDir, destName)
|
||||
if err := os.Rename(path, destPath); err != nil {
|
||||
// Cross-filesystem move: fall back to copy+delete
|
||||
return fmt.Errorf("cannot move to trash: %w", err)
|
||||
}
|
||||
|
||||
// Write .trashinfo file
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
absPath = path
|
||||
}
|
||||
now := time.Now().Format("2006-01-02T15:04:05")
|
||||
infoContent := fmt.Sprintf("[Trash Info]\nPath=%s\nDeletionDate=%s\n", absPath, now)
|
||||
infoPath := filepath.Join(infoDir, destName+".trashinfo")
|
||||
if err := os.WriteFile(infoPath, []byte(infoContent), 0o600); err != nil {
|
||||
// Best-effort: if info file fails, try to move the file back
|
||||
_ = os.Rename(destPath, path)
|
||||
return fmt.Errorf("cannot write trash info: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 RenamePath(sourcePath string, newName string) (string, error) {
|
||||
newName = filepath.Base(filepath.Clean(newName))
|
||||
if newName == "." || newName == "" {
|
||||
return "", fmt.Errorf("invalid target name")
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(filepath.Dir(sourcePath), newName)
|
||||
if same, err := samePath(sourcePath, 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 {
|
||||
return "", ErrOverwrite(targetPath)
|
||||
}
|
||||
|
||||
if err := os.Rename(sourcePath, targetPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
func copyDir(srcDir string, dstDir string, tracker *copyProgressState) error {
|
||||
if tracker != nil && tracker.ctx != nil {
|
||||
if err := tracker.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Count files in this directory so progress total converges
|
||||
if tracker != nil && tracker.discover {
|
||||
fileCount := 0
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() {
|
||||
fileCount++
|
||||
}
|
||||
}
|
||||
tracker.discoverFiles(fileCount, srcDir)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if tracker != nil && tracker.ctx != nil {
|
||||
if err := tracker.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tracker != nil && tracker.ctx != nil {
|
||||
if err := tracker.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyFile(srcPath string, dstPath string, mode os.FileMode, tracker *copyProgressState) error {
|
||||
if tracker != nil && tracker.ctx != nil {
|
||||
if err := tracker.ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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 {
|
||||
_ = dstFile.Close()
|
||||
_ = os.Remove(dstPath)
|
||||
return err
|
||||
}
|
||||
if tracker != nil && tracker.ctx != nil {
|
||||
if err := tracker.ctx.Err(); err != nil {
|
||||
_ = dstFile.Close()
|
||||
_ = os.Remove(dstPath)
|
||||
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) {
|
||||
if w.tracker != nil && w.tracker.ctx != nil {
|
||||
if err := w.tracker.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
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()
|
||||
stage := s.stage
|
||||
if stage == "" {
|
||||
stage = "Transferring data"
|
||||
}
|
||||
s.callback(CopyProgress{
|
||||
FilesDone: s.filesDone,
|
||||
FilesTotal: s.stats.FilesTotal,
|
||||
BytesDone: s.bytesDone,
|
||||
BytesTotal: s.stats.BytesTotal,
|
||||
CurrentPath: currentPath,
|
||||
Stage: stage,
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue