Files

449 lines
17 KiB
Bash
Executable File

#!/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 <<USAGE
Usage: ./scripts/repair-docker-production.sh [options]
Repairs a partially-configured production Docker deployment by:
1) re-running Nextcloud URL/service-auth repairs
2) re-running finish-initial-setup tasks
3) fixing common OCC health warnings (indices/repair/maintenance window)
4) optionally converting accidental SQLite installs to MariaDB
Options:
--mode ssl|nossl Compose mode (default: nossl)
--ssl Alias for --mode ssl
--nossl Alias for --mode nossl
--env-file <path> Env file to use (default: .env.devprod)
--compose-file <path> Explicit compose file path
--convert-sqlite If dbtype=sqlite, convert to configured MariaDB backend
--maintenance-window-start <n> 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:-<unknown>}"
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:-<unknown>}"
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'"