#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/.." && pwd)" mode="nossl" env_file="${repo_root}/.env.devprod" compose_file="" convert_sqlite="0" maintenance_window_start="" usage() { cat < Env file to use (default: .env.devprod) --compose-file Explicit compose file path --convert-sqlite If dbtype=sqlite, convert to configured MariaDB backend --maintenance-window-start Set maintenance_window_start (0-23) if provided -h, --help Show this help USAGE } read_kv() { local key="$1" local line line="$(grep -m1 -E "^${key}=" "${env_file}" || true)" if [[ -z "${line}" ]]; then return 1 fi echo "${line#*=}" } compose_cmd() { docker compose -f "${compose_file}" --env-file "${env_file}" "$@" } occ_has_command() { local cmd_name="$1" compose_cmd exec -T app php occ "${cmd_name}" --help >/dev/null 2>&1 } ensure_mariadb_utf8mb4() { local mysql_db mysql_root_password db_client mysql_db="$(read_kv MYSQL_DATABASE || true)" mysql_root_password="$(read_kv MYSQL_ROOT_PASSWORD || true)" if [[ -z "${mysql_db}" || -z "${mysql_root_password}" ]]; then echo "Skipping utf8mb4 preflight: MYSQL_DATABASE or MYSQL_ROOT_PASSWORD missing." return 0 fi db_client="$(compose_cmd exec -T db sh -lc "if command -v mariadb >/dev/null 2>&1; then echo mariadb; elif command -v mysql >/dev/null 2>&1; then echo mysql; fi" | tr -d '\r' | xargs || true)" if [[ -z "${db_client}" ]]; then echo "Skipping utf8mb4 preflight: neither 'mariadb' nor 'mysql' client is available in db container." return 0 fi echo "Ensuring MariaDB database '${mysql_db}' uses utf8mb4..." compose_cmd exec -T db sh -lc "${db_client} -uroot -p\"${mysql_root_password}\" -e \"ALTER DATABASE \\\`${mysql_db}\\\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;\"" } while [[ $# -gt 0 ]]; do case "$1" in --mode) mode="${2:-}" shift 2 ;; --mode=*) mode="${1#*=}" shift ;; --ssl) mode="ssl" shift ;; --nossl) mode="nossl" shift ;; --env-file) env_file="${2:-}" shift 2 ;; --env-file=*) env_file="${1#*=}" shift ;; --compose-file) compose_file="${2:-}" shift 2 ;; --compose-file=*) compose_file="${1#*=}" shift ;; --convert-sqlite) convert_sqlite="1" shift ;; --maintenance-window-start) maintenance_window_start="${2:-}" shift 2 ;; --maintenance-window-start=*) maintenance_window_start="${1#*=}" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done if [[ "${mode}" != "ssl" && "${mode}" != "nossl" ]]; then echo "Invalid mode: ${mode}" usage exit 1 fi if [[ "${env_file}" != /* ]]; then env_file="${repo_root}/${env_file}" fi if [[ ! -f "${env_file}" ]]; then echo "Missing env file: ${env_file}" exit 1 fi if [[ -z "${compose_file}" ]]; then compose_file="${repo_root}/docker-compose.devprod.nossl.yml" if [[ "${mode}" == "ssl" ]]; then compose_file="${repo_root}/docker-compose.devprod.yml" fi fi if [[ "${compose_file}" != /* ]]; then compose_file="${repo_root}/${compose_file}" fi if [[ ! -f "${compose_file}" ]]; then echo "Missing compose file: ${compose_file}" exit 1 fi if [[ -n "${maintenance_window_start}" ]]; then if ! [[ "${maintenance_window_start}" =~ ^[0-9]+$ ]] || (( maintenance_window_start < 0 || maintenance_window_start > 23 )); then echo "--maintenance-window-start must be an integer from 0-23" exit 1 fi fi echo "Using compose file: ${compose_file}" echo "Using env file: ${env_file}" if [[ -x "${repo_root}/scripts/sync-public-env-urls.sh" ]]; then echo "Synchronizing env-backed public URLs..." "${repo_root}/scripts/sync-public-env-urls.sh" --env-file "${env_file}" --mode "${mode}" || true fi echo "Ensuring core services are up..." compose_cmd up -d db redis app if [[ -x "${repo_root}/scripts/ensure-nextcloud-apps-writable.sh" ]]; then "${repo_root}/scripts/ensure-nextcloud-apps-writable.sh" --mode "${mode}" --env-file "${env_file}" || true fi echo "Repairing URL + service auth..." if [[ -x "${repo_root}/scripts/ensure-nextcloud-url-config.sh" ]]; then "${repo_root}/scripts/ensure-nextcloud-url-config.sh" --compose-file "${compose_file}" --env-file "${env_file}" || true fi if [[ -x "${repo_root}/scripts/ensure-nextcloud-service-auth.sh" ]]; then "${repo_root}/scripts/ensure-nextcloud-service-auth.sh" --compose-file "${compose_file}" --env-file "${env_file}" --repair || true fi echo "Running finish-initial-setup (non-destructive mode)..." "${repo_root}/scripts/finish-initial-setup.sh" \ --mode "${mode}" \ --env-file "${env_file}" \ --skip-register \ --skip-api-key get_occ_dbtype() { local value value="$(compose_cmd exec -T app php occ config:system:get dbtype 2>/dev/null | tr -d '\r' | xargs || true)" echo "${value}" | tr '[:upper:]' '[:lower:]' } echo "Checking database backend..." dbtype="$(get_occ_dbtype)" echo "Detected dbtype: ${dbtype:-}" if [[ -z "${dbtype}" ]]; then echo "Unable to read dbtype from OCC; aborting repair so this is not a false-success run." exit 1 fi if [[ "${dbtype}" == "sqlite" || "${dbtype}" == "sqlite3" ]]; then if [[ "${convert_sqlite}" != "1" ]]; then echo "Warning: SQLite is active. Re-run with --convert-sqlite to migrate into MariaDB." else mysql_db="$(read_kv MYSQL_DATABASE || true)" mysql_user="$(read_kv MYSQL_USER || true)" mysql_password="$(read_kv MYSQL_PASSWORD || true)" if [[ -z "${mysql_db}" || -z "${mysql_user}" || -z "${mysql_password}" ]]; then echo "Cannot convert SQLite: MYSQL_DATABASE / MYSQL_USER / MYSQL_PASSWORD must be set in ${env_file}." exit 1 fi echo "Converting SQLite to MariaDB..." ensure_mariadb_utf8mb4 # Ensure Nextcloud creates utf8mb4-capable schema in MariaDB during conversion. compose_cmd exec -T app php occ config:system:set mysql.utf8mb4 --type=boolean --value=true >/dev/null 2>&1 || true # admin_audit can fail to bootstrap in maintenance mode during db:convert-type on some NC builds. compose_cmd exec -T app php occ app:disable admin_audit >/dev/null 2>&1 || true compose_cmd exec -T app php occ maintenance:mode --on >/dev/null 2>&1 || true set +e compose_cmd exec -T app php occ db:convert-type --all-apps --clear-schema --password="${mysql_password}" mysql "${mysql_user}" db "${mysql_db}" convert_status=$? compose_cmd exec -T app php occ maintenance:mode --off >/dev/null 2>&1 || true compose_cmd exec -T app php occ app:enable --force admin_audit >/dev/null 2>&1 || true set -e if [[ "${convert_status}" -ne 0 ]]; then echo "SQLite conversion failed." exit 1 fi dbtype_after="$(get_occ_dbtype)" echo "dbtype after conversion: ${dbtype_after:-}" if [[ "${dbtype_after}" != "mysql" ]]; then echo "SQLite conversion completed without hard error, but dbtype is not mysql after conversion." exit 1 fi fi elif [[ "${convert_sqlite}" == "1" ]]; then echo "--convert-sqlite requested, but dbtype is already '${dbtype}'. No conversion needed." fi echo "Running OCC repair + index pass..." if [[ -x "${repo_root}/scripts/ensure-nextcloud-default-app-bundle.sh" ]]; then "${repo_root}/scripts/ensure-nextcloud-default-app-bundle.sh" \ --compose-file "${compose_file}" \ --env-file "${env_file}" \ --skip-indices || true fi compose_cmd exec -T app php occ db:add-missing-indices || true compose_cmd exec -T app php occ maintenance:repair || true compose_cmd exec -T app php occ maintenance:repair --include-expensive || true if [[ -z "${maintenance_window_start}" ]]; then maintenance_window_start="$(compose_cmd exec -T app php occ config:system:get maintenance_window_start 2>/dev/null | tr -d '\r' | xargs || true)" if [[ -z "${maintenance_window_start}" ]]; then maintenance_window_start="1" fi fi echo "Setting maintenance_window_start=${maintenance_window_start}" compose_cmd exec -T app php occ config:system:set maintenance_window_start --type=integer --value="${maintenance_window_start}" || true echo "Ensuring Talk HPB + notify_push configuration..." compose_cmd exec -T app php occ app:enable --force spreed >/dev/null 2>&1 || true compose_cmd exec -T app php occ app:enable --force notify_push >/dev/null 2>&1 || true compose_cmd exec -T app php occ app:list | grep -E "spreed|notify_push|user_oidc|qortal_integration" || true talk_prefix="" recording_prefix="" if occ_has_command "talk:signaling:list"; then talk_prefix="talk" elif occ_has_command "spreed:signaling:list"; then talk_prefix="spreed" fi if occ_has_command "talk:recording:add"; then recording_prefix="talk" elif occ_has_command "spreed:recording:add"; then recording_prefix="spreed" fi talk_signaling_public_url="$(read_kv TALK_SIGNALING_PUBLIC_URL || true)" talk_signaling_secret="$(read_kv TALK_SIGNALING_SECRET || true)" talk_stun_server="$(read_kv TALK_STUN_SERVER || true)" talk_turn_server="$(read_kv TALK_TURN_SERVER || true)" talk_domain="$(read_kv SIGNALING_DOMAIN || true)" if [[ -z "${talk_domain}" ]]; then talk_domain="$(read_kv TALK_DOMAIN || true)" fi talk_turn_host="$(read_kv TALK_TURN_HOST || true)" if [[ -z "${talk_turn_host}" ]]; then talk_turn_host="$(read_kv NEXTCLOUD_DOMAIN || true)" fi talk_turn_port="$(read_kv TALK_TURN_PORT || true)" if [[ -z "${talk_turn_port}" ]]; then talk_turn_port="3478" fi talk_turn_secret="$(read_kv TALK_TURN_SECRET || true)" if [[ -z "${talk_turn_server}" ]]; then talk_turn_server="${talk_turn_host}:${talk_turn_port}" fi talk_recording_url="$(read_kv TALK_RECORDING_URL || true)" if [[ -z "${talk_recording_url}" ]]; then talk_recording_url="http://talk_recording:1234" fi talk_recording_secret="$(read_kv TALK_RECORDING_SHARED_SECRET || true)" if [[ -z "${talk_prefix}" ]]; then echo " Note: Talk OCC namespace not found (expected talk:* or spreed:*)." else if [[ -n "${talk_signaling_public_url}" && -n "${talk_signaling_secret}" ]]; then compose_cmd exec -T app php occ "${talk_prefix}:signaling:list" >/tmp/repair_talk_signaling_list.txt 2>/dev/null || true if ! grep -Fq "${talk_signaling_public_url}" /tmp/repair_talk_signaling_list.txt 2>/dev/null; then compose_cmd exec -T app php occ "${talk_prefix}:signaling:add" "${talk_signaling_public_url}" "${talk_signaling_secret}" >/dev/null 2>&1 || true fi if occ_has_command "${talk_prefix}:signaling:delete"; then mapfile -t repair_signaling_delete_ids < <( awk -v keep="${talk_signaling_public_url}" ' /https?:\/\// { if (keep != "" && index($0, keep) > 0) { next; } if (match($0, /\|[[:space:]]*([0-9]+)[[:space:]]*\|/, m)) { print m[1]; next; } if (match($0, /^[[:space:]]*([0-9]+)[[:space:]]+/, m)) { print m[1]; next; } } ' /tmp/repair_talk_signaling_list.txt 2>/dev/null | sort -u ) for signaling_id in "${repair_signaling_delete_ids[@]}"; do if [[ -n "${signaling_id}" ]]; then compose_cmd exec -T app php occ "${talk_prefix}:signaling:delete" "${signaling_id}" >/dev/null 2>&1 || true fi done fi rm -f /tmp/repair_talk_signaling_list.txt >/dev/null 2>&1 || true else echo " Note: signaling auto-config skipped (TALK_SIGNALING_PUBLIC_URL/TALK_SIGNALING_SECRET missing)." fi if [[ -n "${talk_stun_server}" ]]; then compose_cmd exec -T app php occ "${talk_prefix}:stun:list" >/tmp/repair_talk_stun_list.txt 2>/dev/null || true if ! grep -Fq "${talk_stun_server}" /tmp/repair_talk_stun_list.txt 2>/dev/null; then compose_cmd exec -T app php occ "${talk_prefix}:stun:add" "${talk_stun_server}" >/dev/null 2>&1 || true fi if occ_has_command "${talk_prefix}:stun:delete"; then mapfile -t repair_stun_delete_ids < <( awk -v keep="${talk_stun_server}" ' { if (keep != "" && index($0, keep) > 0) { next; } if (match($0, /\|[[:space:]]*([0-9]+)[[:space:]]*\|/, m)) { print m[1]; next; } if (match($0, /^[[:space:]]*([0-9]+)[[:space:]]+/, m)) { print m[1]; next; } } ' /tmp/repair_talk_stun_list.txt 2>/dev/null | sort -u ) for stun_id in "${repair_stun_delete_ids[@]}"; do if [[ -n "${stun_id}" ]]; then compose_cmd exec -T app php occ "${talk_prefix}:stun:delete" "${stun_id}" >/dev/null 2>&1 || true fi done fi rm -f /tmp/repair_talk_stun_list.txt >/dev/null 2>&1 || true else echo " Note: STUN auto-config skipped (TALK_STUN_SERVER missing)." fi if [[ -n "${talk_turn_secret}" && -n "${talk_domain}" ]]; then compose_cmd exec -T app php occ "${talk_prefix}:turn:list" >/tmp/repair_talk_turn_list.txt 2>/dev/null || true if ! grep -Fq "${talk_turn_server}" /tmp/repair_talk_turn_list.txt 2>/dev/null; then compose_cmd exec -T app php occ "${talk_prefix}:turn:add" turn "${talk_turn_server}" udp,tcp --secret "${talk_turn_secret}" >/dev/null 2>&1 || true fi if occ_has_command "${talk_prefix}:turn:delete"; then mapfile -t repair_turn_delete_ids < <( awk -v keep="${talk_turn_server}" ' { if (keep != "" && index($0, keep) > 0) { next; } if (match($0, /\|[[:space:]]*([0-9]+)[[:space:]]*\|/, m)) { print m[1]; next; } if (match($0, /^[[:space:]]*([0-9]+)[[:space:]]+/, m)) { print m[1]; next; } } ' /tmp/repair_talk_turn_list.txt 2>/dev/null | sort -u ) for turn_id in "${repair_turn_delete_ids[@]}"; do if [[ -n "${turn_id}" ]]; then compose_cmd exec -T app php occ "${talk_prefix}:turn:delete" "${turn_id}" >/dev/null 2>&1 || true fi done fi rm -f /tmp/repair_talk_turn_list.txt >/dev/null 2>&1 || true else echo " Note: TURN auto-config skipped (TALK_TURN_SECRET and SIGNALING_DOMAIN/TALK_DOMAIN required)." fi compose_cmd exec -T app php occ "${talk_prefix}:signaling:list" || true compose_cmd exec -T app php occ "${talk_prefix}:stun:list" || true compose_cmd exec -T app php occ "${talk_prefix}:turn:list" || true fi if [[ -n "${recording_prefix}" && -n "${talk_recording_secret}" ]]; then compose_cmd exec -T app php occ "${recording_prefix}:recording:add" "${talk_recording_url}" "${talk_recording_secret}" >/dev/null 2>&1 || \ compose_cmd exec -T app php occ "${recording_prefix}:recording:set" "${talk_recording_url}" "${talk_recording_secret}" >/dev/null 2>&1 || true elif [[ -z "${recording_prefix}" ]]; then echo " Note: recording OCC command not found (checked talk:* and spreed:*)." else echo " Note: recording auto-config skipped (TALK_RECORDING_SHARED_SECRET missing)." fi echo echo "CHD registration token check:" if read_kv "NUQLOUD_CHD_REGISTRATION_TOKEN" >/dev/null 2>&1 || read_kv "SOVEREIGN_CHD_REGISTRATION_TOKEN" >/dev/null 2>&1 || read_kv "SC_CHD_REGISTRATION_TOKEN" >/dev/null 2>&1; then echo " CHD registration token is set in env." else echo " CHD registration token is NOT set in env (intentional unless your connector requires token-gated registration)." fi echo echo "Repair complete. Final checks:" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T app php occ status" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T app php occ config:system:get dbtype" echo " ./scripts/ensure-nextcloud-default-app-bundle.sh --compose-file ${compose_file} --env-file ${env_file}" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T app php occ db:add-missing-indices" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T app php occ maintenance:repair" echo " docker compose -f ${compose_file} --env-file ${env_file} exec -T app php occ app:list | grep -E 'spreed|notify_push|user_oidc|qortal_integration'"