1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 20:54:50 +00:00

Safer cleaning and enhancement capabilities

This commit is contained in:
Tw93
2025-11-29 22:43:57 +09:00
parent 1ded1c9a82
commit 75bd91840f
8 changed files with 242 additions and 100 deletions

View File

@@ -25,11 +25,6 @@ DRY_RUN=false
PROTECT_FINDER_METADATA=false
IS_M_SERIES=$([[ "$(uname -m)" == "arm64" ]] && echo "true" || echo "false")
# Constants
readonly MAX_PARALLEL_JOBS=15 # Maximum parallel background jobs
readonly TEMP_FILE_AGE_DAYS=7 # Age threshold for temp file cleanup
readonly ORPHAN_AGE_DAYS=60 # Age threshold for orphaned data
# Protected Service Worker domains (web-based editing tools)
readonly PROTECTED_SW_DOMAINS=(
"capcut.com"
@@ -64,6 +59,12 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
# Expand tilde to home directory
[[ "$line" == ~* ]] && line="${line/#~/$HOME}"
# Security: reject path traversal attempts
if [[ "$line" =~ \.\. ]]; then
WHITELIST_WARNINGS+=("Path traversal not allowed: $line")
continue
fi
# Path validation with support for spaces and wildcards
# Allow: letters, numbers, /, _, ., -, @, spaces, and * anywhere in path
if [[ ! "$line" =~ ^[a-zA-Z0-9/_.@\ *-]+$ ]]; then
@@ -71,6 +72,12 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
continue
fi
# Require absolute paths (must start with /)
if [[ "$line" != /* ]]; then
WHITELIST_WARNINGS+=("Must be absolute path: $line")
continue
fi
# Reject paths with consecutive slashes (e.g., //)
if [[ "$line" =~ // ]]; then
WHITELIST_WARNINGS+=("Consecutive slashes: $line")
@@ -79,7 +86,7 @@ if [[ -f "$HOME/.config/mole/whitelist" ]]; then
# Prevent critical system directories
case "$line" in
/System/* | /bin/* | /sbin/* | /usr/bin/* | /usr/sbin/* | /etc/* | /var/db/*)
/ | /System | /System/* | /bin | /bin/* | /sbin | /sbin/* | /usr/bin | /usr/bin/* | /usr/sbin | /usr/sbin/* | /etc | /etc/* | /var/db | /var/db/*)
WHITELIST_WARNINGS+=("Protected system path: $line")
continue
;;
@@ -322,7 +329,7 @@ safe_clean() {
pids+=($!)
((idx++))
if ((${#pids[@]} >= MAX_PARALLEL_JOBS)); then
if ((${#pids[@]} >= MOLE_MAX_PARALLEL_JOBS)); then
wait "${pids[0]}" 2> /dev/null || true
pids=("${pids[@]:1}")
((completed++))
@@ -351,7 +358,7 @@ safe_clean() {
if [[ -L "$path" ]]; then
rm "$path" 2> /dev/null || true
else
rm -rf "$path" 2> /dev/null || true
safe_remove "$path" true || true
fi
fi
((total_size_bytes += size))
@@ -380,7 +387,7 @@ safe_clean() {
if [[ -L "$path" ]]; then
rm "$path" 2> /dev/null || true
else
rm -rf "$path" 2> /dev/null || true
safe_remove "$path" true || true
fi
fi
((total_size_bytes += size_bytes))
@@ -606,9 +613,9 @@ perform_cleanup() {
clean_virtualization_tools
end_section
# ===== 11. Application Support logs cleanup =====
start_section "Application Support logs"
# Application Support logs cleanup (delegated to clean_user_data module)
# ===== 11. Application Support logs and caches cleanup =====
start_section "Application Support"
# Clean logs, Service Worker caches, Code Cache, Crashpad, stale updates, Group Containers
clean_application_support_logs
end_section

View File

@@ -190,10 +190,10 @@ cleanup_path() {
fi
local removed=false
if rm -rf "$expanded_path" 2> /dev/null; then
if safe_remove "$expanded_path" true; then
removed=true
elif request_sudo_access "Removing $label requires admin access"; then
if sudo rm -rf "$expanded_path" 2> /dev/null; then
if safe_sudo_remove "$expanded_path"; then
removed=true
fi
fi

View File

@@ -522,7 +522,7 @@ uninstall_applications() {
done
# Remove the application
if rm -rf "$app_path" 2> /dev/null; then
if safe_remove "$app_path" true; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed application"
else
echo -e " ${RED}${ICON_ERROR}${NC} Failed to remove $app_path"
@@ -538,7 +538,7 @@ uninstall_applications() {
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(echo "$file" | sed "s|$HOME|~|" | xargs basename)"
fi
else
if rm -rf "$file" 2> /dev/null; then
if safe_remove "$file" true; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(echo "$file" | sed "s|$HOME|~|" | xargs basename)"
fi
fi
@@ -558,7 +558,7 @@ uninstall_applications() {
echo -e " ${YELLOW}${ICON_ERROR}${NC} Failed to remove: $file"
fi
else
if sudo rm -rf "$file" 2> /dev/null; then
if safe_sudo_remove "$file"; then
echo -e " ${GREEN}${ICON_SUCCESS}${NC} Removed $(basename "$file")"
else
echo -e " ${YELLOW}${ICON_ERROR}${NC} Failed to remove: $file"

View File

@@ -90,7 +90,7 @@ clean_service_worker_cache() {
# Clean if not protected
if [[ "$is_protected" == "false" ]]; then
if [[ "$DRY_RUN" != "true" ]]; then
rm -rf "$cache_dir" 2> /dev/null || true
safe_remove "$cache_dir" true || true
fi
cleaned_size=$((cleaned_size + size))
fi

View File

@@ -1,11 +1,9 @@
#!/bin/bash
# User Data Cleanup Module
# Essential user caches, browsers, cloud storage, office apps
set -euo pipefail
# Clean user essentials (caches, logs, trash, crash reports)
# Env: DRY_RUN
clean_user_essentials() {
safe_clean ~/Library/Caches/* "User app cache"
safe_clean ~/Library/Logs/* "User app logs"
@@ -22,10 +20,13 @@ clean_user_essentials() {
nfs | smbfs | afpfs | cifs | webdav) continue ;;
esac
# Verify volume is mounted
if mount | grep -q "on $volume "; then
# Verify volume is mounted and not a symlink
if mount | grep -q "on $volume " && [[ ! -L "$volume/.Trashes" ]]; then
if [[ "$DRY_RUN" != "true" ]]; then
find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -exec rm -rf {} \; 2> /dev/null || true
# Safely iterate and remove each item
while IFS= read -r -d '' item; do
safe_remove "$item" true || true
done < <(find "$volume/.Trashes" -mindepth 1 -maxdepth 1 -print0 2> /dev/null)
fi
fi
done
@@ -52,7 +53,6 @@ clean_user_essentials() {
}
# Clean Finder metadata (.DS_Store files)
# Env: PROTECT_FINDER_METADATA
clean_finder_metadata() {
if [[ "$PROTECT_FINDER_METADATA" == "true" ]]; then
note_activity
@@ -170,7 +170,7 @@ clean_virtualization_tools() {
safe_clean ~/.vagrant.d/tmp/* "Vagrant temporary files"
}
# Clean Application Support logs
# Clean Application Support logs and caches
clean_application_support_logs() {
# Check permission
if [[ ! -d "$HOME/Library/Application Support" ]] || ! ls "$HOME/Library/Application Support" > /dev/null 2>&1; then
@@ -179,7 +179,7 @@ clean_application_support_logs() {
return 0
fi
# Clean log directories with iteration limit to prevent hanging
# Clean log directories and cache patterns with iteration limit
local iteration_count=0
local max_iterations=200
@@ -201,7 +201,7 @@ clean_application_support_logs() {
;;
esac
# Clean common log directories (only if they exist and are accessible)
# Clean log directories
if [[ -d "$app_dir/log" ]] && ls "$app_dir/log" > /dev/null 2>&1; then
safe_clean "$app_dir/log"/* "App logs: $app_name"
fi
@@ -211,7 +211,44 @@ clean_application_support_logs() {
if [[ -d "$app_dir/activitylog" ]] && ls "$app_dir/activitylog" > /dev/null 2>&1; then
safe_clean "$app_dir/activitylog"/* "Activity logs: $app_name"
fi
# Clean common cache patterns (Service Worker, Code Cache, Crashpad)
if [[ -d "$app_dir/Cache/Cache_Data" ]] && ls "$app_dir/Cache/Cache_Data" > /dev/null 2>&1; then
safe_clean "$app_dir/Cache/Cache_Data" "Cache data: $app_name"
fi
if [[ -d "$app_dir/Code Cache/js" ]] && ls "$app_dir/Code Cache/js" > /dev/null 2>&1; then
safe_clean "$app_dir/Code Cache/js"/* "Code cache: $app_name"
fi
if [[ -d "$app_dir/Crashpad/completed" ]] && ls "$app_dir/Crashpad/completed" > /dev/null 2>&1; then
safe_clean "$app_dir/Crashpad/completed"/* "Crash reports: $app_name"
fi
# Clean Service Worker caches (CacheStorage and ScriptCache)
while IFS= read -r -d '' sw_cache; do
local profile_path=$(dirname "$(dirname "$sw_cache")")
local profile_name=$(basename "$profile_path")
[[ "$profile_name" == "User Data" ]] && profile_name=$(basename "$(dirname "$profile_path")")
clean_service_worker_cache "$app_name ($profile_name)" "$sw_cache"
done < <(find "$app_dir" -maxdepth 4 -type d \( -name "CacheStorage" -o -name "ScriptCache" \) -path "*/Service Worker/*" 2> /dev/null || true)
# Clean stale update downloads (older than 7 days)
if [[ -d "$app_dir/update" ]] && ls "$app_dir/update" > /dev/null 2>&1; then
while IFS= read -r update_dir; do
local dir_age_days=$(( ($(date +%s) - $(get_file_mtime "$update_dir")) / 86400 ))
if [[ $dir_age_days -ge $MOLE_TEMP_FILE_AGE_DAYS ]]; then
safe_clean "$update_dir" "Stale update: $app_name"
fi
done < <(find "$app_dir/update" -mindepth 1 -maxdepth 1 -type d 2> /dev/null || true)
fi
done
# Clean Group Containers logs
if [[ -d "$HOME/Library/Group Containers" ]]; then
while IFS= read -r logs_dir; do
local container_name=$(basename "$(dirname "$logs_dir")")
safe_clean "$logs_dir"/* "Group container logs: $container_name"
done < <(find "$HOME/Library/Group Containers" -maxdepth 2 -type d -name "Logs" 2> /dev/null || true)
fi
}
# Check and show iOS device backup info

View File

@@ -20,30 +20,39 @@ readonly RED="${ESC}[0;31m"
readonly GRAY="${ESC}[0;90m"
readonly NC="${ESC}[0m"
# Icon definitions (shared across modules)
readonly ICON_CONFIRM="◎" # Confirm operation / spinner text
readonly ICON_ADMIN="⚙" # Gear indicator for admin/settings/system info
readonly ICON_SUCCESS="✓" # Success mark
readonly ICON_ERROR="☻" # Error / warning mark
readonly ICON_EMPTY="○" # Hollow circle (empty state / unchecked)
readonly ICON_SOLID="●" # Solid circle (selected / system marker)
readonly ICON_LIST="•" # Basic list bullet
readonly ICON_ARROW="➤" # Pointer / prompt indicator
readonly ICON_WARNING="☻" # Warning marker (shares glyph with error)
readonly ICON_NAV_UP="↑" # Navigation up
readonly ICON_NAV_DOWN="↓" # Navigation down
readonly ICON_NAV_LEFT="←" # Navigation left
readonly ICON_NAV_RIGHT="→" # Navigation right
# Icon definitions
readonly ICON_CONFIRM="◎"
readonly ICON_ADMIN="⚙"
readonly ICON_SUCCESS="✓"
readonly ICON_ERROR="☻"
readonly ICON_EMPTY="○"
readonly ICON_SOLID="●"
readonly ICON_LIST="•"
readonly ICON_ARROW="➤"
readonly ICON_WARNING="☻"
readonly ICON_NAV_UP="↑"
readonly ICON_NAV_DOWN="↓"
readonly ICON_NAV_LEFT="←"
readonly ICON_NAV_RIGHT="→"
# Get spinner characters (ASCII by default, overridable via MO_SPINNER_CHARS env)
# Global configuration constants
readonly MOLE_TEMP_FILE_AGE_DAYS=7 # Temp file cleanup threshold
readonly MOLE_ORPHAN_AGE_DAYS=60 # Orphaned data threshold
readonly MOLE_MAX_PARALLEL_JOBS=15 # Parallel job limit
readonly MOLE_MAIL_DOWNLOADS_MIN_KB=5120 # Mail attachments size threshold (~5MB)
readonly MOLE_LOG_AGE_DAYS=30 # System log retention
readonly MOLE_CRASH_REPORT_AGE_DAYS=30 # Crash report retention
readonly MOLE_SAVED_STATE_AGE_DAYS=7 # App saved state retention
readonly MOLE_TM_BACKUP_SAFE_HOURS=48 # Time Machine failed backup safety window
# Get spinner characters (overridable via MO_SPINNER_CHARS)
mo_spinner_chars() {
local chars="${MO_SPINNER_CHARS:-|/-\\}"
[[ -z "$chars" ]] && chars='|/-\\'
printf "%s" "$chars"
}
# BSD stat compatibility (for users with GNU CoreUtils installed)
# Always use system BSD stat instead of potentially overridden GNU version
# BSD stat compatibility
readonly STAT_BSD="/usr/bin/stat"
# Get file size in bytes using BSD stat
@@ -66,20 +75,7 @@ get_file_owner() {
# Security and Path Validation Functions
# Validates a path for safe deletion
#
# Security checks:
# - Rejects empty paths
# - Requires absolute paths (must start with /)
# - Blocks control characters and newlines
# - Protects critical system directories
#
# Args:
# $1 - Path to validate
#
# Returns:
# 0 if path is safe to delete
# 1 if path fails any validation check
# Validates path for deletion (absolute, no control chars, not system dir)
validate_path_for_deletion() {
local path="$1"
@@ -113,21 +109,8 @@ validate_path_for_deletion() {
return 0
}
# Safe wrapper around rm -rf with validation and logging
#
# Provides a secure alternative to direct rm -rf calls with:
# - Path validation (absolute paths, no control characters)
# - System directory protection
# - Logging of all operations
# - Silent mode for non-critical failures
#
# Usage:
# safe_remove "/path/to/file" # Normal mode with logging
# safe_remove "/path/to/file" true # Silent mode
#
# Returns:
# 0 on success or if path doesn't exist
# 1 on validation failure or deletion error
# Safe wrapper around rm -rf with path validation and logging
# Usage: safe_remove "/path" [silent]
safe_remove() {
local path="$1"
local silent="${2:-false}"
@@ -139,26 +122,121 @@ safe_remove() {
# Check if path exists
if [[ ! -e "$path" ]]; then
[[ "$silent" != "true" ]] && log_warning "Path does not exist, skipping: $path"
return 0
fi
# Log what we're about to delete
if [[ -d "$path" ]]; then
log_info "Removing directory: $path"
else
log_info "Removing file: $path"
fi
# Perform the deletion
# Perform the deletion (log only on error)
if rm -rf "$path" 2> /dev/null; then
return 0
else
log_error "Failed to remove: $path"
[[ "$silent" != "true" ]] && log_error "Failed to remove: $path"
return 1
fi
}
# Safe sudo remove with validation (rejects symlinks)
# Usage: safe_sudo_remove "/path"
safe_sudo_remove() {
local path="$1"
# Validate path
if ! validate_path_for_deletion "$path"; then
log_error "Path validation failed for sudo remove: $path"
return 1
fi
# Check if path exists
if [[ ! -e "$path" ]]; then
return 0
fi
# Additional check: reject symlinks for sudo operations
if [[ -L "$path" ]]; then
log_error "Refusing to sudo remove symlink: $path"
return 1
fi
# Perform the deletion (log only on error)
if sudo rm -rf "$path" 2> /dev/null; then
return 0
else
log_error "Failed to remove (sudo): $path"
return 1
fi
}
# Safe find delete with depth limit and validation
# Usage: safe_find_delete "/dir" "pattern" age_days "f|d"
safe_find_delete() {
local base_dir="$1"
local pattern="$2"
local age_days="${3:-7}"
local type_filter="${4:-f}"
# Validate base directory exists and is not a symlink
if [[ ! -d "$base_dir" ]]; then
log_warning "Base directory does not exist: $base_dir"
return 1
fi
if [[ -L "$base_dir" ]]; then
log_error "Refusing to search symlinked directory: $base_dir"
return 1
fi
# Validate type filter
if [[ "$type_filter" != "f" && "$type_filter" != "d" ]]; then
log_error "Invalid type filter: $type_filter (must be 'f' or 'd')"
return 1
fi
# Execute find with safety limits
find "$base_dir" \
-maxdepth 3 \
-name "$pattern" \
-type "$type_filter" \
-mtime "+$age_days" \
-delete 2> /dev/null || true
return 0
}
# Safe sudo find delete (same as safe_find_delete with sudo)
# Usage: safe_sudo_find_delete "/dir" "pattern" age_days "f|d"
safe_sudo_find_delete() {
local base_dir="$1"
local pattern="$2"
local age_days="${3:-7}"
local type_filter="${4:-f}"
# Validate base directory exists and is not a symlink
if [[ ! -d "$base_dir" ]]; then
log_warning "Base directory does not exist: $base_dir"
return 1
fi
if [[ -L "$base_dir" ]]; then
log_error "Refusing to search symlinked directory: $base_dir"
return 1
fi
# Validate type filter
if [[ "$type_filter" != "f" && "$type_filter" != "d" ]]; then
log_error "Invalid type filter: $type_filter (must be 'f' or 'd')"
return 1
fi
# Execute find with safety limits
sudo find "$base_dir" \
-maxdepth 3 \
-name "$pattern" \
-type "$type_filter" \
-mtime "+$age_days" \
-delete 2> /dev/null || true
return 0
}
# Logging configuration
readonly LOG_FILE="${HOME}/.config/mole/mole.log"
readonly LOG_MAX_SIZE_DEFAULT=1048576 # 1MB
@@ -200,6 +278,22 @@ log_error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $1" >> "$LOG_FILE" 2> /dev/null || true
}
# Run command with optional error handling
# Usage: run_silent command args... # Ignore errors
# run_logged command args... # Log errors but continue
run_silent() {
"$@" > /dev/null 2>&1 || true
}
run_logged() {
local cmd="$1"
if ! "$@" 2>&1 | tee -a "$LOG_FILE" > /dev/null; then
log_warning "Command failed: $cmd"
return 1
fi
return 0
}
# Call rotation check once when common.sh is sourced
rotate_log_once
@@ -261,8 +355,7 @@ show_cursor() {
}
# Read single keypress and return normalized key name
# Returns: ENTER, SPACE, UP, DOWN, LEFT, RIGHT, QUIT, DELETE, CHAR:<c>, etc.
# Env: MOLE_READ_KEY_FORCE_CHAR=1 for filter mode
# Returns: ENTER, SPACE, UP, DOWN, LEFT, RIGHT, QUIT, DELETE, CHAR:<c>
read_key() {
local key rest read_status

View File

@@ -1,11 +1,8 @@
#!/bin/bash
# Optimization Tasks
# Individual optimization operations extracted from execute_optimization
set -euo pipefail
readonly MAIL_DOWNLOADS_MIN_KB=5120 # ~5MB threshold
_opt_get_dir_size_kb() {
local path="$1"
[[ -e "$path" ]] || {
@@ -145,8 +142,8 @@ opt_log_cleanup() {
done
if [[ -d "/Library/Logs/DiagnosticReports" ]]; then
sudo find /Library/Logs/DiagnosticReports -type f -name "*.crash" -delete 2> /dev/null || true
sudo find /Library/Logs/DiagnosticReports -type f -name "*.panic" -delete 2> /dev/null || true
safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*.crash" 0 "f"
safe_sudo_find_delete "/Library/Logs/DiagnosticReports" "*.panic" 0 "f"
echo -e "${GREEN}${ICON_SUCCESS}${NC} System diagnostic logs cleared"
else
echo -e "${GRAY}-${NC} No system diagnostic logs found"
@@ -158,7 +155,7 @@ opt_recent_items() {
echo -e "${BLUE}${ICON_ARROW}${NC} Clearing recent items lists..."
local shared_dir="$HOME/Library/Application Support/com.apple.sharedfilelist"
if [[ -d "$shared_dir" ]]; then
find "$shared_dir" -name "*.sfl2" -type f -delete 2> /dev/null || true
safe_find_delete "$shared_dir" "*.sfl2" 0 "f"
echo -e "${GREEN}${ICON_SUCCESS}${NC} Shared file lists cleared"
fi
@@ -211,16 +208,20 @@ opt_mail_downloads() {
total_kb=$((total_kb + $(_opt_get_dir_size_kb "$target_path")))
done
if [[ $total_kb -lt $MAIL_DOWNLOADS_MIN_KB ]]; then
if [[ $total_kb -lt $MOLE_MAIL_DOWNLOADS_MIN_KB ]]; then
echo -e "${GRAY}-${NC} Only $(bytes_to_human $((total_kb * 1024))) detected, skipping cleanup"
return
fi
# Only delete files older than 30 days (safer)
# Only delete old attachments (safety window)
local deleted=0
for target_path in "${mail_dirs[@]}"; do
if [[ -d "$target_path" ]]; then
deleted=$((deleted + $(find "$target_path" -type f -mtime +30 -delete -print 2> /dev/null | wc -l | tr -d ' ')))
local file_count=$(find "$target_path" -type f -mtime "+$MOLE_LOG_AGE_DAYS" 2> /dev/null | wc -l | tr -d ' ')
if [[ "$file_count" -gt 0 ]]; then
safe_find_delete "$target_path" "*" "$MOLE_LOG_AGE_DAYS" "f"
deleted=$((deleted + file_count))
fi
fi
done
@@ -233,7 +234,7 @@ opt_mail_downloads() {
# Saved state: remove OLD app saved states (7+ days)
opt_saved_state_cleanup() {
echo -e "${BLUE}${ICON_ARROW}${NC} Removing old saved application states (7+ days)..."
echo -e "${BLUE}${ICON_ARROW}${NC} Removing old saved application states (${MOLE_SAVED_STATE_AGE_DAYS}+ days)..."
local state_dir="$HOME/Library/Saved Application State"
if [[ ! -d "$state_dir" ]]; then
@@ -241,9 +242,13 @@ opt_saved_state_cleanup() {
return
fi
# Only delete states older than 7 days (safer - won't lose recent work)
# Only delete old saved states (safety window)
local deleted=0
deleted=$(find "$state_dir" -type d -name "*.savedState" -mtime +7 -exec rm -rf {} \; -print 2> /dev/null | wc -l | tr -d ' ')
while IFS= read -r -d '' state_path; do
if safe_remove "$state_path" true; then
((deleted++))
fi
done < <(find "$state_dir" -type d -name "*.savedState" -mtime "+$MOLE_SAVED_STATE_AGE_DAYS" -print0 2> /dev/null)
if [[ $deleted -gt 0 ]]; then
echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed $deleted old saved state(s)"

View File

@@ -199,16 +199,16 @@ batch_uninstall_applications() {
fi
if [[ -z "$reason" ]]; then
if [[ "$needs_sudo" == true ]]; then
sudo rm -rf "$app_path" 2> /dev/null || reason="remove failed"
safe_sudo_remove "$app_path" || reason="remove failed"
else
rm -rf "$app_path" 2> /dev/null || reason="remove failed"
safe_remove "$app_path" true || reason="remove failed"
fi
fi
if [[ -z "$reason" ]]; then
local files_removed=0
while IFS= read -r file; do
[[ -n "$file" && -e "$file" ]] || continue
rm -rf "$file" 2> /dev/null && ((files_removed++)) || true
safe_remove "$file" true && ((files_removed++)) || true
done <<< "$related_files"
((total_size_freed += total_kb))
((success_count++))