Outlineの標準機能を使用して定期的にエクスポート

Outlineのバックアップや復元は別環境があると認証部分によって正常に動作しない危険性があるので、標準機能を利用してエクスポートすることにしました。これなら復元時は特別なスクリプトを使用しなくても、標準機能でインポート出来ます。
事前にAPIキーを設定しておきます。

Outlineエクスポートスクリプト

mkdir -p ~/outline-tmp
cd ~/outline-tmp
nano outline-export-backup.sh
nano outline-export-install.sh
# 下記スクリプトを貼り付け
# 実行権限を付与
chmod +x outline-export-backup.sh
chmod +x Outline-export-install.sh
bash outline-export-install.sh
rm -rf ~/outline-tmp

outline-export-backup.sh

#!/bin/bash
set -euo pipefail
# =============================================================
#  Outline JSON エクスポート バックアップスクリプト
#
#  Outline 公式 API を使って全コレクションを JSON 形式で
#  エクスポートし、ローカルに保存します。
#
#  使い方:
#    sudo bash outline-export-backup.sh           # バックアップを作成
#    sudo bash outline-export-backup.sh list      # バックアップ一覧を表示
#    sudo bash outline-export-backup.sh clean     # 古いバックアップを削除
#
#  初回セットアップ:
#    1. Outline の Settings → API Keys で APIキーを作成
#    2. このスクリプトの設定欄に OUTLINE_URL と API_KEY を記入
#       または環境変数 OUTLINE_URL / OUTLINE_API_KEY で渡す
#
#  バックアップ保存先: /opt/lxd-data/outline-export/
#  バックアップ内容:
#    - 全コレクションのドキュメント (JSON形式)
#    - 添付ファイル (includeAttachments: true)
#    - プライベートコレクション (includePrivate: true)
#
#  注意:
#    - ユーザー情報・コレクション権限設定は含まれません
#    - 復元は Outline の Settings → Import から ZIP をアップロード
# =============================================================

# ════════════════════════════════════════════════════
#  ★ 設定欄 (環境変数で渡す場合は空欄のままでOK)
# ════════════════════════════════════════════════════
OUTLINE_URL="${OUTLINE_URL:-}"         # 例: https://hostname:3303
API_KEY="${OUTLINE_API_KEY:-}"         # Outline API キー
BACKUP_DIR="${OUTLINE_BACKUP_DIR:-/opt/lxd-data/outline-export}"
KEEP_DAYS="${OUTLINE_KEEP_DAYS:-30}"   # 何日分保持するか (デフォルト30日)

# エクスポートオプション
INCLUDE_ATTACHMENTS="true"   # 添付ファイルを含める
INCLUDE_PRIVATE="true"       # プライベートコレクションを含める

# API ポーリング設定
POLL_INTERVAL=5    # 秒: エクスポート完了確認の間隔
POLL_MAX=120       # 秒: 最大待機時間

# ════════════════════════════════════════════════════
#  カラー出力
# ════════════════════════════════════════════════════
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

info()    { echo -e "${GREEN}[INFO]${NC}  $*"; }
warn()    { echo -e "${YELLOW}[WARN]${NC}  $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }
section() { echo -e "\n${CYAN}==> $*${NC}"; }

print_banner() {
    echo ""
    echo "════════════════════════════════════════════════"
    echo "  Outline JSON エクスポート バックアップ"
    echo "════════════════════════════════════════════════"
    echo ""
}

usage() {
    print_banner
    echo "使い方:"
    echo "  $0            # バックアップを作成"
    echo "  $0 list       # バックアップ一覧を表示"
    echo "  $0 clean      # 古いバックアップを削除 (${KEEP_DAYS}日以上)"
    echo ""
    echo "設定 (環境変数または設定欄に記入):"
    echo "  OUTLINE_URL       Outline の公開URL  例: https://hostname:3303"
    echo "  OUTLINE_API_KEY   Outline の API キー"
    echo "  OUTLINE_BACKUP_DIR バックアップ保存先 (デフォルト: ${BACKUP_DIR})"
    echo "  OUTLINE_KEEP_DAYS  保持日数           (デフォルト: ${KEEP_DAYS}日)"
    echo ""
    echo "保存先: ${BACKUP_DIR}/"
    echo ""
}

# ════════════════════════════════════════════════════
#  依存コマンド確認
# ════════════════════════════════════════════════════
check_deps() {
    local MISSING=()
    for CMD in curl jq; do
        if ! command -v "${CMD}" &>/dev/null; then
            MISSING+=("${CMD}")
        fi
    done
    if [ ${#MISSING[@]} -gt 0 ]; then
        error "以下のコマンドが見つかりません: ${MISSING[*]}"
        error "インストール例: apt-get install -y curl jq"
        exit 1
    fi
}

# ════════════════════════════════════════════════════
#  設定値の確認・対話入力
# ════════════════════════════════════════════════════
check_config() {
    # OUTLINE_URL
    if [ -z "${OUTLINE_URL}" ]; then
        # docker の .env から自動取得を試みる
        local ENV_FILE="/opt/docker/outline/.env"
        if [ -f "${ENV_FILE}" ]; then
            local AUTO_URL
            AUTO_URL=$(grep '^URL=' "${ENV_FILE}" 2>/dev/null \
                | cut -d= -f2- | tr -d '"' || echo "")
            if [ -n "${AUTO_URL}" ]; then
                OUTLINE_URL="${AUTO_URL}"
                info ".env から Outline URL を取得: ${OUTLINE_URL}"
            fi
        fi
    fi

    if [ -z "${OUTLINE_URL}" ]; then
        echo ""
        warn "OUTLINE_URL が設定されていません"
        read -rp "  Outline の URL を入力してください (例: https://hostname:3303): " OUTLINE_URL
        if [ -z "${OUTLINE_URL}" ]; then
            error "URL が入力されませんでした"
            exit 1
        fi
    fi

    # 末尾スラッシュを除去
    OUTLINE_URL="${OUTLINE_URL%/}"

    # API_KEY
    if [ -z "${API_KEY}" ]; then
        echo ""
        warn "OUTLINE_API_KEY が設定されていません"
        echo "  Outline の Settings → API Keys でキーを作成してください"
        echo "  スクリプトの設定欄 (API_KEY=...) に記入すると次回から不要になります"
        read -rsp "  API キーを入力してください: " API_KEY
        echo ""
        if [ -z "${API_KEY}" ]; then
            error "API キーが入力されませんでした"
            exit 1
        fi
    fi
}

# ════════════════════════════════════════════════════
#  API 接続確認
# ════════════════════════════════════════════════════
check_api_connection() {
    section "Outline API に接続確認..."

    local HTTP_CODE
    HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
        -X POST \
        -H "Authorization: Bearer ${API_KEY}" \
        -H "Content-Type: application/json" \
        -d '{}' \
        "${OUTLINE_URL}/api/auth.info" 2>/dev/null || echo "000")

    if [ "${HTTP_CODE}" = "200" ]; then
        info "接続 OK (HTTP ${HTTP_CODE})"
    elif [ "${HTTP_CODE}" = "401" ]; then
        error "認証エラー (HTTP 401): API キーを確認してください"
        exit 1
    elif [ "${HTTP_CODE}" = "000" ]; then
        error "接続できませんでした: ${OUTLINE_URL}"
        error "Outline が起動しているか、URL が正しいか確認してください"
        exit 1
    else
        error "予期しないレスポンス (HTTP ${HTTP_CODE})"
        exit 1
    fi
}

# ════════════════════════════════════════════════════
#  エクスポートジョブを開始
# ════════════════════════════════════════════════════
start_export() {
    section "エクスポートジョブを開始..."

    local RESPONSE
    RESPONSE=$(curl -sS -X POST \
        -H "Authorization: Bearer ${API_KEY}" \
        -H "Content-Type: application/json" \
        -H "Accept: application/json" \
        -d "{
            \"format\": \"json\",
            \"includeAttachments\": ${INCLUDE_ATTACHMENTS},
            \"includePrivate\": ${INCLUDE_PRIVATE}
        }" \
        "${OUTLINE_URL}/api/collections.export_all" 2>/dev/null)

    if [ -z "${RESPONSE}" ]; then
        error "API からレスポンスが返りませんでした"
        exit 1
    fi

    # エラーチェック
    local OK
    OK=$(echo "${RESPONSE}" | jq -r '.ok // false' 2>/dev/null || echo "false")
    if [ "${OK}" != "true" ]; then
        local MSG
        MSG=$(echo "${RESPONSE}" | jq -r '.message // "不明なエラー"' 2>/dev/null || echo "不明なエラー")
        error "エクスポート開始に失敗しました: ${MSG}"
        error "レスポンス: ${RESPONSE}"
        exit 1
    fi

    EXPORT_ID=$(echo "${RESPONSE}" | jq -r '.data.fileOperation.id' 2>/dev/null || echo "")
    if [ -z "${EXPORT_ID}" ] || [ "${EXPORT_ID}" = "null" ]; then
        error "fileOperation.id を取得できませんでした"
        error "レスポンス: ${RESPONSE}"
        exit 1
    fi

    info "エクスポートジョブ ID: ${EXPORT_ID}"
    info "オプション: attachments=${INCLUDE_ATTACHMENTS}, private=${INCLUDE_PRIVATE}"
}

# ════════════════════════════════════════════════════
#  エクスポート完了待機
# ════════════════════════════════════════════════════
wait_for_export() {
    section "エクスポート完了を待機中..."

    local ELAPSED=0
    local STATE=""

    while true; do
        local RESPONSE
        RESPONSE=$(curl -sS -X POST \
            -H "Authorization: Bearer ${API_KEY}" \
            -H "Content-Type: application/json" \
            -d "{\"id\": \"${EXPORT_ID}\"}" \
            "${OUTLINE_URL}/api/fileOperations.info" 2>/dev/null)

        STATE=$(echo "${RESPONSE}" | jq -r '.data.state // "unknown"' 2>/dev/null || echo "unknown")

        case "${STATE}" in
            complete)
                echo ""
                info "エクスポート完了 (${ELAPSED}秒)"
                return 0
                ;;
            error)
                echo ""
                local ERR_MSG
                ERR_MSG=$(echo "${RESPONSE}" | jq -r '.data.error // "不明なエラー"' 2>/dev/null || echo "不明なエラー")
                error "エクスポートに失敗しました: ${ERR_MSG}"
                exit 1
                ;;
            creating|pending|*)
                echo -n "."
                ;;
        esac

        ELAPSED=$(( ELAPSED + POLL_INTERVAL ))
        if [ "${ELAPSED}" -ge "${POLL_MAX}" ]; then
            echo ""
            error "エクスポートがタイムアウトしました (${POLL_MAX}秒)"
            error "最後の状態: ${STATE}"
            exit 1
        fi

        sleep "${POLL_INTERVAL}"
    done
}

# ════════════════════════════════════════════════════
#  ZIP をダウンロード
# ════════════════════════════════════════════════════
download_export() {
    local OUTPUT_FILE="${1}"

    section "ZIP をダウンロード..."

    # fileOperations.redirect は直接ダウンロードURLにリダイレクトする
    local HTTP_CODE
    HTTP_CODE=$(curl -sS -L \
        --retry 5 \
        --retry-delay 3 \
        --retry-all-errors \
        -w "%{http_code}" \
        -o "${OUTPUT_FILE}" \
        -H "Authorization: Bearer ${API_KEY}" \
        -H "Content-Type: application/json" \
        "${OUTLINE_URL}/api/fileOperations.redirect?id=${EXPORT_ID}" 2>/dev/null || echo "000")

    if [ "${HTTP_CODE}" != "200" ]; then
        error "ダウンロードに失敗しました (HTTP ${HTTP_CODE})"
        rm -f "${OUTPUT_FILE}"
        exit 1
    fi

    # ZIP ファイルの検証
    if ! file "${OUTPUT_FILE}" 2>/dev/null | grep -qiE 'zip|archive'; then
        # file コマンドがない環境向けにマジックバイト確認
        local MAGIC
        MAGIC=$(xxd -l 4 "${OUTPUT_FILE}" 2>/dev/null | head -1 || echo "")
        if ! echo "${MAGIC}" | grep -q "504b 0304"; then
            warn "ダウンロードファイルが ZIP 形式でない可能性があります"
            warn "ファイルサイズ: $(du -sh "${OUTPUT_FILE}" | cut -f1)"
        fi
    fi

    local FILE_SIZE
    FILE_SIZE=$(du -sh "${OUTPUT_FILE}" | cut -f1)
    info "ダウンロード完了: ${OUTPUT_FILE} (${FILE_SIZE})"
}

# ════════════════════════════════════════════════════
#  サーバー側の一時ファイルを削除
# ════════════════════════════════════════════════════
delete_server_export() {
    section "サーバー側の一時ファイルを削除..."

    local RESPONSE
    RESPONSE=$(curl -sS -X POST \
        -H "Authorization: Bearer ${API_KEY}" \
        -H "Content-Type: application/json" \
        -d "{\"id\": \"${EXPORT_ID}\"}" \
        "${OUTLINE_URL}/api/fileOperations.delete" 2>/dev/null)

    local OK
    OK=$(echo "${RESPONSE}" | jq -r '.ok // false' 2>/dev/null || echo "false")
    if [ "${OK}" = "true" ]; then
        info "サーバー側の一時ファイルを削除しました"
    else
        warn "サーバー側の一時ファイル削除に失敗しました (無視して続行)"
    fi
}

# ════════════════════════════════════════════════════
#  古いバックアップを自動削除
# ════════════════════════════════════════════════════
auto_clean_old_backups() {
    local OLD_FILES
    OLD_FILES=$(find "${BACKUP_DIR}" -name "outline_export_*.zip" \
        -mtime "+${KEEP_DAYS}" 2>/dev/null || true)

    if [ -n "${OLD_FILES}" ]; then
        local OLD_COUNT
        OLD_COUNT=$(echo "${OLD_FILES}" | wc -l)
        echo "${OLD_FILES}" | while IFS= read -r F; do
            rm -f "${F}" "${F}.sha256"
            info "古いバックアップを削除: $(basename "${F}")"
        done
        info "${OLD_COUNT} 件の古いバックアップを削除しました (${KEEP_DAYS}日以上前)"
    fi
}

# ════════════════════════════════════════════════════
#  バックアップ実行
# ════════════════════════════════════════════════════
do_backup() {
    print_banner
    check_deps
    check_config
    check_api_connection

    mkdir -p "${BACKUP_DIR}"

    local TIMESTAMP
    TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
    local OUTPUT_FILE="${BACKUP_DIR}/outline_export_${TIMESTAMP}.zip"

    # エクスポートジョブ開始 → 完了待機 → ダウンロード → 後片付け
    start_export
    wait_for_export
    download_export "${OUTPUT_FILE}"
    delete_server_export

    # チェックサム生成
    sha256sum "${OUTPUT_FILE}" > "${OUTPUT_FILE}.sha256"
    info "チェックサム: ${OUTPUT_FILE}.sha256"

    # 古いバックアップを自動削除
    auto_clean_old_backups

    local FILE_SIZE
    FILE_SIZE=$(du -sh "${OUTPUT_FILE}" | cut -f1)

    echo ""
    echo "════════════════════════════════════════════════"
    echo -e "  ${GREEN}✅  バックアップ完了!${NC}"
    echo "════════════════════════════════════════════════"
    echo ""
    echo "  📦 ファイル   : ${OUTPUT_FILE}"
    echo "  📏 サイズ     : ${FILE_SIZE}"
    echo "  📅 日時       : $(date '+%Y-%m-%d %H:%M:%S')"
    echo "  🔗 Outline URL: ${OUTLINE_URL}"
    echo ""
    echo "  復元方法:"
    echo "    Outline の Settings → Import → 上記 ZIP をアップロード"
    echo ""
}

# ════════════════════════════════════════════════════
#  バックアップ一覧
# ════════════════════════════════════════════════════
do_list() {
    print_banner

    if [ ! -d "${BACKUP_DIR}" ]; then
        warn "バックアップディレクトリが存在しません: ${BACKUP_DIR}"
        exit 0
    fi

    local FILES
    FILES=$(find "${BACKUP_DIR}" -name "outline_export_*.zip" \
        -printf '%T@ %p\n' 2>/dev/null | sort -rn | cut -d' ' -f2-)

    if [ -z "${FILES}" ]; then
        warn "バックアップファイルが見つかりません"
        exit 0
    fi

    echo "バックアップ一覧: ${BACKUP_DIR}"
    echo ""
    printf "  %-55s  %8s  %s\n" "ファイル名" "サイズ" "作成日時"
    echo "  $(printf '─%.0s' {1..80})"

    while IFS= read -r FILE; do
        local BASENAME SIZE MTIME
        BASENAME=$(basename "${FILE}")
        SIZE=$(du -sh "${FILE}" | cut -f1)
        MTIME=$(stat -c '%y' "${FILE}" | cut -d'.' -f1)
        printf "  %-55s  %8s  %s\n" "${BASENAME}" "${SIZE}" "${MTIME}"
    done <<< "${FILES}"

    echo ""
    local TOTAL_COUNT TOTAL_SIZE
    TOTAL_COUNT=$(echo "${FILES}" | wc -l)
    TOTAL_SIZE=$(du -sh "${BACKUP_DIR}" | cut -f1)
    echo "  合計: ${TOTAL_COUNT} 件 / ${TOTAL_SIZE} (保持日数: ${KEEP_DAYS}日)"
    echo ""
}

# ════════════════════════════════════════════════════
#  古いバックアップ手動削除
# ════════════════════════════════════════════════════
do_clean() {
    print_banner

    local OLD_FILES
    OLD_FILES=$(find "${BACKUP_DIR}" -name "outline_export_*.zip" \
        -mtime "+${KEEP_DAYS}" 2>/dev/null || true)

    if [ -z "${OLD_FILES}" ]; then
        info "${KEEP_DAYS}日以上前のバックアップはありません"
        exit 0
    fi

    warn "以下のファイルを削除します (${KEEP_DAYS}日以上前):"
    echo "${OLD_FILES}" | while IFS= read -r F; do
        echo "  - $(basename "${F}") ($(du -sh "${F}" | cut -f1))"
    done
    echo ""
    read -rp "  削除しますか? [y/N]: " CONFIRM
    if [[ ! "${CONFIRM}" =~ ^[Yy]$ ]]; then
        warn "キャンセルしました"
        exit 0
    fi

    echo "${OLD_FILES}" | while IFS= read -r F; do
        rm -f "${F}" "${F}.sha256"
        info "削除: $(basename "${F}")"
    done

    info "クリーンアップ完了"
    echo ""
}

# ════════════════════════════════════════════════════
#  エントリーポイント
# ════════════════════════════════════════════════════
COMMAND="${1:-backup}"

case "${COMMAND}" in
    backup|"") do_backup ;;
    list)      do_list   ;;
    clean)     do_clean  ;;
    -h|--help|help) usage ;;
    *)
        error "不明なコマンド: ${COMMAND}"
        usage
        exit 1
        ;;
esac

outline-export-install.sh

#!/bin/bash
set -euo pipefail
# =============================================================
#  Outline エクスポートバックアップ インストーラー
#
#  実行するだけで以下を一括セットアップします:
#    - /opt/lxd-data/script/outline/outline-export-backup.sh
#    - API キーの設定
#    - 実行権限の付与
#    - (任意) cron への自動バックアップ登録
#
#  使い方:
#    sudo bash outline-export-install.sh
# =============================================================

SCRIPT_DIR="/opt/lxd-data/script/outline"
SCRIPT_PATH="${SCRIPT_DIR}/outline-export-backup.sh"
BACKUP_DIR="/opt/lxd-data/outline-export"

GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'

info()    { echo -e "${GREEN}[INFO]${NC}  $*"; }
warn()    { echo -e "${YELLOW}[WARN]${NC}  $*"; }
error()   { echo -e "${RED}[ERROR]${NC} $*" >&2; }
section() { echo -e "\n${CYAN}==> $*${NC}"; }

if [ "$(id -u)" -ne 0 ]; then
    error "このスクリプトは root または sudo で実行してください"
    exit 1
fi

echo ""
echo "════════════════════════════════════════════════"
echo "  Outline エクスポートバックアップ インストーラー"
echo "════════════════════════════════════════════════"
echo ""
echo "  スクリプト配置先: ${SCRIPT_PATH}"
echo "  バックアップ保存先: ${BACKUP_DIR}/"
echo ""

# 依存コマンド確認
section "[Step 1] 依存パッケージを確認..."
MISSING=()
for CMD in curl jq; do
    if ! command -v "${CMD}" &>/dev/null; then
        MISSING+=("${CMD}")
    fi
done

if [ ${#MISSING[@]} -gt 0 ]; then
    warn "以下のパッケージをインストールします: ${MISSING[*]}"
    apt-get install -y "${MISSING[@]}"
    info "インストール完了"
else
    info "curl / jq: OK"
fi

# ── Outline URL の取得 ─────────────────────────────
section "[Step 2] Outline URL を確認..."

AUTO_URL=""
ENV_FILE="/opt/docker/outline/.env"
if [ -f "${ENV_FILE}" ]; then
    AUTO_URL=$(grep '^URL=' "${ENV_FILE}" 2>/dev/null \
        | cut -d= -f2- | tr -d '"' || echo "")
    if [ -n "${AUTO_URL}" ]; then
        info ".env から自動取得: ${AUTO_URL}"
    fi
fi

if [ -n "${AUTO_URL}" ]; then
    read -rp "  この URL を使いますか? [Y/n]: " USE_AUTO
    if [[ "${USE_AUTO}" =~ ^[Nn]$ ]]; then
        AUTO_URL=""
    fi
fi

if [ -z "${AUTO_URL}" ]; then
    read -rp "  Outline の URL を入力してください (例: https://hostname:3303): " OUTLINE_URL
else
    OUTLINE_URL="${AUTO_URL}"
fi

OUTLINE_URL="${OUTLINE_URL%/}"
info "Outline URL: ${OUTLINE_URL}"

# ── API キーの入力 ─────────────────────────────────
section "[Step 3] API キーを設定..."
echo ""
echo "  Outline の Settings → API Keys でキーを作成してください。"
echo "  ※ Workspace Admin 権限のユーザーで作成してください"
echo ""
read -rsp "  API キーを入力してください: " API_KEY
echo ""

if [ -z "${API_KEY}" ]; then
    error "API キーが入力されませんでした"
    exit 1
fi

# API 接続テスト
echo ""
info "API 接続を確認中..."
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
    -X POST \
    -H "Authorization: Bearer ${API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{}' \
    "${OUTLINE_URL}/api/auth.info" 2>/dev/null || echo "000")

if [ "${HTTP_CODE}" = "200" ]; then
    info "API 接続 OK"
elif [ "${HTTP_CODE}" = "401" ]; then
    error "認証エラー (HTTP 401): API キーを確認してください"
    exit 1
elif [ "${HTTP_CODE}" = "000" ]; then
    warn "Outline に接続できませんでした (Outline が停止中の可能性があります)"
    warn "スクリプトの設置は続行しますが、実行前に Outline が起動していることを確認してください"
else
    warn "予期しないレスポンス (HTTP ${HTTP_CODE}) — 設置は続行します"
fi

# ── 保持日数の確認 ─────────────────────────────────
section "[Step 4] バックアップ保持日数を設定..."
echo ""
echo "  古いバックアップを自動削除する日数を設定します。"
echo "  例: 30 → 30日より古いバックアップを削除"
echo ""
read -rp "  保持日数 [デフォルト: 30]: " KEEP_DAYS
KEEP_DAYS="${KEEP_DAYS:-30}"
if ! [[ "${KEEP_DAYS}" =~ ^[0-9]+$ ]] || [ "${KEEP_DAYS}" -lt 1 ]; then
    warn "無効な値です。デフォルト (30日) を使用します"
    KEEP_DAYS=30
fi
info "保持日数: ${KEEP_DAYS}日"

# ── cron スケジュールの確認 ───────────────────────
section "[Step 5] 自動バックアップ (cron) の設定..."
echo ""
echo "  スケジュール例:"
echo "    [1] 毎日    深夜 2:00 (推奨)"
echo "    [2] 毎週日曜 深夜 2:00"
echo "    [3] 毎月1日  深夜 2:00"
echo "    [4] cron に登録しない"
echo ""

CRON_CHOICE=""
while true; do
    read -rp "  選択してください [1-4]: " CRON_CHOICE
    case "${CRON_CHOICE}" in 1|2|3|4) break ;; esac
    warn "1 〜 4 を入力してください"
done

case "${CRON_CHOICE}" in
    1) CRON_SCHEDULE="0 2 * * *" ; CRON_LABEL="毎日 深夜 2:00" ;;
    2) CRON_SCHEDULE="0 2 * * 0" ; CRON_LABEL="毎週日曜 深夜 2:00" ;;
    3) CRON_SCHEDULE="0 2 1 * *" ; CRON_LABEL="毎月1日 深夜 2:00" ;;
    4) CRON_SCHEDULE=""           ; CRON_LABEL="登録しない" ;;
esac
info "cron 設定: ${CRON_LABEL}"

# ── ディレクトリ作成 ──────────────────────────────
section "[Step 6] ディレクトリを作成..."
mkdir -p "${SCRIPT_DIR}"
mkdir -p "${BACKUP_DIR}"
info "作成: ${SCRIPT_DIR}"
info "作成: ${BACKUP_DIR}"

# ── スクリプトを配置 ──────────────────────────────
section "[Step 7] スクリプトを配置..."

# このインストーラーと同じディレクトリに outline-export-backup.sh があれば使う
INSTALLER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "${INSTALLER_DIR}/outline-export-backup.sh" ]; then
    cp "${INSTALLER_DIR}/outline-export-backup.sh" "${SCRIPT_PATH}"
    info "outline-export-backup.sh をコピーしました"
else
    error "outline-export-backup.sh が見つかりません: ${INSTALLER_DIR}/"
    error "インストーラーと同じディレクトリに置いてください"
    exit 1
fi

# API キー・URL・保持日数をスクリプトに書き込む
sed -i "s|^OUTLINE_URL=\"\${OUTLINE_URL:-}\"|OUTLINE_URL=\"\${OUTLINE_URL:-${OUTLINE_URL}}\"|" "${SCRIPT_PATH}"
sed -i "s|^API_KEY=\"\${OUTLINE_API_KEY:-}\"|API_KEY=\"\${OUTLINE_API_KEY:-${API_KEY}}\"|" "${SCRIPT_PATH}"
sed -i "s|^KEEP_DAYS=\"\${OUTLINE_KEEP_DAYS:-30}\"|KEEP_DAYS=\"\${OUTLINE_KEEP_DAYS:-${KEEP_DAYS}}\"|" "${SCRIPT_PATH}"

chmod 700 "${SCRIPT_PATH}"   # API キーが含まれるので root のみ読み取り可
info "配置完了: ${SCRIPT_PATH}"
info "パーミッション: 700 (root のみ)"

# ── cron 登録 ─────────────────────────────────────
section "[Step 8] cron の設定..."

if [ -n "${CRON_SCHEDULE}" ]; then
    CRON_FILE="/etc/cron.d/outline-export-backup"
    {
        echo "# Outline 自動エクスポートバックアップ (${CRON_LABEL})"
        echo "${CRON_SCHEDULE} root bash ${SCRIPT_PATH} >> /var/log/outline-export-backup.log 2>&1"
    } > "${CRON_FILE}"
    chmod 644 "${CRON_FILE}"
    info "cron 登録完了: ${CRON_FILE}"
    info "スケジュール  : ${CRON_LABEL}"
    info "ログ出力先    : /var/log/outline-export-backup.log"
else
    info "cron への登録をスキップしました"
    echo ""
    echo "  後から登録する場合:"
    echo "    sudo crontab -e"
    echo "    # 例 (毎日 深夜 2:00):"
    echo "    0 2 * * * bash ${SCRIPT_PATH} >> /var/log/outline-export-backup.log 2>&1"
fi

# ── 完了 ─────────────────────────────────────────
echo ""
echo "════════════════════════════════════════════════"
echo -e "  ${GREEN}✅  インストール完了!${NC}"
echo "════════════════════════════════════════════════"
echo ""
echo "  📂 スクリプト: ${SCRIPT_PATH}"
echo "  📦 保存先    : ${BACKUP_DIR}/"
echo "  📅 保持日数  : ${KEEP_DAYS}日"
echo ""
echo "  📋 使い方:"
echo "    # 今すぐバックアップ"
echo "    sudo bash ${SCRIPT_PATH}"
echo ""
echo "    # バックアップ一覧"
echo "    sudo bash ${SCRIPT_PATH} list"
echo ""
echo "    # 古いバックアップを削除"
echo "    sudo bash ${SCRIPT_PATH} clean"
echo ""
if [ -n "${CRON_SCHEDULE}" ]; then
    echo "  ⏰ 自動バックアップ: ${CRON_LABEL}"
    echo "    ログ: tail -f /var/log/outline-export-backup.log"
    echo ""
fi
echo "  復元方法:"
echo "    Outline の Settings → Import → ZIP をアップロード"
echo ""
タイトルとURLをコピーしました