Finish the pass: log_inspect, service_manager, rsync_magic, disk_cleanup
log_inspect.sh discarded grep's stderr and ignored its exit status, so an unprivileged search over root-owned logs was indistinguishable from a search that genuinely found nothing. grep's three outcomes now mean three different things: matched, matched nothing, or could not read everything -- the last of which says so and exits non-zero. Confirmed grep returns 2 rather than 1 in that case, which is why the naive "status -eq 1" check would never have fired. service_manager.sh validates the action before dispatch and requires root for the five that change system state, leaving status and list open to anyone. $action is quoted at both call sites. rsync_magic.sh had --inplace on unconditionally. It writes straight into destination files instead of to a temporary and renaming, so an interrupted run leaves them partially overwritten -- the opposite of what a backup tool should guarantee. Now opt-in, with a warning when used. Its log lives under /var/log and every line pipes through tee, so under pipefail an unprivileged run died on the first line with a bare permission error; it now falls back to stdout rather than failing the sync over its own logging. --delete also confirms before running, since reversing the two arguments erases the backup. disk_cleanup.sh moves from `set -o pipefail` to full strict mode, with the two pipelines that legitimately return non-zero handled at their call sites rather than by leaving the script lax. Its "largest files" walk also gained -xdev, which it was missing while security_audit.sh next door already had it -- without it the walk descends /proc, /sys and every network mount. All fifteen scripts now run under set -euo pipefail.
This commit is contained in:
+52
-6
@@ -25,18 +25,28 @@ TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
|
||||
|
||||
# ======= Help Function =======
|
||||
usage() {
|
||||
echo "Usage: $0 [--dry-run] <source> <destination>"
|
||||
echo "Optional: --dry-run to simulate the sync"
|
||||
echo "Usage: $0 [--dry-run] [--inplace] [--yes] <source> <destination>"
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user