Files
awoooi/scripts/backup/configure-offsite-rclone.sh
Your Name cfb866d055
Some checks failed
Ansible Lint / lint (push) Successful in 35s
CD Pipeline / tests (push) Failing after 13s
CD Pipeline / build-and-deploy (push) Has been skipped
CD Pipeline / post-deploy-checks (push) Has been skipped
Code Review / ai-code-review (push) Failing after 11s
feat(governance): add agent market automation surfaces
2026-06-04 21:50:55 +08:00

252 lines
8.5 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# =============================================================================
# WOOO AIOps - Configure provider-neutral rclone offsite target
# 2026-05-19 ogt + Codex: Google Drive 成為優先 offsite 目標。
#
# 安全邊界:
# - 這支腳本只寫 /backup/scripts/offsite.env 的 provider/remote/path 設定。
# - Google Drive OAuth token 由 rclone 自己保存在 host-local rclone.conf。
# - 不把 token、refresh token、password 或 recovery code 印到畫面。
# =============================================================================
set -euo pipefail
BACKUP_BASE="${BACKUP_BASE:-/backup}"
OFFSITE_ENV_FILE="${BACKUP_OFFSITE_ENV_FILE:-${BACKUP_BASE}/scripts/offsite.env}"
REQUESTED_REMOTE_NAME="${OFFSITE_RCLONE_REMOTE:-}"
REQUESTED_REMOTE_ROOT="${OFFSITE_REMOTE_ROOT:-}"
if [ -f "${OFFSITE_ENV_FILE}" ]; then
# shellcheck disable=SC1090
source "${OFFSITE_ENV_FILE}"
fi
REMOTE_NAME="${REQUESTED_REMOTE_NAME:-${OFFSITE_RCLONE_REMOTE:-gdrive}}"
REMOTE_ROOT="${REQUESTED_REMOTE_ROOT:-${OFFSITE_REMOTE_ROOT:-${REMOTE_NAME}:awoooi-backups/restic}}"
SOURCE_REMOTE="${OFFSITE_RCLONE_SOURCE_REMOTE:-gdrive}"
ROOT_REMOTE_NAME="${OFFSITE_RCLONE_ROOT_REMOTE:-gdrive_awoooi_restic}"
ROOT_REMOTE_PATH="${OFFSITE_RCLONE_ROOT_PATH:-awoooi-backups/restic}"
MODE="status"
usage() {
cat <<'USAGE'
Usage:
configure-offsite-rclone.sh --status
configure-offsite-rclone.sh --interactive
OFFSITE_RCLONE_REMOTE=gdrive OFFSITE_REMOTE_ROOT=gdrive:awoooi-backups/restic configure-offsite-rclone.sh --write-from-env
OFFSITE_RCLONE_SOURCE_REMOTE=gdrive OFFSITE_RCLONE_ROOT_REMOTE=gdrive_awoooi_restic configure-offsite-rclone.sh --create-root-remote
Notes:
- Google Drive 請先用 --interactive 進入 rclone config建立 remote例如 gdrive。
- --create-root-remote 會用既有 OAuth remote 建立 root-scoped remote避免每次從整個 Drive 查找路徑。
- /backup/scripts/offsite.env 只保存 remote 名稱與路徑,不保存 OAuth token。
- rclone.conf 是 host-local secret必須納入 credential escrow不可進 repo。
USAGE
}
while [ "$#" -gt 0 ]; do
case "$1" in
--status)
MODE="status"
shift
;;
--interactive)
MODE="interactive"
shift
;;
--write-from-env)
MODE="write-from-env"
shift
;;
--create-root-remote)
MODE="create-root-remote"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
quote_shell() {
printf "%s" "$1" | sed "s/'/'\\\\''/g; 1s/^/'/; \$s/\$/'/"
}
rclone_present() {
command -v rclone >/dev/null 2>&1
}
remote_configured() {
rclone_present || return 1
rclone listremotes 2>/dev/null | grep -Fxq "${REMOTE_NAME}:"
}
source_remote_configured() {
rclone_present || return 1
rclone listremotes 2>/dev/null | grep -Fxq "${SOURCE_REMOTE}:"
}
env_mode_ok() {
[ -f "${OFFSITE_ENV_FILE}" ] || return 1
mode="$(stat -c '%a' "${OFFSITE_ENV_FILE}" 2>/dev/null || stat -f '%Lp' "${OFFSITE_ENV_FILE}" 2>/dev/null || echo unknown)"
[ "${mode}" = "600" ]
}
write_env() {
install -d -m 750 "$(dirname "${OFFSITE_ENV_FILE}")"
umask 077
cat > "${OFFSITE_ENV_FILE}" <<EOF
# AWOOOI host-local offsite backup target.
# Created by configure-offsite-rclone.sh.
# No OAuth token or provider secret is stored in this file.
export OFFSITE_PROVIDER=rclone
export OFFSITE_RCLONE_REMOTE=$(quote_shell "${REMOTE_NAME}")
export OFFSITE_REMOTE_ROOT=$(quote_shell "${REMOTE_ROOT}")
export RCLONE_BWLIMIT=\${RCLONE_BWLIMIT:-8M}
export RCLONE_TRANSFERS=\${RCLONE_TRANSFERS:-2}
export RCLONE_CHECKERS=\${RCLONE_CHECKERS:-4}
export OFFSITE_RCLONE_BACKEND=\${OFFSITE_RCLONE_BACKEND:-drive}
export RCLONE_FAST_LIST=\${RCLONE_FAST_LIST:-1}
export RCLONE_DRIVE_USE_TRASH=\${RCLONE_DRIVE_USE_TRASH:-false}
export OFFSITE_SYNC_DELETE_OLD=\${OFFSITE_SYNC_DELETE_OLD:-1}
export OFFSITE_SYNC_MAX_LOAD_1=\${OFFSITE_SYNC_MAX_LOAD_1:-12}
export OFFSITE_SYNC_MAX_BACKUP_DISK_USED_PCT=\${OFFSITE_SYNC_MAX_BACKUP_DISK_USED_PCT:-92}
export OFFSITE_SYNC_REQUIRE_ENABLE_MARKER_FOR_FULL=\${OFFSITE_SYNC_REQUIRE_ENABLE_MARKER_FOR_FULL:-1}
export OFFSITE_SYNC_FULL_MIN_RUNWAY_MINUTES=\${OFFSITE_SYNC_FULL_MIN_RUNWAY_MINUTES:-270}
export OFFSITE_SYNC_NOTIFY_SKIPPED=\${OFFSITE_SYNC_NOTIFY_SKIPPED:-0}
export OFFSITE_SYNC_NOTIFY_SUCCESS=\${OFFSITE_SYNC_NOTIFY_SUCCESS:-0}
EOF
chmod 0600 "${OFFSITE_ENV_FILE}"
}
print_status() {
rclone_present && echo "RCLONE_PRESENT=1" || echo "RCLONE_PRESENT=0"
echo "OFFSITE_PROVIDER=rclone"
echo "OFFSITE_RCLONE_REMOTE=${REMOTE_NAME}"
echo "OFFSITE_REMOTE_ROOT=${REMOTE_ROOT}"
remote_configured && echo "RCLONE_REMOTE_CONFIGURED=1" || echo "RCLONE_REMOTE_CONFIGURED=0"
[ -f "${OFFSITE_ENV_FILE}" ] && echo "OFFSITE_ENV_PRESENT=1" || echo "OFFSITE_ENV_PRESENT=0"
env_mode_ok && echo "OFFSITE_ENV_MODE_OK=1" || echo "OFFSITE_ENV_MODE_OK=0"
}
root_remote_parent_path() {
if [[ "${ROOT_REMOTE_PATH}" == */* ]]; then
printf '%s' "${ROOT_REMOTE_PATH%/*}"
fi
}
root_remote_leaf_name() {
printf '%s/' "${ROOT_REMOTE_PATH##*/}"
}
create_root_scoped_remote() {
local parent_path
local parent_target
local leaf_name
local root_folder_id
local rclone_conf
if ! rclone_present; then
echo "rclone command is missing; install rclone first." >&2
exit 1
fi
if ! source_remote_configured; then
echo "source rclone remote missing: ${SOURCE_REMOTE}:" >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "python3 command is missing; cannot safely update rclone.conf without exposing token." >&2
exit 1
fi
parent_path="$(root_remote_parent_path)"
leaf_name="$(root_remote_leaf_name)"
if [ -n "${parent_path}" ]; then
parent_target="${SOURCE_REMOTE}:${parent_path}"
else
parent_target="${SOURCE_REMOTE}:"
fi
root_folder_id="$(rclone lsf --format pi "${parent_target}" --max-depth 1 \
| awk -F';' -v leaf="${leaf_name}" '$1 == leaf {print $2; exit}')"
if [ -z "${root_folder_id}" ]; then
echo "target Google Drive folder not found below ${SOURCE_REMOTE}: ${ROOT_REMOTE_PATH}" >&2
exit 1
fi
rclone_conf="$(rclone config file | awk 'previous {print; exit} /Configuration file is stored at:/ {previous=1}')"
if [ -z "${rclone_conf}" ] || [ ! -f "${rclone_conf}" ]; then
echo "rclone config file not found" >&2
exit 1
fi
SOURCE_REMOTE="${SOURCE_REMOTE}" ROOT_REMOTE_NAME="${ROOT_REMOTE_NAME}" ROOT_FOLDER_ID="${root_folder_id}" RCLONE_CONF="${rclone_conf}" python3 - <<'PY'
import configparser
import os
path = os.environ["RCLONE_CONF"]
src = os.environ["SOURCE_REMOTE"]
dst = os.environ["ROOT_REMOTE_NAME"]
root_id = os.environ["ROOT_FOLDER_ID"]
cp = configparser.ConfigParser()
cp.read(path)
if not cp.has_section(src):
raise SystemExit("source remote missing")
if not cp.has_section(dst):
cp.add_section(dst)
for key, value in cp.items(src):
cp.set(dst, key, value)
cp.set(dst, "root_folder_id", root_id)
with open(path, "w") as fh:
cp.write(fh)
os.chmod(path, 0o600)
PY
REMOTE_NAME="${ROOT_REMOTE_NAME}"
REMOTE_ROOT="${ROOT_REMOTE_NAME}:"
write_env
echo "ROOT_SCOPED_REMOTE_READY=${ROOT_REMOTE_NAME}:"
echo "ROOT_SCOPED_PATH=${ROOT_REMOTE_PATH}"
print_status
}
case "${MODE}" in
status)
print_status
;;
write-from-env)
write_env
print_status
;;
create-root-remote)
create_root_scoped_remote
;;
interactive)
if ! rclone_present; then
echo "rclone command is missing; install rclone first." >&2
exit 1
fi
echo "Current target remote name: ${REMOTE_NAME}"
read -r -p "Google Drive rclone remote name [${REMOTE_NAME}]: " remote_input
REMOTE_NAME="${remote_input:-${REMOTE_NAME}}"
REMOTE_ROOT="${OFFSITE_REMOTE_ROOT:-${REMOTE_NAME}:awoooi-backups/restic}"
if ! remote_configured; then
echo "rclone remote ${REMOTE_NAME}: 尚未存在,接著會進入 rclone config。"
echo "請選 Google Drive完成 OAuth不要把 token 貼到聊天或 repo。"
rclone config
fi
read -r -p "Offsite remote root [${REMOTE_ROOT}]: " root_input
REMOTE_ROOT="${root_input:-${REMOTE_ROOT}}"
write_env
print_status
;;
esac