fix: pin parent directory and verify owner/type before trashing Trash renamed the item by path after the confirmation dialog, so a parent directory swapped for a symlink since the scan (e.g. by another user, while an administrator cleans their home) redirected the move to an unrelated file. The parent is now opened once and the item is checked (owner and file type as scanned) and renamed relative to that fd. macOS volume trash directories now get the same ownership/symlink checks as freedesktop.org ones on Linux. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
1.8 KiB
Go
78 lines
1.8 KiB
Go
package platform
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
)
|
|
|
|
// TrashName is the user-facing name of the trash.
|
|
const TrashName = "Trash"
|
|
|
|
// Reveal selects the item in Finder.
|
|
func Reveal(path string) error {
|
|
if err := checkAbs(path); err != nil {
|
|
return err
|
|
}
|
|
return startDetached("open", "-R", path)
|
|
}
|
|
|
|
// Trash moves path into ~/.Trash (same volume) or the volume's
|
|
// .Trashes/<uid> directory. It never copies and never deletes.
|
|
func Trash(path string, want Expect) error {
|
|
if err := checkAbs(path); err != nil {
|
|
return err
|
|
}
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dirs := []string{filepath.Join(home, ".Trash")}
|
|
if vol := volumeOf(path); vol != "" {
|
|
dirs = append(dirs, filepath.Join(vol, ".Trashes", fmt.Sprint(os.Getuid())))
|
|
}
|
|
base := filepath.Base(path)
|
|
for vi, dir := range dirs {
|
|
if err := os.MkdirAll(filepath.Dir(dir), 0o700); err != nil {
|
|
continue
|
|
}
|
|
// A volume trash is on a shared filesystem: another user could plant
|
|
// a symlink or an open directory there, so insist on a private one.
|
|
if err := ensurePrivateDir(dir, vi > 0); err != nil {
|
|
continue
|
|
}
|
|
for i := 1; i < 10000; i++ {
|
|
name := base
|
|
if i > 1 {
|
|
name = fmt.Sprintf("%s %d", base, i)
|
|
}
|
|
dst := filepath.Join(dir, name)
|
|
if _, err := os.Lstat(dst); err == nil {
|
|
continue
|
|
}
|
|
err := renameChecked(path, dst, want)
|
|
if errors.Is(err, syscall.EXDEV) {
|
|
break // try the next trash location
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return errors.New("trash: item is on a volume without a usable trash")
|
|
}
|
|
|
|
func volumeOf(path string) string {
|
|
const prefix = "/Volumes/"
|
|
if len(path) > len(prefix) && path[:len(prefix)] == prefix {
|
|
rest := path[len(prefix):]
|
|
for i := 0; i < len(rest); i++ {
|
|
if rest[i] == '/' {
|
|
return prefix + rest[:i]
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
return ""
|
|
}
|