2025-11-28 22:39:11 +09:00
#!/bin/bash
# Application Data Cleanup Module
set -euo pipefail
# Clean .DS_Store (Finder metadata), home uses maxdepth 5, excludes slow paths, max 500 files
# Args: $1=target_dir, $2=label
clean_ds_store_tree( ) {
local target = " $1 "
local label = " $2 "
[ [ -d " $target " ] ] || return 0
local file_count = 0
local total_bytes = 0
local spinner_active = "false"
if [ [ -t 1 ] ] ; then
MOLE_SPINNER_PREFIX = " "
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
-path "*/.Trash" -prune -o
-path "*/node_modules" -prune -o
-path "*/.git" -prune -o
-path "*/Library/Caches" -prune -o
)
# Build find command to avoid unbound array expansion with set -u
2025-12-04 15:06:45 +08:00
local -a find_cmd = ( "command" "find" " $target " )
2025-11-28 22:39:11 +09:00
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 " )
total_bytes = $(( total_bytes + size))
( ( file_count++) )
if [ [ " $DRY_RUN " != "true" ] ] ; then
rm -f " $ds_file " 2> /dev/null || true
fi
# Stop after 500 files to avoid hanging
if [ [ $file_count -ge 500 ] ] ; then
break
fi
2025-12-05 20:09:15 +08:00
done < <( " ${ find_cmd [@] } " 2> /dev/null || true )
2025-11-28 22:39:11 +09:00
if [ [ " $spinner_active " = = "true" ] ] ; then
stop_inline_spinner
echo -ne "\r\033[K"
fi
if [ [ $file_count -gt 0 ] ] ; then
local size_human
size_human = $( bytes_to_human " $total_bytes " )
if [ [ " $DRY_RUN " = = "true" ] ] ; then
echo -e " ${ YELLOW } → ${ NC } $label ${ YELLOW } ( $file_count files, $size_human dry) ${ NC } "
else
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } $label ${ GREEN } ( $file_count files, $size_human ) ${ NC } "
fi
local size_kb = $(( ( total_bytes + 1023 ) / 1024 ))
( ( files_cleaned += file_count) )
( ( total_size_cleaned += size_kb) )
( ( total_items++) )
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
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
echo -e " ${ YELLOW } ${ ICON_WARNING } ${ NC } Skipped: No permission to access Library folders "
return 0
fi
# Build list of installed/active apps
local installed_bundles = $( create_temp_file)
MOLE_SPINNER_PREFIX = " " start_inline_spinner "Scanning installed apps..."
# Scan all Applications directories
local -a app_dirs = (
"/Applications"
"/System/Applications"
" $HOME /Applications "
)
for app_dir in " ${ app_dirs [@] } " ; do
[ [ -d " $app_dir " ] ] || continue
2025-12-06 09:17:58 +08:00
run_with_timeout 10 sh -c " find ' $app_dir ' -name '*.app' -maxdepth 3 -type d 2> /dev/null " | while IFS = read -r app_path; do
2025-11-28 22:39:11 +09:00
local bundle_id
bundle_id = $( defaults read " $app_path /Contents/Info.plist " CFBundleIdentifier 2> /dev/null || echo "" )
[ [ -n " $bundle_id " ] ] && echo " $bundle_id "
done >> " $installed_bundles "
done
# Get running applications and LaunchAgents with timeout protection
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' >> " $installed_bundles "
run_with_timeout 5 find ~/Library/LaunchAgents /Library/LaunchAgents \
2025-12-05 14:21:18 +08:00
-name "*.plist" -type f 2> /dev/null |
xargs -I { } basename { } .plist >> " $installed_bundles " 2> /dev/null || true
2025-11-28 22:39:11 +09:00
# Deduplicate
sort -u " $installed_bundles " -o " $installed_bundles "
local app_count = $( wc -l < " $installed_bundles " 2> /dev/null | tr -d ' ' )
stop_inline_spinner
echo -e " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Found $app_count active/installed apps "
# Track statistics
local orphaned_count = 0
local total_orphaned_kb = 0
# Check if bundle is orphaned - conservative approach
is_orphaned( ) {
local bundle_id = " $1 "
local directory_path = " $2 "
# Skip system-critical and protected apps
if should_protect_data " $bundle_id " ; then
return 1
fi
# Check if app exists in our scan
2025-12-08 15:34:51 +08:00
if grep -Fxq " $bundle_id " " $installed_bundles " 2> /dev/null; then
2025-11-28 22:39:11 +09:00
return 1
fi
# Extra check for system bundles
case " $bundle_id " in
com.apple.* | loginwindow | dock | systempreferences | finder | safari)
return 1
; ;
esac
# Skip major vendors
case " $bundle_id " in
com.adobe.* | com.microsoft.* | com.google.* | org.mozilla.* | com.jetbrains.* | com.docker.*)
return 1
; ;
esac
# Check file age - only clean if 60+ days inactive
# Use modification time (mtime) instead of access time (atime)
# because macOS disables atime updates by default for performance
if [ [ -e " $directory_path " ] ] ; then
local last_modified_epoch = $( get_file_mtime " $directory_path " )
local current_epoch = $( date +%s)
local days_since_modified = $(( ( current_epoch - last_modified_epoch) / 86400 ))
if [ [ $days_since_modified -lt ${ ORPHAN_AGE_THRESHOLD :- 60 } ] ] ; then
return 1
fi
fi
return 0
}
# Unified orphaned resource scanner (caches, logs, states, webkit, HTTP, cookies)
MOLE_SPINNER_PREFIX = " " start_inline_spinner "Scanning orphaned app resources..."
# Define resource types to scan
# 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.* "
" $HOME /Library/Saved Application State|States|*.savedState "
" $HOME /Library/WebKit|WebKit|com.*:org.*:net.*:io.* "
" $HOME /Library/HTTPStorages|HTTP|com.*:org.*:net.*:io.* "
" $HOME /Library/Cookies|Cookies|*.binarycookies "
)
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
local max_iterations = 100
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 $max_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_orphaned " $bundle_id " " $match " ; then
2025-12-05 14:21:18 +08:00
# Use timeout to prevent du from hanging on network mounts or problematic paths
2025-11-28 22:39:11 +09:00
local size_kb
2025-12-08 15:34:51 +08:00
size_kb = $( get_path_size_kb " $match " )
2025-11-28 22:39:11 +09:00
if [ [ -z " $size_kb " || " $size_kb " = = "0" ] ] ; then
continue
fi
safe_clean " $match " " Orphaned $label : $bundle_id "
( ( orphaned_count++) )
( ( total_orphaned_kb += size_kb) )
fi
done
done
done
stop_inline_spinner
if [ [ $orphaned_count -gt 0 ] ] ; then
local orphaned_mb = $( echo " $total_orphaned_kb " | awk '{printf "%.1f", $1/1024}' )
echo " ${ GREEN } ${ ICON_SUCCESS } ${ NC } Cleaned $orphaned_count items (~ ${ orphaned_mb } MB) "
note_activity
fi
rm -f " $installed_bundles "
}