-
-
Save Limbicnation/6763b69ab6a406790f3b7d4b56a2f6e8 to your computer and use it in GitHub Desktop.
| #!/usr/bin/env bash | |
| # Security-Hardened Ubuntu Cleanup Script | |
| # This script performs comprehensive system cleanup with enterprise-grade security | |
| # EXCLUDES: hy3dgen folder from any deletion operations | |
| # | |
| # Security improvements: | |
| # - Comprehensive error handling with trap handlers | |
| # - Safe configuration loading without arbitrary code execution | |
| # - APT and script-level locking mechanisms | |
| # - Kernel retention validation (N-1 policy) | |
| # - System snapshot before destructive operations | |
| # - Syslog integration for audit trail | |
| # - Resource limit enforcement | |
| # - Proper exit codes for monitoring | |
| # Exit codes | |
| readonly EXIT_SUCCESS=0 | |
| readonly EXIT_LOCK_FAILED=1 | |
| readonly EXIT_APT_LOCKED=2 | |
| readonly EXIT_NO_PRIVILEGES=3 | |
| readonly EXIT_DEPENDENCY_MISSING=4 | |
| readonly EXIT_USER_ABORT=5 | |
| readonly EXIT_OPERATION_FAILED=10 | |
| readonly EXIT_CONFIG_INVALID=11 | |
| # Strict error handling | |
| set -euo pipefail | |
| IFS=$'\n\t' | |
| # Configuration variables | |
| CONFIG_FILE="${HOME}/.config/ubuntu_cleanup.conf" | |
| LOG_DIR="/var/log/system_cleanup" | |
| LOG_FILE="${LOG_DIR}/cleanup_$(date +%Y%m%d_%H%M%S).log" | |
| LOCK_FILE="/var/lock/ubuntu_cleanup.lock" | |
| LOCK_FD=200 | |
| DRY_RUN=0 | |
| VERBOSE=0 | |
| TIMEOUT_DURATION=60 | |
| PARALLEL_JOBS=2 | |
| MAX_RESOURCE_USAGE=50 | |
| DEFAULT_RETENTION_DAYS=10 | |
| HAS_ROOT=0 | |
| # EXCLUSION PATTERNS - Add folders/files to protect here | |
| EXCLUDED_PATTERNS=( | |
| "hy3dgen" | |
| "Hunyuan3D" | |
| "huggingface" | |
| ".git" | |
| ".venv" | |
| "node_modules" | |
| "venv" | |
| "env" | |
| ".env" | |
| ) | |
| # Logging function with syslog integration | |
| log_operation() { | |
| local severity=$1 | |
| local message=$2 | |
| echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$severity] $message" | |
| # Send to syslog if logger is available | |
| if command -v logger &>/dev/null; then | |
| logger -t ubuntu_cleanup -p "user.$severity" "$message" | |
| fi | |
| } | |
| # Cleanup handler for trap | |
| cleanup_on_error() { | |
| local exit_code=$? | |
| if [ $exit_code -ne 0 ]; then | |
| log_operation "err" "Script failed with exit code $exit_code at line ${BASH_LINENO[0]}" | |
| log_operation "err" "Failed command: ${BASH_COMMAND}" | |
| fi | |
| # Release lock if held | |
| release_lock | |
| exit $exit_code | |
| } | |
| # Set trap handlers | |
| trap cleanup_on_error ERR EXIT | |
| trap 'log_operation "warning" "Script interrupted by user"; exit $EXIT_USER_ABORT' SIGTERM SIGINT | |
| # Create log directory if it doesn't exist with proper permissions | |
| mkdir -p "$LOG_DIR" 2>/dev/null || true | |
| chmod 750 "$LOG_DIR" 2>/dev/null || true | |
| # Setup logging with rotation | |
| exec > >(tee -a "$LOG_FILE") 2>&1 | |
| # Safe configuration loader - no arbitrary code execution | |
| load_safe_config() { | |
| if [ ! -f "$CONFIG_FILE" ]; then | |
| return 0 | |
| fi | |
| log_operation "info" "Loading configuration from $CONFIG_FILE" | |
| # Validate config file syntax | |
| if ! grep -qE '^\s*(#|[A-Z_]+=|$)' "$CONFIG_FILE"; then | |
| log_operation "err" "Config file contains invalid syntax" | |
| return $EXIT_CONFIG_INVALID | |
| fi | |
| # Load only whitelisted variables with validation | |
| while IFS='=' read -r key value; do | |
| # Skip comments and empty lines | |
| [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue | |
| # Remove leading/trailing whitespace | |
| key=$(echo "$key" | xargs) | |
| value=$(echo "$value" | xargs) | |
| case "$key" in | |
| TIMEOUT_DURATION|PARALLEL_JOBS|MAX_RESOURCE_USAGE|DEFAULT_RETENTION_DAYS) | |
| # Validate value is numeric and within reasonable range | |
| if [[ "$value" =~ ^[0-9]+$ ]] && [ "$value" -ge 1 ] && [ "$value" -le 9999 ]; then | |
| declare -g "$key=$value" | |
| log_operation "info" "Loaded config: $key=$value" | |
| else | |
| log_operation "warning" "Invalid value for $key: $value (skipped)" | |
| fi | |
| ;; | |
| *) | |
| log_operation "warning" "Unknown config variable: $key (skipped)" | |
| ;; | |
| esac | |
| done < <(grep -v '^#' "$CONFIG_FILE" | grep -v '^$') | |
| } | |
| # Script-level locking mechanism | |
| acquire_lock() { | |
| exec 200>"$LOCK_FILE" | |
| if ! flock -n 200; then | |
| log_operation "err" "Another instance is already running" | |
| exit $EXIT_LOCK_FAILED | |
| fi | |
| echo $$ >&200 | |
| log_operation "info" "Lock acquired (PID: $$)" | |
| } | |
| release_lock() { | |
| if [ -n "${LOCK_FD:-}" ]; then | |
| flock -u $LOCK_FD 2>/dev/null || true | |
| rm -f "$LOCK_FILE" 2>/dev/null || true | |
| fi | |
| } | |
| # APT lock checking with timeout | |
| check_apt_lock() { | |
| local max_wait=300 # 5 minutes | |
| local waited=0 | |
| local lock_files=( | |
| "/var/lib/dpkg/lock-frontend" | |
| "/var/lib/apt/lists/lock" | |
| "/var/cache/apt/archives/lock" | |
| ) | |
| while true; do | |
| local locked=0 | |
| for lock_file in "${lock_files[@]}"; do | |
| if fuser "$lock_file" >/dev/null 2>&1; then | |
| locked=1 | |
| break | |
| fi | |
| done | |
| if [ $locked -eq 0 ]; then | |
| return 0 | |
| fi | |
| if [ $waited -ge $max_wait ]; then | |
| log_operation "err" "APT lock held for $max_wait seconds, aborting" | |
| exit $EXIT_APT_LOCKED | |
| fi | |
| log_operation "warning" "Waiting for APT lock to be released... ($waited/$max_wait seconds)" | |
| sleep 5 | |
| waited=$((waited + 5)) | |
| done | |
| } | |
| # Check for root privileges | |
| check_root() { | |
| if [[ $EUID -eq 0 ]]; then | |
| HAS_ROOT=1 | |
| log_operation "info" "Running with root privileges" | |
| else | |
| HAS_ROOT=0 | |
| log_operation "info" "Running without root, will use sudo for privileged operations" | |
| # Test sudo access | |
| if ! sudo -n true 2>/dev/null; then | |
| log_operation "warning" "Sudo password may be required" | |
| fi | |
| fi | |
| } | |
| # Function to handle privileged commands (no eval!) | |
| run_with_privileges() { | |
| if [[ $HAS_ROOT -eq 1 ]]; then | |
| "$@" | |
| else | |
| sudo "$@" | |
| fi | |
| } | |
| # Resource limit enforcement | |
| enforce_resource_limits() { | |
| # Set nice priority for CPU | |
| renice -n 10 -p $$ >/dev/null 2>&1 || true | |
| # Set ionice for disk I/O (idle class) | |
| if command -v ionice &>/dev/null; then | |
| ionice -c 3 -p $$ >/dev/null 2>&1 || true | |
| fi | |
| log_operation "info" "Resource limits enforced: nice=10, ionice=idle" | |
| } | |
| # Enhanced path validation with parent directory checking | |
| is_safe_path() { | |
| local path=$1 | |
| # Resolve to absolute path | |
| local abs_path | |
| abs_path=$(readlink -f "$path" 2>/dev/null) || { | |
| log_operation "warning" "Cannot resolve path: $path" | |
| return 1 | |
| } | |
| # Critical system paths and their subdirectories | |
| local dangerous_paths=( | |
| "/" | |
| "/bin" | |
| "/boot" | |
| "/dev" | |
| "/etc" | |
| "/lib" | |
| "/lib64" | |
| "/proc" | |
| "/root" | |
| "/sbin" | |
| "/sys" | |
| "/usr" | |
| ) | |
| for dangerous_path in "${dangerous_paths[@]}"; do | |
| # Check if path is exactly the dangerous path or under it | |
| if [[ "$abs_path" == "$dangerous_path" ]] || [[ "$abs_path" == "$dangerous_path"/* ]]; then | |
| log_operation "err" "Refusing to remove path under critical system directory: $abs_path" | |
| return 1 | |
| fi | |
| done | |
| return 0 | |
| } | |
| # Function to check if path should be excluded | |
| is_excluded() { | |
| local path=$1 | |
| for pattern in "${EXCLUDED_PATTERNS[@]}"; do | |
| if [[ "$path" == *"$pattern"* ]]; then | |
| log_operation "info" "Excluding protected path: $path (matches pattern: $pattern)" | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| # Function for timeout handling with user prompts | |
| prompt_with_timeout() { | |
| local prompt=$1 | |
| local timeout=$TIMEOUT_DURATION | |
| local response | |
| read -t "$timeout" -p "$prompt" response || true | |
| if [ -z "$response" ]; then | |
| echo "Timeout reached, assuming default answer (n)" | |
| return 1 | |
| fi | |
| if [[ "$response" =~ ^[Yy]$ ]]; then | |
| return 0 | |
| else | |
| return 1 | |
| fi | |
| } | |
| # Function to safely remove files with exclusion check | |
| safe_remove() { | |
| local target=$1 | |
| local force=${2:-0} | |
| # Check if target should be excluded | |
| if is_excluded "$target"; then | |
| return 0 | |
| fi | |
| # Check if path is safe | |
| if ! is_safe_path "$target"; then | |
| return 1 | |
| fi | |
| # Check if target exists | |
| if [ ! -e "$target" ]; then | |
| [ $VERBOSE -eq 1 ] && log_operation "info" "Target does not exist, skipping: $target" | |
| return 0 | |
| fi | |
| # Safe removal | |
| if [ -d "$target" ] && [ ! -L "$target" ]; then | |
| log_operation "info" "Removing contents of directory $target" | |
| if [ $force -eq 1 ]; then | |
| rm -rf "${target:?}"/* 2>/dev/null || true | |
| else | |
| rm -r "${target:?}"/* 2>/dev/null || true | |
| fi | |
| else | |
| log_operation "info" "Removing $target" | |
| if [ $force -eq 1 ]; then | |
| rm -f "$target" 2>/dev/null || true | |
| else | |
| rm "$target" 2>/dev/null || true | |
| fi | |
| fi | |
| return 0 | |
| } | |
| # Enhanced find command that excludes protected patterns | |
| safe_find() { | |
| local base_path=$1 | |
| shift | |
| local find_args=("$@") | |
| # Build exclusion arguments for find | |
| local exclude_args=() | |
| for pattern in "${EXCLUDED_PATTERNS[@]}"; do | |
| exclude_args+=(-not -path "*${pattern}*") | |
| done | |
| find "$base_path" "${exclude_args[@]}" "${find_args[@]}" 2>/dev/null || true | |
| } | |
| # System snapshot before destructive operations | |
| create_system_snapshot() { | |
| local snapshot_dir="/var/backups/ubuntu_cleanup_$(date +%Y%m%d_%H%M%S)" | |
| log_operation "info" "Creating system snapshot: $snapshot_dir" | |
| run_with_privileges mkdir -p "$snapshot_dir" | |
| # Backup package state | |
| dpkg --get-selections > "$snapshot_dir/package_selections.txt" 2>/dev/null || true | |
| apt-mark showmanual > "$snapshot_dir/manual_packages.txt" 2>/dev/null || true | |
| # Backup kernel list | |
| dpkg -l | grep -E 'linux-image|linux-headers' > "$snapshot_dir/kernel_list.txt" 2>/dev/null || true | |
| # Backup sources list | |
| run_with_privileges cp -r /etc/apt/sources.list* "$snapshot_dir/" 2>/dev/null || true | |
| log_operation "info" "System snapshot created: $snapshot_dir" | |
| echo "$snapshot_dir" > /tmp/ubuntu_cleanup_snapshot.path | |
| } | |
| # Safe kernel cleanup with N-1 retention validation | |
| safe_kernel_cleanup() { | |
| local current_kernel=$(uname -r) | |
| local installed_kernels | |
| installed_kernels=$(dpkg -l | grep -E '^ii.*linux-image-[0-9]' | awk '{print $2}' | grep -v "linux-image-generic" || true) | |
| local kernel_count=$(echo "$installed_kernels" | grep -v '^$' | wc -l) | |
| log_operation "info" "Current kernel: $current_kernel" | |
| log_operation "info" "Installed kernels count: $kernel_count" | |
| if [ $kernel_count -le 2 ]; then | |
| log_operation "warning" "Only $kernel_count kernels installed. Skipping cleanup to maintain N-1 policy (minimum 2 kernels required)" | |
| return 0 | |
| fi | |
| echo "Installed kernels:" | |
| echo "$installed_kernels" | |
| echo "" | |
| echo "This will retain current kernel + at least 1 previous version" | |
| if prompt_with_timeout "Remove old kernels (keeping current + 1)? (y/n): "; then | |
| check_apt_lock | |
| if run_with_privileges apt-get autoremove --purge -y; then | |
| log_operation "info" "Kernel cleanup completed successfully" | |
| # Verify we still have enough kernels | |
| local remaining_kernels | |
| remaining_kernels=$(dpkg -l | grep -E '^ii.*linux-image-[0-9]' | awk '{print $2}' | grep -v "linux-image-generic" | wc -l) | |
| if [ $remaining_kernels -lt 2 ]; then | |
| log_operation "err" "CRITICAL: Less than 2 kernels remaining after cleanup!" | |
| return 1 | |
| fi | |
| log_operation "info" "Remaining kernels: $remaining_kernels" | |
| else | |
| log_operation "err" "Kernel cleanup failed" | |
| return 1 | |
| fi | |
| fi | |
| } | |
| # Log rotation for script logs | |
| rotate_old_logs() { | |
| log_operation "info" "Rotating old cleanup logs" | |
| # Keep only last 30 days of cleanup logs | |
| find "$LOG_DIR" -name "cleanup_*.log" -mtime +30 -delete 2>/dev/null || true | |
| # Keep max 50 log files | |
| local log_files | |
| log_files=$(ls -t "$LOG_DIR"/cleanup_*.log 2>/dev/null | tail -n +51) | |
| if [ -n "$log_files" ]; then | |
| echo "$log_files" | xargs rm -f 2>/dev/null || true | |
| fi | |
| } | |
| # Check dependencies | |
| check_dependencies() { | |
| local missing_tools=() | |
| for tool in apt-get find grep awk bc fuser flock; do | |
| if ! command -v "$tool" &>/dev/null; then | |
| missing_tools+=("$tool") | |
| fi | |
| done | |
| if [ ${#missing_tools[@]} -gt 0 ]; then | |
| log_operation "err" "Missing required tools: ${missing_tools[*]}" | |
| exit $EXIT_DEPENDENCY_MISSING | |
| fi | |
| } | |
| # Record initial disk space | |
| record_disk_space() { | |
| log_operation "info" "Initial disk space usage:" | |
| df -h / /home | |
| initial_space=$(df / | awk 'NR==2 {print $4}') | |
| } | |
| # Parse command line options | |
| while getopts "dnvt:j:r:k:" opt; do | |
| case $opt in | |
| d|n) DRY_RUN=1 ;; | |
| v) VERBOSE=1 ;; | |
| t) TIMEOUT_DURATION=$OPTARG ;; | |
| j) PARALLEL_JOBS=$OPTARG ;; | |
| r) MAX_RESOURCE_USAGE=$OPTARG ;; | |
| k) DEFAULT_RETENTION_DAYS=$OPTARG ;; | |
| *) echo "Usage: $0 [-d|-n] [-v] [-t timeout] [-j jobs] [-r max_cpu] [-k retention_days]" >&2 | |
| echo " -d,-n Dry run (show what would be done)" | |
| echo " -v Verbose output" | |
| echo " -t Timeout for user prompts in seconds (default: 60)" | |
| echo " -j Number of parallel jobs (default: 2)" | |
| echo " -r Maximum CPU percentage (default: 50)" | |
| echo " -k Retention days for logs (default: 10)" | |
| exit 1 ;; | |
| esac | |
| done | |
| # Progress function | |
| total_steps=20 | |
| current_step=0 | |
| progress() { | |
| current_step=$((current_step + 1)) | |
| percentage=$((current_step * 100 / total_steps)) | |
| log_operation "info" "[$current_step/$total_steps - $percentage%] $1" | |
| } | |
| # Main execution starts here | |
| log_operation "info" "=== Ubuntu Cleanup Script (Hardened) Started ===" | |
| log_operation "info" "PID: $$" | |
| # Acquire lock first | |
| acquire_lock | |
| # Load configuration safely | |
| load_safe_config || exit $EXIT_CONFIG_INVALID | |
| # Initialize | |
| check_dependencies | |
| check_root | |
| enforce_resource_limits | |
| rotate_old_logs | |
| record_disk_space | |
| log_operation "info" "Protected patterns: ${EXCLUDED_PATTERNS[*]}" | |
| [ $DRY_RUN -eq 1 ] && log_operation "warning" "DRY RUN MODE - No changes will be made" | |
| # Confirmation prompt | |
| if [ $DRY_RUN -eq 0 ]; then | |
| if ! prompt_with_timeout "Proceed with cleanup? (y/n): "; then | |
| log_operation "warning" "User aborted cleanup" | |
| exit $EXIT_USER_ABORT | |
| fi | |
| # Create system snapshot before any destructive operations | |
| create_system_snapshot | |
| fi | |
| # 1. Update package list | |
| progress "Updating package list" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| run_with_privileges apt-get update || log_operation "warning" "Failed to update package list" | |
| fi | |
| # 2. Clear user cache (with exclusions) | |
| progress "Clearing user cache (excluding protected folders)" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| cache_size_before=$(du -sh ~/.cache 2>/dev/null | cut -f1) | |
| log_operation "info" "Cache size before: $cache_size_before" | |
| # Remove cache files older than 3 days, excluding protected patterns | |
| safe_find ~/.cache -type f -mtime +3 -delete | |
| cache_size_after=$(du -sh ~/.cache 2>/dev/null | cut -f1) | |
| log_operation "info" "Cache size after: $cache_size_after" | |
| fi | |
| # 3. Clean APT cache | |
| progress "Cleaning APT cache" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| run_with_privileges apt-get clean | |
| fi | |
| # 4. Remove obsolete packages | |
| progress "Removing obsolete packages" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| run_with_privileges apt-get autoclean | |
| fi | |
| # 5. Remove unused packages | |
| progress "Removing unused packages" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| check_apt_lock | |
| log_operation "info" "Packages to be removed:" | |
| run_with_privileges apt-get autoremove -y --dry-run | grep "^Remv" || log_operation "info" "No packages to remove" | |
| if prompt_with_timeout "Remove these packages? (y/n): "; then | |
| run_with_privileges apt-get autoremove -y | |
| fi | |
| fi | |
| # 6. Remove old kernels (SAFE with N-1 validation) | |
| progress "Checking old kernel versions" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| safe_kernel_cleanup | |
| fi | |
| # 7. Clean Snap packages | |
| progress "Cleaning Snap packages" | |
| if [ $DRY_RUN -eq 0 ] && command -v snap &> /dev/null; then | |
| run_with_privileges snap list --all | awk '/disabled/{print $1, $3}' | \ | |
| while read -r snapname revision; do | |
| log_operation "info" "Removing $snapname revision $revision" | |
| run_with_privileges snap remove "$snapname" --revision="$revision" || true | |
| done | |
| fi | |
| # 8. Clean Flatpak | |
| progress "Cleaning Flatpak" | |
| if [ $DRY_RUN -eq 0 ] && command -v flatpak &> /dev/null; then | |
| run_with_privileges flatpak uninstall --unused -y || true | |
| fi | |
| # 9. Clear thumbnails (with age limit) | |
| progress "Clearing old thumbnails" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| safe_find ~/.cache/thumbnails -type f -mtime +30 -delete | |
| fi | |
| # 10. Clean journal logs | |
| progress "Cleaning systemd journal logs" | |
| if [ $DRY_RUN -eq 0 ] && command -v journalctl &> /dev/null; then | |
| run_with_privileges journalctl --vacuum-time="${DEFAULT_RETENTION_DAYS}d" | |
| run_with_privileges journalctl --vacuum-size=50M | |
| fi | |
| # 11. Clean /tmp (carefully) | |
| progress "Cleaning /tmp directory" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Only remove files older than retention days and not in use | |
| run_with_privileges find /tmp -type f -atime +$DEFAULT_RETENTION_DAYS \ | |
| -not -exec fuser -s {} \; -delete 2>/dev/null || true | |
| fi | |
| # 12. Clean browser caches (with exclusions) | |
| progress "Cleaning browser caches" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Firefox | |
| if [ -d ~/.mozilla/firefox ]; then | |
| safe_find ~/.mozilla/firefox -name "*Cache*" -type d -exec rm -rf {} + 2>/dev/null || true | |
| fi | |
| # Chrome/Chromium | |
| for browser_dir in ~/.config/google-chrome ~/.config/chromium; do | |
| if [ -d "$browser_dir" ]; then | |
| safe_find "$browser_dir" -name "Cache" -type d -exec rm -rf {} + 2>/dev/null || true | |
| fi | |
| done | |
| fi | |
| # 13. Manage log files | |
| progress "Managing log files" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Compress old logs | |
| run_with_privileges find /var/log -type f -name "*.log" -mtime +7 ! -exec fuser -s {} \; -exec gzip -9 {} \; 2>/dev/null || true | |
| # Remove very old compressed logs | |
| run_with_privileges find /var/log -type f -name "*.gz" -mtime +$DEFAULT_RETENTION_DAYS -delete 2>/dev/null || true | |
| fi | |
| # 14. Check core dumps | |
| progress "Checking core dumps" | |
| if [ $DRY_RUN -eq 0 ] && [ -d /var/lib/apport/coredump ]; then | |
| core_count=$(find /var/lib/apport/coredump -type f -name "core*" 2>/dev/null | wc -l) | |
| if [ $core_count -gt 0 ]; then | |
| log_operation "info" "Found $core_count core dump files" | |
| if prompt_with_timeout "Delete core dumps? (y/n): "; then | |
| run_with_privileges find /var/lib/apport/coredump -type f -name 'core*' -delete | |
| fi | |
| fi | |
| fi | |
| # 15. Clean package backups | |
| progress "Cleaning package backups" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| # Keep only recent backups (last 5) | |
| run_with_privileges bash -c "ls -t /var/backups/dpkg.status.* 2>/dev/null | tail -n +6 | xargs rm -f" 2>/dev/null || true | |
| fi | |
| # 16. Clean pip cache | |
| progress "Cleaning pip cache" | |
| if [ $DRY_RUN -eq 0 ] && command -v pip &> /dev/null; then | |
| pip cache purge 2>/dev/null || true | |
| fi | |
| # 17. Clean conda cache | |
| progress "Cleaning conda cache" | |
| if [ $DRY_RUN -eq 0 ] && command -v conda &> /dev/null; then | |
| conda clean --all -y 2>/dev/null || true | |
| fi | |
| # 18. Clean npm cache | |
| progress "Cleaning npm cache" | |
| if [ $DRY_RUN -eq 0 ] && [ -d ~/.npm ]; then | |
| # Exclude node_modules and other important npm folders | |
| safe_find ~/.npm/_cache -type f -mtime +7 -delete 2>/dev/null || true | |
| fi | |
| # 19. REMOVED: Automatic PPA addition (security risk) | |
| progress "Skipping automatic third-party repository addition" | |
| log_operation "info" "Automatic PPA addition removed for security. Install ucaresystem-core manually if needed." | |
| # 20. Final update | |
| progress "Final system update" | |
| if [ $DRY_RUN -eq 0 ]; then | |
| if prompt_with_timeout "Update all packages? (y/n): "; then | |
| check_apt_lock | |
| run_with_privileges apt-get update && run_with_privileges apt-get upgrade -y | |
| fi | |
| fi | |
| # Show results | |
| log_operation "info" "Final disk space usage:" | |
| df -h / /home | |
| final_space=$(df / | awk 'NR==2 {print $4}') | |
| log_operation "info" "=== Cleanup completed successfully ===" | |
| log_operation "info" "Initial free space: $initial_space KB" | |
| log_operation "info" "Final free space: $final_space KB" | |
| space_freed=$((final_space - initial_space)) | |
| log_operation "info" "Space freed: $space_freed KB" | |
| log_operation "info" "Log file: $LOG_FILE" | |
| log_operation "info" "Protected patterns excluded: ${EXCLUDED_PATTERNS[*]}" | |
| if [ -f /tmp/ubuntu_cleanup_snapshot.path ]; then | |
| snapshot_path=$(cat /tmp/ubuntu_cleanup_snapshot.path) | |
| log_operation "info" "System snapshot available at: $snapshot_path" | |
| rm -f /tmp/ubuntu_cleanup_snapshot.path | |
| fi | |
| exit $EXIT_SUCCESS |
I updated the script to make it safer, and now you can be more selective about which folders you want to remove from the .cache
✅ Shebang line: Uses #!/usr/bin/env bash (already implemented)
✅ localepurge: Added with automatic installation prompt
✅ ucaresystem-core: Added with automatic installation prompt
What these tools do:
localepurge:
Removes unnecessary locale files (language packs)
Can free up significant space if you only use one or two languages
Automatically configures which locales to keep during installation
ucaresystem-core:
All-in-one Ubuntu maintenance tool
Performs: updates, removes old kernels, cleans apt cache, removes orphaned packages
Basically does many of the same tasks but in one command
The -u flag runs it in unattended mode
This script is specifically designed for Ubuntu/Debian systems and uses Linux-specific commands that don't exist on macOS.
I am using a macos theme, this is Ubuntu 24.04 : )
Hi, thanks for reporting this issue!
I've identified the problem - the interactive localepurge dialog in Step #19 was causing the script to hang. I've updated the script to resolve this issue for now.
Fix: Step #19 has been replaced with:
# 19. Localepurge - Remove unnecessary locale files (SKIPPED)
progress "Skipping locale files cleanup"
if [ $DRY_RUN -eq 0 ]; then
echo "Localepurge step skipped to avoid interactive dialog"
echo "✓ Locale cleanup skipped"
fiThank you so much : )
You're welcome! Thank you for reporting the issue. :)

Useful script. I'd add localepurge too. I found also a useful ucaresystem-core app.