mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:04:42 +00:00
feat: enhance clean logic
1. Add recursive empty directory cleanup for Application Support and Caches. 2. Add support for cleaning old Edge Updater versions.
This commit is contained in:
@@ -22,6 +22,7 @@ clean_empty_library_items() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 1. Clean top-level empty directories in Library
|
||||||
local -a empty_dirs=()
|
local -a empty_dirs=()
|
||||||
while IFS= read -r -d '' dir; do
|
while IFS= read -r -d '' dir; do
|
||||||
[[ -d "$dir" ]] && empty_dirs+=("$dir")
|
[[ -d "$dir" ]] && empty_dirs+=("$dir")
|
||||||
@@ -31,6 +32,48 @@ clean_empty_library_items() {
|
|||||||
safe_clean "${empty_dirs[@]}" "Empty Library folders"
|
safe_clean "${empty_dirs[@]}" "Empty Library folders"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 2. Clean empty subdirectories in Application Support and other key locations
|
||||||
|
# Iteratively remove empty directories until no more are found
|
||||||
|
local -a key_locations=(
|
||||||
|
"$HOME/Library/Application Support"
|
||||||
|
"$HOME/Library/Caches"
|
||||||
|
)
|
||||||
|
|
||||||
|
for location in "${key_locations[@]}"; do
|
||||||
|
[[ -d "$location" ]] || continue
|
||||||
|
|
||||||
|
# Limit passes to keep cleanup fast; one extra pass catches most parents.
|
||||||
|
local max_iterations=2
|
||||||
|
local iteration=0
|
||||||
|
|
||||||
|
while [[ $iteration -lt $max_iterations ]]; do
|
||||||
|
local -a nested_empty_dirs=()
|
||||||
|
# Find empty directories
|
||||||
|
while IFS= read -r -d '' dir; do
|
||||||
|
# Skip if whitelisted
|
||||||
|
if is_path_whitelisted "$dir"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# Skip protected system components
|
||||||
|
local dir_name=$(basename "$dir")
|
||||||
|
if is_critical_system_component "$dir_name"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
[[ -d "$dir" ]] && nested_empty_dirs+=("$dir")
|
||||||
|
done < <(find "$location" -mindepth 1 -type d -empty -print0 2> /dev/null)
|
||||||
|
|
||||||
|
# If no empty dirs found, we're done with this location
|
||||||
|
if [[ ${#nested_empty_dirs[@]} -eq 0 ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
local location_name=$(basename "$location")
|
||||||
|
safe_clean "${nested_empty_dirs[@]}" "Empty $location_name subdirs"
|
||||||
|
|
||||||
|
((iteration++))
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
# Empty file cleanup is skipped to avoid removing app sentinel files.
|
# Empty file cleanup is skipped to avoid removing app sentinel files.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +237,68 @@ clean_edge_old_versions() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Remove old Microsoft EdgeUpdater versions while keeping latest.
|
||||||
|
clean_edge_updater_old_versions() {
|
||||||
|
local updater_dir="$HOME/Library/Application Support/Microsoft/EdgeUpdater/apps/msedge-stable"
|
||||||
|
[[ -d "$updater_dir" ]] || return 0
|
||||||
|
|
||||||
|
if pgrep -f "Microsoft Edge" > /dev/null 2>&1; then
|
||||||
|
echo -e " ${YELLOW}${ICON_WARNING}${NC} Microsoft Edge running · updater cleanup skipped"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a version_dirs=()
|
||||||
|
local dir
|
||||||
|
for dir in "$updater_dir"/*; do
|
||||||
|
[[ -d "$dir" ]] || continue
|
||||||
|
version_dirs+=("$dir")
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#version_dirs[@]} -lt 2 ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local latest_version
|
||||||
|
latest_version=$(printf '%s\n' "${version_dirs[@]##*/}" | sort -V | tail -n 1)
|
||||||
|
[[ -n "$latest_version" ]] || return 0
|
||||||
|
|
||||||
|
local cleaned_count=0
|
||||||
|
local total_size=0
|
||||||
|
local cleaned_any=false
|
||||||
|
|
||||||
|
for dir in "${version_dirs[@]}"; do
|
||||||
|
local name
|
||||||
|
name=$(basename "$dir")
|
||||||
|
[[ "$name" == "$latest_version" ]] && continue
|
||||||
|
if is_path_whitelisted "$dir"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
local size_kb
|
||||||
|
size_kb=$(get_path_size_kb "$dir" || echo 0)
|
||||||
|
size_kb="${size_kb:-0}"
|
||||||
|
total_size=$((total_size + size_kb))
|
||||||
|
((cleaned_count++))
|
||||||
|
cleaned_any=true
|
||||||
|
if [[ "$DRY_RUN" != "true" ]]; then
|
||||||
|
safe_remove "$dir" true > /dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$cleaned_any" == "true" ]]; then
|
||||||
|
local size_human
|
||||||
|
size_human=$(bytes_to_human "$((total_size * 1024))")
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Edge updater old versions ${YELLOW}(${cleaned_count} dirs, $size_human dry)${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Edge updater old versions ${GREEN}(${cleaned_count} dirs, $size_human)${NC}"
|
||||||
|
fi
|
||||||
|
((files_cleaned += cleaned_count))
|
||||||
|
((total_size_cleaned += total_size))
|
||||||
|
((total_items++))
|
||||||
|
note_activity
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
scan_external_volumes() {
|
scan_external_volumes() {
|
||||||
[[ -d "/Volumes" ]] || return 0
|
[[ -d "/Volumes" ]] || return 0
|
||||||
local -a candidate_volumes=()
|
local -a candidate_volumes=()
|
||||||
@@ -296,7 +401,7 @@ clean_recent_items() {
|
|||||||
}
|
}
|
||||||
clean_mail_downloads() {
|
clean_mail_downloads() {
|
||||||
stop_section_spinner
|
stop_section_spinner
|
||||||
local mail_age_days=${MOLE_MAIL_AGE_DAYS:-30}
|
local mail_age_days=$MOLE_MAIL_AGE_DAYS
|
||||||
if ! [[ "$mail_age_days" =~ ^[0-9]+$ ]]; then
|
if ! [[ "$mail_age_days" =~ ^[0-9]+$ ]]; then
|
||||||
mail_age_days=30
|
mail_age_days=30
|
||||||
fi
|
fi
|
||||||
@@ -313,7 +418,7 @@ clean_mail_downloads() {
|
|||||||
if ! [[ "$dir_size_kb" =~ ^[0-9]+$ ]]; then
|
if ! [[ "$dir_size_kb" =~ ^[0-9]+$ ]]; then
|
||||||
dir_size_kb=0
|
dir_size_kb=0
|
||||||
fi
|
fi
|
||||||
local min_kb="${MOLE_MAIL_DOWNLOADS_MIN_KB:-5120}"
|
local min_kb="$MOLE_MAIL_DOWNLOADS_MIN_KB"
|
||||||
if ! [[ "$min_kb" =~ ^[0-9]+$ ]]; then
|
if ! [[ "$min_kb" =~ ^[0-9]+$ ]]; then
|
||||||
min_kb=5120
|
min_kb=5120
|
||||||
fi
|
fi
|
||||||
@@ -426,6 +531,7 @@ clean_browsers() {
|
|||||||
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
safe_clean ~/Library/Application\ Support/Firefox/Profiles/*/cache2/* "Firefox profile cache"
|
||||||
clean_chrome_old_versions
|
clean_chrome_old_versions
|
||||||
clean_edge_old_versions
|
clean_edge_old_versions
|
||||||
|
clean_edge_updater_old_versions
|
||||||
}
|
}
|
||||||
# Cloud storage caches.
|
# Cloud storage caches.
|
||||||
clean_cloud_storage() {
|
clean_cloud_storage() {
|
||||||
|
|||||||
@@ -103,6 +103,39 @@ is_path_whitelisted() {
|
|||||||
[[ "$1" == *"128.0.0.0"* ]] && return 0
|
[[ "$1" == *"128.0.0.0"* ]] && return 0
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "clean_edge_updater_old_versions keeps latest version" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" DRY_RUN=true bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
|
||||||
|
pgrep() { return 1; }
|
||||||
|
export -f pgrep
|
||||||
|
|
||||||
|
UPDATER_DIR="$HOME/Library/Application Support/Microsoft/EdgeUpdater/apps/msedge-stable"
|
||||||
|
mkdir -p "$UPDATER_DIR"/{117.0.2045.60,118.0.2088.46,119.0.2108.9}
|
||||||
|
|
||||||
|
is_path_whitelisted() { return 1; }
|
||||||
|
get_path_size_kb() { echo "10240"; }
|
||||||
|
bytes_to_human() { echo "10M"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
export -f is_path_whitelisted get_path_size_kb bytes_to_human note_activity
|
||||||
|
|
||||||
|
files_cleaned=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
|
||||||
|
clean_edge_updater_old_versions
|
||||||
|
|
||||||
|
echo "Cleaned: $files_cleaned items"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Edge updater old versions"* ]]
|
||||||
|
[[ "$output" == *"dry"* ]]
|
||||||
|
[[ "$output" == *"Cleaned: 2 items"* ]]
|
||||||
|
}
|
||||||
get_path_size_kb() { echo "10240"; }
|
get_path_size_kb() { echo "10240"; }
|
||||||
bytes_to_human() { echo "10M"; }
|
bytes_to_human() { echo "10M"; }
|
||||||
note_activity() { :; }
|
note_activity() { :; }
|
||||||
|
|||||||
@@ -69,34 +69,6 @@ EOF
|
|||||||
[[ "$output" != *"Maven repository cache"* ]]
|
[[ "$output" != *"Maven repository cache"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo clean respects MO_BREW_TIMEOUT environment variable" {
|
|
||||||
if ! command -v brew > /dev/null 2>&1; then
|
|
||||||
skip "Homebrew not installed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
|
||||||
set -euo pipefail
|
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
|
||||||
source "$PROJECT_ROOT/lib/clean/brew.sh"
|
|
||||||
|
|
||||||
MO_BREW_TIMEOUT=5
|
|
||||||
CALL_LOG="$HOME/timeout.log"
|
|
||||||
|
|
||||||
run_with_timeout() {
|
|
||||||
echo "$1" >> "$CALL_LOG"
|
|
||||||
shift
|
|
||||||
"$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
brew() { return 0; }
|
|
||||||
|
|
||||||
clean_homebrew
|
|
||||||
cat "$CALL_LOG"
|
|
||||||
EOF
|
|
||||||
[ "$status" -eq 0 ]
|
|
||||||
[[ "$output" == *"5"* ]]
|
|
||||||
}
|
|
||||||
|
|
||||||
@test "FINDER_METADATA_SENTINEL in whitelist protects .DS_Store files" {
|
@test "FINDER_METADATA_SENTINEL in whitelist protects .DS_Store files" {
|
||||||
mkdir -p "$HOME/Documents"
|
mkdir -p "$HOME/Documents"
|
||||||
touch "$HOME/Documents/.DS_Store"
|
touch "$HOME/Documents/.DS_Store"
|
||||||
@@ -277,3 +249,92 @@ EOF
|
|||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"Time Machine backup in progress, skipping cleanup"* ]]
|
[[ "$output" == *"Time Machine backup in progress, skipping cleanup"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@test "clean_empty_library_items removes nested empty directories in Application Support" {
|
||||||
|
# Create nested empty directory structure
|
||||||
|
mkdir -p "$HOME/Library/Application Support/UninstalledApp1/SubDir/DeepDir"
|
||||||
|
mkdir -p "$HOME/Library/Application Support/UninstalledApp2/Cache"
|
||||||
|
mkdir -p "$HOME/Library/Application Support/ActiveApp/Data"
|
||||||
|
mkdir -p "$HOME/Library/Caches/EmptyCache/SubCache"
|
||||||
|
|
||||||
|
# Create a file in ActiveApp to make it non-empty
|
||||||
|
touch "$HOME/Library/Application Support/ActiveApp/Data/config.json"
|
||||||
|
|
||||||
|
# Create top-level empty directory in Library
|
||||||
|
mkdir -p "$HOME/Library/EmptyTopLevel"
|
||||||
|
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
|
||||||
|
# Mock dependencies
|
||||||
|
is_path_whitelisted() { return 1; }
|
||||||
|
is_critical_system_component() { return 1; }
|
||||||
|
bytes_to_human() { echo "$1"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
safe_clean() {
|
||||||
|
# Actually remove the directories for testing
|
||||||
|
for path in "$@"; do
|
||||||
|
if [ "$path" != "${@: -1}" ]; then # Skip the description (last arg)
|
||||||
|
rm -rf "$path" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_empty_library_items
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
# Empty nested dirs should be removed
|
||||||
|
[ ! -d "$HOME/Library/Application Support/UninstalledApp1" ]
|
||||||
|
[ ! -d "$HOME/Library/Application Support/UninstalledApp2" ]
|
||||||
|
[ ! -d "$HOME/Library/Caches/EmptyCache" ]
|
||||||
|
[ ! -d "$HOME/Library/EmptyTopLevel" ]
|
||||||
|
|
||||||
|
# Non-empty directory should remain
|
||||||
|
[ -d "$HOME/Library/Application Support/ActiveApp" ]
|
||||||
|
[ -f "$HOME/Library/Application Support/ActiveApp/Data/config.json" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "clean_empty_library_items respects whitelist for empty directories" {
|
||||||
|
mkdir -p "$HOME/Library/Application Support/ProtectedEmptyApp"
|
||||||
|
mkdir -p "$HOME/Library/Application Support/UnprotectedEmptyApp"
|
||||||
|
mkdir -p "$HOME/.config/mole"
|
||||||
|
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/clean/user.sh"
|
||||||
|
|
||||||
|
# Mock dependencies
|
||||||
|
is_critical_system_component() { return 1; }
|
||||||
|
bytes_to_human() { echo "$1"; }
|
||||||
|
note_activity() { :; }
|
||||||
|
|
||||||
|
# Mock whitelist to protect ProtectedEmptyApp
|
||||||
|
is_path_whitelisted() {
|
||||||
|
[[ "$1" == *"ProtectedEmptyApp"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
safe_clean() {
|
||||||
|
# Actually remove the directories for testing
|
||||||
|
for path in "$@"; do
|
||||||
|
if [ "$path" != "${@: -1}" ]; then # Skip the description (last arg)
|
||||||
|
rm -rf "$path" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
clean_empty_library_items
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
# Whitelisted directory should remain even if empty
|
||||||
|
[ -d "$HOME/Library/Application Support/ProtectedEmptyApp" ]
|
||||||
|
|
||||||
|
# Non-whitelisted directory should be removed
|
||||||
|
[ ! -d "$HOME/Library/Application Support/UnprotectedEmptyApp" ]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user