1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 16:49:41 +00:00
Files
Mole/cmd/analyze/delete.go
Tw93 7d43e669a8 fix(analyze): improve deletion safety and UI clarity
- Update UI status to 'Moving to Trash...' for clarity
- Use os.Lstat instead of os.Stat to correctly handle broken symlinks during deletion checks
2026-01-10 08:51:14 +08:00

147 lines
3.4 KiB
Go

package main
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"sync/atomic"
"time"
tea "github.com/charmbracelet/bubbletea"
)
const trashTimeout = 30 * time.Second
func deletePathCmd(path string, counter *int64) tea.Cmd {
return func() tea.Msg {
count, err := trashPathWithProgress(path, counter)
return deleteProgressMsg{
done: true,
err: err,
count: count,
path: path,
}
}
}
// deleteMultiplePathsCmd moves paths to Trash and aggregates results.
func deleteMultiplePathsCmd(paths []string, counter *int64) tea.Cmd {
return func() tea.Msg {
var totalCount int64
var errors []string
// Process deeper paths first to avoid parent/child conflicts.
pathsToDelete := append([]string(nil), paths...)
sort.Slice(pathsToDelete, func(i, j int) bool {
return strings.Count(pathsToDelete[i], string(filepath.Separator)) > strings.Count(pathsToDelete[j], string(filepath.Separator))
})
for _, path := range pathsToDelete {
count, err := trashPathWithProgress(path, counter)
totalCount += count
if err != nil {
if os.IsNotExist(err) {
continue
}
errors = append(errors, err.Error())
}
}
var resultErr error
if len(errors) > 0 {
resultErr = &multiDeleteError{errors: errors}
}
return deleteProgressMsg{
done: true,
err: resultErr,
count: totalCount,
path: "",
}
}
}
// multiDeleteError holds multiple deletion errors.
type multiDeleteError struct {
errors []string
}
func (e *multiDeleteError) Error() string {
if len(e.errors) == 1 {
return e.errors[0]
}
return strings.Join(e.errors[:min(3, len(e.errors))], "; ")
}
// trashPathWithProgress moves a path to Trash using Finder.
// This allows users to recover accidentally deleted files.
func trashPathWithProgress(root string, counter *int64) (int64, error) {
// Verify path exists (use Lstat to handle broken symlinks).
info, err := os.Lstat(root)
if err != nil {
return 0, err
}
// Count items for progress reporting.
var count int64
if info.IsDir() {
_ = filepath.WalkDir(root, func(_ string, d os.DirEntry, err error) error {
if err != nil {
return nil
}
if !d.IsDir() {
count++
if counter != nil {
atomic.StoreInt64(counter, count)
}
}
return nil
})
} else {
count = 1
if counter != nil {
atomic.StoreInt64(counter, 1)
}
}
// Move to Trash using Finder AppleScript.
if err := moveToTrash(root); err != nil {
return 0, err
}
return count, nil
}
// moveToTrash uses macOS Finder to move a file/directory to Trash.
// This is the safest method as it uses the system's native trash mechanism.
func moveToTrash(path string) error {
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("failed to resolve path: %w", err)
}
// Escape path for AppleScript (handle quotes and backslashes).
escapedPath := strings.ReplaceAll(absPath, "\\", "\\\\")
escapedPath = strings.ReplaceAll(escapedPath, "\"", "\\\"")
script := fmt.Sprintf(`tell application "Finder" to delete POSIX file "%s"`, escapedPath)
ctx, cancel := context.WithTimeout(context.Background(), trashTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "osascript", "-e", script)
output, err := cmd.CombinedOutput()
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("timeout moving to Trash")
}
return fmt.Errorf("failed to move to Trash: %s", strings.TrimSpace(string(output)))
}
return nil
}