From 7c0dd2185ae1ea332f64a08f76338aaa7cd80fec Mon Sep 17 00:00:00 2001 From: blake Date: Thu, 9 Oct 2025 03:58:16 -0500 Subject: [PATCH] added backup browse --- bin/backup_browse.sh | 203 ++++++++++++++++++ bin/backup_browse.sh.bak | 173 +++++++++++++++ bin/backup_browse_fzf.sh | 64 ++++++ .../homelab/services/arr/prowlarr/default.nix | 2 +- 4 files changed, 441 insertions(+), 1 deletion(-) create mode 100755 bin/backup_browse.sh create mode 100755 bin/backup_browse.sh.bak create mode 100755 bin/backup_browse_fzf.sh diff --git a/bin/backup_browse.sh b/bin/backup_browse.sh new file mode 100755 index 0000000..0dbd084 --- /dev/null +++ b/bin/backup_browse.sh @@ -0,0 +1,203 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --- SUDO CHECK --- +if [ "$EUID" -ne 0 ]; then + echo "This script requires root privileges. Re-running with sudo..." + exec sudo "$0" "$@" +fi + +# --- HANDLE OPTIONS --- +BORG_PASSPHRASE="" +SHOW_FOLDERS_ONLY=false +SHOW_FILES=true # Default to showing files and directories + +while getopts "k:fp" opt; do + case "$opt" in + k) + BORG_PASSPHRASE=$(<"$OPTARG") + if [ -z "$BORG_PASSPHRASE" ]; then + echo "Error: The key file is empty." + exit 1 + fi + echo "Using passphrase from key file: $OPTARG" + ;; + f) + SHOW_FOLDERS_ONLY=true + echo "Only directories will be shown in fzf by default." + SHOW_FILES=false + ;; + p) + SHOW_FILES=true + echo "Both files and directories will be shown in fzf." + ;; + *) + echo "Usage: $0 [-k passphrase_file] [-f] [-p] " + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + +# --- FALLBACK TO /run/secrets/borg_passwd IF NO KEY FILE --- +if [ -z "$BORG_PASSPHRASE" ]; then + if [ $# -eq 0 ]; then + BORG_PASSPHRASE=$(<"/run/secrets/borg_passwd") + echo "Using passphrase from /run/secrets/borg_passwd" + else + # Prompt user for passphrase if neither -k nor /run/secrets/borg_passwd is available + read -s -p "Enter Borg repository passphrase: " BORG_PASSPHRASE + echo + fi +fi + +export BORG_PASSPHRASE + +# --- DEFAULT REPO --- +REPO="${1:-/holocron/backups}" + +# --- CHECK REQUIRED COMMANDS --- +for cmd in borg fzf find tree cp mkdir; do + command -v "$cmd" >/dev/null || { echo "Error: '$cmd' is required but not installed."; exit 1; } +done + +# --- LIST ARCHIVES (sorted, newest last) --- +mapfile -t archives < <(borg list --format="{archive}{NL}" "$REPO" | sort -r) +if [ ${#archives[@]} -eq 0 ]; then + echo "No archives found in $REPO" + exit 1 +fi + +# --- FZF ARCHIVE SELECT --- +selected=$(printf '%s\n' "${archives[@]}" | fzf --prompt="Select archive: " --height=40% --border) +if [ -z "$selected" ]; then + echo "No archive selected." + exit 1 +fi +echo "Selected archive: $selected" + +# --- GENERATE A UNIQUE, SHORTER MOUNT POINT --- +MOUNT_POINT="/tmp/$(uuidgen | sha256sum | head -c 2)-restore-${selected}" +mkdir -p "$MOUNT_POINT" + +# --- MOUNT ARCHIVE --- +echo "Mounting '$selected' to $MOUNT_POINT..." +borg mount "$REPO::$selected" "$MOUNT_POINT" + +if [ ! -d "$MOUNT_POINT" ]; then + echo "Error: mount failed." + exit 1 +fi + +# --- LIST FILES AND DIRECTORIES --- +echo "Scanning files and directories..." +if command -v fd >/dev/null 2>&1; then + # List both files and directories with fd + if [ "$SHOW_FILES" = true ]; then + files=$(fd . "$MOUNT_POINT" | sort) # Show both files and directories + else + files=$(fd --type d . "$MOUNT_POINT" | sort) # Only directories + fi +else + # Fall back to find if fd is not available + if [ "$SHOW_FILES" = true ]; then + files=$(find "$MOUNT_POINT" | sort) # Show both files and directories + else + files=$(find "$MOUNT_POINT" -type d | sort) # Only directories + fi +fi + +if [ -z "$files" ]; then + echo "No files or directories found in archive." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 1 +fi + +# --- FZF FILE/DIRECTORY SELECTION --- +if [ "$SHOW_FILES" = true ]; then + # If showing files and directories, we pass everything to fzf + selected_items=$(printf '%s\n' "$files" | sed "s|$MOUNT_POINT/||" | tac | fzf \ + --multi \ + --height=50% \ + --border \ + --prompt="Select files or directories to restore: " \ + --preview "tree -C -L 5 $MOUNT_POINT/$(dirname {})" \ + --preview-window=right:50% \ + --delimiter='/' \ + --with-nth=1..) +else + # If only showing directories, pass directories to fzf + selected_items=$(printf '%s\n' "$files" | sed "s|$MOUNT_POINT/||" | tac | fzf \ + --multi \ + --height=50% \ + --border \ + --prompt="Select directories to restore: " \ + --preview "tree -C -d -L 5 $MOUNT_POINT/$(dirname {})" \ + --preview-window=right:50% \ + --delimiter='/' \ + --with-nth=1..) +fi + +if [ -z "$selected_items" ]; then + echo "No items selected. Exiting." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 0 +fi + +# --- SUMMARY OF SELECTED ITEMS --- +echo "Selected items:" +for item in $selected_items; do + echo " $item" +done + +# --- OPTIONS MENU (concise) --- +# Default to option 1 if no input is given +echo "Select restore destination: 1) Restore to ./${selected}_restore 2) Restore to original dirs 3) Quit" +read -p "Enter your choice (1/2/3) [default: 1]: " choice +# Default to option 1 if user presses Enter without providing input +choice="${choice:-1}" + +# --- SET RESTORE DESTINATION BASED ON USER CHOICE --- +case "$choice" in + 1) + DEST="./${selected}_restore" + ;; + 2) + DEST="$MOUNT_POINT" + ;; + 3) + echo "Quitting. No items restored." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 0 + ;; + *) + echo "Invalid choice. Exiting." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 1 + ;; +esac + +mkdir -p "$DEST" + +# --- RESTORE SELECTED ITEMS (FILES OR DIRECTORIES) --- +echo "Restoring selected items..." +while IFS= read -r item; do + dest_path="$DEST/$item" + mkdir -p "$(dirname "$dest_path")" + if [ -d "$MOUNT_POINT/$item" ]; then + cp -r "$MOUNT_POINT/$item" "$dest_path" + else + cp -a "$MOUNT_POINT/$item" "$dest_path" + fi + echo "Restored: $item" +done <<< "$selected_items" + +# --- CLEANUP --- +borg umount "$MOUNT_POINT" +rm -rf "$MOUNT_POINT" +echo "Restore complete." + diff --git a/bin/backup_browse.sh.bak b/bin/backup_browse.sh.bak new file mode 100755 index 0000000..82b514d --- /dev/null +++ b/bin/backup_browse.sh.bak @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +set -euo pipefail + +# --- SUDO CHECK --- +if [ "$EUID" -ne 0 ]; then + echo "This script requires root privileges. Re-running with sudo..." + exec sudo "$0" "$@" +fi + +# --- HANDLE -k OPTION FOR KEY FILE --- +BORG_PASSPHRASE="" + +while getopts "k:" opt; do + case "$opt" in + k) + BORG_PASSPHRASE=$(<"$OPTARG") + if [ -z "$BORG_PASSPHRASE" ]; then + echo "Error: The key file is empty." + exit 1 + fi + echo "Using passphrase from key file: $OPTARG" + ;; + *) + echo "Usage: $0 [-k passphrase_file] " + exit 1 + ;; + esac +done +shift $((OPTIND - 1)) + +# --- FALLBACK TO /run/secrets/borg_passwd IF NO KEY FILE --- +if [ -z "$BORG_PASSPHRASE" ]; then + if [ -f "/run/secrets/borg_passwd" ]; then + BORG_PASSPHRASE=$(<"/run/secrets/borg_passwd") + echo "Using passphrase from /run/secrets/borg_passwd" + else + # Prompt user for passphrase if neither -k nor /run/secrets/borg_passwd is available + read -s -p "Enter Borg repository passphrase: " BORG_PASSPHRASE + echo + fi +fi + +export BORG_PASSPHRASE + +# --- DEFAULT REPO --- +REPO="${1:-/holocron/backups}" + +# --- CHECK REQUIRED COMMANDS --- +for cmd in borg fzf find tree cp mkdir; do + command -v "$cmd" >/dev/null || { echo "Error: '$cmd' is required but not installed."; exit 1; } +done + +# --- LIST ARCHIVES (sorted, newest last) --- +mapfile -t archives < <(borg list --format="{archive}{NL}" "$REPO" | sort) +if [ ${#archives[@]} -eq 0 ]; then + echo "No archives found in $REPO" + exit 1 +fi + +# --- FZF ARCHIVE SELECT --- +selected=$(printf '%s\n' "${archives[@]}" | fzf --prompt="Select archive: " --height=40% --border --reverse) +if [ -z "$selected" ]; then + echo "No archive selected." + exit 1 +fi +echo "Selected archive: $selected" + +# --- GENERATE A UNIQUE, SHORTER MOUNT POINT --- +MOUNT_POINT="/tmp/borg-mount-${selected}-$(uuidgen | sha256sum | head -c 6)" +mkdir -p "$MOUNT_POINT" + +# --- MOUNT ARCHIVE --- +echo "Mounting '$selected' to $MOUNT_POINT..." +borg mount "$REPO::$selected" "$MOUNT_POINT" + +if [ ! -d "$MOUNT_POINT" ]; then + echo "Error: mount failed." + exit 1 +fi + +# --- LIST FILES AND DIRECTORIES --- +echo "Scanning files and directories..." +if command -v fd >/dev/null 2>&1; then + # List files and directories using fd (can handle both files and dirs) + files=$(fd --type f --type d . "$MOUNT_POINT" | sort) +else + # Fall back to find if fd is not available + files=$(find "$MOUNT_POINT" -type f -o -type d | sort) +fi + +if [ -z "$files" ]; then + echo "No files or directories found in archive." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 1 +fi + +# --- HIERARCHICAL FZF FILE/DIRECTORY SELECTION (REVERSED) --- +# We reverse the order of files to display the latest (newest) files/folders at the top. +selected_files=$(printf '%s\n' "$files" | sed "s|$MOUNT_POINT/||" | tac | fzf \ + --multi \ + --height=50% \ + --border \ + --prompt="Select files or directories to restore: " \ + --preview "tree -C -L 5 $MOUNT_POINT/$(dirname {})" \ + --preview-window=right:50% \ + --delimiter='/' \ + --with-nth=1..) + +if [ -z "$selected_files" ]; then + echo "No files or directories selected. Exiting." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 0 +fi + +# --- SUMMARY OF SELECTED FILES/DIRECTORIES --- +echo "Selected files and directories:" +for file in $selected_files; do + echo " $file" +done + +# --- OPTIONS MENU (concise) --- +# Default to option 1 if no input is given +echo "Select restore destination: 1) Restore to ./${selected}_restore 2) Restore to original dirs 3) Quit" +read -p "Enter your choice (1/2/3) [default: 1]: " choice +# Default to option 1 if user presses Enter without providing input +choice="${choice:-1}" + +# --- SET RESTORE DESTINATION BASED ON USER CHOICE --- +case "$choice" in + 1) + DEST="./${selected}_restore" + ;; + 2) + DEST="$MOUNT_POINT" + ;; + 3) + echo "Quitting. No files restored." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 0 + ;; + *) + echo "Invalid choice. Exiting." + borg umount "$MOUNT_POINT" + rm -rf "$MOUNT_POINT" + exit 1 + ;; +esac + +mkdir -p "$DEST" + +# --- RESTORE FILES AND DIRECTORIES --- +echo "Restoring selected files and directories..." +while IFS= read -r file; do + # Path is already stripped of /tmp, so no need for further modification + dest_path="$DEST/$file" + mkdir -p "$(dirname "$dest_path")" + # If it's a directory, we use cp -r to ensure the directory structure is restored + if [ -d "$MOUNT_POINT/$file" ]; then + cp -r "$MOUNT_POINT/$file" "$dest_path" + else + cp -a "$MOUNT_POINT/$file" "$dest_path" + fi + echo "Restored: $file" +done <<< "$selected_files" + +# --- CLEANUP --- +borg umount "$MOUNT_POINT" +rm -rf "$MOUNT_POINT" +echo "Restore complete." + diff --git a/bin/backup_browse_fzf.sh b/bin/backup_browse_fzf.sh new file mode 100755 index 0000000..d3bb223 --- /dev/null +++ b/bin/backup_browse_fzf.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# borg-browser.sh — fzf-based Borg archive browser with passphrase prompt + +[ "$EUID" -ne 0 ] && { echo "Please run as root."; exec sudo "$0" "$@"; } + +REPO="/holocron/backups" + +# Prompt once for Borg passphrase +read -rs -p "Borg passphrase: " BORG_PASSPHRASE +echo +export BORG_PASSPHRASE + +# Pick an archive +ARCHIVE=$(borg list --short "$REPO" | fzf --prompt="Select archive: ") || { + unset BORG_PASSPHRASE + exit +} +[ -z "$ARCHIVE" ] && { unset BORG_PASSPHRASE; exit; } + +# Function to browse directories hierarchically +browse_borg_dir() { + local prefix="$1" + + while true; do + # Get immediate children of the current path + ITEMS=$(borg list --format='{path}{NL}' "$REPO::$ARCHIVE" \ + | awk -v p="$prefix" -F/ ' + BEGIN{n=split(p,a,"/")} + index($0,p)==1 && NF>n { + if (NF==n+1) print $NF; + else print $(n+1)"/"; + }' \ + | sort -u) + + [ -z "$ITEMS" ] && { echo "No items found in $prefix"; return; } + + SELECTION=$(echo -e "../\n$ITEMS" | fzf --prompt="${prefix:-/}> ") + case "$SELECTION" in + "../") + prefix="${prefix%/*}" + prefix="${prefix%/}" + ;; + "") + return + ;; + */) + prefix="${prefix:+$prefix/}${SELECTION%/}" + ;; + *) + local fullpath="${prefix:+$prefix/}$SELECTION" + echo "Selected file: $fullpath" + read -rp "Extract it here? [y/N]: " yn + if [[ $yn =~ ^[Yy]$ ]]; then + borg extract "$REPO::$ARCHIVE" "$fullpath" + fi + return + ;; + esac + done +} + +browse_borg_dir "" +unset BORG_PASSPHRASE + diff --git a/modules/homelab/services/arr/prowlarr/default.nix b/modules/homelab/services/arr/prowlarr/default.nix index ddde5e0..4cd2f27 100644 --- a/modules/homelab/services/arr/prowlarr/default.nix +++ b/modules/homelab/services/arr/prowlarr/default.nix @@ -4,7 +4,7 @@ let cfg = config.modules.services.prowlarr; ids = 2004; default_port = 9696; - data_dir = "/var/lib/prowlarr"; + data_dir = "/var/lib/private"; in { options.modules.services.prowlarr = {