1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 17:24:45 +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:
Tw93
2025-12-22 11:24:04 +08:00
parent 5c6e643b0c
commit d2dc68da90
16 changed files with 270 additions and 102 deletions

View File

@@ -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())
}
}

View 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)
}
}

View File

@@ -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

View File

@@ -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

View 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)
}
}