diff --git a/backup.sh b/backup.sh index 80b9b52..ea55f01 100644 --- a/backup.sh +++ b/backup.sh @@ -17,9 +17,11 @@ # # Usage: backup.sh # Description: Creates a tar.gz archive of the source directory in the destination. - -SRC="$1" -DEST="$2" + +set -euo pipefail + +SRC="${1:-}" +DEST="${2:-}" if [ -z "$SRC" ] || [ -z "$DEST" ]; then echo "Usage: $0 " exit 1 @@ -33,11 +35,29 @@ if [ ! -d "$DEST" ]; then fi base_name="$(basename "$SRC")" -date_str="$(date +%Y%m%d)" +# Seconds, not just the date. The old %Y%m%d name meant a second run on +# the same day silently overwrote the first -- losing a good backup at +# the exact moment someone was trying to take another one. +date_str="$(date +%Y%m%d-%H%M%S)" archive_name="${base_name}-backup-${date_str}.tar.gz" -tar -czf "$DEST/$archive_name" -C "$(dirname "$SRC")" "$base_name" -if [ $? -eq 0 ]; then - echo "Backup successful: $DEST/$archive_name" +archive_path="$DEST/$archive_name" + +if [ -e "$archive_path" ]; then + echo "Refusing to overwrite existing archive: $archive_path" + exit 1 +fi + +# Write to a partial name and rename only on success, so an interrupted +# run cannot leave a truncated file sitting there looking like a backup. +tmp_path="${archive_path}.partial" +trap 'rm -f -- "$tmp_path"' EXIT + +if tar -czf "$tmp_path" -C "$(dirname "$SRC")" "$base_name"; then + mv -- "$tmp_path" "$archive_path" + trap - EXIT + echo "Backup successful: $archive_path" + ls -lh -- "$archive_path" else - echo "Backup failed for $SRC" + echo "Backup failed for $SRC" >&2 + exit 1 fi diff --git a/restore.sh b/restore.sh index 27331c1..fe5018d 100644 --- a/restore.sh +++ b/restore.sh @@ -17,9 +17,13 @@ # # Usage: restore.sh [target_directory] # Description: Extracts the tar.gz archive into the target directory (current dir if not specified). - -ARCHIVE="$1" -TARGET="$2" +# +# Extraction options are deliberately conservative -- see the tar call. + +set -euo pipefail + +ARCHIVE="${1:-}" +TARGET="${2:-}" if [ -z "$ARCHIVE" ]; then echo "Usage: $0 [target_directory]" exit 1 @@ -36,10 +40,31 @@ else fi fi -tar -xzf "$ARCHIVE" -C "$TARGET" -status=$? -if [ $status -eq 0 ]; then +# 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 with error code $status" + echo "Restore failed" >&2 + exit 1 fi