mirror of
https://github.com/tw93/Mole.git
synced 2026-02-15 23:11:11 +00:00
npm/venv scan speed improvement
This commit is contained in:
@@ -308,6 +308,12 @@ type overviewSizeMsg struct {
|
||||
|
||||
type tickMsg time.Time
|
||||
|
||||
type deleteProgressMsg struct {
|
||||
done bool
|
||||
err error
|
||||
count int64
|
||||
}
|
||||
|
||||
type model struct {
|
||||
path string
|
||||
history []historyEntry
|
||||
@@ -327,6 +333,8 @@ type model struct {
|
||||
isOverview bool
|
||||
deleteConfirm bool
|
||||
deleteTarget *dirEntry
|
||||
deleting bool
|
||||
deleteCount *int64
|
||||
cache map[string]historyEntry
|
||||
largeSelected int
|
||||
largeOffset int
|
||||
@@ -602,6 +610,28 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
return m.updateKey(msg)
|
||||
case deleteProgressMsg:
|
||||
if msg.done {
|
||||
m.deleting = false
|
||||
if msg.err != nil {
|
||||
m.status = fmt.Sprintf("Failed to delete: %v", msg.err)
|
||||
} else {
|
||||
m.status = fmt.Sprintf("Deleted %d items", msg.count)
|
||||
// Mark all caches as dirty
|
||||
for i := range m.history {
|
||||
m.history[i].dirty = true
|
||||
}
|
||||
for path := range m.cache {
|
||||
entry := m.cache[path]
|
||||
entry.dirty = true
|
||||
m.cache[path] = entry
|
||||
}
|
||||
// Refresh the view
|
||||
m.scanning = true
|
||||
return m, tea.Batch(m.scanCmd(m.path), tickCmd())
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
case scanResultMsg:
|
||||
m.scanning = false
|
||||
if msg.err != nil {
|
||||
@@ -658,8 +688,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
return m, nil
|
||||
case tickMsg:
|
||||
if m.scanning || (m.isOverview && m.overviewScanning) {
|
||||
if m.scanning || m.deleting || (m.isOverview && m.overviewScanning) {
|
||||
m.spinner = (m.spinner + 1) % len(spinnerFrames)
|
||||
// Update delete progress status
|
||||
if m.deleting && m.deleteCount != nil {
|
||||
count := atomic.LoadInt64(m.deleteCount)
|
||||
if count > 0 {
|
||||
m.status = fmt.Sprintf("Deleting... %s items removed", formatNumber(count))
|
||||
}
|
||||
}
|
||||
return m, tickCmd()
|
||||
}
|
||||
return m, nil
|
||||
@@ -672,27 +709,17 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
// Handle delete confirmation
|
||||
if m.deleteConfirm {
|
||||
if msg.String() == "delete" || msg.String() == "backspace" {
|
||||
// Confirm delete
|
||||
// Confirm delete - start async deletion
|
||||
if m.deleteTarget != nil {
|
||||
err := os.RemoveAll(m.deleteTarget.path)
|
||||
if err != nil {
|
||||
m.status = fmt.Sprintf("Failed to delete: %v", err)
|
||||
} else {
|
||||
m.status = fmt.Sprintf("Deleted %s", m.deleteTarget.name)
|
||||
for i := range m.history {
|
||||
m.history[i].dirty = true
|
||||
}
|
||||
for path := range m.cache {
|
||||
entry := m.cache[path]
|
||||
entry.dirty = true
|
||||
m.cache[path] = entry
|
||||
}
|
||||
// Refresh the view
|
||||
m.scanning = true
|
||||
m.deleteConfirm = false
|
||||
m.deleteTarget = nil
|
||||
return m, tea.Batch(m.scanCmd(m.path), tickCmd())
|
||||
}
|
||||
m.deleteConfirm = false
|
||||
m.deleting = true
|
||||
var deleteCount int64
|
||||
m.deleteCount = &deleteCount
|
||||
targetPath := m.deleteTarget.path
|
||||
targetName := m.deleteTarget.name
|
||||
m.deleteTarget = nil
|
||||
m.status = fmt.Sprintf("Deleting %s...", targetName)
|
||||
return m, tea.Batch(deletePathCmd(targetPath, m.deleteCount), tickCmd())
|
||||
}
|
||||
m.deleteConfirm = false
|
||||
m.deleteTarget = nil
|
||||
@@ -967,6 +994,22 @@ func (m model) View() string {
|
||||
fmt.Fprintln(&b)
|
||||
}
|
||||
|
||||
if m.deleting {
|
||||
// Show delete progress
|
||||
count := int64(0)
|
||||
if m.deleteCount != nil {
|
||||
count = atomic.LoadInt64(m.deleteCount)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, "\n%s%s%s%s Deleting: %s%s items%s removed, please wait...\n",
|
||||
colorCyan, colorBold,
|
||||
spinnerFrames[m.spinner],
|
||||
colorReset,
|
||||
colorYellow, formatNumber(count), colorReset)
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
if m.scanning {
|
||||
filesScanned, dirsScanned, bytesScanned := m.getScanProgress()
|
||||
|
||||
@@ -1347,9 +1390,19 @@ func shouldFoldDirWithPath(name, path string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Special case: .npm directory - fold all single-letter subdirectories (npm cache structure)
|
||||
if strings.Contains(path, "/.npm/") && len(name) == 1 {
|
||||
return true
|
||||
// Special case: .npm directory - fold all subdirectories under cache folders
|
||||
// This includes: .npm/_quick/*, .npm/_cacache/*, .npm/*/
|
||||
if strings.Contains(path, "/.npm/") {
|
||||
// Get the parent directory name
|
||||
parent := filepath.Base(filepath.Dir(path))
|
||||
// If parent is a cache folder (_quick, _cacache, etc) or .npm itself, fold it
|
||||
if parent == ".npm" || strings.HasPrefix(parent, "_") {
|
||||
return true
|
||||
}
|
||||
// Also fold single-letter subdirectories (npm cache structure)
|
||||
if len(name) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -1361,9 +1414,9 @@ func calculateDirSizeWithDu(path string) int64 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Use -sb for exact byte count (matches info.Size() behavior)
|
||||
// -s: summarize (don't show subdirs), -b: bytes (not blocks)
|
||||
cmd := exec.CommandContext(ctx, "du", "-sb", path)
|
||||
// Use -sk for 1K-block output, then convert to bytes
|
||||
// macOS du doesn't support -b flag
|
||||
cmd := exec.CommandContext(ctx, "du", "-sk", path)
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return 0
|
||||
@@ -1374,12 +1427,12 @@ func calculateDirSizeWithDu(path string) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
bytes, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
kb, err := strconv.ParseInt(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return bytes
|
||||
return kb * 1024
|
||||
}
|
||||
|
||||
func shouldSkipFileForLargeTracking(path string) bool {
|
||||
@@ -2073,12 +2126,60 @@ func scanOverviewPathCmd(path string, index int) tea.Cmd {
|
||||
}
|
||||
}
|
||||
|
||||
// deletePathCmd deletes a path recursively with progress tracking
|
||||
func deletePathCmd(path string, counter *int64) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
count, err := deletePathWithProgress(path, counter)
|
||||
return deleteProgressMsg{
|
||||
done: true,
|
||||
err: err,
|
||||
count: count,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deletePathWithProgress recursively deletes a path and tracks progress
|
||||
func deletePathWithProgress(root string, counter *int64) (int64, error) {
|
||||
var count int64
|
||||
|
||||
// Walk the directory tree and delete files
|
||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
// If we can't read a path, skip it but continue
|
||||
return nil
|
||||
}
|
||||
|
||||
// Don't delete directories yet, just count and delete files
|
||||
if !d.IsDir() {
|
||||
if removeErr := os.Remove(path); removeErr == nil {
|
||||
count++
|
||||
if counter != nil {
|
||||
atomic.StoreInt64(counter, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
|
||||
// Now remove all empty directories using RemoveAll
|
||||
// This is safe because we've already deleted all files
|
||||
if err := os.RemoveAll(root); err != nil {
|
||||
return count, err
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// measureOverviewSize calculates the size of a directory using multiple strategies:
|
||||
// 1. Check JSON cache (fast)
|
||||
// 2. Try mdls metadata (fast, macOS only)
|
||||
// 3. Walk the directory to get logical size (matches detailed scans)
|
||||
// 4. Try du command (moderate speed, physical size)
|
||||
// 5. Check gob cache (fallback)
|
||||
// 2. Try du command (fast and accurate)
|
||||
// 3. Walk the directory to get logical size (accurate but slower)
|
||||
// 4. Check gob cache (fallback)
|
||||
func measureOverviewSize(path string) (int64, error) {
|
||||
if path == "" {
|
||||
return 0, fmt.Errorf("empty path")
|
||||
@@ -2099,25 +2200,19 @@ func measureOverviewSize(path string) (int64, error) {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
// Strategy 2: Try mdls (fastest, macOS only)
|
||||
if metadataSize, err := getDirectorySizeFromMetadata(path); err == nil && metadataSize > 0 {
|
||||
_ = storeOverviewSize(path, metadataSize)
|
||||
return metadataSize, nil
|
||||
}
|
||||
|
||||
// Strategy 3: Fall back to a quick logical size walk so the result matches detailed scans.
|
||||
if logicalSize, err := getDirectoryLogicalSize(path); err == nil && logicalSize > 0 {
|
||||
_ = storeOverviewSize(path, logicalSize)
|
||||
return logicalSize, nil
|
||||
}
|
||||
|
||||
// Strategy 4: Try du command (fast and reliable for physical size)
|
||||
// Strategy 2: Try du command first (fast and accurate with -s flag)
|
||||
if duSize, err := getDirectorySizeFromDu(path); err == nil && duSize > 0 {
|
||||
_ = storeOverviewSize(path, duSize)
|
||||
return duSize, nil
|
||||
}
|
||||
|
||||
// Strategy 5: Check gob cache as fallback
|
||||
// Strategy 3: Fall back to logical size walk (accurate but slower)
|
||||
if logicalSize, err := getDirectoryLogicalSize(path); err == nil && logicalSize > 0 {
|
||||
_ = storeOverviewSize(path, logicalSize)
|
||||
return logicalSize, nil
|
||||
}
|
||||
|
||||
// Strategy 4: Check gob cache as fallback
|
||||
if cached, err := loadCacheFromDisk(path); err == nil {
|
||||
_ = storeOverviewSize(path, cached.TotalSize)
|
||||
return cached.TotalSize, nil
|
||||
@@ -2171,12 +2266,14 @@ func getDirectorySizeFromMetadata(path string) (int64, error) {
|
||||
}
|
||||
|
||||
// getDirectorySizeFromDu calculates directory size using the du command.
|
||||
// Uses -d 0 to avoid recursing into subdirectories, and includes a timeout protection.
|
||||
// Uses -s to summarize total size including all subdirectories.
|
||||
func getDirectorySizeFromDu(path string) (int64, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), duTimeout)
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.CommandContext(ctx, "du", "-sk", "-d", "0", path)
|
||||
// Use -sk for 1K-block size output, -s for summary
|
||||
// Note: -k and -s are separate flags (not -sk -s)
|
||||
cmd := exec.CommandContext(ctx, "du", "-sk", path)
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout = &stdout
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
Reference in New Issue
Block a user