Initial commit; base scripting and systemd user event code for borg based backup
This commit is contained in:
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
APP_NAME="nextcloud-borg-backup"
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
BIN_DIR="${HOME}/.local/bin"
|
||||
CONFIG_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}"
|
||||
STATE_DIR="${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}"
|
||||
SYSTEMD_USER_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
|
||||
|
||||
mkdir -p "$BIN_DIR" "$CONFIG_DIR" "$STATE_DIR/logs" "$SYSTEMD_USER_DIR"
|
||||
|
||||
install -m 0750 "$ROOT_DIR/scripts/nextcloud-borg-backup" "$BIN_DIR/nextcloud-borg-backup"
|
||||
|
||||
if [[ ! -f "$CONFIG_DIR/config" ]]; then
|
||||
install -m 0600 "$ROOT_DIR/etc/config.example" "$CONFIG_DIR/config"
|
||||
printf 'Installed config: %s\n' "$CONFIG_DIR/config"
|
||||
else
|
||||
printf 'Config exists, not overwriting: %s\n' "$CONFIG_DIR/config"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CONFIG_DIR/excludes" ]]; then
|
||||
install -m 0600 "$ROOT_DIR/etc/excludes.example" "$CONFIG_DIR/excludes"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$CONFIG_DIR/passphrase" ]]; then
|
||||
if command -v openssl >/dev/null 2>&1; then
|
||||
openssl rand -base64 48 > "$CONFIG_DIR/passphrase"
|
||||
else
|
||||
od -An -tx1 -N48 /dev/urandom | tr -d ' \n' > "$CONFIG_DIR/passphrase"
|
||||
printf '\n' >> "$CONFIG_DIR/passphrase"
|
||||
fi
|
||||
chmod 0600 "$CONFIG_DIR/passphrase"
|
||||
printf 'Created Borg passphrase file: %s\n' "$CONFIG_DIR/passphrase"
|
||||
printf 'Store a copy of this passphrase somewhere safe for disaster recovery.\n'
|
||||
fi
|
||||
|
||||
install -m 0644 "$ROOT_DIR/systemd/user/nextcloud-borg-backup.service" "$SYSTEMD_USER_DIR/nextcloud-borg-backup.service"
|
||||
install -m 0644 "$ROOT_DIR/systemd/user/nextcloud-borg-backup.timer" "$SYSTEMD_USER_DIR/nextcloud-borg-backup.timer"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user enable --now nextcloud-borg-backup.timer
|
||||
|
||||
printf '\nInstalled %s.\n' "$APP_NAME"
|
||||
printf 'Next steps:\n'
|
||||
printf ' 1. Check config: %s\n' "$CONFIG_DIR/config"
|
||||
printf ' 2. Run first backup: systemctl --user start nextcloud-borg-backup.service\n'
|
||||
printf ' 3. Watch logs: journalctl --user -u nextcloud-borg-backup.service -f\n'
|
||||
Executable
+438
@@ -0,0 +1,438 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
APP_NAME="nextcloud-borg-backup"
|
||||
APP_VERSION="1.0.0"
|
||||
|
||||
CONFIG_FILE="${NCB_CONFIG:-${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/config}"
|
||||
|
||||
die() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf 'WARN: %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
info() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
have() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
shell_quote() {
|
||||
printf '%q' "$1"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
nextcloud-borg-backup - Borg backup helper for a mounted Nextcloud folder
|
||||
|
||||
Usage:
|
||||
nextcloud-borg-backup backup
|
||||
nextcloud-borg-backup init
|
||||
nextcloud-borg-backup list
|
||||
nextcloud-borg-backup info [ARCHIVE]
|
||||
nextcloud-borg-backup search PATTERN [ARCHIVE]
|
||||
nextcloud-borg-backup mount [ARCHIVE] [MOUNT_DIR]
|
||||
nextcloud-borg-backup umount [MOUNT_DIR]
|
||||
nextcloud-borg-backup restore [ARCHIVE] [PATH] [DEST_DIR]
|
||||
nextcloud-borg-backup check [--verify-data]
|
||||
nextcloud-borg-backup prune
|
||||
nextcloud-borg-backup status
|
||||
nextcloud-borg-backup break-lock
|
||||
|
||||
Environment:
|
||||
NCB_CONFIG=/path/to/config
|
||||
EOF
|
||||
}
|
||||
|
||||
load_config() {
|
||||
[[ -r "$CONFIG_FILE" ]] || die "Config not readable: $CONFIG_FILE"
|
||||
# shellcheck disable=SC1090
|
||||
source "$CONFIG_FILE"
|
||||
|
||||
: "${SOURCE_DIR:=${HOME}/Nextcloud}"
|
||||
: "${REPO_DIR:=/net/thor/volume1/NetBackup/Nextcloud/borg-repo}"
|
||||
: "${ARCHIVE_PREFIX:=nextcloud}"
|
||||
: "${BORG_BIN:=borg}"
|
||||
: "${BORG_ENCRYPTION:=repokey-blake2}"
|
||||
: "${BORG_PASSPHRASE_FILE:=${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/passphrase}"
|
||||
: "${BORG_COMPRESSION:=zstd,6}"
|
||||
: "${BORG_REMOTE_PATH:=borg}"
|
||||
: "${BORG_FILES_CACHE:=}"
|
||||
: "${RETENTION_WITHIN:=4w}"
|
||||
: "${PRUNE_AFTER_BACKUP:=true}"
|
||||
: "${COMPACT_AFTER_PRUNE:=true}"
|
||||
: "${REPOSITORY_CHECK_AFTER_BACKUP:=true}"
|
||||
: "${FULL_CHECK_INTERVAL_DAYS:=7}"
|
||||
: "${FULL_CHECK_VERIFY_DATA:=false}"
|
||||
: "${ALLOW_EMPTY_SOURCE:=false}"
|
||||
: "${MIN_FREE_REPO_GB:=5}"
|
||||
: "${PRECHECK_TIMEOUT_SECONDS:=60}"
|
||||
: "${LOG_DIR:=${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}/logs}"
|
||||
: "${STATE_DIR:=${XDG_STATE_HOME:-${HOME}/.local/state}/${APP_NAME}}"
|
||||
: "${RESTORE_DIR:=${HOME}/Nextcloud-Restore}"
|
||||
: "${MOUNT_DIR:=${XDG_RUNTIME_DIR:-/tmp}/${APP_NAME}-mount}"
|
||||
: "${EXCLUDE_FILE:=${XDG_CONFIG_HOME:-${HOME}/.config}/${APP_NAME}/excludes}"
|
||||
: "${EXCLUDE_CACHES:=true}"
|
||||
: "${ONE_FILE_SYSTEM:=true}"
|
||||
: "${LOCK_FILE:=${STATE_DIR}/${APP_NAME}.lock}"
|
||||
|
||||
export BORG_RELOCATED_REPO_ACCESS_IS_OK="${BORG_RELOCATED_REPO_ACCESS_IS_OK:-yes}"
|
||||
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK="${BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK:-yes}"
|
||||
export BORG_REMOTE_PATH
|
||||
[[ -n "$BORG_FILES_CACHE" ]] && export BORG_FILES_CACHE
|
||||
|
||||
if [[ -f "$BORG_PASSPHRASE_FILE" ]]; then
|
||||
export BORG_PASSCOMMAND="cat $(shell_quote "$BORG_PASSPHRASE_FILE")"
|
||||
fi
|
||||
}
|
||||
|
||||
require_command() {
|
||||
have "$1" || die "Required command missing: $1"
|
||||
}
|
||||
|
||||
ensure_dirs() {
|
||||
mkdir -p "$LOG_DIR" "$STATE_DIR"
|
||||
}
|
||||
|
||||
log_file_for_run() {
|
||||
local timestamp
|
||||
timestamp="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||
printf '%s/%s_%s.log' "$LOG_DIR" "$APP_NAME" "$timestamp"
|
||||
}
|
||||
|
||||
start_logging() {
|
||||
ensure_dirs
|
||||
local log_file
|
||||
log_file="$(log_file_for_run)"
|
||||
exec > >(tee -a "$log_file") 2>&1
|
||||
info "== ${APP_NAME} ${APP_VERSION} =="
|
||||
info "-- Time: $(date -Is)"
|
||||
info "-- Config: $CONFIG_FILE"
|
||||
info "-- Log: $log_file"
|
||||
}
|
||||
|
||||
with_lock() {
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 || die "Another ${APP_NAME} run is already active: $LOCK_FILE"
|
||||
}
|
||||
|
||||
repo_parent() {
|
||||
dirname "$REPO_DIR"
|
||||
}
|
||||
|
||||
check_passphrase_for_encryption() {
|
||||
case "$BORG_ENCRYPTION" in
|
||||
none|authenticated*) return 0 ;;
|
||||
esac
|
||||
[[ -r "$BORG_PASSPHRASE_FILE" ]] || die "BORG_PASSPHRASE_FILE missing/read-protected: $BORG_PASSPHRASE_FILE"
|
||||
}
|
||||
|
||||
borg_repo_exists() {
|
||||
[[ -d "$REPO_DIR" && -f "$REPO_DIR/config" ]]
|
||||
}
|
||||
|
||||
init_repo() {
|
||||
require_command "$BORG_BIN"
|
||||
check_passphrase_for_encryption
|
||||
mkdir -p "$(repo_parent)"
|
||||
if borg_repo_exists; then
|
||||
info "-- Borg repository already exists: $REPO_DIR"
|
||||
return 0
|
||||
fi
|
||||
info "-- Initializing Borg repository: $REPO_DIR"
|
||||
"$BORG_BIN" init --encryption="$BORG_ENCRYPTION" "$REPO_DIR"
|
||||
}
|
||||
|
||||
timeout_cmd() {
|
||||
if have timeout; then
|
||||
timeout "$PRECHECK_TIMEOUT_SECONDS" "$@"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
count_source_entries() {
|
||||
timeout_cmd find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -print -quit | wc -l
|
||||
}
|
||||
|
||||
free_repo_gb() {
|
||||
df -PBG "$(repo_parent)" | awk 'NR==2 { gsub(/G/, "", $4); print $4 }'
|
||||
}
|
||||
|
||||
preflight() {
|
||||
require_command "$BORG_BIN"
|
||||
require_command flock
|
||||
require_command df
|
||||
require_command find
|
||||
|
||||
info "-- Source: $SOURCE_DIR"
|
||||
info "-- Repository: $REPO_DIR"
|
||||
|
||||
timeout_cmd test -d "$SOURCE_DIR" || die "Source directory is not reachable: $SOURCE_DIR"
|
||||
timeout_cmd test -r "$SOURCE_DIR" || die "Source directory is not readable: $SOURCE_DIR"
|
||||
|
||||
local entry_count
|
||||
entry_count="$(count_source_entries | tr -d '[:space:]')"
|
||||
if [[ "$ALLOW_EMPTY_SOURCE" != "true" && "${entry_count:-0}" -eq 0 ]]; then
|
||||
die "Source directory looks empty. Refusing to create an empty backup."
|
||||
fi
|
||||
|
||||
mkdir -p "$(repo_parent)"
|
||||
timeout_cmd test -w "$(repo_parent)" || die "Repository parent is not writable/reachable: $(repo_parent)"
|
||||
|
||||
local free_gb
|
||||
free_gb="$(free_repo_gb)"
|
||||
info "-- Free space at repository parent: ${free_gb}G"
|
||||
[[ "${free_gb:-0}" -ge "$MIN_FREE_REPO_GB" ]] || die "Not enough free space at repository parent; need ${MIN_FREE_REPO_GB}G"
|
||||
|
||||
if ! borg_repo_exists; then
|
||||
init_repo
|
||||
fi
|
||||
}
|
||||
|
||||
create_archive() {
|
||||
local timestamp archive
|
||||
timestamp="$(date '+%Y-%m-%d_%H-%M-%S')"
|
||||
archive="${ARCHIVE_PREFIX}-${timestamp}"
|
||||
|
||||
local borg_args
|
||||
borg_args=(
|
||||
create
|
||||
--stats
|
||||
--show-rc
|
||||
--compression "$BORG_COMPRESSION"
|
||||
)
|
||||
|
||||
[[ "$EXCLUDE_CACHES" == "true" ]] && borg_args+=(--exclude-caches)
|
||||
[[ "$ONE_FILE_SYSTEM" == "true" ]] && borg_args+=(--one-file-system)
|
||||
[[ -f "$EXCLUDE_FILE" ]] && borg_args+=(--exclude-from "$EXCLUDE_FILE")
|
||||
|
||||
info "-- Creating archive: $archive"
|
||||
(
|
||||
cd "$SOURCE_DIR"
|
||||
"$BORG_BIN" "${borg_args[@]}" "${REPO_DIR}::${archive}" .
|
||||
)
|
||||
}
|
||||
|
||||
archive_names() {
|
||||
"$BORG_BIN" list --short --glob-archives "${ARCHIVE_PREFIX}-*" "$REPO_DIR"
|
||||
}
|
||||
|
||||
latest_archive() {
|
||||
archive_names | tail -n 1
|
||||
}
|
||||
|
||||
prune_repo() {
|
||||
info "-- Pruning archives with retention: keep-within $RETENTION_WITHIN"
|
||||
"$BORG_BIN" prune --list --show-rc --glob-archives "${ARCHIVE_PREFIX}-*" --keep-within "$RETENTION_WITHIN" "$REPO_DIR"
|
||||
}
|
||||
|
||||
compact_repo() {
|
||||
info "-- Compacting repository"
|
||||
"$BORG_BIN" compact "$REPO_DIR"
|
||||
}
|
||||
|
||||
days_since_stamp() {
|
||||
local stamp="$1"
|
||||
[[ -f "$stamp" ]] || { printf '999999\n'; return 0; }
|
||||
local now then
|
||||
now="$(date +%s)"
|
||||
then="$(stat -c %Y "$stamp")"
|
||||
printf '%s\n' "$(((now - then) / 86400))"
|
||||
}
|
||||
|
||||
check_repo() {
|
||||
local verify_data="${1:-false}"
|
||||
if [[ "$verify_data" == "true" ]]; then
|
||||
info "-- Running full Borg check with data verification"
|
||||
"$BORG_BIN" check --verify-data "$REPO_DIR"
|
||||
else
|
||||
info "-- Running Borg repository check"
|
||||
"$BORG_BIN" check --repository-only "$REPO_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
maybe_periodic_full_check() {
|
||||
[[ "$FULL_CHECK_VERIFY_DATA" == "true" ]] || return 0
|
||||
local stamp="${STATE_DIR}/last-full-check"
|
||||
local age_days
|
||||
age_days="$(days_since_stamp "$stamp")"
|
||||
[[ "$age_days" -ge "$FULL_CHECK_INTERVAL_DAYS" ]] || return 0
|
||||
check_repo true
|
||||
date -Is > "$stamp"
|
||||
}
|
||||
|
||||
cmd_backup() {
|
||||
load_config
|
||||
start_logging
|
||||
with_lock
|
||||
preflight
|
||||
create_archive
|
||||
if [[ "$PRUNE_AFTER_BACKUP" == "true" ]]; then
|
||||
prune_repo
|
||||
[[ "$COMPACT_AFTER_PRUNE" == "true" ]] && compact_repo
|
||||
fi
|
||||
[[ "$REPOSITORY_CHECK_AFTER_BACKUP" == "true" ]] && check_repo false
|
||||
maybe_periodic_full_check
|
||||
info "-- Backup finished successfully"
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
"$BORG_BIN" list "$REPO_DIR"
|
||||
}
|
||||
|
||||
cmd_info() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
local archive="${1:-}"
|
||||
[[ -n "$archive" ]] || archive="$(latest_archive)"
|
||||
[[ -n "$archive" ]] || die "No archive found"
|
||||
"$BORG_BIN" info "${REPO_DIR}::${archive}"
|
||||
}
|
||||
|
||||
cmd_search() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
local pattern="${1:-}"
|
||||
local archive="${2:-}"
|
||||
[[ -n "$pattern" ]] || die "Missing search pattern"
|
||||
|
||||
if [[ -n "$archive" ]]; then
|
||||
"$BORG_BIN" list --short "${REPO_DIR}::${archive}" | grep -i -- "$pattern" || true
|
||||
return 0
|
||||
fi
|
||||
|
||||
local found=0
|
||||
while IFS= read -r archive; do
|
||||
[[ -n "$archive" ]] || continue
|
||||
while IFS= read -r path; do
|
||||
printf '%s\t%s\n' "$archive" "$path"
|
||||
found=1
|
||||
done < <("$BORG_BIN" list --short "${REPO_DIR}::${archive}" | grep -i -- "$pattern" || true)
|
||||
done < <(archive_names)
|
||||
[[ "$found" -eq 1 ]] || warn "No matches found for: $pattern"
|
||||
}
|
||||
|
||||
cmd_mount() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
local archive="${1:-}"
|
||||
local mount_dir="${2:-$MOUNT_DIR}"
|
||||
mkdir -p "$mount_dir"
|
||||
if [[ -n "$archive" ]]; then
|
||||
info "-- Mounting archive $archive at $mount_dir"
|
||||
"$BORG_BIN" mount "${REPO_DIR}::${archive}" "$mount_dir"
|
||||
else
|
||||
info "-- Mounting repository archive overview at $mount_dir"
|
||||
"$BORG_BIN" mount "$REPO_DIR" "$mount_dir"
|
||||
fi
|
||||
info "-- Mounted at: $mount_dir"
|
||||
}
|
||||
|
||||
cmd_umount() {
|
||||
load_config
|
||||
local mount_dir="${1:-$MOUNT_DIR}"
|
||||
if have borg; then
|
||||
"$BORG_BIN" umount "$mount_dir" 2>/dev/null && return 0
|
||||
fi
|
||||
if have fusermount3; then
|
||||
fusermount3 -u "$mount_dir"
|
||||
elif have fusermount; then
|
||||
fusermount -u "$mount_dir"
|
||||
else
|
||||
umount "$mount_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
normalize_restore_path() {
|
||||
local path="$1"
|
||||
path="${path#/}"
|
||||
path="${path#./}"
|
||||
printf '%s\n' "$path"
|
||||
}
|
||||
|
||||
cmd_restore() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
local archive="${1:-}"
|
||||
local restore_path="${2:-}"
|
||||
local dest_dir="${3:-}"
|
||||
|
||||
[[ -n "$archive" ]] || archive="$(latest_archive)"
|
||||
[[ -n "$archive" ]] || die "No archive found"
|
||||
[[ -n "$dest_dir" ]] || dest_dir="${RESTORE_DIR}/${archive}"
|
||||
mkdir -p "$dest_dir"
|
||||
|
||||
info "-- Restoring from archive: $archive"
|
||||
info "-- Destination: $dest_dir"
|
||||
(
|
||||
cd "$dest_dir"
|
||||
if [[ -n "$restore_path" ]]; then
|
||||
restore_path="$(normalize_restore_path "$restore_path")"
|
||||
"$BORG_BIN" extract "${REPO_DIR}::${archive}" "$restore_path"
|
||||
else
|
||||
"$BORG_BIN" extract "${REPO_DIR}::${archive}"
|
||||
fi
|
||||
)
|
||||
info "-- Restore finished: $dest_dir"
|
||||
}
|
||||
|
||||
cmd_check() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
local verify_data=false
|
||||
[[ "${1:-}" == "--verify-data" ]] && verify_data=true
|
||||
check_repo "$verify_data"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
info "Config: $CONFIG_FILE"
|
||||
info "Source: $SOURCE_DIR"
|
||||
info "Repository: $REPO_DIR"
|
||||
info "Retention: $RETENTION_WITHIN"
|
||||
info "Latest archive: $(latest_archive || true)"
|
||||
info
|
||||
"$BORG_BIN" info "$REPO_DIR"
|
||||
}
|
||||
|
||||
cmd_break_lock() {
|
||||
load_config
|
||||
require_command "$BORG_BIN"
|
||||
"$BORG_BIN" break-lock "$REPO_DIR"
|
||||
}
|
||||
|
||||
main() {
|
||||
local cmd="${1:-}"
|
||||
[[ $# -gt 0 ]] && shift || true
|
||||
|
||||
case "$cmd" in
|
||||
backup) cmd_backup "$@" ;;
|
||||
init) load_config; init_repo "$@" ;;
|
||||
list) cmd_list "$@" ;;
|
||||
info) cmd_info "$@" ;;
|
||||
search) cmd_search "$@" ;;
|
||||
mount) cmd_mount "$@" ;;
|
||||
umount|unmount) cmd_umount "$@" ;;
|
||||
restore) cmd_restore "$@" ;;
|
||||
check) cmd_check "$@" ;;
|
||||
prune) load_config; prune_repo "$@"; compact_repo ;;
|
||||
status) cmd_status "$@" ;;
|
||||
break-lock|unlock) cmd_break_lock "$@" ;;
|
||||
-h|--help|help|"") usage ;;
|
||||
--version|version) printf '%s %s\n' "$APP_NAME" "$APP_VERSION" ;;
|
||||
*) usage >&2; die "Unknown command: $cmd" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
APP_NAME="nextcloud-borg-backup"
|
||||
BIN_DIR="${HOME}/.local/bin"
|
||||
SYSTEMD_USER_DIR="${XDG_CONFIG_HOME:-${HOME}/.config}/systemd/user"
|
||||
|
||||
systemctl --user disable --now "${APP_NAME}.timer" 2>/dev/null || true
|
||||
systemctl --user daemon-reload
|
||||
|
||||
rm -f "${SYSTEMD_USER_DIR}/${APP_NAME}.service" "${SYSTEMD_USER_DIR}/${APP_NAME}.timer"
|
||||
rm -f "${BIN_DIR}/${APP_NAME}"
|
||||
systemctl --user daemon-reload
|
||||
|
||||
printf 'Removed user service, timer, and executable.\n'
|
||||
printf 'Kept config, passphrase, logs, restore directory, and Borg repository.\n'
|
||||
Reference in New Issue
Block a user