#!/bin/bash set -euo pipefail # --- Functions --- print_usage() { echo "Usage: $0 [backup_output_path] [--list file_with_paths] -- ..." echo echo "Examples:" echo " $0 /backup/my_backup.tar.gz -- /home/user /etc" echo " BACKUP_PATH=/backup/auto.tar.gz $0 --list dirs.txt -- /var/log" echo " $0 /mnt/backup/nextcloud_mirror -- /srv/nextcloud" exit 1 } parse_args() { local -n _target_path=$1 local -n _src_dirs=$2 local -n _list_file=$3 local -n _archive_type=$4 local positional=() local in_sources=0 while [[ $# -gt 4 ]]; do case "${5}" in --list) shift 5 _list_file="${1:-}" if [[ -z "$_list_file" || ! -f "$_list_file" ]]; then echo "Error: list file '$_list_file' not found." >&2 print_usage exit 1 fi shift ;; --) in_sources=1 shift ;; *) if [[ $in_sources -eq 1 ]]; then _src_dirs+=("$5") else positional+=("$5") fi shift ;; esac done if [[ ${#positional[@]} -gt 1 ]]; then echo "Error: Too many positional arguments before --" >&2 print_usage elif [[ ${#positional[@]} -eq 1 ]]; then _target_path="${positional[0]}" else _target_path="${BACKUP_PATH:-}" fi if [[ -z "$_target_path" ]]; then echo "Error: No target path provided and BACKUP_PATH is not set." >&2 print_usage exit 1 fi case "$_target_path" in *.tar) _archive_type="tar";; *.tar.gz|*.tgz) _archive_type="targz";; *.tar.xz|*.txz) _archive_type="tarxz";; *.tar.bz2|*.tbz) _archive_type="tarbz2";; *.7z) _archive_type="7z";; *) _archive_type="NONE";; esac } load_dirs_from_list_file() { local -n _src_dirs=$1 local list_file="$2" if [[ -n "$list_file" ]]; then while IFS= read -r line || [[ -n "$line" ]]; do [[ -z "$line" || "$line" =~ ^# ]] && continue _src_dirs+=("$line") done < "$list_file" fi } validate_src_dirs() { local -n _src_dirs=$1 local valid_dirs=() for dir in "${_src_dirs[@]}"; do if [[ -d "$dir" ]]; then valid_dirs+=("$dir") else echo "Warning: '$dir' is not a valid directory. Skipping." >&2 fi done _src_dirs=("${valid_dirs[@]}") if [[ ${#_src_dirs[@]} -eq 0 ]]; then echo "Error: No valid source directories to back up." >&2 print_usage exit 1 fi } init_tmp_dir() { mktemp -d -t backup-tmp-XXXXXX } count_files() { local src_dirs=("$@") local count=0 echo "Counting directories: " "${src_dirs[@]}" 1>&2 for dir in "${src_dirs[@]}"; do echo "Counting directory: " "$dir" 1>&2 for subdir in `find . -maxdepth 2` count=$((count + $(find "$dir" -xdev | wc -l))) done echo "$count" } run_rsync_backup() { local src_dirs=("${!1}") local dest_path="$2" local file_count="$3" echo "Starting backup..." for dir in "${src_dirs[@]}"; do base="$(basename "$dir")" dest_dir="$dest_path/$base" rsync -Aavx "$dir"/ "$dest_dir"/ | pv -ltps "$file_count" > /dev/null done echo "Rsync completed." } compress_backup() { local tmp_dir="$1" local target_path="$2" local archive_type="$3" echo "Creating archive: $target_path" case "$archive_type" in targz) tar czf - -C "$tmp_dir" . | pv -s "$(du -sb "$tmp_dir" | awk '{print $1}')" > "$target_path" ;; tar) tar cf - -C "$tmp_dir" . | pv -s "$(du -sb "$tmp_dir" | awk '{print $1}')" > "$target_path" ;; tarxz) tar cJf - -C "$tmp_dir" . | pv -s "$(du -sb "$tmp_dir" | awk '{print $1}')" > "$target_path" ;; tarbz2) tar cjf - -C "$tmp_dir" . | pv -s "$(du -sb "$tmp_dir" | awk '{print $1}')" > "$target_path" ;; 7z) 7z a -mx=9 "$target_path" "$tmp_dir"/* ;; *) echo "Unsupported archive type or no compression." >&2 return 1 ;; esac echo "Archive created successfully." } cleanup_tmp_dir() { local tmp_dir="$1" if [[ -n "$tmp_dir" && -d "$tmp_dir" ]]; then echo "Cleaning up temporary files..." rm -rf "$tmp_dir" echo "Done." fi } main() { local TARGET_PATH="" local SRC_DIRS=() local LIST_FILE="" local ARCHIVE_TYPE="NONE" parse_args TARGET_PATH SRC_DIRS LIST_FILE ARCHIVE_TYPE "$@" load_dirs_from_list_file SRC_DIRS "$LIST_FILE" validate_src_dirs SRC_DIRS local FILE_COUNT FILE_COUNT=$(count_files "${SRC_DIRS[@]}") if [[ "$ARCHIVE_TYPE" != "NONE" ]]; then local TMP_DIR TMP_DIR=$(init_tmp_dir) run_rsync_backup SRC_DIRS[@] "$TMP_DIR" "$FILE_COUNT" compress_backup "$TMP_DIR" "$TARGET_PATH" "$ARCHIVE_TYPE" cleanup_tmp_dir "$TMP_DIR" else mkdir -p "$TARGET_PATH" run_rsync_backup SRC_DIRS[@] "$TARGET_PATH" "$FILE_COUNT" fi } # --- Entry Point --- main "$@"