Skip to content

Commit 10658de

Browse files
authored
Merge pull request #33 from buildplan/allow_files
allow file based backups and improved notification functions
2 parents cc1df1c + 1375cc5 commit 10658de

File tree

2 files changed

+122
-93
lines changed

2 files changed

+122
-93
lines changed

restic-backup.sh

Lines changed: 121 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
#!/usr/bin/env bash
22

33
# =================================================================
4-
# Restic Backup Script v0.39 - 2025.10.25
4+
# Restic Backup Script v0.40 - 2025.11.18
55
# =================================================================
66

77
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
88
set -euo pipefail
99
umask 077
1010

1111
# --- Script Constants ---
12-
SCRIPT_VERSION="0.39"
12+
SCRIPT_VERSION="0.40"
1313
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
1414
PROG_NAME=$(basename "$0"); readonly PROG_NAME
1515
CONFIG_FILE="${SCRIPT_DIR}/restic-backup.conf"
@@ -47,31 +47,46 @@ fi
4747
# =================================================================
4848

4949
import_restic_key() {
50-
local fpr="CF8F18F2844575973F79D4E191A6868BD3F7A907"
51-
# Check local user keyring
50+
local fpr servers debian_keyring
51+
52+
# Official Fingerprint
53+
fpr="CF8F18F2844575973F79D4E191A6868BD3F7A907"
54+
55+
# 1. Check local user keyring first
5256
if gpg --list-keys "$fpr" >/dev/null 2>&1; then
5357
return 0
5458
fi
55-
# Check Debian/Ubuntu system keyring
56-
local debian_keyring="/usr/share/keyrings/restic-archive-keyring.gpg"
57-
if [[ -f "$debian_keyring" ]]; then
58-
echo "Found debian keyring, checking for key..."
59-
if gpg --no-default-keyring --keyring "$debian_keyring" --list-keys "$fpr" >/dev/null 2>&1; then
60-
echo "Importing trusted key from system keyring..."
61-
gpg --no-default-keyring --keyring "$debian_keyring" --export "$fpr" | gpg --import >/dev/null 2>&1
62-
return $?
63-
fi
59+
60+
echo "Restic PGP key not found. Attempting import..."
61+
62+
# 2. Attempt Direct Download from Restic.net
63+
echo "Attempting direct download from restic.net..."
64+
if curl -sL "https://restic.net/gpg-key-alex.asc" | gpg --import >/dev/null 2>&1; then
65+
echo "Key imported successfully via direct download."
66+
return 0
6467
fi
65-
# Try public keyservers fallback
66-
local servers=( "hkps://keys.openpgp.org" "hkps://keyserver.ubuntu.com" )
68+
69+
# 3. Try Keyservers
70+
servers=( "hkps://keyserver.ubuntu.com" "hkps://keys.openpgp.org" "hkps://pgp.mit.edu" )
6771
for server in "${servers[@]}"; do
6872
echo "Attempting to fetch from $server..."
69-
if gpg --keyserver "$server" --recv-keys "$fpr"; then
70-
echo "Key imported successfully."
73+
if gpg --keyserver "$server" --recv-keys "$fpr" >/dev/null 2>&1; then
74+
echo "Key imported successfully from $server."
7175
return 0
7276
fi
7377
done
74-
echo "Failed to import restic PGP key." >&2
78+
79+
# 4. Check Debian/Ubuntu system keyring (Fallback for apt-installed systems)
80+
debian_keyring="/usr/share/keyrings/restic-archive-keyring.gpg"
81+
if [[ -f "$debian_keyring" ]]; then
82+
echo "Checking system keyring..."
83+
if gpg --no-default-keyring --keyring "$debian_keyring" --export "$fpr" | gpg --import >/dev/null 2>&1; then
84+
echo "Imported from system keyring."
85+
return 0
86+
fi
87+
fi
88+
89+
echo -e "${C_RED}Failed to import restic PGP key from all sources.${C_RESET}" >&2
7590
return 1
7691
}
7792

@@ -258,7 +273,7 @@ if [ ! -f "$CONFIG_FILE" ]; then
258273
echo -e "${C_RED}ERROR: Configuration file not found: $CONFIG_FILE${C_RESET}" >&2
259274
exit 1
260275
fi
261-
# shellcheck source=restic-backup.conf
276+
# shellcheck source=/dev/null
262277
source "$CONFIG_FILE"
263278
REQUIRED_VARS=(
264279
"RESTIC_REPOSITORY"
@@ -326,7 +341,7 @@ display_help() {
326341
echo -e "${C_BOLD}${C_YELLOW}DEPENDENCIES:${C_RESET}"
327342
echo -e " This script requires: ${C_GREEN}restic, curl, gpg, bzip2, less, jq, flock${C_RESET}"
328343
echo
329-
echo -e "Config: ${C_DIM}${CONFIG_FILE}${C_RESET} Log: ${C_DIM}${LOG_FILE}${C_RESET}"
344+
echo -e "Config: ${C_DIM}${CONFIG_FILE}${C_RESET} Log: ${C_DIM}${LOG_FILE:-"(not set)"}${C_RESET}"
330345
echo
331346
echo -e "For full details, see the online documentation: \e]8;;${readme_url}\a${C_CYAN}README.md${C_RESET}\e]8;;\a"
332347
echo -e "${C_YELLOW}Note:${C_RESET} For restic official documentation See: https://restic.readthedocs.io/"
@@ -354,7 +369,9 @@ handle_crash() {
354369

355370
build_backup_command() {
356371
local cmd=(restic)
357-
cmd+=($(get_verbosity_flags))
372+
local -a v_flags
373+
read -ra v_flags <<< "$(get_verbosity_flags)"
374+
cmd+=("${v_flags[@]}")
358375
if [ -n "${SFTP_CONNECTIONS:-}" ]; then
359376
cmd+=(-o "sftp.connections=${SFTP_CONNECTIONS}")
360377
fi
@@ -462,7 +479,7 @@ run_unlock() {
462479
echo -e "${C_YELLOW}Found stale locks in the repository:${C_RESET}"
463480
echo "$lock_info"
464481
local other_processes
465-
other_processes=$(ps aux | grep 'restic ' | grep -v 'grep' || true)
482+
other_processes=$(pgrep -ax restic || true)
466483
if [ -n "$other_processes" ]; then
467484
echo -e "${C_YELLOW}WARNING: Another restic process appears to be running:${C_RESET}"
468485
echo "$other_processes"
@@ -563,9 +580,11 @@ send_ntfy() {
563580
if [[ "${NTFY_ENABLED:-false}" != "true" ]] || [ -z "${NTFY_TOKEN:-}" ] || [ -z "${NTFY_URL:-}" ]; then
564581
return 0
565582
fi
583+
local safe_title
584+
safe_title=$(echo "$title" | jq -R -r 'sub("\n"; " "; "g")')
566585
curl -s --max-time 15 \
567586
-u ":$NTFY_TOKEN" \
568-
-H "Title: $title" \
587+
-H "Title: $safe_title" \
569588
-H "Tags: $tags" \
570589
-H "Priority: $priority" \
571590
-d "$message" \
@@ -586,12 +605,13 @@ send_discord() {
586605
failure) color=15158332 ;;
587606
*) color=9807270 ;;
588607
esac
589-
local escaped_title escaped_message
590-
escaped_title=$(echo "$title" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
591-
escaped_message=$(echo "$message" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
592608
local json_payload
593-
printf -v json_payload '{"embeds": [{"title": "%s", "description": "%s", "color": %d, "timestamp": "%s"}]}' \
594-
"$escaped_title" "$escaped_message" "$color" "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"
609+
json_payload=$(jq -n \
610+
--arg title "$title" \
611+
--arg desc "$message" \
612+
--argjson color "$color" \
613+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)" \
614+
'{embeds: [{title: $title, description: $desc, color: $color, timestamp: $ts}]}')
595615
curl -s --max-time 15 \
596616
-H "Content-Type: application/json" \
597617
-d "$json_payload" \
@@ -602,6 +622,7 @@ send_teams() {
602622
local title="$1"
603623
local status="$2"
604624
local message="$3"
625+
605626
if [[ "${TEAMS_ENABLED:-false}" != "true" ]] || [ -z "${TEAMS_WEBHOOK_URL:-}" ]; then
606627
return 0
607628
fi
@@ -612,39 +633,39 @@ send_teams() {
612633
failure) color="attention" ;;
613634
*) color="default" ;;
614635
esac
615-
local escaped_title
616-
escaped_title=$(echo "$title" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
617-
local escaped_message
618-
escaped_message=$(echo "$message" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
619636
local json_payload
620-
printf -v json_payload '{
621-
"type": "message",
622-
"attachments": [{
623-
"contentType": "application/vnd.microsoft.card.adaptive",
624-
"content": {
625-
"type": "AdaptiveCard",
626-
"version": "1.4",
627-
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
628-
"body": [
629-
{
630-
"type": "TextBlock",
631-
"text": "%s",
632-
"weight": "bolder",
633-
"size": "large",
634-
"wrap": true,
635-
"color": "%s"
636-
},
637-
{
638-
"type": "TextBlock",
639-
"text": "%s",
640-
"wrap": true,
641-
"separator": true
642-
}
643-
],
644-
"msteams": { "width": "full", "entities": [] }
645-
}
646-
}]
647-
}' "$escaped_title" "$color" "$escaped_message"
637+
json_payload=$(jq -n \
638+
--arg title "$title" \
639+
--arg msg "$message" \
640+
--arg color "$color" \
641+
'{
642+
type: "message",
643+
attachments: [{
644+
contentType: "application/vnd.microsoft.card.adaptive",
645+
content: {
646+
type: "AdaptiveCard",
647+
version: "1.4",
648+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
649+
body: [
650+
{
651+
type: "TextBlock",
652+
text: $title,
653+
weight: "bolder",
654+
size: "large",
655+
wrap: true,
656+
color: $color
657+
},
658+
{
659+
type: "TextBlock",
660+
text: $msg,
661+
wrap: true,
662+
separator: true
663+
}
664+
],
665+
msteams: { width: "full", entities: [] }
666+
}
667+
}]
668+
}')
648669
curl -s --max-time 15 \
649670
-H "Content-Type: application/json" \
650671
-d "$json_payload" \
@@ -665,33 +686,35 @@ send_slack() {
665686
failure) color="#d50200" ;;
666687
*) color="#808080" ;;
667688
esac
668-
local escaped_title escaped_message
669-
escaped_title=$(echo "$title" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g')
670-
escaped_message=$(echo "$message" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | sed ':a;N;$!ba;s/\n/\\n/g')
671689
local json_payload
672-
printf -v json_payload '{
673-
"attachments": [
674-
{
675-
"color": "%s",
676-
"blocks": [
677-
{
678-
"type": "header",
679-
"text": {
680-
"type": "plain_text",
681-
"text": "%s"
682-
}
683-
},
684-
{
685-
"type": "section",
686-
"text": {
687-
"type": "mrkdwn",
688-
"text": "%s"
690+
json_payload=$(jq -n \
691+
--arg title "$title" \
692+
--arg msg "$message" \
693+
--arg color "$color" \
694+
'{
695+
attachments: [
696+
{
697+
color: $color,
698+
blocks: [
699+
{
700+
type: "header",
701+
text: {
702+
type: "plain_text",
703+
text: $title,
704+
emoji: true
705+
}
706+
},
707+
{
708+
type: "section",
709+
text: {
710+
type: "mrkdwn",
711+
text: $msg
712+
}
713+
}
714+
]
689715
}
690-
}
691-
]
692-
}
693-
]
694-
}' "$color" "$escaped_title" "$escaped_message"
716+
]
717+
}')
695718
curl -s --max-time 15 \
696719
-H "Content-Type: application/json" \
697720
-d "$json_payload" \
@@ -876,8 +899,9 @@ run_preflight_checks() {
876899
fi
877900
for source in "${BACKUP_SOURCES[@]}"; do
878901
if [[ "$verbosity" == "verbose" ]]; then printf " %-65s" "Source directory ('$source')..."; fi
879-
if [ ! -d "$source" ] || [ ! -r "$source" ]; then
880-
handle_failure "Source directory not found or not readable: $source" "13"
902+
# Changed -d (directory) to -e (exists) to allow single file backups (v0.40)
903+
if [ ! -e "$source" ] || [ ! -r "$source" ]; then
904+
handle_failure "Source path not found or not readable: $source" "13"
881905
fi
882906
if [[ "$verbosity" == "verbose" ]]; then echo -e "[${C_GREEN} OK ${C_RESET}]"; fi
883907
done
@@ -1251,7 +1275,7 @@ run_backup() {
12511275
files_changed=$(grep "Files:" "$backup_log" | tail -1 | awk '{print $4}')
12521276
files_unmodified=$(grep "Files:" "$backup_log" | tail -1 | awk '{print $6}')
12531277
data_added=$(grep "Added to the repository:" "$backup_log" | tail -1 | awk '{print $5" "$6}')
1254-
data_processed=$(grep "processed" "$backup_log" | tail -1 | awk '{print $1" "$2}')
1278+
data_processed=$(grep "processed" "$backup_log" | tail -1 | awk '{print $2" "$3}' | tr -d ',')
12551279
fi
12561280
cat "$backup_log" >> "$LOG_FILE"
12571281
rm -f "$backup_log"
@@ -1261,10 +1285,11 @@ run_backup() {
12611285
log_message "Backup completed successfully"
12621286
echo -e "${C_GREEN}✅ Backup completed${C_RESET}"
12631287
local stats_msg
1264-
printf -v stats_msg "Files: %s new, %s changed, %s unmodified\nData added: %s\nDuration: %dm %ds" \
1288+
printf -v stats_msg "Files: %s new, %s changed, %s unmodified\nProcessed: %s\nData added: %s\nDuration: %dm %ds" \
12651289
"${files_new:-0}" \
12661290
"${files_changed:-0}" \
12671291
"${files_unmodified:-0}" \
1292+
"${data_processed:-0}" \
12681293
"${data_added:-Not applicable}" \
12691294
"$((duration / 60))" \
12701295
"$((duration % 60))"
@@ -1283,7 +1308,9 @@ run_forget() {
12831308
echo -e "${C_BOLD}--- Cleaning Old Snapshots ---${C_RESET}"
12841309
log_message "Running retention policy"
12851310
local forget_cmd=(restic)
1286-
forget_cmd+=($(get_verbosity_flags))
1311+
local -a v_flags
1312+
read -ra v_flags <<< "$(get_verbosity_flags)"
1313+
forget_cmd+=("${v_flags[@]}")
12871314
forget_cmd+=(forget)
12881315
[ -n "${KEEP_LAST:-}" ] && forget_cmd+=(--keep-last "$KEEP_LAST")
12891316
[ -n "${KEEP_DAILY:-}" ] && forget_cmd+=(--keep-daily "$KEEP_DAILY")
@@ -1459,7 +1486,9 @@ _run_restore_command() {
14591486
shift 2
14601487
mkdir -p "$restore_dest"
14611488
local restic_cmd=(restic)
1462-
restic_cmd+=($(get_verbosity_flags))
1489+
local -a v_flags
1490+
read -ra v_flags <<< "$(get_verbosity_flags)"
1491+
restic_cmd+=("${v_flags[@]}")
14631492
restic_cmd+=(restore "$snapshot_id" --target "$restore_dest")
14641493
if [ $# -gt 0 ]; then
14651494
for path in "$@"; do
@@ -1642,7 +1671,7 @@ echo "To restore a specific directory from the latest snapshot:"
16421671
# restic restore latest --target /mnt/restore --include "/home/user_files"
16431672
16441673
EOF
1645-
chmod 400 "$tmpfile"
1674+
chmod 400 "$tmpfile"
16461675
mv -f "$tmpfile" "$recovery_file"
16471676
echo -e "\n${C_GREEN}✅ Recovery Kit generated: ${C_BOLD}${recovery_file}${C_RESET}"
16481677
echo -e "${C_BOLD}${C_RED}WARNING: This file contains your repository password.${C_RESET}"

restic-backup.sh.sha256

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
6fa38405aef2bdeb4f6418630601ab1d91021c59ddbeea7fcfa219d39cc477c8 restic-backup.sh
1+
7a80f69fd6b2c4f9c4073abbe1f75504f69345634190a43cd4400975880fba15 restic-backup.sh

0 commit comments

Comments
 (0)