nextExplorerとOnlyOfficeを連携

先日、セルフホスト可能なWebベースのファイラーで、かなり使い勝手の良さそうな「nextExplorer」(GitHub)を紹介ましたが、どうやらOnlyOfficeとも連携出来るようなので、試してみました。

構成の全体像

[ブラウザ]
    ↕ HTTPS (Tailscale)
[nextExplorer :3317]  ←→  [OnlyOffice :3322]
    ↕ ファイルAPI / コールバック
  (同一コンテナ内 → localhost で疎通可能)

OnlyOfficeはTailscale Serveで別ポート(例:3322)として公開し、nextExplorerからは http://127.0.0.1:3322 で参照します。


LXDコンテナ内にnextExplorerとOnlyOfficeをセットアップ

#!/usr/bin/env bash
# =============================================================================
#  nextExplorer + OnlyOffice セットアップスクリプト v4
#  - /opt/nextexplorer に nextExplorer を配置
#  - /opt/onlyoffice に OnlyOffice Document Server を配置
#  - Tailscale Serve: nextExplorer :3317 / OnlyOffice :3322
#  - 既存の Serve 設定は壊さない(--bg)
#  - JWT シークレットを自動生成して両サービスに共有
# =============================================================================
set -euo pipefail

# ── 固定設定 ──────────────────────────────────────────────────────────────────
INSTALL_DIR="/opt/nextexplorer"
OO_INSTALL_DIR="/opt/onlyoffice"
HOST_PORT=3317
OO_HOST_PORT=3322
CONTAINER_PORT=3000
OO_CONTAINER_PORT=80
DATA_DIR="/srv/nextexplorer"
DEFAULT_MOUNT_SRC="/opt/lxd-data"
DEFAULT_MOUNT_LABEL="Files"
PUID=$(id -u)
PGID=$(id -g)

# ── 色付きログ ─────────────────────────────────────────────────────────────────
info()  { echo -e "\033[1;34m[INFO]\033[0m  $*"; }
ok()    { echo -e "\033[1;32m[ OK ]\033[0m  $*"; }
warn()  { echo -e "\033[1;33m[WARN]\033[0m  $*"; }
die()   { echo -e "\033[1;31m[ERR ]\033[0m  $*" >&2; exit 1; }

# ── 前提チェック ───────────────────────────────────────────────────────────────
info "前提確認..."
command -v docker        >/dev/null 2>&1 || die "docker が見つかりません"
docker compose version   >/dev/null 2>&1 || die "docker compose (v2) が見つかりません"
command -v tailscale     >/dev/null 2>&1 || die "tailscale が見つかりません"
tailscale status         >/dev/null 2>&1 || die "Tailscale が認証されていません"
ok "前提 OK"

# ── Tailscale hostname 取得 ────────────────────────────────────────────────────
TS_HOSTNAME=$(tailscale status --json \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['Self']['DNSName'].rstrip('.'))" \
  2>/dev/null) || die "Tailscale の DNSName を取得できませんでした"

PUBLIC_URL="https://${TS_HOSTNAME}:${HOST_PORT}"
OO_PUBLIC_URL="https://${TS_HOSTNAME}:${OO_HOST_PORT}"
info "Tailscale ホスト   : ${TS_HOSTNAME}"
info "nextExplorer URL  : ${PUBLIC_URL}"
info "OnlyOffice URL    : ${OO_PUBLIC_URL}"

# ── OnlyOffice インストール確認 ────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
read -rp "  OnlyOffice Document Server もインストールしますか? [Y/n]: " install_oo
install_oo="${install_oo:-Y}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# ── マウントディレクトリの入力 ────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  マウントするディレクトリを設定します。"
echo "  複数指定可能。空 Enter で終了(1つも入力しない場合はデフォルト設定を使用)。"
echo "  デフォルト: ${DEFAULT_MOUNT_SRC} → UI ラベル「${DEFAULT_MOUNT_LABEL}」"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

declare -a MOUNT_ENTRIES=()
mount_index=1

while true; do
  read -rp "  マウント #${mount_index} ホストパス(空 Enter でスキップ/終了): " input_path
  [[ -z "${input_path}" ]] && break

  if [[ ! -d "${input_path}" ]]; then
    warn "${input_path} は存在しません。作成しますか? [y/N]: "
    read -rp "  " yn
    if [[ "${yn}" =~ ^[Yy]$ ]]; then
      mkdir -p "${input_path}"
      ok "${input_path} を作成しました"
    else
      warn "スキップします"
      continue
    fi
  fi

  default_label=$(basename "${input_path}")
  read -rp "  マウント #${mount_index} UI ラベル名(空 Enter で「${default_label}」): " input_label
  [[ -z "${input_label}" ]] && input_label="${default_label}"

  MOUNT_ENTRIES+=("${input_path}:${input_label}")
  ok "追加: ${input_path} → /mnt/${input_label}"
  (( mount_index++ ))
done

if [[ ${#MOUNT_ENTRIES[@]} -eq 0 ]]; then
  warn "入力なし。デフォルト設定を使用します: ${DEFAULT_MOUNT_SRC} → ${DEFAULT_MOUNT_LABEL}"
  mkdir -p "${DEFAULT_MOUNT_SRC}"
  MOUNT_ENTRIES=("${DEFAULT_MOUNT_SRC}:${DEFAULT_MOUNT_LABEL}")
fi

echo ""
info "マウント設定:"
for entry in "${MOUNT_ENTRIES[@]}"; do
  echo "    ${entry%%:*} → /mnt/${entry##*:}"
done
echo ""

# ── ディレクトリ作成 & パーミッション設定 ─────────────────────────────────────
info "ディレクトリ作成..."
mkdir -p "${INSTALL_DIR}"
mkdir -p "${DATA_DIR}/config"
mkdir -p "${DATA_DIR}/cache"

# DATA_DIR(config/cache)のみパーミッション設定。
# マウントソース(/opt/lxd-data 等)は他サービスのファイルが混在する可能性があるため触らない。
chown -R "${PUID}:${PGID}" "${DATA_DIR}"
chmod -R u=rwX,g=rwX,o=rX "${DATA_DIR}"
if command -v setfacl >/dev/null 2>&1; then
  setfacl -R -m u:1000:rwX "${DATA_DIR}"
  setfacl -R -d -m u:1000:rwX "${DATA_DIR}"
  info "ACL: ${DATA_DIR} に uid=1000:rwX を付与しました"
else
  chmod -R o+w "${DATA_DIR}/config" "${DATA_DIR}/cache"
  warn "acl 未インストールのため chmod o+w で代替しました(sudo apt install acl で改善可)"
fi
ok "ディレクトリ & パーミッション設定完了"
warn "マウントソースのパーミッションは変更していません。アップロード失敗時は手動で設定してください。"

# ── シークレット生成 ───────────────────────────────────────────────────────────
# SESSION_SECRET(既存があれば再利用)
SECRET_FILE="${INSTALL_DIR}/.session_secret"
if [[ -f "${SECRET_FILE}" ]]; then
  SESSION_SECRET=$(cat "${SECRET_FILE}")
  info "既存の SESSION_SECRET を再利用します"
else
  SESSION_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
  echo "${SESSION_SECRET}" > "${SECRET_FILE}"
  chmod 600 "${SECRET_FILE}"
  info "SESSION_SECRET を新規生成しました"
fi

# ONLYOFFICE JWT シークレット(既存があれば再利用)
OO_SECRET_FILE="${INSTALL_DIR}/.onlyoffice_secret"
if [[ -f "${OO_SECRET_FILE}" ]]; then
  OO_SECRET=$(cat "${OO_SECRET_FILE}")
  info "既存の ONLYOFFICE_SECRET を再利用します"
else
  OO_SECRET=$(python3 -c "import secrets; print(secrets.token_hex(32))")
  echo "${OO_SECRET}" > "${OO_SECRET_FILE}"
  chmod 600 "${OO_SECRET_FILE}"
  info "ONLYOFFICE_SECRET を新規生成しました"
fi

# ── OnlyOffice セットアップ ────────────────────────────────────────────────────
if [[ "${install_oo}" =~ ^[Yy] ]]; then
  info "OnlyOffice のディレクトリを作成..."
  mkdir -p "${OO_INSTALL_DIR}"/{logs,data,lib,db}

  cat > "${OO_INSTALL_DIR}/docker-compose.yml" <<EOF
# OnlyOffice Document Server — docker-compose.yml
# 生成日: $(date '+%Y-%m-%d %H:%M:%S')
# アクセス URL: ${OO_PUBLIC_URL}

services:
  onlyoffice:
    image: onlyoffice/documentserver:latest
    container_name: onlyoffice
    restart: unless-stopped
    ports:
      - "127.0.0.1:${OO_HOST_PORT}:${OO_CONTAINER_PORT}"
    environment:
      JWT_ENABLED: "true"
      JWT_SECRET: "${OO_SECRET}"
    volumes:
      - ${OO_INSTALL_DIR}/logs:/var/log/onlyoffice
      - ${OO_INSTALL_DIR}/data:/var/www/onlyoffice/Data
      - ${OO_INSTALL_DIR}/lib:/var/lib/onlyoffice
      - ${OO_INSTALL_DIR}/db:/var/lib/postgresql
EOF
  ok "OnlyOffice docker-compose.yml → ${OO_INSTALL_DIR}/docker-compose.yml"

  info "OnlyOffice コンテナを起動..."
  docker compose -f "${OO_INSTALL_DIR}/docker-compose.yml" pull
  docker compose -f "${OO_INSTALL_DIR}/docker-compose.yml" up -d
  ok "OnlyOffice コンテナ起動完了(初回起動は2〜3分かかります)"

  # Tailscale Serve に OnlyOffice を追加
  EXISTING_SERVE=$(tailscale serve status 2>/dev/null || true)
  if echo "${EXISTING_SERVE}" | grep -q ":${OO_HOST_PORT}"; then
    warn "ポート ${OO_HOST_PORT} はすでに Tailscale Serve に登録されています。スキップします。"
  else
    info "Tailscale Serve にポート ${OO_HOST_PORT} を追加..."
    tailscale serve --bg --https="${OO_HOST_PORT}" "http://127.0.0.1:${OO_HOST_PORT}"
    ok "OnlyOffice の Tailscale Serve 設定追加完了"
  fi
else
  info "OnlyOffice のインストールをスキップします"
  # 既存の OnlyOffice シークレットを流用するか確認
  if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^onlyoffice$"; then
    info "既存の onlyoffice コンテナを検出。シークレットを取得します..."
    DETECTED=$(docker exec onlyoffice \
      jq -r '.services.CoAuthoring.secret.session.string' \
      /etc/onlyoffice/documentserver/local.json 2>/dev/null || true)
    if [[ -n "${DETECTED}" ]]; then
      OO_SECRET="${DETECTED}"
      echo "${OO_SECRET}" > "${OO_SECRET_FILE}"
      ok "既存コンテナからシークレットを取得しました"
    fi
  fi
fi

# ── nextExplorer docker-compose.yml 生成 ──────────────────────────────────────
info "nextExplorer の docker-compose.yml を生成..."

VOLUMES_YAML="      - ${DATA_DIR}/config:/config"$'\n'
VOLUMES_YAML+="      - ${DATA_DIR}/cache:/cache"
for entry in "${MOUNT_ENTRIES[@]}"; do
  VOLUMES_YAML+=$'\n'"      - ${entry%%:*}:/mnt/${entry##*:}"
done

# OnlyOffice 環境変数ブロック(インストールした or 既存コンテナがある場合に追加)
if [[ "${install_oo}" =~ ^[Yy] ]] || docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^onlyoffice$"; then
  OO_ENV_BLOCK="      ONLYOFFICE_URL: \"${OO_PUBLIC_URL}\"
      ONLYOFFICE_SECRET: \"${OO_SECRET}\"
      ONLYOFFICE_LANG: \"ja\""
else
  OO_ENV_BLOCK="      # ONLYOFFICE_URL: \"https://${TS_HOSTNAME}:3322\"
      # ONLYOFFICE_SECRET: \"your-secret-here\"
      # ONLYOFFICE_LANG: \"ja\""
fi

cat > "${INSTALL_DIR}/docker-compose.yml" <<EOF
# nextExplorer — docker-compose.yml
# 生成日: $(date '+%Y-%m-%d %H:%M:%S')
# アクセス URL: ${PUBLIC_URL}

services:
  nextexplorer:
    image: nxzai/explorer:latest
    container_name: nextexplorer
    restart: unless-stopped
    ports:
      - "127.0.0.1:${HOST_PORT}:${CONTAINER_PORT}"
    environment:
      NODE_ENV: production
      PORT: "${CONTAINER_PORT}"
      PUBLIC_URL: "${PUBLIC_URL}"
      TRUST_PROXY: "loopback"
      SESSION_SECRET: "${SESSION_SECRET}"
      PUID: "${PUID}"
      PGID: "${PGID}"
${OO_ENV_BLOCK}
    volumes:
${VOLUMES_YAML}
EOF
ok "docker-compose.yml → ${INSTALL_DIR}/docker-compose.yml"

# ── nextExplorer 起動 ─────────────────────────────────────────────────────────
info "nextExplorer コンテナを起動..."
docker compose -f "${INSTALL_DIR}/docker-compose.yml" pull
docker compose -f "${INSTALL_DIR}/docker-compose.yml" up -d
ok "nextExplorer コンテナ起動完了"

# ── nextExplorer の Tailscale Serve 設定 ──────────────────────────────────────
EXISTING_SERVE=$(tailscale serve status 2>/dev/null || true)
if echo "${EXISTING_SERVE}" | grep -q ":${HOST_PORT}"; then
  warn "ポート ${HOST_PORT} はすでに Tailscale Serve に登録されています。スキップします。"
else
  info "Tailscale Serve にポート ${HOST_PORT} を追加..."
  tailscale serve --bg --https="${HOST_PORT}" "http://127.0.0.1:${HOST_PORT}"
  ok "nextExplorer の Tailscale Serve 設定追加完了"
fi

# ── 完了サマリー ──────────────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
ok "セットアップ完了!"
echo ""
echo "  nextExplorer  : ${PUBLIC_URL}"
if [[ "${install_oo}" =~ ^[Yy] ]]; then
echo "  OnlyOffice    : ${OO_PUBLIC_URL}"
echo ""
fi
echo ""
echo "  設定ファイル  : ${INSTALL_DIR}/docker-compose.yml"
echo "  データ領域    : ${DATA_DIR}/"
echo ""
echo "  マウント済みボリューム:"
for entry in "${MOUNT_ENTRIES[@]}"; do
  echo "    ${entry%%:*} → UI ラベル「${entry##*:}」"
done
echo ""
echo "  現在の Tailscale Serve 設定:"
tailscale serve status
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

書き込み権限を変更

マウントソースは他サービスのファイルが混在する共有領域なので、スクリプトからは一切触らない設計にしています。アップロードや書き込みに失敗する場合は対象サブディレクトリだけ手動で権限を変更します。

# 書き込みたいディレクトリだけピンポイントで
sudo setfacl -R -m u:1000:rwX /opt/lxd-data/対象フォルダ
sudo setfacl -R -d -m u:1000:rwX /opt/lxd-data/対象フォルダ

空のOfficeファイルも新規作成可能

これで、nextExplorer上でOfficeファイルをダブルクリックすれば、OnlyOfficeで開いて編集できるはずです。初回は読み込みに時間がかかり、初回は表示中に一度接続が切れるかもしれませんが、2回目以降はスムーズに表示されるはず。
nextExplorer上でファイルを新規作成する場合は、デフォルトではtxtファイルになりますが、拡張子を変えることで、空のmdファイルやxlsxファイル、docxファイルなども作成出来るのは地味に便利ですね。

Nextcloudでメール機能やノート機能などを利用していないなら、これで置き換えが出来そうです。

OnlyOfficeにフォントを追加

OnlyOfficeにフォントを追加します。/opt/lxd-data/Fonts にフォントファイルを置いておいてください。スクリプトを実行すれば、このフォントファイルをセットアップ済みのOnlyOfficeコンテナの/usr/share/fonts/customにマウントして、コンテナを再作成しフォントを再生成します。

#!/usr/bin/env bash
# =============================================================================
#  OnlyOffice フォント追加スクリプト
#  - /opt/lxd-data/Fonts/ を /usr/share/fonts/custom にマウント追加
#  - docker-compose.yml を更新してコンテナ再作成
#  - OnlyOffice 内のフォントキャッシュを再生成
# =============================================================================
set -euo pipefail

OO_INSTALL_DIR="/opt/onlyoffice"
FONT_SRC="/opt/lxd-data/Fonts"
FONT_DST="/usr/share/fonts/custom"
COMPOSE_FILE="${OO_INSTALL_DIR}/docker-compose.yml"

# ── 色付きログ ─────────────────────────────────────────────────────────────────
info()  { echo -e "\033[1;34m[INFO]\033[0m  $*"; }
ok()    { echo -e "\033[1;32m[ OK ]\033[0m  $*"; }
warn()  { echo -e "\033[1;33m[WARN]\033[0m  $*"; }
die()   { echo -e "\033[1;31m[ERR ]\033[0m  $*" >&2; exit 1; }

# ── 前提チェック ───────────────────────────────────────────────────────────────
info "前提確認..."
[[ -f "${COMPOSE_FILE}" ]]  || die "compose ファイルが見つかりません: ${COMPOSE_FILE}"
[[ -d "${FONT_SRC}" ]]      || die "フォントディレクトリが見つかりません: ${FONT_SRC}"

FONT_COUNT=$(find "${FONT_SRC}" -maxdepth 2 -type f \( \
  -iname "*.ttf" -o -iname "*.otf" -o -iname "*.woff" -o -iname "*.woff2" \
\) | wc -l)
[[ "${FONT_COUNT}" -gt 0 ]] || die "フォントファイル(ttf/otf/woff)が ${FONT_SRC} に見つかりません"
ok "フォントファイル ${FONT_COUNT} 件を確認"

docker compose version >/dev/null 2>&1 || die "docker compose (v2) が見つかりません"
docker ps --format '{{.Names}}' | grep -q "^onlyoffice$" \
  || die "onlyoffice コンテナが起動していません(docker ps で確認してください)"
ok "前提 OK"

# ── compose.yml にマウントが既に存在するか確認 ─────────────────────────────────
if grep -q "${FONT_DST}" "${COMPOSE_FILE}"; then
  warn "すでに ${FONT_DST} のマウントが登録されています。"
  warn "フォントキャッシュの再生成のみ行います。"
  SKIP_COMPOSE_EDIT=true
else
  SKIP_COMPOSE_EDIT=false
fi

# ── docker-compose.yml にマウント行を追加 ──────────────────────────────────────
if [[ "${SKIP_COMPOSE_EDIT}" == false ]]; then
  info "docker-compose.yml にフォントマウントを追加..."

  # volumes: ブロックの末尾に追記(既存の最後のボリューム行の直後に挿入)
  # sedで "volumes:" セクション内の最後のエントリの後に追加
  python3 - "${COMPOSE_FILE}" "${FONT_SRC}" "${FONT_DST}" <<'PYEOF'
import sys, re

compose_path = sys.argv[1]
font_src     = sys.argv[2]
font_dst     = sys.argv[3]
new_line     = f"      - {font_src}:{font_dst}:ro"

with open(compose_path) as f:
    content = f.read()

# volumes: ブロックを探して末尾に追記
# "    volumes:" の後に続く "      - ..." 行群の最後を特定
pattern = r'(    volumes:(?:\n      - [^\n]+)+)'

def append_volume(m):
    return m.group(0) + "\n" + new_line

new_content, n = re.subn(pattern, append_volume, content)
if n == 0:
    # volumes: セクションが無い場合は作って追加
    new_content = content.rstrip() + f"\n    volumes:\n{new_line}\n"

with open(compose_path, "w") as f:
    f.write(new_content)

print("OK")
PYEOF

  ok "docker-compose.yml を更新しました"
  info "追加内容: ${FONT_SRC} → ${FONT_DST} (read-only)"
fi

# ── バックアップ ───────────────────────────────────────────────────────────────
BACKUP="${COMPOSE_FILE}.bak.$(date +%Y%m%d_%H%M%S)"
cp "${COMPOSE_FILE}" "${BACKUP}"
info "compose ファイルのバックアップ: ${BACKUP}"

# ── コンテナ再作成 ─────────────────────────────────────────────────────────────
info "コンテナを再作成します(データは保持されます)..."
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate
ok "コンテナ再作成完了"

# ── OnlyOffice 起動待ち ────────────────────────────────────────────────────────
info "OnlyOffice の起動を待機中..."
for i in $(seq 1 30); do
  if docker exec onlyoffice supervisorctl status ds:docservice 2>/dev/null \
      | grep -q "RUNNING"; then
    ok "OnlyOffice 起動確認(${i}秒)"
    break
  fi
  if [[ "${i}" -eq 30 ]]; then
    warn "30秒待機しましたが起動確認できませんでした。フォント生成を続行します。"
  fi
  sleep 1
done

# ── フォントキャッシュ再生成 ───────────────────────────────────────────────────
info "コンテナ内でフォントキャッシュを再生成します..."

docker exec onlyoffice bash -c "
  set -e
  echo '[1/4] fc-cache を実行...'
  fc-cache -f -v /usr/share/fonts/custom 2>&1 | tail -5

  echo '[2/4] AllFonts.js を生成...'
  cd /var/www/onlyoffice/documentserver
  node core/DocService/sources/AllFontsGen.js \
    --fonts-dir=/usr/share/fonts \
    --out=/var/www/onlyoffice/documentserver/core-fonts 2>&1 | tail -5 || \
  node tools/fontgen/allfonts.js 2>&1 | tail -5 || \
  /usr/bin/documentserver-generate-allfonts.sh 2>&1 | tail -5 || \
  true

  echo '[3/4] プレゼンテーションテーマを再生成...'
  /usr/bin/documentserver-generate-all-themes.sh 2>&1 | tail -3 || true

  echo '[4/4] JS キャッシュを再生成...'
  /usr/bin/documentserver-pluginsmanager.sh 2>&1 | tail -3 || true
"

# ── nginx リロード ─────────────────────────────────────────────────────────────
info "nginx をリロード..."
docker exec onlyoffice nginx -s reload 2>/dev/null || true

# ── フォント認識確認 ───────────────────────────────────────────────────────────
echo ""
info "認識されたカスタムフォント一覧:"
docker exec onlyoffice fc-list /usr/share/fonts/custom 2>/dev/null \
  | awk -F: '{print "   ", $2}' | sort -u \
  || warn "fc-list での確認に失敗しました(フォント自体は追加されています)"

# ── 完了 ───────────────────────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
ok "フォント追加完了!"
echo ""
echo "  フォントソース : ${FONT_SRC} (${FONT_COUNT} ファイル)"
echo "  コンテナ内パス : ${FONT_DST} (read-only マウント)"
echo "  compose ファイル: ${COMPOSE_FILE}"
echo "  バックアップ   : ${BACKUP}"
echo ""
echo "  ブラウザキャッシュをクリアしてから OnlyOffice でフォントを確認してください。"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

なお、フォントを追加・変更した場合は同じスクリプトを再実行すれば再生成されます(マウント行の二重追加はスキップされます)。

マウントするディレクトリを追加

#!/usr/bin/env bash
# =============================================================================
#  nextExplorer Location 追加スクリプト
#  - 現在のマウント設定を表示
#  - 追加するマウントソース・ラベルを対話入力
#  - docker-compose.yml を更新してコンテナ再起動
# =============================================================================
set -euo pipefail

INSTALL_DIR="/opt/nextexplorer"
COMPOSE_FILE="${INSTALL_DIR}/docker-compose.yml"

# ── 色付きログ ─────────────────────────────────────────────────────────────────
info()  { echo -e "\033[1;34m[INFO]\033[0m  $*"; }
ok()    { echo -e "\033[1;32m[ OK ]\033[0m  $*"; }
warn()  { echo -e "\033[1;33m[WARN]\033[0m  $*"; }
die()   { echo -e "\033[1;31m[ERR ]\033[0m  $*" >&2; exit 1; }

# ── 前提チェック ───────────────────────────────────────────────────────────────
[[ -f "${COMPOSE_FILE}" ]] || die "compose ファイルが見つかりません: ${COMPOSE_FILE}"
docker compose version >/dev/null 2>&1 || die "docker compose (v2) が見つかりません"

# ── 現在の Location 表示 ───────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  現在の Location(/mnt/ マウント):"
echo ""
grep -E "^\s+- .+:/mnt/" "${COMPOSE_FILE}" | while read -r line; do
  src=$(echo "${line}" | sed 's|.*- \(.*\):/mnt/.*|\1|')
  label=$(echo "${line}" | sed 's|.*/mnt/\(.*\)|\1|')
  echo "    ${src}  →  UI ラベル「${label}」"
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""

# ── 追加するマウントの入力 ─────────────────────────────────────────────────────
declare -a NEW_ENTRIES=()
mount_index=1

while true; do
  read -rp "  追加 #${mount_index} ホストパス(空 Enter で終了): " input_path
  [[ -z "${input_path}" ]] && break

  # 既存マウントの重複チェック
  if grep -qF "${input_path}:/mnt/" "${COMPOSE_FILE}"; then
    warn "${input_path} はすでに登録されています。スキップします。"
    continue
  fi

  # パスの存在確認
  if [[ ! -d "${input_path}" ]]; then
    read -rp "  ${input_path} は存在しません。作成しますか? [y/N]: " yn
    if [[ "${yn}" =~ ^[Yy]$ ]]; then
      mkdir -p "${input_path}"
      ok "${input_path} を作成しました"
    else
      warn "スキップします"
      continue
    fi
  fi

  default_label=$(basename "${input_path}")
  read -rp "  追加 #${mount_index} UI ラベル名(空 Enter で「${default_label}」): " input_label
  [[ -z "${input_label}" ]] && input_label="${default_label}"

  # ラベルの重複チェック
  if grep -qF "/mnt/${input_label}" "${COMPOSE_FILE}"; then
    warn "ラベル「${input_label}」はすでに使われています。別の名前を入力してください。"
    continue
  fi

  NEW_ENTRIES+=("${input_path}:${input_label}")
  ok "追加予定: ${input_path} → UI ラベル「${input_label}」"
  (( mount_index++ ))
done

# ── 入力がなければ終了 ─────────────────────────────────────────────────────────
if [[ ${#NEW_ENTRIES[@]} -eq 0 ]]; then
  warn "追加エントリがありません。終了します。"
  exit 0
fi

# ── 確認 ───────────────────────────────────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  以下を追加します:"
for entry in "${NEW_ENTRIES[@]}"; do
  echo "    ${entry%%:*}  →  UI ラベル「${entry##*:}」"
done
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
read -rp "  続行しますか? [Y/n]: " confirm
confirm="${confirm:-Y}"
[[ "${confirm}" =~ ^[Yy] ]] || { warn "キャンセルしました。"; exit 0; }

# ── バックアップ ───────────────────────────────────────────────────────────────
BACKUP="${COMPOSE_FILE}.bak.$(date +%Y%m%d_%H%M%S)"
cp "${COMPOSE_FILE}" "${BACKUP}"
info "バックアップ: ${BACKUP}"

# ── compose.yml に追記(Python3 で安全にパース) ───────────────────────────────
python3 - "${COMPOSE_FILE}" "${NEW_ENTRIES[@]}" << 'PYEOF'
import sys, re

compose_path = sys.argv[1]
entries = sys.argv[2:]  # "src:label" の配列

with open(compose_path) as f:
    content = f.read()

new_lines = "\n".join(f"      - {e.split(':')[0]}:/mnt/{e.split(':')[1]}" for e in entries)

# volumes: ブロック末尾に追記
pattern = r'(    volumes:(?:\n      - [^\n]+)+)'
def append_volume(m):
    return m.group(0) + "\n" + new_lines

new_content, n = re.subn(pattern, append_volume, content)
if n == 0:
    die("volumes: セクションが見つかりませんでした")

with open(compose_path, "w") as f:
    f.write(new_content)

print("OK")
PYEOF

ok "docker-compose.yml を更新しました"

# ── コンテナ再起動 ─────────────────────────────────────────────────────────────
info "コンテナを再起動します..."
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate
ok "コンテナ再起動完了"

# ── 完了:更新後の Location 一覧を表示 ────────────────────────────────────────
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
ok "Location 追加完了!"
echo ""
echo "  更新後の Location 一覧:"
grep -E "^\s+- .+:/mnt/" "${COMPOSE_FILE}" | while read -r line; do
  src=$(echo "${line}" | sed 's|.*- \(.*\):/mnt/.*|\1|')
  label=$(echo "${line}" | sed 's|.*/mnt/\(.*\)|\1|')
  echo "    ${src}  →  UI ラベル「${label}」"
done
echo ""
echo "  バックアップ: ${BACKUP}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

アップデート

nextExplorer

docker compose -f /opt/nextexplorer/docker-compose.yml pull
docker compose -f /opt/nextexplorer/docker-compose.yml up -d

OnlyOffice

docker compose -f /opt/onlyoffice/docker-compose.yml pull
docker compose -f /opt/onlyoffice/docker-compose.yml up -d

どちらも pull で最新イメージを取得し、up -d でコンテナを入れ替えます。データ・設定・シークレットはすべてボリューム側に残るので消えません。

アンインストール

nextExplorer

# コンテナ停止・削除
docker compose -f /opt/nextexplorer/docker-compose.yml down

# Tailscale Serve から削除
tailscale serve --https=3317 off

# ファイル削除(データも消す場合)
rm -rf /opt/nextexplorer
rm -rf /srv/nextexplorer   # config / cache
# マウントしたディレクトリ(/opt/lxd-data など)は手動で判断してください

# イメージも消す場合
docker rmi nxzai/explorer

OnlyOffice

# コンテナ停止・削除
docker compose -f /opt/onlyoffice/docker-compose.yml down

# Tailscale Serve から削除
tailscale serve --https=3322 off

# ファイル削除(データも消す場合)
rm -rf /opt/onlyoffice   # logs / data / lib / db / compose.yml

# イメージも消す場合
docker rmi onlyoffice/documentserver

注意点:

nextExplorerOnlyOffice
ユーザーDB/srv/nextexplorer/config//opt/onlyoffice/db/
アップロードファイルマウント先(/opt/lxd-data等)/opt/onlyoffice/data/
シークレット/opt/nextexplorer/.session_secret
/opt/nextexplorer/.onlyoffice_secret
compose.yml の JWT_SECRET

アンインストール後に再インストールする場合は .session_secret.onlyoffice_secret を残しておくと、セットアップスクリプトが自動で再利用してシークレットが変わりません。

タイトルとURLをコピーしました