diff --git a/disk_cleanup.sh b/disk_cleanup.sh index f3364d4..5f0452f 100644 --- a/disk_cleanup.sh +++ b/disk_cleanup.sh @@ -32,7 +32,11 @@ # disk_cleanup.sh --clean --age 14 --dry-run # Preview cleanup with 14-day threshold # -set -o pipefail +# 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 @@ -158,8 +162,12 @@ show_disk_usage() { df -h -x tmpfs -x devtmpfs || error_exit "Failed to get disk usage information" echo -e "\n==== Top 10 Largest Files ====" - find / -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}' || \ + # -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" } @@ -218,7 +226,9 @@ clean_temporary_files() { # Count files that would be deleted local file_count - file_count=$(find "$dir" -type f -mtime +"$AGE_THRESHOLD" 2>/dev/null | wc -l) + # `|| 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 diff --git a/log_inspect.sh b/log_inspect.sh index bef6581..f01ef74 100644 --- a/log_inspect.sh +++ b/log_inspect.sh @@ -17,17 +17,39 @@ # # Usage: log_inspect.sh [search | tail ] # Description: Searches across /var/log for a pattern, or tails a specific log file. - -if [ "$1" = "search" ]; then - pattern="$2" +# +# Most of /var/log is root-only. Run this with sudo, or results will be +# quietly partial -- see the note on the search branch. + +set -euo pipefail + +if [ "${1:-}" = "search" ]; then + pattern="${2:-}" if [ -z "$pattern" ]; then echo "Usage: $0 search "; exit 1 fi echo "Searching for '$pattern' in /var/log..." - grep -R -i --color=auto "$pattern" /var/log 2>/dev/null + # 2>/dev/null hid permission errors, so an unprivileged run looked like + # "no matches" rather than "could not read most of /var/log". Say so + # explicitly instead. grep exits 1 on no match, which is not an error. + # grep distinguishes three outcomes and they mean different things + # here: 0 matched, 1 matched nothing, 2 could not read everything. The + # original discarded stderr and ignored the status, so an unprivileged + # run over root-owned logs was indistinguishable from a clean search. + status=0 + grep -R -i --color=auto -- "$pattern" /var/log 2>/dev/null || status=$? + case "$status" in + 0) ;; + 1) echo "No matches found." ;; + *) + echo "Search was incomplete -- some files under /var/log could not be read." >&2 + [ "$EUID" -ne 0 ] && echo "Re-run with sudo for a complete search." >&2 + exit "$status" + ;; + esac exit 0 -elif [ "$1" = "tail" ]; then - logfile="$2" +elif [ "${1:-}" = "tail" ]; then + logfile="${2:-}" if [ -z "$logfile" ]; then echo "Usage: $0 tail "; exit 1 fi diff --git a/rsync_magic.sh b/rsync_magic.sh index 3363add..1118601 100644 --- a/rsync_magic.sh +++ b/rsync_magic.sh @@ -25,18 +25,28 @@ TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") # ======= Help Function ======= usage() { - echo "Usage: $0 [--dry-run] " - echo "Optional: --dry-run to simulate the sync" + echo "Usage: $0 [--dry-run] [--inplace] [--yes] " + echo " --dry-run simulate the sync, change nothing" + echo " --inplace write into destination files directly (see note below)" + echo " --yes skip the confirmation prompt for --delete" exit 1 } # ======= Argument Parsing ======= DRY_RUN=0 +INPLACE=0 +ASSUME_YES=0 -if [[ "${1:-}" == "--dry-run" ]]; then - DRY_RUN=1 +while [[ "${1:-}" == --* ]]; do + case "$1" in + --dry-run) DRY_RUN=1 ;; + --inplace) INPLACE=1 ;; + --yes) ASSUME_YES=1 ;; + --help|-h) usage ;; + *) echo "Unknown option: $1" >&2; usage ;; + esac shift -fi +done SOURCE="${1:-}" DEST="${2:-}" @@ -66,11 +76,22 @@ RSYNC_OPTS=( -X # preserve extended attributes --delete # delete extraneous files from destination --numeric-ids # don't map uid/gid numbers to usernames - --inplace # update destination files in place --backup # backup overwritten files --backup-dir="${DEST}/.backup-${TIMESTAMP}" # backup location ) +# --inplace was previously always on. It writes directly into the +# destination file rather than to a temporary and renaming, so an +# interrupted transfer leaves the destination partially overwritten and +# corrupt -- the opposite of what a backup should guarantee. rsync's +# default (temp file, atomic rename) costs extra space on the target and +# is worth it here, so --inplace is now opt-in for the cases that need +# it, such as very large files on space-constrained targets. +if [[ "$INPLACE" -eq 1 ]]; then + RSYNC_OPTS+=(--inplace) + echo "WARNING: --inplace means an interrupted run can leave corrupt files at the destination." +fi + # Add dry-run flag if needed if [[ "$DRY_RUN" -eq 1 ]]; then RSYNC_OPTS+=(--dry-run) @@ -82,6 +103,31 @@ if [[ -f "$EXCLUDES" ]]; then RSYNC_OPTS+=(--exclude-from="$EXCLUDES") fi +# The log lives under /var/log, which needs root. Every echo below pipes +# through `tee -a`, so with set -o pipefail an unprivileged run died on +# the first line with a bare "Permission denied" and no explanation. +# Fall back to stdout instead of failing the sync over its logging. +if ! { [ -w "$LOG_FILE" ] || { [ ! -e "$LOG_FILE" ] && [ -w "$(dirname "$LOG_FILE")" ]; }; }; then + echo "NOTE: cannot write $LOG_FILE (need root); logging to stdout only." >&2 + LOG_FILE=/dev/null +fi + +# --delete removes anything at the destination that is not in the source. +# Reversing the two arguments therefore erases the backup. --backup-dir +# above catches the deleted files, but confirm anyway -- the prompt is +# cheaper than discovering the mistake later. +if [[ "$DRY_RUN" -ne 1 && "$ASSUME_YES" -ne 1 ]]; then + echo "About to sync with --delete:" + echo " FROM: $SOURCE/" + echo " TO: $DEST/ (extraneous files here will be removed)" + if [ ! -t 0 ]; then + echo "Refusing to run unattended without --yes." >&2 + exit 1 + fi + read -r -p "Proceed? (yes/NO): " reply + [ "$reply" = "yes" ] || { echo "Cancelled."; exit 0; } +fi + # ======= Run Rsync ======= echo "Starting rsync at $TIMESTAMP" | tee -a "$LOG_FILE" echo "Source: $SOURCE" | tee -a "$LOG_FILE" diff --git a/service_manager.sh b/service_manager.sh index 31fee02..38cd6ce 100644 --- a/service_manager.sh +++ b/service_manager.sh @@ -18,9 +18,31 @@ # Usage: service_manager.sh # Actions: start, stop, restart, status, enable, disable, list # Description: Uses systemctl or service to control services. - -action="$1" -service="$2" +# +# status and list are read-only; everything else changes system state +# and needs root. + +set -euo pipefail + +action="${1:-}" +service="${2:-}" + +# Validate up front so an unknown action cannot reach systemctl as a +# bare word, and so the privilege check below has something to gate on. +case "$action" in + start|stop|restart|status|enable|disable|list) ;; + "") echo "Usage: $0 {start|stop|restart|status|enable|disable|list} " >&2; exit 1 ;; + *) echo "Invalid action '$action'. Use start, stop, restart, status, enable, disable, or list." >&2; exit 1 ;; +esac + +case "$action" in + start|stop|restart|enable|disable) + if [ "$EUID" -ne 0 ]; then + echo "'$action' changes system state and requires root. Re-run with sudo." >&2 + exit 1 + fi + ;; +esac if [ "$action" = "list" ]; then # List running services @@ -41,21 +63,16 @@ fi if command -v systemctl &> /dev/null; then case "$action" in - start|stop|restart|status) - systemctl $action "$service" - ;; - enable|disable) - systemctl $action "$service" - ;; - *) - echo "Invalid action. Use start, stop, restart, status, enable, disable, or list." - exit 1 + start|stop|restart|status|enable|disable) + # Quoted: $action is validated above, but leaving it bare invites + # word-splitting the moment anyone passes it through a variable. + systemctl "$action" "$service" ;; esac elif command -v service &> /dev/null; then case "$action" in start|stop|restart) - service "$service" $action + service "$service" "$action" ;; status) service "$service" status