449 lines
17 KiB
Bash
Executable File
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'"
|