master
Sergey Marinkevich 8 months ago
parent 961b8a902d
commit 8dc1ec73ae

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

Loading…
Cancel
Save