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.
139 lines
4.7 KiB
Bash
139 lines
4.7 KiB
Bash
#!/bin/bash
|
|
|
|
# rsync_magic.sh - A smart, feature-rich rsync wrapper for safe and powerful file synchronization.
|
|
#
|
|
# Copyright (C) 2025 LINUXexpert.org
|
|
#
|
|
# 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/>.
|
|
|
|
set -euo pipefail
|
|
|
|
# ======= Configuration =======
|
|
LOG_FILE="/var/log/rsync_magic.log"
|
|
EXCLUDES="/etc/rsync_magic_excludes.txt"
|
|
TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S")
|
|
|
|
# ======= Help Function =======
|
|
usage() {
|
|
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
|
|
|
|
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
|
|
done
|
|
|
|
SOURCE="${1:-}"
|
|
DEST="${2:-}"
|
|
|
|
if [[ -z "$SOURCE" || -z "$DEST" ]]; then
|
|
usage
|
|
fi
|
|
|
|
if [[ ! -d "$SOURCE" ]]; then
|
|
echo "Error: Source directory '$SOURCE' not found!"
|
|
exit 2
|
|
fi
|
|
|
|
mkdir -p "$DEST"
|
|
|
|
# ======= Construct Rsync Command =======
|
|
RSYNC_OPTS=(
|
|
-a # archive mode (recursive, preserve symlinks, perms, times, groups, etc.)
|
|
-v # verbose
|
|
-z # compress during transfer
|
|
-h # human-readable output
|
|
-u # skip files that are newer on the receiver
|
|
-P # show progress during transfer and allow partial transfer resume
|
|
-c # compare files using checksum
|
|
-x # don't cross filesystem boundaries
|
|
-A # preserve ACLs
|
|
-X # preserve extended attributes
|
|
--delete # delete extraneous files from destination
|
|
--numeric-ids # don't map uid/gid numbers to usernames
|
|
--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)
|
|
echo "Running in DRY RUN mode..."
|
|
fi
|
|
|
|
# Add exclude file if it exists
|
|
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"
|
|
echo "Destination: $DEST" | tee -a "$LOG_FILE"
|
|
|
|
rsync "${RSYNC_OPTS[@]}" "$SOURCE/" "$DEST/" 2>&1 | tee -a "$LOG_FILE"
|
|
|
|
echo "rsync completed at $(date +"%Y-%m-%d_%H-%M-%S")" | tee -a "$LOG_FILE"
|