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

Linkwardenのバックアップも標準のエクスポート機能を利用して定期的にバックアップするようにしました。
こちらも事前にアクセストークンを作成しておきます。

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

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

linkwarden-export-backup.sh

#!/bin/bash
set -euo pipefail
# =============================================================
#  Linkwarden JSON エクスポート バックアップスクリプト
#
#  Linkwarden 公式 API (/api/v1/migration) を使って
#  全コレクション・リンク・タグを JSON 形式でエクスポートします。
#
#  使い方:
#    sudo bash linkwarden-export-backup.sh          # バックアップを作成
#    sudo bash linkwarden-export-backup.sh list     # バックアップ一覧を表示
#    sudo bash linkwarden-export-backup.sh clean    # 古いバックアップを削除
#
#  初回セットアップ:
#    1. Linkwarden の Settings → API Keys でトークンを発行
#    2. このスクリプトの設定欄に LINKWARDEN_URL と API_TOKEN を記入
#       または環境変数 LINKWARDEN_URL / LINKWARDEN_API_TOKEN で渡す
#
#  バックアップ保存先: /opt/lxd-data/linkwarden-export/
#  バックアップ内容:
#    - 全コレクション・リンク・タグ (JSON形式)
#  ※ スクリーンショット・PDF などのアーカイブファイルは含まれません
#     それらは Docker ボリューム (linkwarden_data) に保存されています
#
#  復元方法:
#    Linkwarden の Settings → Import & Export →
#    「Linkwarden」を選択して JSON をアップロード
# =============================================================

# ════════════════════════════════════════════════════
#  ★ 設定欄 (環境変数で渡す場合は空欄のままでOK)
# ════════════════════════════════════════════════════
LINKWARDEN_URL="${LINKWARDEN_URL:-}"             # 例: https://hostname:3301
API_TOKEN="${LINKWARDEN_API_TOKEN:-}"            # Linkwarden API トークン
BACKUP_DIR="${LINKWARDEN_BACKUP_DIR:-/opt/lxd-data/linkwarden-export}"
KEEP_DAYS="${LINKWARDEN_KEEP_DAYS:-30}"          # 何日分保持するか (デフォルト30日)

# ════════════════════════════════════════════════════
#  カラー出力
# ════════════════════════════════════════════════════
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 "  Linkwarden JSON エクスポート バックアップ"
    echo "════════════════════════════════════════════════"
    echo ""
}

usage() {
    print_banner
    echo "使い方:"
    echo "  $0            # バックアップを作成"
    echo "  $0 list       # バックアップ一覧を表示"
    echo "  $0 clean      # 古いバックアップを削除 (${KEEP_DAYS}日以上)"
    echo ""
    echo "設定 (環境変数またはスクリプト内の設定欄に記入):"
    echo "  LINKWARDEN_URL        Linkwarden の公開URL  例: https://hostname:3301"
    echo "  LINKWARDEN_API_TOKEN  Linkwarden の API トークン"
    echo "  LINKWARDEN_BACKUP_DIR バックアップ保存先 (デフォルト: ${BACKUP_DIR})"
    echo "  LINKWARDEN_KEEP_DAYS  保持日数           (デフォルト: ${KEEP_DAYS}日)"
    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() {
    # LINKWARDEN_URL — docker-compose.yml から自動取得を試みる
    if [ -z "${LINKWARDEN_URL}" ]; then
        local COMPOSE_FILE="/opt/docker/linkwarden/docker-compose.yml"
        if [ -f "${COMPOSE_FILE}" ]; then
            local AUTO_URL
            AUTO_URL=$(grep 'NEXTAUTH_URL' "${COMPOSE_FILE}" 2>/dev/null \
                | head -1 | sed 's/.*NEXTAUTH_URL: //' | tr -d '"' || echo "")
            if [ -n "${AUTO_URL}" ]; then
                LINKWARDEN_URL="${AUTO_URL}"
                info "docker-compose.yml から URL を取得: ${LINKWARDEN_URL}"
            fi
        fi
    fi

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

    LINKWARDEN_URL="${LINKWARDEN_URL%/}"

    # API_TOKEN
    if [ -z "${API_TOKEN}" ]; then
        echo ""
        warn "LINKWARDEN_API_TOKEN が設定されていません"
        echo "  Linkwarden の Settings → API Keys でトークンを発行してください"
        read -rsp "  API トークンを入力してください: " API_TOKEN
        echo ""
        [ -z "${API_TOKEN}" ] && { error "API トークンが入力されませんでした"; exit 1; }
    fi
}

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

    local HTTP_CODE
    HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
        -H "Authorization: Bearer ${API_TOKEN}" \
        "${LINKWARDEN_URL}/api/v1/users/me" 2>/dev/null || echo "000")

    case "${HTTP_CODE}" in
        200) info "接続 OK (HTTP ${HTTP_CODE})" ;;
        401) error "認証エラー (HTTP 401): API トークンを確認してください"; exit 1 ;;
        000) error "接続できませんでした: ${LINKWARDEN_URL}"
             error "Linkwarden が起動しているか、URL が正しいか確認してください"
             exit 1 ;;
        *)   error "予期しないレスポンス (HTTP ${HTTP_CODE})"; exit 1 ;;
    esac
}

# ════════════════════════════════════════════════════
#  エクスポート実行 & 保存
# ════════════════════════════════════════════════════
do_export() {
    local OUTPUT_FILE="${1}"

    section "エクスポートを実行..."

    local HTTP_CODE
    HTTP_CODE=$(curl -sS -w "%{http_code}" \
        -o "${OUTPUT_FILE}.tmp" \
        -H "Authorization: Bearer ${API_TOKEN}" \
        -H "Accept: application/json" \
        "${LINKWARDEN_URL}/api/v1/migration" 2>/dev/null || echo "000")

    if [ "${HTTP_CODE}" != "200" ]; then
        rm -f "${OUTPUT_FILE}.tmp"
        error "エクスポートに失敗しました (HTTP ${HTTP_CODE})"
        exit 1
    fi

    # レスポンスが JSON か確認
    if ! jq empty "${OUTPUT_FILE}.tmp" 2>/dev/null; then
        rm -f "${OUTPUT_FILE}.tmp"
        error "レスポンスが JSON 形式ではありません"
        exit 1
    fi

    # gzip 圧縮して保存
    gzip -c "${OUTPUT_FILE}.tmp" > "${OUTPUT_FILE}"
    rm -f "${OUTPUT_FILE}.tmp"

    local FILE_SIZE
    FILE_SIZE=$(du -sh "${OUTPUT_FILE}" | cut -f1)
    info "エクスポート完了: ${OUTPUT_FILE} (${FILE_SIZE})"
}

# ════════════════════════════════════════════════════
#  エクスポート内容のサマリー表示
# ════════════════════════════════════════════════════
show_summary() {
    local OUTPUT_FILE="${1}"

    section "エクスポート内容のサマリー..."

    local SUMMARY
    SUMMARY=$(zcat "${OUTPUT_FILE}" 2>/dev/null | jq -r '
        "  コレクション数 : \(.collections | length // 0)",
        "  リンク数       : \(.collections | map(.links // [] | length) | add // 0)",
        "  タグ数         : \(.collections | map(.links // [] | map(.tags // []) | flatten) | flatten | unique | length // 0)"
    ' 2>/dev/null || echo "  (サマリー取得に失敗しました)")

    echo "${SUMMARY}"
}

# ════════════════════════════════════════════════════
#  古いバックアップを自動削除
# ════════════════════════════════════════════════════
auto_clean_old_backups() {
    local OLD_FILES
    OLD_FILES=$(find "${BACKUP_DIR}" -name "linkwarden_export_*.json.gz" \
        -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}/linkwarden_export_${TIMESTAMP}.json.gz"

    do_export "${OUTPUT_FILE}"
    show_summary "${OUTPUT_FILE}"

    # チェックサム生成
    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 "  🔗 Linkwarden URL : ${LINKWARDEN_URL}"
    echo ""
    echo "  復元方法:"
    echo "    1. zcat ${OUTPUT_FILE} > backup.json"
    echo "    2. Linkwarden の Settings → Import & Export"
    echo "       → Linkwarden を選択して backup.json をアップロード"
    echo ""
}

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

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

    local FILES
    FILES=$(find "${BACKUP_DIR}" -name "linkwarden_export_*.json.gz" \
        -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 "  %-52s  %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 "  %-52s  %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 "linkwarden_export_*.json.gz" \
        -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

linkwarden-export-install.sh

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

SCRIPT_DIR="/opt/lxd-data/script/linkwarden"
SCRIPT_PATH="${SCRIPT_DIR}/linkwarden-export-backup.sh"
BACKUP_DIR="/opt/lxd-data/linkwarden-export"
LOG_FILE="/var/log/linkwarden-export-backup.log"

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 "  Linkwarden エクスポートバックアップ インストーラー"
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

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

AUTO_URL=""
COMPOSE_FILE="/opt/docker/linkwarden/docker-compose.yml"
if [ -f "${COMPOSE_FILE}" ]; then
    AUTO_URL=$(grep 'NEXTAUTH_URL' "${COMPOSE_FILE}" 2>/dev/null \
        | head -1 | sed 's/.*NEXTAUTH_URL: //' | tr -d '"' || echo "")
    if [ -n "${AUTO_URL}" ]; then
        info "docker-compose.yml から自動取得: ${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 "  Linkwarden の URL を入力してください (例: https://hostname:3301): " LINKWARDEN_URL
else
    LINKWARDEN_URL="${AUTO_URL}"
fi

LINKWARDEN_URL="${LINKWARDEN_URL%/}"
info "Linkwarden URL: ${LINKWARDEN_URL}"

# ── API トークンの入力 ────────────────────────────
section "[Step 3] API トークンを設定..."
echo ""
echo "  Linkwarden の Settings → API Keys でトークンを発行してください。"
echo ""
read -rsp "  API トークンを入力してください: " API_TOKEN
echo ""

[ -z "${API_TOKEN}" ] && { error "API トークンが入力されませんでした"; exit 1; }

# 接続テスト
echo ""
info "API 接続を確認中..."
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
    -H "Authorization: Bearer ${API_TOKEN}" \
    "${LINKWARDEN_URL}/api/v1/users/me" 2>/dev/null || echo "000")

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

# ── 保持日数の確認 ────────────────────────────────
section "[Step 4] バックアップ保持日数を設定..."
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:30 (推奨)"
echo "    [2] 毎週日曜 深夜 2:30"
echo "    [3] 毎月1日  深夜 2:30"
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="30 2 * * *" ; CRON_LABEL="毎日 深夜 2:30" ;;
    2) CRON_SCHEDULE="30 2 * * 0" ; CRON_LABEL="毎週日曜 深夜 2:30" ;;
    3) CRON_SCHEDULE="30 2 1 * *" ; CRON_LABEL="毎月1日 深夜 2:30" ;;
    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] スクリプトを配置..."

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

# URL・トークン・保持日数をスクリプトに書き込む
sed -i "s|^LINKWARDEN_URL=\"\${LINKWARDEN_URL:-}\"|LINKWARDEN_URL=\"\${LINKWARDEN_URL:-${LINKWARDEN_URL}}\"|" "${SCRIPT_PATH}"
sed -i "s|^API_TOKEN=\"\${LINKWARDEN_API_TOKEN:-}\"|API_TOKEN=\"\${LINKWARDEN_API_TOKEN:-${API_TOKEN}}\"|" "${SCRIPT_PATH}"
sed -i "s|^KEEP_DAYS=\"\${LINKWARDEN_KEEP_DAYS:-30}\"|KEEP_DAYS=\"\${LINKWARDEN_KEEP_DAYS:-${KEEP_DAYS}}\"|" "${SCRIPT_PATH}"

chmod 700 "${SCRIPT_PATH}"
info "配置完了: ${SCRIPT_PATH}"
info "パーミッション: 700 (root のみ)"

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

if [ -n "${CRON_SCHEDULE}" ]; then

    # --- cron パッケージの確認・インストール ---
    CRON_SVC=""
    for SVC in cron crond; do
        if command -v "${SVC}" &>/dev/null; then
            CRON_SVC="${SVC}"
            break
        fi
    done
    if command -v systemctl &>/dev/null; then
        for SVC in cron crond; do
            if systemctl list-unit-files "${SVC}.service" 2>/dev/null | grep -q "${SVC}"; then
                CRON_SVC="${SVC}"
                break
            fi
        done
    fi

    if [ -z "${CRON_SVC}" ]; then
        warn "cron が見つかりません。インストールします..."
        apt-get update -qq
        apt-get install -y cron
        CRON_SVC="cron"
        info "cron をインストールしました"
    else
        info "cron: OK (${CRON_SVC})"
    fi

    # --- cron.d ファイルを正しい書式で作成 ---
    # SHELL/PATH 宣言が必須・パーミッションは 644 でないと cron が無視する
    CRON_FILE="/etc/cron.d/linkwarden-export-backup"
    cat > "${CRON_FILE}" << CRONEOF
# Linkwarden 自動エクスポートバックアップ (${CRON_LABEL})
# インストール日: $(date '+%Y-%m-%d %H:%M:%S')
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

${CRON_SCHEDULE} root bash ${SCRIPT_PATH} >> ${LOG_FILE} 2>&1
CRONEOF

    chmod 644 "${CRON_FILE}"
    info "cron ファイル作成: ${CRON_FILE} (パーミッション: 644)"

    # --- cron サービスの起動・有効化 ---
    CRON_STARTED=false
    if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
        if ! systemctl is-active --quiet "${CRON_SVC}" 2>/dev/null; then
            warn "${CRON_SVC} が停止中です。起動します..."
            systemctl start "${CRON_SVC}"  && CRON_STARTED=true
            systemctl enable "${CRON_SVC}" 2>/dev/null || true
        else
            CRON_STARTED=true
        fi
    elif command -v service &>/dev/null; then
        if ! service "${CRON_SVC}" status &>/dev/null; then
            warn "${CRON_SVC} が停止中です。起動します..."
            service "${CRON_SVC}" start && CRON_STARTED=true
            command -v update-rc.d &>/dev/null \
                && update-rc.d "${CRON_SVC}" enable 2>/dev/null || true
        else
            CRON_STARTED=true
        fi
    else
        pgrep -x "${CRON_SVC}" &>/dev/null && CRON_STARTED=true
    fi

    $CRON_STARTED \
        && info "${CRON_SVC}: 動作中" \
        || warn "${CRON_SVC} の起動確認ができませんでした。手動で起動してください: service cron start"

    # --- cron をリロードして新しいファイルを読み込ませる ---
    RELOADED=false
    if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
        systemctl reload "${CRON_SVC}" 2>/dev/null \
            || systemctl restart "${CRON_SVC}" 2>/dev/null \
            && RELOADED=true || true
    elif command -v service &>/dev/null; then
        service "${CRON_SVC}" reload 2>/dev/null \
            || service "${CRON_SVC}" restart 2>/dev/null \
            && RELOADED=true || true
    fi
    $RELOADED \
        && info "cron をリロードしました" \
        || warn "cron のリロードに失敗しました。再起動後に自動で読み込まれます"

    info "cron 登録完了: ${CRON_FILE}"
    info "スケジュール  : ${CRON_LABEL}"
    info "ログ出力先    : ${LOG_FILE}"

else
    info "cron への登録をスキップしました"
    echo ""
    echo "  後から登録する場合は付属の linkwarden-cron-register.sh を使用してください:"
    echo "    sudo bash linkwarden-cron-register.sh"
    echo ""
    echo "  または手動で:"
    echo "    sudo crontab -e"
    echo "    # 例 (毎日 深夜 2:30):"
    echo "    30 2 * * * bash ${SCRIPT_PATH} >> ${LOG_FILE} 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 ${LOG_FILE}"
    echo ""
    echo "  cron の登録確認:"
    echo "    sudo bash linkwarden-cron-register.sh --show"
    echo ""
fi
echo "  復元方法:"
echo "    1. zcat <バックアップファイル>.json.gz > backup.json"
echo "    2. Linkwarden の Settings → Import & Export"
echo "       → Linkwarden を選択して backup.json をアップロード"
echo ""
echo "  ⚠️  スクリーンショット・PDF などのアーカイブファイルは"
echo "     Docker ボリューム (linkwarden_data) に保存されています。"
echo "     それらも含めてバックアップする場合は別途ボリュームのバックアップが必要です。"
echo ""

cronの登録を後から実行

既にインストール済みの環境で、cronによる定期実行を追加したい場合は下記を実行してください。

#!/bin/bash
set -euo pipefail
# =============================================================
#  Linkwarden バックアップ cron 登録スクリプト
#
#  linkwarden-export-backup.sh を cron に登録します。
#  LXDコンテナ環境に対応: cron の未インストール/未起動を検出・修正します。
#
#  使い方:
#    sudo bash linkwarden-cron-register.sh
#    sudo bash linkwarden-cron-register.sh --show     # 現在の登録内容を確認
#    sudo bash linkwarden-cron-register.sh --remove   # cron 登録を削除
# =============================================================

SCRIPT_PATH="/opt/lxd-data/script/linkwarden/linkwarden-export-backup.sh"
CRON_FILE="/etc/cron.d/linkwarden-export-backup"
LOG_FILE="/var/log/linkwarden-export-backup.log"

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

# ── サブコマンド処理 ──────────────────────────────
show_cron() {
    echo ""
    echo "════════════════════════════════════════════════"
    echo "  現在の cron 登録状況"
    echo "════════════════════════════════════════════════"
    echo ""

    if [ -f "${CRON_FILE}" ]; then
        echo "  📄 ${CRON_FILE}:"
        cat "${CRON_FILE}" | sed 's/^/    /'
        echo ""
    else
        warn "${CRON_FILE} は存在しません"
        echo ""
    fi

    echo "  📋 root の crontab -l (linkwarden 関連):"
    crontab -l 2>/dev/null | grep -i "linkwarden" | sed 's/^/    /' || echo "    (linkwarden エントリなし)"
    echo ""

    echo "  ⚙️  cron サービス状態:"
    if command -v systemctl &>/dev/null && systemctl list-units --type=service 2>/dev/null | grep -qE "cron|crond"; then
        local SVC
        SVC=$(systemctl list-units --type=service 2>/dev/null | grep -oE "(cron|crond)\.service" | head -1)
        systemctl status "${SVC}" --no-pager 2>/dev/null | head -5 | sed 's/^/    /' || true
    else
        ps aux 2>/dev/null | grep -v grep | grep -qE "cron" \
            && echo "    cron プロセス: 動作中" \
            || echo "    cron プロセス: 停止中"
    fi
    echo ""
}

remove_cron() {
    echo ""
    echo "════════════════════════════════════════════════"
    echo "  cron 登録を削除"
    echo "════════════════════════════════════════════════"
    echo ""

    if [ -f "${CRON_FILE}" ]; then
        rm -f "${CRON_FILE}"
        info "削除しました: ${CRON_FILE}"
    else
        warn "登録ファイルが見つかりません: ${CRON_FILE}"
    fi

    if crontab -l 2>/dev/null | grep -q "linkwarden-export-backup"; then
        crontab -l 2>/dev/null | grep -v "linkwarden-export-backup" | crontab -
        info "crontab からも linkwarden-export-backup のエントリを削除しました"
    fi

    info "削除完了"
    echo ""
}

case "${1:-}" in
    --show|-s)   show_cron;   exit 0 ;;
    --remove|-r) remove_cron; exit 0 ;;
    --help|-h)
        echo "使い方:"
        echo "  sudo bash $0              # cron に登録"
        echo "  sudo bash $0 --show      # 現在の登録内容を確認"
        echo "  sudo bash $0 --remove    # cron 登録を削除"
        exit 0
        ;;
    "") ;;
    *)
        error "不明なオプション: ${1}"
        echo "  sudo bash $0 --help で使い方を確認してください"
        exit 1
        ;;
esac

# ════════════════════════════════════════════════
#  メイン: cron 登録
# ════════════════════════════════════════════════

echo ""
echo "════════════════════════════════════════════════"
echo "  Linkwarden バックアップ cron 登録"
echo "════════════════════════════════════════════════"
echo ""

# ── バックアップスクリプトの確認 ─────────────────
section "[Step 1] バックアップスクリプトを確認..."

if [ ! -f "${SCRIPT_PATH}" ]; then
    error "バックアップスクリプトが見つかりません: ${SCRIPT_PATH}"
    error "先に linkwarden-export-install.sh を実行してください"
    exit 1
fi

chmod 700 "${SCRIPT_PATH}" 2>/dev/null || true
info "スクリプト: OK (${SCRIPT_PATH})"

# ── cron パッケージの確認・インストール ──────────
section "[Step 2] cron パッケージを確認..."

CRON_SVC=""
for SVC in cron crond; do
    if command -v "${SVC}" &>/dev/null; then
        CRON_SVC="${SVC}"
        break
    fi
done
if command -v systemctl &>/dev/null; then
    for SVC in cron crond; do
        if systemctl list-unit-files "${SVC}.service" 2>/dev/null | grep -q "${SVC}"; then
            CRON_SVC="${SVC}"
            break
        fi
    done
fi

if [ -z "${CRON_SVC}" ]; then
    warn "cron が見つかりません。インストールします..."
    apt-get update -qq
    apt-get install -y cron
    CRON_SVC="cron"
    info "cron をインストールしました"
else
    info "cron: OK (${CRON_SVC})"
fi

# ── cron サービスの起動確認 ───────────────────────
section "[Step 3] cron サービスを確認・起動..."

if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
    if ! systemctl is-active --quiet "${CRON_SVC}" 2>/dev/null; then
        warn "${CRON_SVC} が停止しています。起動します..."
        systemctl start "${CRON_SVC}"
        systemctl enable "${CRON_SVC}"
        info "${CRON_SVC} を起動・自動起動に設定しました"
    else
        info "${CRON_SVC}: 動作中"
    fi
elif command -v service &>/dev/null; then
    if ! service "${CRON_SVC}" status &>/dev/null; then
        warn "${CRON_SVC} が停止しています。起動します..."
        service "${CRON_SVC}" start || true
        command -v update-rc.d &>/dev/null \
            && update-rc.d "${CRON_SVC}" enable 2>/dev/null || true
        info "${CRON_SVC} を起動しました"
    else
        info "${CRON_SVC}: 動作中"
    fi
else
    pgrep -x "${CRON_SVC}" &>/dev/null \
        && info "${CRON_SVC}: 動作中" \
        || warn "${CRON_SVC} が動作していません。手動で起動してください: service cron start"
fi

# ── スケジュール選択 ──────────────────────────────
section "[Step 4] スケジュールを選択..."
echo ""
echo "  スケジュール例:"
echo "    [1] 毎日    深夜 2:30 (推奨)"
echo "    [2] 毎週日曜 深夜 2:30"
echo "    [3] 毎月1日  深夜 2:30"
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="30 2 * * *" ; CRON_LABEL="毎日 深夜 2:30" ;;
    2) CRON_SCHEDULE="30 2 * * 0" ; CRON_LABEL="毎週日曜 深夜 2:30" ;;
    3) CRON_SCHEDULE="30 2 1 * *" ; CRON_LABEL="毎月1日 深夜 2:30" ;;
    4)
        echo ""
        echo "  cron 書式: 分 時 日 月 曜日"
        echo "  例: 0 3 * * 1   → 毎週月曜 3:00"
        read -rp "  スケジュールを入力: " CRON_SCHEDULE
        CRON_LABEL="${CRON_SCHEDULE}"
        ;;
esac
info "スケジュール: ${CRON_LABEL}"

# ── cron.d にファイルを書き込む ───────────────────
section "[Step 5] cron.d に登録..."

cat > "${CRON_FILE}" << CRONEOF
# Linkwarden 自動エクスポートバックアップ
# 登録日: $(date '+%Y-%m-%d %H:%M:%S')
# スケジュール: ${CRON_LABEL}
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

${CRON_SCHEDULE} root bash ${SCRIPT_PATH} >> ${LOG_FILE} 2>&1
CRONEOF

chmod 644 "${CRON_FILE}"
info "登録完了: ${CRON_FILE}"

# ── cron サービスをリロード ───────────────────────
section "[Step 6] cron をリロード..."

RELOADED=false
if command -v systemctl &>/dev/null && systemctl is-system-running &>/dev/null 2>&1; then
    systemctl reload "${CRON_SVC}" 2>/dev/null \
        || systemctl restart "${CRON_SVC}" 2>/dev/null \
        && RELOADED=true || true
elif command -v service &>/dev/null; then
    service "${CRON_SVC}" reload 2>/dev/null \
        || service "${CRON_SVC}" restart 2>/dev/null \
        && RELOADED=true || true
fi

$RELOADED \
    && info "cron をリロードしました" \
    || warn "cron のリロードに失敗しました。手動でリロードしてください: service cron reload"

# ── 登録内容の確認 ────────────────────────────────
section "[Step 7] 登録内容を確認..."
echo ""
echo "  登録ファイルの内容:"
cat "${CRON_FILE}" | sed 's/^/    /'
echo ""

# ── 完了 ─────────────────────────────────────────
echo "════════════════════════════════════════════════"
echo -e "  ${GREEN}✅  cron 登録完了!${NC}"
echo "════════════════════════════════════════════════"
echo ""
echo "  ⏰ スケジュール : ${CRON_LABEL}"
echo "  📄 cron ファイル: ${CRON_FILE}"
echo "  📋 ログ出力先  : ${LOG_FILE}"
echo ""
echo "  確認コマンド:"
echo "    sudo bash $0 --show            # 登録内容の確認"
echo "    sudo tail -f ${LOG_FILE}       # ログ監視"
echo "    sudo bash ${SCRIPT_PATH}       # 今すぐ手動実行"
echo ""
echo "  削除コマンド:"
echo "    sudo bash $0 --remove          # cron 登録の削除"
echo ""

設定内容を確認するには下記

cat /etc/cron.d/linkwarden-export-backup
タイトルとURLをコピーしました