From 5d77001a721a795bf740d10826d3f39cbcb9de6c Mon Sep 17 00:00:00 2001 From: Tw93 Date: Mon, 12 Jan 2026 17:49:51 +0800 Subject: [PATCH] Optimize the effect and speed of scanning --- bin/purge.sh | 104 +++++++++++++++++++++++++++++++++++-- lib/clean/project.sh | 119 +++++++++++++++++++++++++++---------------- tests/purge.bats | 21 ++++++++ 3 files changed, 195 insertions(+), 49 deletions(-) diff --git a/bin/purge.sh b/bin/purge.sh index 1574243..92ec119 100755 --- a/bin/purge.sh +++ b/bin/purge.sh @@ -47,21 +47,119 @@ start_purge() { printf '\033[2J\033[H' fi printf '\n' - echo -e "${PURPLE_BOLD}Purge Project Artifacts${NC}" # Initialize stats file in user cache directory local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole" ensure_user_dir "$stats_dir" ensure_user_file "$stats_dir/purge_stats" ensure_user_file "$stats_dir/purge_count" + ensure_user_file "$stats_dir/purge_scanning" echo "0" > "$stats_dir/purge_stats" echo "0" > "$stats_dir/purge_count" + echo "" > "$stats_dir/purge_scanning" } # Perform the purge perform_purge() { + local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole" + local monitor_pid="" + + # Cleanup function + cleanup_monitor() { + # Remove scanning file to stop monitor + rm -f "$stats_dir/purge_scanning" 2> /dev/null || true + + if [[ -n "$monitor_pid" ]]; then + kill "$monitor_pid" 2> /dev/null || true + wait "$monitor_pid" 2> /dev/null || true + fi + if [[ -t 1 ]]; then + printf '\r\033[K\n\033[K\033[A' + fi + } + + # Set up trap for cleanup + trap cleanup_monitor INT TERM + + # Show scanning with spinner on same line as title + if [[ -t 1 ]]; then + # Print title first + printf '%s' "${PURPLE_BOLD}Purge Project Artifacts${NC} " + + # Start background monitor with ASCII spinner + ( + local spinner_chars="|/-\\" + local spinner_idx=0 + local last_path="" + + # Set up trap to exit cleanly + trap 'exit 0' INT TERM + + # Function to truncate path in the middle + truncate_path() { + local path="$1" + local max_len=80 + + if [[ ${#path} -le $max_len ]]; then + echo "$path" + return + fi + + # Calculate how much to show on each side + local side_len=$(( (max_len - 3) / 2 )) + local start="${path:0:$side_len}" + local end="${path: -$side_len}" + echo "${start}...${end}" + } + + while [[ -f "$stats_dir/purge_scanning" ]]; do + local current_path=$(cat "$stats_dir/purge_scanning" 2> /dev/null || echo "") + local display_path="" + + if [[ -n "$current_path" ]]; then + display_path="${current_path/#$HOME/~}" + display_path=$(truncate_path "$display_path") + last_path="$display_path" + elif [[ -n "$last_path" ]]; then + display_path="$last_path" + fi + + # Get current spinner character + local spin_char="${spinner_chars:$spinner_idx:1}" + spinner_idx=$(( (spinner_idx + 1) % ${#spinner_chars} )) + + # Show title on first line, spinner and scanning info on second line + if [[ -n "$display_path" ]]; then + printf '\r%s\n%s %sScanning %s\033[K\033[A' \ + "${PURPLE_BOLD}Purge Project Artifacts${NC}" \ + "${BLUE}${spin_char}${NC}" \ + "${GRAY}" "$display_path" + else + printf '\r%s\n%s %sScanning...\033[K\033[A' \ + "${PURPLE_BOLD}Purge Project Artifacts${NC}" \ + "${BLUE}${spin_char}${NC}" \ + "${GRAY}" + fi + + sleep 0.05 + done + exit 0 + ) & + monitor_pid=$! + else + echo -e "${PURPLE_BOLD}Purge Project Artifacts${NC}" + fi + clean_project_artifacts local exit_code=$? + + # Clean up + trap - INT TERM + cleanup_monitor + + if [[ -t 1 ]]; then + echo -e "${PURPLE_BOLD}Purge Project Artifacts${NC}" + fi # Exit codes: # 0 = success, show summary @@ -79,15 +177,11 @@ perform_purge() { local total_size_cleaned=0 local total_items_cleaned=0 - # Read stats from user cache directory - local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole" - if [[ -f "$stats_dir/purge_stats" ]]; then total_size_cleaned=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0") rm -f "$stats_dir/purge_stats" fi - # Read count if [[ -f "$stats_dir/purge_count" ]]; then total_items_cleaned=$(cat "$stats_dir/purge_count" 2> /dev/null || echo "0") rm -f "$stats_dir/purge_count" diff --git a/lib/clean/project.sh b/lib/clean/project.sh index 7a70ba9..15908bd 100644 --- a/lib/clean/project.sh +++ b/lib/clean/project.sh @@ -45,7 +45,7 @@ readonly PURGE_TARGETS=( readonly MIN_AGE_DAYS=7 # Scan depth defaults (relative to search root). readonly PURGE_MIN_DEPTH_DEFAULT=2 -readonly PURGE_MAX_DEPTH_DEFAULT=8 +readonly PURGE_MAX_DEPTH_DEFAULT=4 # Search paths (default, can be overridden via config file). readonly DEFAULT_PURGE_SEARCH_PATHS=( "$HOME/www" @@ -339,6 +339,11 @@ scan_purge_targets() { if [[ ! -d "$search_path" ]]; then return fi + + # Update current scanning path + local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole" + echo "$search_path" > "$stats_dir/purge_scanning" 2> /dev/null || true + if command -v fd > /dev/null 2>&1; then # Escape regex special characters in target names for fd patterns local escaped_targets=() @@ -356,28 +361,39 @@ scan_purge_targets() { "--type" "d" "--min-depth" "$min_depth" "--max-depth" "$max_depth" - "--threads" "4" + "--threads" "8" "--exclude" ".git" "--exclude" "Library" "--exclude" ".Trash" "--exclude" "Applications" ) - fd "${fd_args[@]}" "$pattern" "$search_path" 2> /dev/null | while IFS= read -r item; do - if is_safe_project_artifact "$item" "$search_path"; then - echo "$item" - fi - done | filter_nested_artifacts | filter_protected_artifacts > "$output_file" + # Write to temp file first, then filter - more efficient than piping + fd "${fd_args[@]}" "$pattern" "$search_path" 2> /dev/null > "$output_file.raw" || true + + # Single pass: safe + nested + protected + if [[ -f "$output_file.raw" ]]; then + while IFS= read -r item; do + # Check if we should abort (scanning file removed by Ctrl+C) + if [[ ! -f "$stats_dir/purge_scanning" ]]; then + rm -f "$output_file.raw" + return + fi + + if [[ -n "$item" ]] && is_safe_project_artifact "$item" "$search_path"; then + echo "$item" + # Update scanning path to show current project directory + local project_dir=$(dirname "$item") + echo "$project_dir" > "$stats_dir/purge_scanning" 2> /dev/null || true + fi + done < "$output_file.raw" | filter_nested_artifacts | filter_protected_artifacts > "$output_file" + rm -f "$output_file.raw" + else + touch "$output_file" + fi else # Pruned find avoids descending into heavy directories. - local prune_args=() - local prune_dirs=(".git" "Library" ".Trash" "Applications") - for dir in "${prune_dirs[@]}"; do - prune_args+=("-name" "$dir" "-prune" "-o") - done - for target in "${PURGE_TARGETS[@]}"; do - prune_args+=("-name" "$target" "-print" "-prune" "-o") - done local find_expr=() + local prune_dirs=(".git" "Library" ".Trash" "Applications") for dir in "${prune_dirs[@]}"; do find_expr+=("-name" "$dir" "-prune" "-o") done @@ -390,28 +406,49 @@ scan_purge_targets() { ((i++)) done command find "$search_path" -mindepth "$min_depth" -maxdepth "$max_depth" -type d \ - \( "${find_expr[@]}" \) 2> /dev/null | while IFS= read -r item; do - if is_safe_project_artifact "$item" "$search_path"; then - echo "$item" - fi - done | filter_nested_artifacts | filter_protected_artifacts > "$output_file" + \( "${find_expr[@]}" \) 2> /dev/null > "$output_file.raw" || true + + # Single pass: safe + nested + protected + if [[ -f "$output_file.raw" ]]; then + while IFS= read -r item; do + # Check if we should abort (scanning file removed by Ctrl+C) + if [[ ! -f "$stats_dir/purge_scanning" ]]; then + rm -f "$output_file.raw" + return + fi + + if [[ -n "$item" ]] && is_safe_project_artifact "$item" "$search_path"; then + echo "$item" + # Update scanning path to show current project directory + local project_dir=$(dirname "$item") + echo "$project_dir" > "$stats_dir/purge_scanning" 2> /dev/null || true + fi + done < "$output_file.raw" | filter_nested_artifacts | filter_protected_artifacts > "$output_file" + rm -f "$output_file.raw" + else + touch "$output_file" + fi fi } -# Filter out nested artifacts (e.g. node_modules inside node_modules). +# Filter out nested artifacts (e.g. node_modules inside node_modules, .build inside build). +# Optimized: Sort paths to put parents before children, then filter in single pass. filter_nested_artifacts() { - while IFS= read -r item; do - local parent_dir=$(dirname "$item") - local is_nested=false - for target in "${PURGE_TARGETS[@]}"; do - if [[ "$parent_dir" == *"/$target/"* || "$parent_dir" == *"/$target" ]]; then - is_nested=true - break - fi - done - if [[ "$is_nested" == "false" ]]; then - echo "$item" - fi - done + # 1. Append trailing slash to each path (to ensure /foo/bar starts with /foo/) + # 2. Sort to group parents and children (LC_COLLATE=C ensures standard sorting) + # 3. Use awk to filter out paths that start with the previous kept path + # 4. Remove trailing slash + sed 's|[^/]$|&/|' | LC_COLLATE=C sort | awk ' + BEGIN { last_kept = "" } + { + current = $0 + # If current path starts with last_kept, it is nested + # Only check if last_kept is not empty + if (last_kept == "" || index(current, last_kept) != 1) { + print current + last_kept = current + } + } + ' | sed 's|/$||' } filter_protected_artifacts() { @@ -703,17 +740,14 @@ clean_project_artifacts() { for temp in "${scan_temps[@]+"${scan_temps[@]}"}"; do rm -f "$temp" 2> /dev/null || true done - if [[ -t 1 ]]; then - stop_inline_spinner - fi + # Clean up purge scanning file + local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole" + rm -f "$stats_dir/purge_scanning" 2> /dev/null || true echo "" exit 130 } trap cleanup_scan INT TERM - # Start parallel scanning of all paths at once - if [[ -t 1 ]]; then - start_inline_spinner "Scanning projects..." - fi + # Scanning is started from purge.sh with start_inline_spinner # Launch all scans in parallel for path in "${PURGE_SEARCH_PATHS[@]}"; do if [[ -d "$path" ]]; then @@ -730,9 +764,6 @@ clean_project_artifacts() { for pid in "${scan_pids[@]+"${scan_pids[@]}"}"; do wait "$pid" 2> /dev/null || true done - if [[ -t 1 ]]; then - stop_inline_spinner - fi # Collect all results for scan_output in "${scan_temps[@]+"${scan_temps[@]}"}"; do if [[ -f "$scan_output" ]]; then diff --git a/tests/purge.bats b/tests/purge.bats index 5ccab1f..7819b89 100644 --- a/tests/purge.bats +++ b/tests/purge.bats @@ -101,6 +101,27 @@ setup() { [[ "$result" == "2" ]] } +@test "filter_nested_artifacts: removes Xcode build subdirectories (Mac projects)" { + # Simulate Mac Xcode project with nested .build directories: + # ~/www/testapp/build + # ~/www/testapp/build/Framework.build + # ~/www/testapp/build/Package.build + mkdir -p "$HOME/www/testapp/build/Framework.build" + mkdir -p "$HOME/www/testapp/build/Package.build" + + result=$(bash -c " + source '$PROJECT_ROOT/lib/clean/project.sh' + printf '%s\n' \ + '$HOME/www/testapp/build' \ + '$HOME/www/testapp/build/Framework.build' \ + '$HOME/www/testapp/build/Package.build' | \ + filter_nested_artifacts | wc -l | tr -d ' ' + ") + + # Should only keep the top-level 'build' directory, filtering out nested .build dirs + [[ "$result" == "1" ]] +} + # Vendor protection unit tests @test "is_rails_project_root: detects valid Rails project" { mkdir -p "$HOME/www/test-rails/config"