mirror of
https://github.com/tw93/Mole.git
synced 2026-02-15 01:45:06 +00:00
Optimize the effect and speed of scanning
This commit is contained in:
104
bin/purge.sh
104
bin/purge.sh
@@ -47,21 +47,119 @@ start_purge() {
|
|||||||
printf '\033[2J\033[H'
|
printf '\033[2J\033[H'
|
||||||
fi
|
fi
|
||||||
printf '\n'
|
printf '\n'
|
||||||
echo -e "${PURPLE_BOLD}Purge Project Artifacts${NC}"
|
|
||||||
|
|
||||||
# Initialize stats file in user cache directory
|
# Initialize stats file in user cache directory
|
||||||
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
|
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
|
||||||
ensure_user_dir "$stats_dir"
|
ensure_user_dir "$stats_dir"
|
||||||
ensure_user_file "$stats_dir/purge_stats"
|
ensure_user_file "$stats_dir/purge_stats"
|
||||||
ensure_user_file "$stats_dir/purge_count"
|
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_stats"
|
||||||
echo "0" > "$stats_dir/purge_count"
|
echo "0" > "$stats_dir/purge_count"
|
||||||
|
echo "" > "$stats_dir/purge_scanning"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Perform the purge
|
# Perform the purge
|
||||||
perform_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
|
clean_project_artifacts
|
||||||
local exit_code=$?
|
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:
|
# Exit codes:
|
||||||
# 0 = success, show summary
|
# 0 = success, show summary
|
||||||
@@ -79,15 +177,11 @@ perform_purge() {
|
|||||||
local total_size_cleaned=0
|
local total_size_cleaned=0
|
||||||
local total_items_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
|
if [[ -f "$stats_dir/purge_stats" ]]; then
|
||||||
total_size_cleaned=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
|
total_size_cleaned=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
|
||||||
rm -f "$stats_dir/purge_stats"
|
rm -f "$stats_dir/purge_stats"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Read count
|
|
||||||
if [[ -f "$stats_dir/purge_count" ]]; then
|
if [[ -f "$stats_dir/purge_count" ]]; then
|
||||||
total_items_cleaned=$(cat "$stats_dir/purge_count" 2> /dev/null || echo "0")
|
total_items_cleaned=$(cat "$stats_dir/purge_count" 2> /dev/null || echo "0")
|
||||||
rm -f "$stats_dir/purge_count"
|
rm -f "$stats_dir/purge_count"
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ readonly PURGE_TARGETS=(
|
|||||||
readonly MIN_AGE_DAYS=7
|
readonly MIN_AGE_DAYS=7
|
||||||
# Scan depth defaults (relative to search root).
|
# Scan depth defaults (relative to search root).
|
||||||
readonly PURGE_MIN_DEPTH_DEFAULT=2
|
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).
|
# Search paths (default, can be overridden via config file).
|
||||||
readonly DEFAULT_PURGE_SEARCH_PATHS=(
|
readonly DEFAULT_PURGE_SEARCH_PATHS=(
|
||||||
"$HOME/www"
|
"$HOME/www"
|
||||||
@@ -339,6 +339,11 @@ scan_purge_targets() {
|
|||||||
if [[ ! -d "$search_path" ]]; then
|
if [[ ! -d "$search_path" ]]; then
|
||||||
return
|
return
|
||||||
fi
|
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
|
if command -v fd > /dev/null 2>&1; then
|
||||||
# Escape regex special characters in target names for fd patterns
|
# Escape regex special characters in target names for fd patterns
|
||||||
local escaped_targets=()
|
local escaped_targets=()
|
||||||
@@ -356,28 +361,39 @@ scan_purge_targets() {
|
|||||||
"--type" "d"
|
"--type" "d"
|
||||||
"--min-depth" "$min_depth"
|
"--min-depth" "$min_depth"
|
||||||
"--max-depth" "$max_depth"
|
"--max-depth" "$max_depth"
|
||||||
"--threads" "4"
|
"--threads" "8"
|
||||||
"--exclude" ".git"
|
"--exclude" ".git"
|
||||||
"--exclude" "Library"
|
"--exclude" "Library"
|
||||||
"--exclude" ".Trash"
|
"--exclude" ".Trash"
|
||||||
"--exclude" "Applications"
|
"--exclude" "Applications"
|
||||||
)
|
)
|
||||||
fd "${fd_args[@]}" "$pattern" "$search_path" 2> /dev/null | while IFS= read -r item; do
|
# Write to temp file first, then filter - more efficient than piping
|
||||||
if is_safe_project_artifact "$item" "$search_path"; then
|
fd "${fd_args[@]}" "$pattern" "$search_path" 2> /dev/null > "$output_file.raw" || true
|
||||||
echo "$item"
|
|
||||||
fi
|
# Single pass: safe + nested + protected
|
||||||
done | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
|
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
|
else
|
||||||
# Pruned find avoids descending into heavy directories.
|
# 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 find_expr=()
|
||||||
|
local prune_dirs=(".git" "Library" ".Trash" "Applications")
|
||||||
for dir in "${prune_dirs[@]}"; do
|
for dir in "${prune_dirs[@]}"; do
|
||||||
find_expr+=("-name" "$dir" "-prune" "-o")
|
find_expr+=("-name" "$dir" "-prune" "-o")
|
||||||
done
|
done
|
||||||
@@ -390,28 +406,49 @@ scan_purge_targets() {
|
|||||||
((i++))
|
((i++))
|
||||||
done
|
done
|
||||||
command find "$search_path" -mindepth "$min_depth" -maxdepth "$max_depth" -type d \
|
command find "$search_path" -mindepth "$min_depth" -maxdepth "$max_depth" -type d \
|
||||||
\( "${find_expr[@]}" \) 2> /dev/null | while IFS= read -r item; do
|
\( "${find_expr[@]}" \) 2> /dev/null > "$output_file.raw" || true
|
||||||
if is_safe_project_artifact "$item" "$search_path"; then
|
|
||||||
echo "$item"
|
# Single pass: safe + nested + protected
|
||||||
fi
|
if [[ -f "$output_file.raw" ]]; then
|
||||||
done | filter_nested_artifacts | filter_protected_artifacts > "$output_file"
|
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
|
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() {
|
filter_nested_artifacts() {
|
||||||
while IFS= read -r item; do
|
# 1. Append trailing slash to each path (to ensure /foo/bar starts with /foo/)
|
||||||
local parent_dir=$(dirname "$item")
|
# 2. Sort to group parents and children (LC_COLLATE=C ensures standard sorting)
|
||||||
local is_nested=false
|
# 3. Use awk to filter out paths that start with the previous kept path
|
||||||
for target in "${PURGE_TARGETS[@]}"; do
|
# 4. Remove trailing slash
|
||||||
if [[ "$parent_dir" == *"/$target/"* || "$parent_dir" == *"/$target" ]]; then
|
sed 's|[^/]$|&/|' | LC_COLLATE=C sort | awk '
|
||||||
is_nested=true
|
BEGIN { last_kept = "" }
|
||||||
break
|
{
|
||||||
fi
|
current = $0
|
||||||
done
|
# If current path starts with last_kept, it is nested
|
||||||
if [[ "$is_nested" == "false" ]]; then
|
# Only check if last_kept is not empty
|
||||||
echo "$item"
|
if (last_kept == "" || index(current, last_kept) != 1) {
|
||||||
fi
|
print current
|
||||||
done
|
last_kept = current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
' | sed 's|/$||'
|
||||||
}
|
}
|
||||||
|
|
||||||
filter_protected_artifacts() {
|
filter_protected_artifacts() {
|
||||||
@@ -703,17 +740,14 @@ clean_project_artifacts() {
|
|||||||
for temp in "${scan_temps[@]+"${scan_temps[@]}"}"; do
|
for temp in "${scan_temps[@]+"${scan_temps[@]}"}"; do
|
||||||
rm -f "$temp" 2> /dev/null || true
|
rm -f "$temp" 2> /dev/null || true
|
||||||
done
|
done
|
||||||
if [[ -t 1 ]]; then
|
# Clean up purge scanning file
|
||||||
stop_inline_spinner
|
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
|
||||||
fi
|
rm -f "$stats_dir/purge_scanning" 2> /dev/null || true
|
||||||
echo ""
|
echo ""
|
||||||
exit 130
|
exit 130
|
||||||
}
|
}
|
||||||
trap cleanup_scan INT TERM
|
trap cleanup_scan INT TERM
|
||||||
# Start parallel scanning of all paths at once
|
# Scanning is started from purge.sh with start_inline_spinner
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
start_inline_spinner "Scanning projects..."
|
|
||||||
fi
|
|
||||||
# Launch all scans in parallel
|
# Launch all scans in parallel
|
||||||
for path in "${PURGE_SEARCH_PATHS[@]}"; do
|
for path in "${PURGE_SEARCH_PATHS[@]}"; do
|
||||||
if [[ -d "$path" ]]; then
|
if [[ -d "$path" ]]; then
|
||||||
@@ -730,9 +764,6 @@ clean_project_artifacts() {
|
|||||||
for pid in "${scan_pids[@]+"${scan_pids[@]}"}"; do
|
for pid in "${scan_pids[@]+"${scan_pids[@]}"}"; do
|
||||||
wait "$pid" 2> /dev/null || true
|
wait "$pid" 2> /dev/null || true
|
||||||
done
|
done
|
||||||
if [[ -t 1 ]]; then
|
|
||||||
stop_inline_spinner
|
|
||||||
fi
|
|
||||||
# Collect all results
|
# Collect all results
|
||||||
for scan_output in "${scan_temps[@]+"${scan_temps[@]}"}"; do
|
for scan_output in "${scan_temps[@]+"${scan_temps[@]}"}"; do
|
||||||
if [[ -f "$scan_output" ]]; then
|
if [[ -f "$scan_output" ]]; then
|
||||||
|
|||||||
@@ -101,6 +101,27 @@ setup() {
|
|||||||
[[ "$result" == "2" ]]
|
[[ "$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
|
# Vendor protection unit tests
|
||||||
@test "is_rails_project_root: detects valid Rails project" {
|
@test "is_rails_project_root: detects valid Rails project" {
|
||||||
mkdir -p "$HOME/www/test-rails/config"
|
mkdir -p "$HOME/www/test-rails/config"
|
||||||
|
|||||||
Reference in New Issue
Block a user