backup.sh named archives with %Y%m%d, so a second run on the same day silently overwrote the first -- destroying a good backup at the moment someone was trying to take another. Names now carry seconds, and an existing path is refused rather than clobbered. The archive is also written to a .partial name and renamed only on success, with a trap to clean up, so an interrupted run cannot leave a truncated file that looks like a backup. restore.sh extracted with tar's defaults. An archive is untrusted input -- whoever produced it chooses the paths, ownership and modes inside it -- and extracting as root let the tarball dictate uid/gid and restore setuid bits directly. Now --no-same-owner --no-same-permissions, with -P still absent so tar keeps stripping leading "/" and refusing ".." members. It also lists what it is about to extract and confirms first, since it silently overwrote whatever was already in the target. Both scripts also gained set -euo pipefail, and both replaced the `cmd; if [ $? -eq 0 ]` pattern with a direct `if cmd; then`, which is what that idiom was reaching for and gets wrong as soon as any command is inserted between the two lines. Verified end to end: two same-second-apart backups both survive, no .partial residue, restore refuses non-interactively, and the round trip diffs identical.
71 lines
2.5 KiB
Bash
71 lines
2.5 KiB
Bash
#!/bin/bash
|
|
# restore.sh - Restore files from a backup archive
|
|
#
|
|
# 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/>.
|
|
#
|
|
# Usage: restore.sh <backup_archive.tar.gz> [target_directory]
|
|
# Description: Extracts the tar.gz archive into the target directory (current dir if not specified).
|
|
#
|
|
# Extraction options are deliberately conservative -- see the tar call.
|
|
|
|
set -euo pipefail
|
|
|
|
ARCHIVE="${1:-}"
|
|
TARGET="${2:-}"
|
|
if [ -z "$ARCHIVE" ]; then
|
|
echo "Usage: $0 <archive.tar.gz> [target_directory]"
|
|
exit 1
|
|
fi
|
|
if [ ! -f "$ARCHIVE" ]; then
|
|
echo "Backup archive '$ARCHIVE' not found!"; exit 1
|
|
fi
|
|
|
|
if [ -z "$TARGET" ]; then
|
|
TARGET="."
|
|
else
|
|
if [ ! -d "$TARGET" ]; then
|
|
mkdir -p "$TARGET" || { echo "Failed to create target directory '$TARGET'"; exit 1; }
|
|
fi
|
|
fi
|
|
|
|
# An archive is untrusted input: whoever produced it chooses the paths,
|
|
# the ownership and the modes inside it.
|
|
# --no-same-owner do not let the archive pick uid/gid. Extracting
|
|
# as root previously handed files to whatever
|
|
# owner the tarball named.
|
|
# --no-same-permissions apply the umask rather than restoring setuid
|
|
# bits straight out of the archive.
|
|
# -P is NOT used, so tar strips leading "/" and refuses ".." members.
|
|
echo "Contents to be extracted into $TARGET:"
|
|
tar -tzf "$ARCHIVE" | head -n 20
|
|
total="$(tar -tzf "$ARCHIVE" | grep -c . || true)"
|
|
[ "$total" -gt 20 ] && echo " ... and $((total - 20)) more entries"
|
|
|
|
if [ "${ASSUME_YES:-}" != "1" ]; then
|
|
if [ ! -t 0 ]; then
|
|
echo "Refusing to extract without confirmation; set ASSUME_YES=1 for unattended use." >&2
|
|
exit 1
|
|
fi
|
|
read -r -p "Extract $total entries into $TARGET, overwriting existing files? (yes/NO): " reply
|
|
[ "$reply" = "yes" ] || { echo "Cancelled."; exit 0; }
|
|
fi
|
|
|
|
if tar -xzf "$ARCHIVE" -C "$TARGET" --no-same-owner --no-same-permissions; then
|
|
echo "Restore successful to directory: $TARGET"
|
|
else
|
|
echo "Restore failed" >&2
|
|
exit 1
|
|
fi
|