1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-16 02:36:12 +00:00

Migrate recent items and mail downloads cleanup to the user module and add scanning spinners

This commit is contained in:
Tw93
2025-12-27 10:17:39 +08:00
parent 4d00794b04
commit 71d0f2c3cc

View File

@@ -5,7 +5,12 @@ set -euo pipefail
# Clean user essentials (caches, logs, trash) # Clean user essentials (caches, logs, trash)
clean_user_essentials() { clean_user_essentials() {
start_section_spinner "Scanning caches..."
safe_clean ~/Library/Caches/* "User app cache" safe_clean ~/Library/Caches/* "User app cache"
stop_section_spinner
safe_clean ~/Library/Logs/* "User app logs" safe_clean ~/Library/Logs/* "User app logs"
safe_clean ~/.Trash/* "Trash" safe_clean ~/.Trash/* "Trash"
} }
@@ -56,16 +61,14 @@ scan_external_volumes() {
if [[ $volume_count -eq 0 ]]; then if [[ $volume_count -eq 0 ]]; then
# Show info if network volumes were skipped # Show info if network volumes were skipped
if [[ $network_count -gt 0 ]]; then if [[ $network_count -gt 0 ]]; then
echo -e " ${GRAY}${NC} External volumes (${network_count} network volume(s) skipped)" echo -e " ${GRAY}${ICON_LIST}${NC} External volumes (${network_count} network volume(s) skipped)"
note_activity note_activity
fi fi
return 0 return 0
fi fi
# We have local external volumes, now perform full scan # We have local external volumes, now perform full scan
if [[ -t 1 ]]; then start_section_spinner "Scanning $volume_count external volume(s)..."
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning $volume_count external volume(s)..."
fi
for volume in "${candidate_volumes[@]}"; do for volume in "${candidate_volumes[@]}"; do
# Verify volume is actually mounted (reduced timeout from 2s to 1s) # Verify volume is actually mounted (reduced timeout from 2s to 1s)
@@ -85,14 +88,16 @@ scan_external_volumes() {
fi fi
done done
if [[ -t 1 ]]; then stop_inline_spinner; fi stop_section_spinner
} }
# Clean Finder metadata (.DS_Store files) # Clean Finder metadata (.DS_Store files)
clean_finder_metadata() { clean_finder_metadata() {
stop_section_spinner
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
note_activity note_activity
echo -e " ${GRAY}${NC} Finder metadata (protected)" echo -e " ${GREEN}${ICON_EMPTY}${NC} Finder metadata · whitelist protected"
return return
fi fi
@@ -101,6 +106,8 @@ clean_finder_metadata() {
# Clean macOS system caches # Clean macOS system caches
clean_macos_system_caches() { clean_macos_system_caches() {
stop_section_spinner
# Clean saved application states with protection for System Settings # Clean saved application states with protection for System Settings
# Note: safe_clean already calls should_protect_path for each file # Note: safe_clean already calls should_protect_path for each file
safe_clean ~/Library/Saved\ Application\ State/* "Saved application states" safe_clean ~/Library/Saved\ Application\ State/* "Saved application states"
@@ -121,20 +128,100 @@ clean_macos_system_caches() {
safe_clean ~/Library/Caches/CloudKit/* "CloudKit cache" safe_clean ~/Library/Caches/CloudKit/* "CloudKit cache"
# Clean incomplete downloads # Clean incomplete downloads
safe_clean ~/Downloads/*.download "Incomplete downloads (Safari)" safe_clean ~/Downloads/*.download "Safari incomplete downloads"
safe_clean ~/Downloads/*.crdownload "Incomplete downloads (Chrome)" safe_clean ~/Downloads/*.crdownload "Chrome incomplete downloads"
safe_clean ~/Downloads/*.part "Incomplete downloads (partial)" safe_clean ~/Downloads/*.part "Partial incomplete downloads"
# Additional user-level caches # Additional user-level caches
safe_clean ~/Library/Autosave\ Information/* "Autosave information" safe_clean ~/Library/Autosave\ Information/* "Autosave information"
safe_clean ~/Library/IdentityCaches/* "Identity caches" safe_clean ~/Library/IdentityCaches/* "Identity caches"
safe_clean ~/Library/Suggestions/* "Suggestions cache (Siri)" safe_clean ~/Library/Suggestions/* "Siri suggestions cache"
safe_clean ~/Library/Calendars/Calendar\ Cache "Calendar cache" safe_clean ~/Library/Calendars/Calendar\ Cache "Calendar cache"
safe_clean ~/Library/Application\ Support/AddressBook/Sources/*/Photos.cache "Address Book photo cache" safe_clean ~/Library/Application\ Support/AddressBook/Sources/*/Photos.cache "Address Book photo cache"
} }
# Clean recent items lists
clean_recent_items() {
stop_section_spinner
local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist"
# Target only the global recent item lists to avoid touching per-app/System Settings SFL files
local -a recent_lists=(
"$shared_dir/com.apple.LSSharedFileList.RecentApplications.sfl2"
"$shared_dir/com.apple.LSSharedFileList.RecentDocuments.sfl2"
"$shared_dir/com.apple.LSSharedFileList.RecentServers.sfl2"
"$shared_dir/com.apple.LSSharedFileList.RecentHosts.sfl2"
"$shared_dir/com.apple.LSSharedFileList.RecentApplications.sfl"
"$shared_dir/com.apple.LSSharedFileList.RecentDocuments.sfl"
"$shared_dir/com.apple.LSSharedFileList.RecentServers.sfl"
"$shared_dir/com.apple.LSSharedFileList.RecentHosts.sfl"
)
if [[ -d "$shared_dir" ]]; then
for sfl_file in "${recent_lists[@]}"; do
[[ -e "$sfl_file" ]] && safe_clean "$sfl_file" "Recent items list"
done
fi
# Clean recent items preferences
safe_clean ~/Library/Preferences/com.apple.recentitems.plist "Recent items preferences"
}
# Clean old mail downloads
clean_mail_downloads() {
stop_section_spinner
local mail_age_days=${MOLE_MAIL_AGE_DAYS:-30}
if ! [[ "$mail_age_days" =~ ^[0-9]+$ ]]; then
mail_age_days=30
fi
local -a mail_dirs=(
"$HOME/Library/Mail Downloads"
"$HOME/Library/Containers/com.apple.mail/Data/Library/Mail Downloads"
)
local count=0
local cleaned_kb=0
for target_path in "${mail_dirs[@]}"; do
if [[ -d "$target_path" ]]; then
# Check directory size threshold
local dir_size_kb=0
if command -v du > /dev/null 2>&1; then
dir_size_kb=$(du -sk "$target_path" 2> /dev/null | awk '{print $1}' || echo "0")
fi
# Skip if below threshold
if [[ $dir_size_kb -lt ${MOLE_MAIL_DOWNLOADS_MIN_KB:-5120} ]]; then
continue
fi
# Find and remove files older than specified days
while IFS= read -r -d '' file_path; do
if [[ -f "$file_path" ]]; then
local file_size_kb=$(du -sk "$file_path" 2> /dev/null | awk '{print $1}' || echo "0")
if safe_remove "$file_path" true; then
((count++))
((cleaned_kb += file_size_kb))
fi
fi
done < <(command find "$target_path" -type f -mtime +"$mail_age_days" -print0 2> /dev/null || true)
fi
done
if [[ $count -gt 0 ]]; then
local cleaned_mb=$(echo "$cleaned_kb" | awk '{printf "%.1f", $1/1024}')
echo " ${GREEN}${ICON_SUCCESS}${NC} Cleaned $count mail attachments (~${cleaned_mb}MB)"
note_activity
fi
}
# Clean sandboxed app caches # Clean sandboxed app caches
clean_sandboxed_app_caches() { clean_sandboxed_app_caches() {
stop_section_spinner
safe_clean ~/Library/Containers/com.apple.wallpaper.agent/Data/Library/Caches/* "Wallpaper agent cache" safe_clean ~/Library/Containers/com.apple.wallpaper.agent/Data/Library/Caches/* "Wallpaper agent cache"
safe_clean ~/Library/Containers/com.apple.mediaanalysisd/Data/Library/Caches/* "Media analysis cache" safe_clean ~/Library/Containers/com.apple.mediaanalysisd/Data/Library/Caches/* "Media analysis cache"
safe_clean ~/Library/Containers/com.apple.AppStore/Data/Library/Caches/* "App Store cache" safe_clean ~/Library/Containers/com.apple.AppStore/Data/Library/Caches/* "App Store cache"
@@ -144,41 +231,38 @@ clean_sandboxed_app_caches() {
local containers_dir="$HOME/Library/Containers" local containers_dir="$HOME/Library/Containers"
[[ ! -d "$containers_dir" ]] && return 0 [[ ! -d "$containers_dir" ]] && return 0
# Enable nullglob for safe globbing; restore afterwards start_section_spinner "Scanning sandboxed apps..."
local _ng_state
_ng_state=$(shopt -p nullglob || true)
shopt -s nullglob
if [[ -t 1 ]]; then
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning sandboxed apps..."
fi
local total_size=0 local total_size=0
local cleaned_count=0 local cleaned_count=0
local found_any=false local found_any=false
# Enable nullglob for safe globbing; restore afterwards
local _ng_state
_ng_state=$(shopt -p nullglob || true)
shopt -s nullglob
for container_dir in "$containers_dir"/*; do for container_dir in "$containers_dir"/*; do
process_container_cache "$container_dir" process_container_cache "$container_dir"
done done
if [[ -t 1 ]]; then stop_inline_spinner; fi # Restore nullglob to previous state
eval "$_ng_state"
stop_section_spinner
if [[ "$found_any" == "true" ]]; then if [[ "$found_any" == "true" ]]; then
local size_human=$(bytes_to_human "$((total_size * 1024))") local size_human=$(bytes_to_human "$((total_size * 1024))")
if [[ "$DRY_RUN" == "true" ]]; then if [[ "$DRY_RUN" == "true" ]]; then
echo -e " ${YELLOW}${NC} Sandboxed app caches ${YELLOW}($size_human dry)${NC}" echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Sandboxed app caches ${YELLOW}($size_human dry)${NC}"
else else
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Sandboxed app caches ${GREEN}($size_human)${NC}" echo -e " ${GREEN}${ICON_SUCCESS}${NC} Sandboxed app caches ${GREEN}($size_human)${NC}"
fi fi
# Update global counters
((files_cleaned += cleaned_count)) ((files_cleaned += cleaned_count))
((total_size_cleaned += total_size)) ((total_size_cleaned += total_size))
((total_items++)) ((total_items++))
note_activity note_activity
fi fi
# Restore nullglob to previous state
eval "$_ng_state"
} }
# Process a single container cache directory (reduces nesting) # Process a single container cache directory (reduces nesting)
@@ -208,21 +292,25 @@ process_container_cache() {
((cleaned_count++)) ((cleaned_count++))
if [[ "$DRY_RUN" != "true" ]]; then if [[ "$DRY_RUN" != "true" ]]; then
# Clean contents safely (rm -rf is restricted by safe_remove) # Clean contents safely with local nullglob management
local _ng_item_state local _ng_state
_ng_item_state=$(shopt -p nullglob || true) _ng_state=$(shopt -p nullglob || true)
shopt -s nullglob shopt -s nullglob
for item in "$cache_dir"/*; do for item in "$cache_dir"/*; do
[[ -e "$item" ]] || continue [[ -e "$item" ]] || continue
safe_remove "$item" true || true safe_remove "$item" true || true
done done
eval "$_ng_item_state"
eval "$_ng_state"
fi fi
fi fi
} }
# Clean browser caches (Safari, Chrome, Edge, Firefox, etc.) # Clean browser caches (Safari, Chrome, Edge, Firefox, etc.)
clean_browsers() { clean_browsers() {
stop_section_spinner
safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache" safe_clean ~/Library/Caches/com.apple.Safari/* "Safari cache"
# Chrome/Chromium # Chrome/Chromium
@@ -246,6 +334,8 @@ clean_browsers() {
# Clean cloud storage app caches # Clean cloud storage app caches
clean_cloud_storage() { clean_cloud_storage() {
stop_section_spinner
safe_clean ~/Library/Caches/com.dropbox.* "Dropbox cache" safe_clean ~/Library/Caches/com.dropbox.* "Dropbox cache"
safe_clean ~/Library/Caches/com.getdropbox.dropbox "Dropbox cache" safe_clean ~/Library/Caches/com.getdropbox.dropbox "Dropbox cache"
safe_clean ~/Library/Caches/com.google.GoogleDrive "Google Drive cache" safe_clean ~/Library/Caches/com.google.GoogleDrive "Google Drive cache"
@@ -257,6 +347,8 @@ clean_cloud_storage() {
# Clean office application caches # Clean office application caches
clean_office_applications() { clean_office_applications() {
stop_section_spinner
safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache" safe_clean ~/Library/Caches/com.microsoft.Word "Microsoft Word cache"
safe_clean ~/Library/Caches/com.microsoft.Excel "Microsoft Excel cache" safe_clean ~/Library/Caches/com.microsoft.Excel "Microsoft Excel cache"
safe_clean ~/Library/Caches/com.microsoft.Powerpoint "Microsoft PowerPoint cache" safe_clean ~/Library/Caches/com.microsoft.Powerpoint "Microsoft PowerPoint cache"
@@ -269,6 +361,8 @@ clean_office_applications() {
# Clean virtualization tools # Clean virtualization tools
clean_virtualization_tools() { clean_virtualization_tools() {
stop_section_spinner
safe_clean ~/Library/Caches/com.vmware.fusion "VMware Fusion cache" safe_clean ~/Library/Caches/com.vmware.fusion "VMware Fusion cache"
safe_clean ~/Library/Caches/com.parallels.* "Parallels cache" safe_clean ~/Library/Caches/com.parallels.* "Parallels cache"
safe_clean ~/VirtualBox\ VMs/.cache "VirtualBox cache" safe_clean ~/VirtualBox\ VMs/.cache "VirtualBox cache"
@@ -277,24 +371,26 @@ clean_virtualization_tools() {
# Clean Application Support logs and caches # Clean Application Support logs and caches
clean_application_support_logs() { clean_application_support_logs() {
stop_section_spinner
if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then
note_activity note_activity
echo -e " ${YELLOW}${ICON_WARNING}${NC} Skipped: No permission to access Application Support" echo -e " ${YELLOW}${ICON_WARNING}${NC} Skipped: No permission to access Application Support"
return 0 return 0
fi fi
if [[ -t 1 ]]; then start_section_spinner "Scanning Application Support..."
MOLE_SPINNER_PREFIX=" " start_inline_spinner "Scanning Application Support..."
fi
local total_size=0 local total_size=0
local cleaned_count=0 local cleaned_count=0
local found_any=false local found_any=false
# Clean log directories and cache patterns # Enable nullglob for safe globbing
local _ng_app_state local _ng_state
_ng_app_state=$(shopt -p nullglob || true) _ng_state=$(shopt -p nullglob || true)
shopt -s nullglob shopt -s nullglob
# Clean log directories and cache patterns
for app_dir in ~/Library/Application\ Support/*; do for app_dir in ~/Library/Application\ Support/*; do
[[ -d "$app_dir" ]] || continue [[ -d "$app_dir" ]] || continue
@@ -327,20 +423,15 @@ clean_application_support_logs() {
found_any=true found_any=true
if [[ "$DRY_RUN" != "true" ]]; then if [[ "$DRY_RUN" != "true" ]]; then
local _ng_candidate_state
_ng_candidate_state=$(shopt -p nullglob || true)
shopt -s nullglob
for item in "$candidate"/*; do for item in "$candidate"/*; do
[[ -e "$item" ]] || continue [[ -e "$item" ]] || continue
safe_remove "$item" true > /dev/null 2>&1 || true safe_remove "$item" true > /dev/null 2>&1 || true
done done
eval "$_ng_candidate_state"
fi fi
fi fi
fi fi
done done
done done
eval "$_ng_app_state"
# Clean Group Containers logs # Clean Group Containers logs
local known_group_containers=( local known_group_containers=(
@@ -370,16 +461,18 @@ clean_application_support_logs() {
done done
done done
if [[ -t 1 ]]; then stop_inline_spinner; fi # Restore nullglob to previous state
eval "$_ng_state"
stop_section_spinner
if [[ "$found_any" == "true" ]]; then if [[ "$found_any" == "true" ]]; then
local size_human=$(bytes_to_human "$((total_size * 1024))") local size_human=$(bytes_to_human "$((total_size * 1024))")
if [[ "$DRY_RUN" == "true" ]]; then if [[ "$DRY_RUN" == "true" ]]; then
echo -e " ${YELLOW}${NC} Application Support logs/caches ${YELLOW}($size_human dry)${NC}" echo -e " ${YELLOW}${ICON_DRY_RUN}${NC} Application Support logs/caches ${YELLOW}($size_human dry)${NC}"
else else
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Application Support logs/caches ${GREEN}($size_human)${NC}" echo -e " ${GREEN}${ICON_SUCCESS}${NC} Application Support logs/caches ${GREEN}($size_human)${NC}"
fi fi
# Update global counters
((files_cleaned += cleaned_count)) ((files_cleaned += cleaned_count))
((total_size_cleaned += total_size)) ((total_size_cleaned += total_size))
((total_items++)) ((total_items++))