1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-11 15:53:59 +00:00

feat: Enhance clean, optimize, analyze, and status commands, and update security audit documentation.

This commit is contained in:
Tw93
2025-12-31 16:23:31 +08:00
parent 8ac59da0e2
commit 9aa569cbb6
53 changed files with 538 additions and 1659 deletions

View File

@@ -2,7 +2,6 @@
# Application Data Cleanup Module
set -euo pipefail
# Args: $1=target_dir, $2=label
# Clean .DS_Store (Finder metadata), home uses maxdepth 5, excludes slow paths, max 500 files
clean_ds_store_tree() {
local target="$1"
local label="$2"
@@ -15,7 +14,6 @@ clean_ds_store_tree() {
start_inline_spinner "Cleaning Finder metadata..."
spinner_active="true"
fi
# Build exclusion paths for find (skip common slow/large directories)
local -a exclude_paths=(
-path "*/Library/Application Support/MobileSync" -prune -o
-path "*/Library/Developer" -prune -o
@@ -24,13 +22,11 @@ clean_ds_store_tree() {
-path "*/.git" -prune -o
-path "*/Library/Caches" -prune -o
)
# Build find command to avoid unbound array expansion with set -u
local -a find_cmd=("command" "find" "$target")
if [[ "$target" == "$HOME" ]]; then
find_cmd+=("-maxdepth" "5")
fi
find_cmd+=("${exclude_paths[@]}" "-type" "f" "-name" ".DS_Store" "-print0")
# Find .DS_Store files with exclusions and depth limit
while IFS= read -r -d '' ds_file; do
local size
size=$(get_file_size "$ds_file")
@@ -61,14 +57,11 @@ clean_ds_store_tree() {
note_activity
fi
}
# Clean data for uninstalled apps (caches/logs/states older than 60 days)
# Protects system apps, major vendors, scans /Applications+running processes
# Max 100 items/pattern, 2s du timeout. Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
# Orphaned app data (60+ days inactive). Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
# Usage: scan_installed_apps "output_file"
# Scan system for installed application bundle IDs
scan_installed_apps() {
local installed_bundles="$1"
# Performance optimization: cache results for 5 minutes
# Cache installed app scan briefly to speed repeated runs.
local cache_file="$HOME/.cache/mole/installed_apps_cache"
local cache_age_seconds=300 # 5 minutes
if [[ -f "$cache_file" ]]; then
@@ -77,7 +70,6 @@ scan_installed_apps() {
local age=$((current_time - cache_mtime))
if [[ $age -lt $cache_age_seconds ]]; then
debug_log "Using cached app list (age: ${age}s)"
# Verify cache file is readable and not empty
if [[ -r "$cache_file" ]] && [[ -s "$cache_file" ]]; then
if cat "$cache_file" > "$installed_bundles" 2> /dev/null; then
return 0
@@ -90,26 +82,22 @@ scan_installed_apps() {
fi
fi
debug_log "Scanning installed applications (cache expired or missing)"
# Scan all Applications directories
local -a app_dirs=(
"/Applications"
"/System/Applications"
"$HOME/Applications"
)
# Create a temp dir for parallel results to avoid write contention
# Temp dir avoids write contention across parallel scans.
local scan_tmp_dir=$(create_temp_dir)
# Parallel scan for applications
local pids=()
local dir_idx=0
for app_dir in "${app_dirs[@]}"; do
[[ -d "$app_dir" ]] || continue
(
# Quickly find all .app bundles first
local -a app_paths=()
while IFS= read -r app_path; do
[[ -n "$app_path" ]] && app_paths+=("$app_path")
done < <(find "$app_dir" -name '*.app' -maxdepth 3 -type d 2> /dev/null)
# Read bundle IDs with PlistBuddy
local count=0
for app_path in "${app_paths[@]:-}"; do
local plist_path="$app_path/Contents/Info.plist"
@@ -124,7 +112,7 @@ scan_installed_apps() {
pids+=($!)
((dir_idx++))
done
# Get running applications and LaunchAgents in parallel
# Collect running apps and LaunchAgents to avoid false orphan cleanup.
(
local running_apps=$(run_with_timeout 5 osascript -e 'tell application "System Events" to get bundle identifier of every application process' 2> /dev/null || echo "")
echo "$running_apps" | tr ',' '\n' | sed -e 's/^ *//;s/ *$//' -e '/^$/d' > "$scan_tmp_dir/running.txt"
@@ -136,7 +124,6 @@ scan_installed_apps() {
xargs -I {} basename {} .plist > "$scan_tmp_dir/agents.txt" 2> /dev/null || true
) &
pids+=($!)
# Wait for all background scans to complete
debug_log "Waiting for ${#pids[@]} background processes: ${pids[*]}"
for pid in "${pids[@]}"; do
wait "$pid" 2> /dev/null || true
@@ -145,37 +132,30 @@ scan_installed_apps() {
cat "$scan_tmp_dir"/*.txt >> "$installed_bundles" 2> /dev/null || true
safe_remove "$scan_tmp_dir" true
sort -u "$installed_bundles" -o "$installed_bundles"
# Cache the results
ensure_user_dir "$(dirname "$cache_file")"
cp "$installed_bundles" "$cache_file" 2> /dev/null || true
local app_count=$(wc -l < "$installed_bundles" 2> /dev/null | tr -d ' ')
debug_log "Scanned $app_count unique applications"
}
# Usage: is_bundle_orphaned "bundle_id" "directory_path" "installed_bundles_file"
# Check if bundle is orphaned
is_bundle_orphaned() {
local bundle_id="$1"
local directory_path="$2"
local installed_bundles="$3"
# Skip system-critical and protected apps
if should_protect_data "$bundle_id"; then
return 1
fi
# Check if app exists in our scan
if grep -Fxq "$bundle_id" "$installed_bundles" 2> /dev/null; then
return 1
fi
# Check against centralized protected patterns (app_protection.sh)
if should_protect_data "$bundle_id"; then
return 1
fi
# Extra check for specific system bundles not covered by patterns
case "$bundle_id" in
loginwindow | dock | systempreferences | systemsettings | settings | controlcenter | finder | safari)
return 1
;;
esac
# Check file age - only clean if 60+ days inactive
if [[ -e "$directory_path" ]]; then
local last_modified_epoch=$(get_file_mtime "$directory_path")
local current_epoch=$(date +%s)
@@ -186,31 +166,23 @@ is_bundle_orphaned() {
fi
return 0
}
# Clean data for uninstalled apps (caches/logs/states older than 60 days)
# Max 100 items/pattern, 2s du timeout. Env: ORPHAN_AGE_THRESHOLD, DRY_RUN
# Protects system apps, major vendors, scans /Applications+running processes
# Orphaned app data sweep.
clean_orphaned_app_data() {
# Quick permission check - if we can't access Library folders, skip
if ! ls "$HOME/Library/Caches" > /dev/null 2>&1; then
stop_section_spinner
echo -e " ${YELLOW}${ICON_WARNING}${NC} Skipped: No permission to access Library folders"
return 0
fi
# Build list of installed/active apps
start_section_spinner "Scanning installed apps..."
local installed_bundles=$(create_temp_file)
scan_installed_apps "$installed_bundles"
stop_section_spinner
# Display scan results
local app_count=$(wc -l < "$installed_bundles" 2> /dev/null | tr -d ' ')
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Found $app_count active/installed apps"
# Track statistics
local orphaned_count=0
local total_orphaned_kb=0
# Unified orphaned resource scanner (caches, logs, states, webkit, HTTP, cookies)
start_section_spinner "Scanning orphaned app resources..."
# Define resource types to scan
# CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps)
# CRITICAL: NEVER add LaunchAgents or LaunchDaemons (breaks login items/startup apps).
local -a resource_types=(
"$HOME/Library/Caches|Caches|com.*:org.*:net.*:io.*"
"$HOME/Library/Logs|Logs|com.*:org.*:net.*:io.*"
@@ -222,38 +194,29 @@ clean_orphaned_app_data() {
orphaned_count=0
for resource_type in "${resource_types[@]}"; do
IFS='|' read -r base_path label patterns <<< "$resource_type"
# Check both existence and permission to avoid hanging
if [[ ! -d "$base_path" ]]; then
continue
fi
# Quick permission check - if we can't ls the directory, skip it
if ! ls "$base_path" > /dev/null 2>&1; then
continue
fi
# Build file pattern array
local -a file_patterns=()
IFS=':' read -ra pattern_arr <<< "$patterns"
for pat in "${pattern_arr[@]}"; do
file_patterns+=("$base_path/$pat")
done
# Scan and clean orphaned items
for item_path in "${file_patterns[@]}"; do
# Use shell glob (no ls needed)
# Limit iterations to prevent hanging on directories with too many files
local iteration_count=0
for match in $item_path; do
[[ -e "$match" ]] || continue
# Safety: limit iterations to prevent infinite loops on massive directories
((iteration_count++))
if [[ $iteration_count -gt $MOLE_MAX_ORPHAN_ITERATIONS ]]; then
break
fi
# Extract bundle ID from filename
local bundle_id=$(basename "$match")
bundle_id="${bundle_id%.savedState}"
bundle_id="${bundle_id%.binarycookies}"
if is_bundle_orphaned "$bundle_id" "$match" "$installed_bundles"; then
# Use timeout to prevent du from hanging on network mounts or problematic paths
local size_kb
size_kb=$(get_path_size_kb "$match")
if [[ -z "$size_kb" || "$size_kb" == "0" ]]; then