|
|
#!/bin/bash |
|
|
|
|
|
set -e |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
log() { |
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [RESTIC-RESTORE] $*" |
|
|
} |
|
|
|
|
|
log_success() { |
|
|
echo -e "\033[32m[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS] $*\033[0m" |
|
|
} |
|
|
|
|
|
log_error() { |
|
|
echo -e "\033[31m[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*\033[0m" |
|
|
} |
|
|
|
|
|
log_warn() { |
|
|
echo -e "\033[33m[$(date '+%Y-%m-%d %H:%M:%S')] [WARNING] $*\033[0m" |
|
|
} |
|
|
|
|
|
log_info() { |
|
|
echo -e "\033[36m[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*\033[0m" |
|
|
} |
|
|
|
|
|
|
|
|
show_usage() { |
|
|
cat << EOF |
|
|
Restic Data Restore Script (Simplified - Only supports latest snapshot) |
|
|
|
|
|
Usage: $0 [options] |
|
|
|
|
|
Options: |
|
|
-h, --help Show this help message |
|
|
-l, --list List available snapshots |
|
|
-c, --check Check repository integrity |
|
|
-r, --repo PATH Specify repository path (default: \$RESTIC_REPOSITORY_PATH) |
|
|
-p, --password-file FILE Specify password file (default: \$HOME/config/restic-password) |
|
|
-t, --target PATH Specify restore target path (default: /tmp/restic_restore) |
|
|
-m, --mode MODE Restore mode: |
|
|
- safe: Safe mode, restore to specified directory (default) |
|
|
- direct: Direct mode, restore directly to target location |
|
|
- replace: Replace mode, clear target directory before restore |
|
|
-i, --include PATTERN Only restore matching paths (can be used multiple times) |
|
|
-e, --exclude PATTERN Exclude matching paths (can be used multiple times) |
|
|
-v, --verbose Verbose output |
|
|
-f, --force Force execution without confirmation |
|
|
--dry-run Dry run, don't perform actual restore |
|
|
|
|
|
Note: This version only supports restoring the latest snapshot, simplifying complex snapshot ID handling logic |
|
|
|
|
|
Examples: |
|
|
# List available snapshots |
|
|
$0 --list |
|
|
|
|
|
# Safe restore latest snapshot to default temp directory |
|
|
$0 |
|
|
|
|
|
# Direct restore to specified location |
|
|
$0 --mode direct --target /home/user |
|
|
|
|
|
# Full replacement mode (clear target directory first) |
|
|
$0 --mode replace --target /home/user |
|
|
|
|
|
# Only restore config files |
|
|
$0 --include "/home/user/config" |
|
|
|
|
|
Environment Variables: |
|
|
RESTIC_REPOSITORY_PATH Restic repository path |
|
|
RESTIC_PASSWORD Restic password |
|
|
EOF |
|
|
} |
|
|
|
|
|
|
|
|
check_restic() { |
|
|
if ! command -v restic &> /dev/null; then |
|
|
log_error "restic is not installed, please install restic first" |
|
|
return 1 |
|
|
fi |
|
|
return 0 |
|
|
} |
|
|
|
|
|
|
|
|
setup_environment() { |
|
|
|
|
|
USER_HOME="${HOME:-/home/user}" |
|
|
REPO_PATH="${RESTIC_REPOSITORY_PATH:-$USER_HOME/data/restic-repo}" |
|
|
PASSWORD_FILE="${PASSWORD_FILE:-$USER_HOME/config/restic-password}" |
|
|
TARGET_PATH="${TARGET_PATH:-/tmp/restic_restore_$(date +%s)}" |
|
|
RESTORE_MODE="${RESTORE_MODE:-safe}" |
|
|
|
|
|
|
|
|
if [[ ! "$REPO_PATH" =~ ^(s3:|sftp:|rest:|rclone:) && ! -d "$REPO_PATH" ]]; then |
|
|
log_warn "Local Restic repository does not exist: $REPO_PATH" |
|
|
log_warn "This might be the first startup, repository not yet initialized" |
|
|
log_warn "Please check RESTIC_REPOSITORY_PATH environment variable or use -r option" |
|
|
return 1 |
|
|
fi |
|
|
|
|
|
|
|
|
if [[ -n "$RESTIC_PASSWORD" ]]; then |
|
|
export RESTIC_PASSWORD |
|
|
elif [[ -f "$PASSWORD_FILE" ]]; then |
|
|
export RESTIC_PASSWORD="$(cat "$PASSWORD_FILE")" |
|
|
log_info "Reading password from file: $PASSWORD_FILE" |
|
|
else |
|
|
log_error "Password not found, please set RESTIC_PASSWORD environment variable or password file: $PASSWORD_FILE" |
|
|
return 1 |
|
|
fi |
|
|
|
|
|
|
|
|
export RESTIC_REPOSITORY="$REPO_PATH" |
|
|
|
|
|
log_info "Environment configuration:" |
|
|
log_info " Repository path: $REPO_PATH" |
|
|
log_info " Target path: $TARGET_PATH" |
|
|
log_info " Restore mode: $RESTORE_MODE" |
|
|
log_info " Snapshot: latest (fixed to use latest snapshot)" |
|
|
|
|
|
return 0 |
|
|
} |
|
|
|
|
|
|
|
|
check_repository() { |
|
|
log_info "Checking repository status..." |
|
|
|
|
|
if ! restic snapshots > /dev/null 2>&1; then |
|
|
log_warn "Cannot access Restic repository, please check repository path and password" |
|
|
log_warn "This might be the first startup or repository not yet initialized" |
|
|
return 1 |
|
|
fi |
|
|
|
|
|
log_success "Repository status normal" |
|
|
return 0 |
|
|
} |
|
|
|
|
|
|
|
|
list_snapshots() { |
|
|
log_info "Available snapshots list:" |
|
|
restic snapshots --compact |
|
|
} |
|
|
|
|
|
|
|
|
perform_restore() { |
|
|
local target_path="$1" |
|
|
|
|
|
log_info "Restoring latest snapshot to: $target_path" |
|
|
|
|
|
|
|
|
log_info "Analyzing latest snapshot backup paths, preparing for content restore..." |
|
|
|
|
|
|
|
|
local backup_paths=() |
|
|
local snapshot_info |
|
|
if snapshot_info=$(restic snapshots --json --latest 1 2>/dev/null); then |
|
|
local paths_json |
|
|
paths_json=$(echo "$snapshot_info" | jq -r '.[0].paths[]?' 2>/dev/null) |
|
|
if [[ -n "$paths_json" ]]; then |
|
|
while IFS= read -r path; do |
|
|
[[ -n "$path" ]] && backup_paths+=("$path") |
|
|
done <<< "$paths_json" |
|
|
log_info "Detected backup paths: ${backup_paths[*]}" |
|
|
fi |
|
|
fi |
|
|
|
|
|
|
|
|
local snapshot_target="latest" |
|
|
if [[ ${#backup_paths[@]} -gt 0 ]]; then |
|
|
|
|
|
local content_root="${backup_paths[0]}" |
|
|
log_info "Using content restore mode, only restore $content_root contents to $target_path" |
|
|
|
|
|
|
|
|
snapshot_target="latest:$content_root" |
|
|
log_info "Converting to content restore: $snapshot_target" |
|
|
else |
|
|
log_warn "Cannot determine backup paths, using traditional full path restore" |
|
|
fi |
|
|
|
|
|
|
|
|
if [[ "$RESTORE_MODE" == "replace" ]]; then |
|
|
log_info "Replace mode: preparing to clear target directory..." |
|
|
|
|
|
if [[ -d "$target_path" && -n "$(ls -A "$target_path" 2>/dev/null)" ]]; then |
|
|
log_warn "Replace mode will clear target directory: $target_path" |
|
|
if [[ "$FORCE" != "true" ]]; then |
|
|
read -p "Confirm clear target directory and restore? (y/N): " -n 1 -r |
|
|
echo |
|
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { log_info "User cancelled operation"; return 1; } |
|
|
fi |
|
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then |
|
|
log_info "Dry run mode: will clear $target_path" |
|
|
else |
|
|
log_info "Clearing target directory: $target_path" |
|
|
|
|
|
if [[ "$target_path" =~ ^/(bin|sbin|usr|lib|lib64|etc|proc|sys|dev|boot)$ ]]; then |
|
|
log_error "Refusing to clear system directory: $target_path" |
|
|
return 1 |
|
|
fi |
|
|
|
|
|
|
|
|
find "$target_path" -mindepth 1 -delete 2>/dev/null || { |
|
|
|
|
|
for item in "$target_path"/{*,.[^.]*}; do |
|
|
[[ -e "$item" ]] && rm -rf "$item" |
|
|
done |
|
|
} |
|
|
log_success "Target directory cleared: $target_path" |
|
|
fi |
|
|
else |
|
|
log_info "Target directory is empty or does not exist, skipping clear: $target_path" |
|
|
fi |
|
|
fi |
|
|
|
|
|
|
|
|
if [[ "$RESTORE_MODE" == "direct" && -d "$target_path" && -n "$(ls -A "$target_path" 2>/dev/null)" ]]; then |
|
|
log_warn "Target path is not empty, may overwrite existing files: $target_path" |
|
|
if [[ "$FORCE" != "true" ]]; then |
|
|
read -p "Continue? (y/N): " -n 1 -r |
|
|
echo |
|
|
[[ ! $REPLY =~ ^[Yy]$ ]] && { log_info "User cancelled operation"; return 1; } |
|
|
fi |
|
|
fi |
|
|
|
|
|
|
|
|
mkdir -p "$target_path" |
|
|
|
|
|
|
|
|
local args=("restore" "$snapshot_target" "--target" "$target_path") |
|
|
|
|
|
|
|
|
for pattern in "${INCLUDE_PATTERNS[@]}"; do |
|
|
args+=("--include" "$pattern") |
|
|
done |
|
|
|
|
|
for pattern in "${EXCLUDE_PATTERNS[@]}"; do |
|
|
args+=("--exclude" "$pattern") |
|
|
done |
|
|
|
|
|
[[ "$VERBOSE" == "true" ]] && args+=("--verbose") |
|
|
|
|
|
log_info "Executing restore command: restic ${args[*]}" |
|
|
|
|
|
if [[ "$DRY_RUN" == "true" ]]; then |
|
|
log_info "Dry run mode, not performing actual restore" |
|
|
return 0 |
|
|
fi |
|
|
|
|
|
if restic "${args[@]}"; then |
|
|
log_success "Restore completed: $target_path" |
|
|
|
|
|
|
|
|
if [[ "$RESTORE_MODE" == "safe" ]]; then |
|
|
log_info "Safe mode: Data restored to safe directory, please check and manually copy to final location" |
|
|
log_info "Example: rsync -av $target_path/ /desired/location/" |
|
|
elif [[ "$RESTORE_MODE" == "direct" ]]; then |
|
|
log_info "Direct mode: Data restored directly to specified directory" |
|
|
elif [[ "$RESTORE_MODE" == "replace" ]]; then |
|
|
log_info "Replace mode: Target directory intelligently cleared and data restored" |
|
|
fi |
|
|
|
|
|
return 0 |
|
|
else |
|
|
log_error "Restore failed" |
|
|
return 1 |
|
|
fi |
|
|
} |
|
|
|
|
|
|
|
|
main() { |
|
|
local action="restore" |
|
|
|
|
|
|
|
|
INCLUDE_PATTERNS=() |
|
|
EXCLUDE_PATTERNS=() |
|
|
|
|
|
|
|
|
while [[ $# -gt 0 ]]; do |
|
|
case $1 in |
|
|
-h|--help) |
|
|
show_usage |
|
|
exit 0 |
|
|
;; |
|
|
-l|--list) |
|
|
action="list" |
|
|
shift |
|
|
;; |
|
|
-c|--check) |
|
|
action="check" |
|
|
shift |
|
|
;; |
|
|
-r|--repo) |
|
|
RESTIC_REPOSITORY_PATH="$2" |
|
|
shift 2 |
|
|
;; |
|
|
-p|--password-file) |
|
|
PASSWORD_FILE="$2" |
|
|
shift 2 |
|
|
;; |
|
|
-t|--target) |
|
|
TARGET_PATH="$2" |
|
|
shift 2 |
|
|
;; |
|
|
-m|--mode) |
|
|
RESTORE_MODE="$2" |
|
|
if [[ "$RESTORE_MODE" != "safe" && "$RESTORE_MODE" != "direct" && "$RESTORE_MODE" != "replace" ]]; then |
|
|
log_error "Invalid restore mode: $RESTORE_MODE (only supports: safe, direct, replace)" |
|
|
exit 1 |
|
|
fi |
|
|
shift 2 |
|
|
;; |
|
|
-i|--include) |
|
|
INCLUDE_PATTERNS+=("$2") |
|
|
shift 2 |
|
|
;; |
|
|
-e|--exclude) |
|
|
EXCLUDE_PATTERNS+=("$2") |
|
|
shift 2 |
|
|
;; |
|
|
-v|--verbose) |
|
|
VERBOSE="true" |
|
|
shift |
|
|
;; |
|
|
-f|--force) |
|
|
FORCE="true" |
|
|
shift |
|
|
;; |
|
|
--dry-run) |
|
|
DRY_RUN="true" |
|
|
shift |
|
|
;; |
|
|
-*) |
|
|
log_error "Unknown option: $1" |
|
|
show_usage |
|
|
exit 1 |
|
|
;; |
|
|
*) |
|
|
log_warn "Ignoring argument: $1 (this version only supports latest snapshot)" |
|
|
shift |
|
|
;; |
|
|
esac |
|
|
done |
|
|
|
|
|
|
|
|
if ! check_restic; then |
|
|
exit 1 |
|
|
fi |
|
|
|
|
|
|
|
|
if ! setup_environment; then |
|
|
exit 1 |
|
|
fi |
|
|
|
|
|
|
|
|
if ! check_repository; then |
|
|
exit 1 |
|
|
fi |
|
|
|
|
|
|
|
|
case $action in |
|
|
list) |
|
|
list_snapshots |
|
|
;; |
|
|
check) |
|
|
log_info "Performing repository integrity check..." |
|
|
if restic check; then |
|
|
log_success "Repository integrity check passed" |
|
|
else |
|
|
log_error "Repository integrity check failed" |
|
|
exit 1 |
|
|
fi |
|
|
;; |
|
|
restore) |
|
|
|
|
|
perform_restore "$TARGET_PATH" |
|
|
;; |
|
|
esac |
|
|
} |
|
|
|
|
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then |
|
|
main "$@" |
|
|
fi |