The scripts were attributed to LINUXexpert.org, which is being retired as a site and is no longer where this work lives. Coffey Labs is the organisation these projects belong to. One line per script, fifteen of them, and nothing else. LICENSE is deliberately untouched: its "Copyright (C) <year> <name of author>" lines are GPL boilerplate showing you how to write your own notice, and the Free Software Foundation's own copyright on the licence text is not ours to edit. Both git contributors are the same person, so there is no third-party copyright here that could not be restated.
328 lines
9.7 KiB
Bash
328 lines
9.7 KiB
Bash
#!/bin/bash
|
|
# disk_cleanup.sh - Show disk usage and optionally clean temporary files
|
|
#
|
|
# Copyright (C) 2025 Coffey Labs
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
# under the terms of the GNU General Public License as published by the
|
|
# Free Software Foundation, version 3 of the License.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but
|
|
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
# for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License along
|
|
# with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# Usage: disk_cleanup.sh [OPTIONS]
|
|
# Options:
|
|
# --clean Perform cleanup of package caches and temporary files
|
|
# --dry-run Show what would be cleaned without actually removing files
|
|
# --age DAYS Set age threshold for temp file cleanup (default: 7)
|
|
# --dirs DIR1,DIR2 Specify custom directories to clean (default: /tmp,/var/tmp)
|
|
# Protected system directories are refused.
|
|
# --yes Skip the confirmation prompt for non-default --dirs
|
|
# --help Display this help message
|
|
#
|
|
# Examples:
|
|
# disk_cleanup.sh # Show disk usage overview
|
|
# disk_cleanup.sh --clean # Perform cleanup with defaults
|
|
# disk_cleanup.sh --dry-run # Preview cleanup actions
|
|
# disk_cleanup.sh --clean --age 14 --dry-run # Preview cleanup with 14-day threshold
|
|
#
|
|
|
|
# Full strict mode. The two pipelines below that legitimately return
|
|
# non-zero -- find hitting unreadable directories, and head closing a
|
|
# pipe early -- are handled at their call sites rather than by leaving
|
|
# the whole script lax.
|
|
set -euo pipefail
|
|
|
|
# ===== CONFIGURATION =====
|
|
AGE_THRESHOLD=7
|
|
DIRS_TO_CLEAN=("/tmp" "/var/tmp")
|
|
DRY_RUN=false
|
|
CLEAN_MODE=false
|
|
ASSUME_YES=false
|
|
LOG_FILE="/var/log/disk_cleanup.log"
|
|
|
|
# ===== UTILITY FUNCTIONS =====
|
|
|
|
# Print usage information
|
|
usage() {
|
|
# Print the comment block between the "# Usage:" line and the blank
|
|
# comment that ends it. Previously this sliced fixed line numbers
|
|
# (head -22 | tail -n +18), which silently truncated the output at
|
|
# --age as soon as the block grew -- hiding --dirs entirely.
|
|
sed -n '/^# Usage:/,/^#$/p' "$0" | sed 's/^# \{0,1\}//'
|
|
}
|
|
|
|
# Log messages to file and stdout
|
|
log() {
|
|
local message="$1"
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $message" | tee -a "$LOG_FILE" 2>/dev/null || echo "$message"
|
|
}
|
|
|
|
# Error handler
|
|
error_exit() {
|
|
local message="$1"
|
|
local exit_code="${2:-1}"
|
|
echo "ERROR: $message" >&2
|
|
exit "$exit_code"
|
|
}
|
|
|
|
# Check for required commands
|
|
check_dependencies() {
|
|
local dependencies=("df" "find" "awk" "sort")
|
|
local missing_deps=()
|
|
|
|
for cmd in "${dependencies[@]}"; do
|
|
if ! command -v "$cmd" &> /dev/null; then
|
|
missing_deps+=("$cmd")
|
|
fi
|
|
done
|
|
|
|
if [ ${#missing_deps[@]} -gt 0 ]; then
|
|
error_exit "Missing required commands: ${missing_deps[*]}"
|
|
fi
|
|
}
|
|
|
|
# Verify root privileges
|
|
check_root() {
|
|
if [ $EUID -ne 0 ]; then
|
|
error_exit "This action requires root privileges. Please run with sudo." 1
|
|
fi
|
|
}
|
|
|
|
# Refuse to run -delete against paths that are not plausibly scratch
|
|
# space. --dirs previously accepted anything: `--clean --dirs /home`
|
|
# removed every file in /home older than the age threshold, and
|
|
# `--dirs /` did it system-wide, as root, with no confirmation.
|
|
validate_clean_dirs() {
|
|
local dir real
|
|
local protected=(
|
|
/ /bin /boot /dev /etc /home /lib /lib32 /lib64 /opt /proc /root
|
|
/run /sbin /srv /sys /usr /var
|
|
)
|
|
|
|
for dir in "${DIRS_TO_CLEAN[@]}"; do
|
|
[ -n "$dir" ] || error_exit "Empty directory in --dirs list." 1
|
|
|
|
case "$dir" in
|
|
/*) ;;
|
|
*) error_exit "--dirs requires absolute paths; got '$dir'." 1 ;;
|
|
esac
|
|
|
|
# Compare the resolved path, so symlinks and /tmp/../home cannot
|
|
# smuggle a protected directory past the check.
|
|
real="$(readlink -f -- "$dir" 2>/dev/null || echo "$dir")"
|
|
# Normalise to a trailing-slash-free form, except "/" itself, which
|
|
# must stay "/" -- stripping it yields "" and matches nothing.
|
|
[ "$real" = "/" ] || real="${real%/}"
|
|
[ -n "$real" ] || real=/
|
|
|
|
for p in "${protected[@]}"; do
|
|
if [ "$real" = "$p" ]; then
|
|
error_exit "Refusing to clean '$dir' (resolves to '$real'): protected directory." 1
|
|
fi
|
|
done
|
|
done
|
|
}
|
|
|
|
# Anything outside the built-in defaults gets an explicit confirmation,
|
|
# because that is where the destructive surprises live.
|
|
confirm_nondefault_dirs() {
|
|
local dir nondefault=()
|
|
for dir in "${DIRS_TO_CLEAN[@]}"; do
|
|
case "$dir" in
|
|
/tmp|/var/tmp) ;;
|
|
*) nondefault+=("$dir") ;;
|
|
esac
|
|
done
|
|
|
|
[ ${#nondefault[@]} -eq 0 ] && return 0
|
|
[ "$DRY_RUN" = true ] && return 0
|
|
[ "$ASSUME_YES" = true ] && return 0
|
|
|
|
if [ ! -t 0 ]; then
|
|
error_exit "Non-default --dirs (${nondefault[*]}) need confirmation; pass --yes to run unattended." 1
|
|
fi
|
|
|
|
echo "About to delete files older than $AGE_THRESHOLD days from:"
|
|
printf ' %s\n' "${nondefault[@]}"
|
|
read -r -p "Proceed? (yes/NO): " reply
|
|
[ "$reply" = "yes" ] || error_exit "Cancelled." 0
|
|
}
|
|
|
|
# ===== DISPLAY FUNCTIONS =====
|
|
|
|
# Show disk usage overview
|
|
show_disk_usage() {
|
|
echo "==== Disk Usage Overview ===="
|
|
df -h -x tmpfs -x devtmpfs || error_exit "Failed to get disk usage information"
|
|
|
|
echo -e "\n==== Top 10 Largest Files ===="
|
|
# -xdev: without it this walks /proc, /sys and every network mount,
|
|
# which is slow and reports files that are not really taking up disk.
|
|
# The `|| true` absorbs both find's non-zero on unreadable directories
|
|
# and the SIGPIPE head causes once it has its ten lines.
|
|
{ find / -xdev -type f -printf '%s %p\n' 2>/dev/null | sort -nr | head -n 10 | \
|
|
awk '{size=$1/1024/1024; printf("%.1f MB - ", size); $1=""; print $0}'; } || \
|
|
echo "Warning: Could not retrieve largest files"
|
|
}
|
|
|
|
# ===== CLEANUP FUNCTIONS =====
|
|
|
|
# Clean package manager caches
|
|
clean_package_caches() {
|
|
local cleaned=false
|
|
|
|
if command -v apt-get &> /dev/null; then
|
|
log "Cleaning apt package cache..."
|
|
if [ "$DRY_RUN" = false ]; then
|
|
apt-get clean || log "Warning: apt-get clean failed"
|
|
fi
|
|
cleaned=true
|
|
fi
|
|
|
|
if command -v dnf &> /dev/null; then
|
|
log "Cleaning dnf package cache..."
|
|
if [ "$DRY_RUN" = false ]; then
|
|
dnf clean all -y &> /dev/null || log "Warning: dnf clean failed"
|
|
fi
|
|
cleaned=true
|
|
fi
|
|
|
|
if command -v yum &> /dev/null; then
|
|
log "Cleaning yum package cache..."
|
|
if [ "$DRY_RUN" = false ]; then
|
|
yum clean all -y &> /dev/null || log "Warning: yum clean failed"
|
|
fi
|
|
cleaned=true
|
|
fi
|
|
|
|
if command -v pacman &> /dev/null; then
|
|
log "Cleaning pacman package cache..."
|
|
if [ "$DRY_RUN" = false ]; then
|
|
pacman -Scc --noconfirm &> /dev/null || log "Warning: pacman clean failed"
|
|
fi
|
|
cleaned=true
|
|
fi
|
|
|
|
if [ "$cleaned" = false ]; then
|
|
log "Warning: No supported package manager found"
|
|
fi
|
|
}
|
|
|
|
# Clean temporary files
|
|
clean_temporary_files() {
|
|
for dir in "${DIRS_TO_CLEAN[@]}"; do
|
|
if [ ! -d "$dir" ]; then
|
|
log "Warning: Directory $dir does not exist, skipping..."
|
|
continue
|
|
fi
|
|
|
|
log "Cleaning files older than $AGE_THRESHOLD days in $dir..."
|
|
|
|
# Count files that would be deleted
|
|
local file_count
|
|
# `|| true` on the pipeline: find exits non-zero for unreadable
|
|
# subdirectories, which under pipefail would abort the whole cleanup.
|
|
file_count=$(find "$dir" -type f -mtime +"$AGE_THRESHOLD" 2>/dev/null | wc -l || true)
|
|
|
|
if [ "$file_count" -gt 0 ]; then
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN: Would remove $file_count files from $dir"
|
|
find "$dir" -type f -mtime +"$AGE_THRESHOLD" 2>/dev/null | head -n 5 | while read -r file; do
|
|
log " - $file"
|
|
done
|
|
if [ "$file_count" -gt 5 ]; then
|
|
log " ... and $((file_count - 5)) more files"
|
|
fi
|
|
else
|
|
if find "$dir" -type f -mtime +"$AGE_THRESHOLD" -delete 2>/dev/null; then
|
|
log "Removed $file_count old files from $dir"
|
|
else
|
|
log "Warning: Failed to remove all files from $dir"
|
|
fi
|
|
fi
|
|
else
|
|
log "No files older than $AGE_THRESHOLD days found in $dir"
|
|
fi
|
|
|
|
# Clean empty directories
|
|
if [ "$DRY_RUN" = false ]; then
|
|
find "$dir" -type d -empty -mtime +"$AGE_THRESHOLD" -delete 2>/dev/null || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ===== PARSE ARGUMENTS =====
|
|
|
|
parse_arguments() {
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--clean)
|
|
CLEAN_MODE=true
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN=true
|
|
;;
|
|
--yes)
|
|
ASSUME_YES=true
|
|
;;
|
|
--age)
|
|
if [ -z "$2" ] || ! [[ "$2" =~ ^[0-9]+$ ]]; then
|
|
error_exit "Invalid age value. Must be a number." 1
|
|
fi
|
|
AGE_THRESHOLD="$2"
|
|
shift
|
|
;;
|
|
--dirs)
|
|
if [ -z "$2" ]; then
|
|
error_exit "Missing directory list for --dirs option." 1
|
|
fi
|
|
IFS=',' read -ra DIRS_TO_CLEAN <<< "$2"
|
|
shift
|
|
;;
|
|
--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
error_exit "Unknown option: $1. Use --help for usage information." 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
}
|
|
|
|
# ===== MAIN EXECUTION =====
|
|
|
|
main() {
|
|
check_dependencies
|
|
parse_arguments "$@"
|
|
|
|
if [ "$CLEAN_MODE" = false ]; then
|
|
# Display mode
|
|
show_disk_usage
|
|
echo -e "\n(To actually free space, run: sudo $0 --clean)"
|
|
else
|
|
# Cleanup mode
|
|
check_root
|
|
validate_clean_dirs
|
|
confirm_nondefault_dirs
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log "DRY RUN MODE - No files will be deleted"
|
|
fi
|
|
|
|
log "Starting disk cleanup process..."
|
|
clean_package_caches
|
|
clean_temporary_files
|
|
|
|
log "Disk cleanup process completed."
|
|
fi
|
|
}
|
|
|
|
main "$@" |