mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 14:26:46 +00:00
feat: Enhance app protection with centralized critical component checks, improve UI string width calculation, refine analysis and cleaning logic, and add new tests.
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -28,10 +29,19 @@ func deleteMultiplePathsCmd(paths []string, counter *int64) tea.Cmd {
|
||||
var totalCount int64
|
||||
var errors []string
|
||||
|
||||
for _, path := range paths {
|
||||
// Delete deeper paths first to avoid parent removal triggering child not-exist errors
|
||||
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 := deletePathWithProgress(path, counter)
|
||||
totalCount += count
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue // Parent already removed - not an actionable error
|
||||
}
|
||||
errors = append(errors, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
45
cmd/analyze/delete_test.go
Normal file
45
cmd/analyze/delete_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeleteMultiplePathsCmdHandlesParentChild(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
parent := filepath.Join(base, "parent")
|
||||
child := filepath.Join(parent, "child")
|
||||
|
||||
// Create structure:
|
||||
// parent/fileA
|
||||
// parent/child/fileC
|
||||
if err := os.MkdirAll(child, 0o755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(parent, "fileA"), []byte("a"), 0o644); err != nil {
|
||||
t.Fatalf("write fileA: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(child, "fileC"), []byte("c"), 0o644); err != nil {
|
||||
t.Fatalf("write fileC: %v", err)
|
||||
}
|
||||
|
||||
var counter int64
|
||||
msg := deleteMultiplePathsCmd([]string{parent, child}, &counter)()
|
||||
progress, ok := msg.(deleteProgressMsg)
|
||||
if !ok {
|
||||
t.Fatalf("expected deleteProgressMsg, got %T", msg)
|
||||
}
|
||||
if progress.err != nil {
|
||||
t.Fatalf("unexpected error: %v", progress.err)
|
||||
}
|
||||
if progress.count != 2 {
|
||||
t.Fatalf("expected 2 files deleted, got %d", progress.count)
|
||||
}
|
||||
if _, err := os.Stat(parent); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected parent to be removed, err=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(child); !os.IsNotExist(err) {
|
||||
t.Fatalf("expected child to be removed, err=%v", err)
|
||||
}
|
||||
}
|
||||
@@ -579,10 +579,12 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
if len(pathsToDelete) == 1 {
|
||||
m.status = fmt.Sprintf("Deleting %s...", filepath.Base(pathsToDelete[0]))
|
||||
} else {
|
||||
m.status = fmt.Sprintf("Deleting %d items...", len(pathsToDelete))
|
||||
targetPath := pathsToDelete[0]
|
||||
m.status = fmt.Sprintf("Deleting %s...", filepath.Base(targetPath))
|
||||
return m, tea.Batch(deletePathCmd(targetPath, m.deleteCount), tickCmd())
|
||||
}
|
||||
|
||||
m.status = fmt.Sprintf("Deleting %d items...", len(pathsToDelete))
|
||||
return m, tea.Batch(deleteMultiplePathsCmd(pathsToDelete, m.deleteCount), tickCmd())
|
||||
case "esc", "q":
|
||||
// Cancel delete with ESC or Q
|
||||
|
||||
@@ -607,49 +607,64 @@ func getDirectorySizeFromDu(path string) (int64, error) {
|
||||
}
|
||||
|
||||
func getDirectorySizeFromDuWithExclude(path string, excludePath string) (int64, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), duTimeout)
|
||||
defer cancel()
|
||||
runDuSize := func(target string) (int64, error) {
|
||||
if _, err := os.Stat(target); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
args := []string{"-sk"}
|
||||
// macOS du uses -I to ignore files/directories matching a pattern
|
||||
ctx, cancel := context.WithTimeout(context.Background(), duTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "du", "-sk", target)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return 0, fmt.Errorf("du timeout after %v", duTimeout)
|
||||
}
|
||||
if stderr.Len() > 0 {
|
||||
return 0, fmt.Errorf("du failed: %v (%s)", err, stderr.String())
|
||||
}
|
||||
return 0, fmt.Errorf("du failed: %v", err)
|
||||
}
|
||||
fields := strings.Fields(stdout.String())
|
||||
if len(fields) == 0 {
|
||||
return 0, fmt.Errorf("du output empty")
|
||||
}
|
||||
kb, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse du output: %v", err)
|
||||
}
|
||||
if kb <= 0 {
|
||||
return 0, fmt.Errorf("du size invalid: %d", kb)
|
||||
}
|
||||
return kb * 1024, nil
|
||||
}
|
||||
|
||||
// When excluding a path (e.g., ~/Library), subtract only that exact directory instead of ignoring every "Library"
|
||||
if excludePath != "" {
|
||||
// Extract just the directory name from the full path
|
||||
excludeName := filepath.Base(excludePath)
|
||||
args = append(args, "-I", excludeName)
|
||||
}
|
||||
args = append(args, path)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "du", args...)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return 0, fmt.Errorf("du timeout after %v", duTimeout)
|
||||
totalSize, err := runDuSize(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if stderr.Len() > 0 {
|
||||
return 0, fmt.Errorf("du failed: %v (%s)", err, stderr.String())
|
||||
excludeSize, err := runDuSize(excludePath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return 0, err
|
||||
}
|
||||
excludeSize = 0
|
||||
}
|
||||
return 0, fmt.Errorf("du failed: %v", err)
|
||||
if excludeSize > totalSize {
|
||||
excludeSize = 0
|
||||
}
|
||||
return totalSize - excludeSize, nil
|
||||
}
|
||||
fields := strings.Fields(stdout.String())
|
||||
if len(fields) == 0 {
|
||||
return 0, fmt.Errorf("du output empty")
|
||||
}
|
||||
kb, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse du output: %v", err)
|
||||
}
|
||||
if kb <= 0 {
|
||||
return 0, fmt.Errorf("du size invalid: %d", kb)
|
||||
}
|
||||
return kb * 1024, nil
|
||||
|
||||
return runDuSize(path)
|
||||
}
|
||||
|
||||
func getDirectoryLogicalSize(path string) (int64, error) {
|
||||
return getDirectoryLogicalSizeWithExclude(path, "")
|
||||
}
|
||||
|
||||
func getDirectoryLogicalSizeWithExclude(path string, excludePath string) (int64, error) {
|
||||
var total int64
|
||||
|
||||
45
cmd/analyze/scanner_test.go
Normal file
45
cmd/analyze/scanner_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeFileWithSize(t *testing.T, path string, size int) {
|
||||
t.Helper()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatalf("mkdir %s: %v", path, err)
|
||||
}
|
||||
content := make([]byte, size)
|
||||
if err := os.WriteFile(path, content, 0o644); err != nil {
|
||||
t.Fatalf("write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDirectoryLogicalSizeWithExclude(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
homeFile := filepath.Join(base, "fileA")
|
||||
libFile := filepath.Join(base, "Library", "fileB")
|
||||
projectLibFile := filepath.Join(base, "Projects", "Library", "fileC")
|
||||
|
||||
writeFileWithSize(t, homeFile, 100)
|
||||
writeFileWithSize(t, libFile, 200)
|
||||
writeFileWithSize(t, projectLibFile, 300)
|
||||
|
||||
total, err := getDirectoryLogicalSizeWithExclude(base, "")
|
||||
if err != nil {
|
||||
t.Fatalf("getDirectoryLogicalSizeWithExclude (no exclude) error: %v", err)
|
||||
}
|
||||
if total != 600 {
|
||||
t.Fatalf("expected total 600 bytes, got %d", total)
|
||||
}
|
||||
|
||||
excluding, err := getDirectoryLogicalSizeWithExclude(base, filepath.Join(base, "Library"))
|
||||
if err != nil {
|
||||
t.Fatalf("getDirectoryLogicalSizeWithExclude (exclude Library) error: %v", err)
|
||||
}
|
||||
if excluding != 400 {
|
||||
t.Fatalf("expected 400 bytes when excluding top-level Library, got %d", excluding)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user