#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/../.." && pwd)" source "${script_dir}/../lib-nextcloud-apps-path.sh" occ_path="" nextcloud_path="" output_mode="text" output_file="" usage() { cat < Explicit path to Nextcloud occ --nextcloud-path Explicit Nextcloud root path (used to derive occ) --json JSON output --text Human-readable output (default) --output-file Write output to a file instead of stdout -h, --help Show this help USAGE } json_escape() { local input="${1:-}" input="${input//\\/\\\\}" input="${input//\"/\\\"}" input="${input//$'\n'/\\n}" input="${input//$'\r'/}" input="${input//$'\t'/\\t}" printf '%s' "${input}" } bool_json() { if [[ "${1:-0}" == "1" ]]; then printf 'true' else printf 'false' fi } join_lines_json() { local input="${1:-}" local first="1" local line result="" while IFS= read -r line; do [[ -z "${line}" ]] && continue if [[ "${first}" == "1" ]]; then result="\"$(json_escape "${line}")\"" first="0" else result="${result},\"$(json_escape "${line}")\"" fi done <<< "${input}" printf '%s' "${result}" } apps_paths_json() { local input="${1:-}" local first="1" local path="" local url="" local writable="" while IFS=$'\t' read -r path url writable; do [[ -n "${path}" ]] || continue if [[ "${first}" == "1" ]]; then first="0" else printf ',' fi printf '{"path":"%s","url":"%s","writable":%s}' \ "$(json_escape "${path}")" \ "$(json_escape "${url}")" \ "$(bool_json "${writable}")" done <<< "${input}" } append_line() { local var_name="$1" local line="${2:-}" local current="${!var_name:-}" if [[ -z "${line}" ]]; then return 0 fi if [[ -z "${current}" ]]; then printf -v "${var_name}" '%s' "${line}" else printf -v "${var_name}" '%s\n%s' "${current}" "${line}" fi } run_timeout_cmd() { if command -v timeout >/dev/null 2>&1; then timeout 20 "$@" else "$@" fi } detect_occ_path() { local candidates=( "/var/www/nextcloud/occ" "/var/www/html/nextcloud/occ" "/var/www/html/occ" "/srv/www/nextcloud/occ" "/var/lib/nextcloud/occ" ) local candidate for candidate in "${candidates[@]}"; do if [[ -f "${candidate}" ]]; then printf '%s\n' "${candidate}" return 0 fi done return 1 } occ_runner() { if [[ -z "${occ_path}" || ! -f "${occ_path}" ]]; then return 1 fi if [[ -z "$(command -v php || true)" ]]; then return 1 fi if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then if command -v sudo >/dev/null 2>&1; then run_timeout_cmd sudo -u www-data php "${occ_path}" "$@" return $? fi if command -v runuser >/dev/null 2>&1; then run_timeout_cmd runuser -u www-data -- php "${occ_path}" "$@" return $? fi fi run_timeout_cmd php "${occ_path}" "$@" } detect_service_state() { local service_name="${1:-}" if [[ -z "${service_name}" || -z "$(command -v systemctl || true)" ]]; then return 1 fi systemctl is-active "${service_name}" 2>/dev/null || true } docker_access_cmd="" docker_daemon_reachable="0" docker_access_mode="unavailable" ensure_docker_best_effort() { local docker_path="" docker_path="$(type -P docker || true)" if [[ -z "${docker_path}" ]]; then return 1 fi if "${docker_path}" info >/dev/null 2>&1; then docker_access_cmd="${docker_path}" docker_daemon_reachable="1" docker_access_mode="direct" return 0 fi if command -v sudo >/dev/null 2>&1 && sudo -n "${docker_path}" info >/dev/null 2>&1; then docker_access_cmd="sudo ${docker_path}" docker_daemon_reachable="1" docker_access_mode="sudo" return 0 fi return 1 } run_docker_cmd() { if [[ -z "${docker_access_cmd}" ]]; then return 1 fi bash -lc "${docker_access_cmd} $*" } while [[ $# -gt 0 ]]; do case "$1" in --occ-path) occ_path="${2:-}" shift 2 ;; --occ-path=*) occ_path="${1#*=}" shift ;; --nextcloud-path) nextcloud_path="${2:-}" shift 2 ;; --nextcloud-path=*) nextcloud_path="${1#*=}" shift ;; --json) output_mode="json" shift ;; --text) output_mode="text" shift ;; --output-file) output_file="${2:-}" shift 2 ;; --output-file=*) output_file="${1#*=}" shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown option: $1" usage exit 1 ;; esac done if [[ -n "${nextcloud_path}" && "${nextcloud_path}" != /* ]]; then nextcloud_path="$(cd "${nextcloud_path}" 2>/dev/null && pwd || true)" fi if [[ -n "${occ_path}" && "${occ_path}" != /* ]]; then occ_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd)/$(basename "${occ_path}")" fi if [[ -z "${occ_path}" && -n "${nextcloud_path}" && -f "${nextcloud_path}/occ" ]]; then occ_path="${nextcloud_path}/occ" fi if [[ -z "${occ_path}" ]]; then occ_path="$(detect_occ_path || true)" fi if [[ -n "${occ_path}" && -z "${nextcloud_path}" ]]; then nextcloud_path="$(cd "$(dirname "${occ_path}")" 2>/dev/null && pwd || true)" fi host_name="$(hostname 2>/dev/null || true)" kernel_name="$(uname -sr 2>/dev/null || true)" current_user="$(id -un 2>/dev/null || true)" php_path="$(command -v php || true)" php_version="$(php -v 2>/dev/null | head -n1 || true)" apache_state="$(detect_service_state apache2)" httpd_state="$(detect_service_state httpd)" nginx_state="$(detect_service_state nginx)" caddy_state="$(detect_service_state caddy)" docker_state="$(detect_service_state docker)" mariadb_state="$(detect_service_state mariadb)" mysql_state="$(detect_service_state mysql)" postgres_state="$(detect_service_state postgresql)" redis_state="$(detect_service_state redis-server)" listening_ports="" if command -v ss >/dev/null 2>&1; then listening_ports="$(ss -ltn 2>/dev/null | awk 'NR==1 || /:80 |:80$|:443 |:443$|:8080 |:8080$|:8081 |:8081$|:9980 |:9980$|:3000 |:3000$|:3191 |:3191$|:1234 |:1234$|:12391 |:12391$|:3478 |:3478$|:3479 |:3479$/' || true)" fi occ_found="0" occ_status_ok="0" occ_status_json="" occ_status_text="" nc_version="" nc_installed="" nc_maintenance="" nc_needs_db_upgrade="" nc_overwrite_url="" nc_dbtype="" nc_dbhost="" nc_datadirectory="" nc_trusted_domain_0="" nc_enabled_apps="" nc_apps_of_interest="" nc_apps_paths="" nc_selected_apps_path="" if [[ -n "${occ_path}" && -f "${occ_path}" ]]; then occ_found="1" occ_status_json="$(occ_runner status --output=json 2>/dev/null || true)" if [[ -n "${occ_status_json}" ]]; then occ_status_ok="1" fi occ_status_text="$(occ_runner status 2>/dev/null || true)" nc_version="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- versionstring:[[:space:]]*//p' | head -n1)" nc_installed="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- installed:[[:space:]]*//p' | head -n1)" nc_maintenance="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- maintenance:[[:space:]]*//p' | head -n1)" nc_needs_db_upgrade="$(echo "${occ_status_text}" | sed -n 's/^[[:space:]]*- needsDbUpgrade:[[:space:]]*//p' | head -n1)" nc_overwrite_url="$(occ_runner config:system:get overwrite.cli.url 2>/dev/null | tr -d '\r' | head -n1 || true)" nc_dbtype="$(occ_runner config:system:get dbtype 2>/dev/null | tr -d '\r' | head -n1 || true)" nc_dbhost="$(occ_runner config:system:get dbhost 2>/dev/null | tr -d '\r' | head -n1 || true)" nc_datadirectory="$(occ_runner config:system:get datadirectory 2>/dev/null | tr -d '\r' | head -n1 || true)" nc_trusted_domain_0="$(occ_runner config:system:get trusted_domains 0 2>/dev/null | tr -d '\r' | head -n1 || true)" nc_enabled_apps="$(occ_runner app:list 2>/dev/null || true)" nc_apps_of_interest="$(printf '%s\n' "${nc_enabled_apps}" | grep -E 'qortal_integration|user_oidc|notify_push|spreed|richdocuments|custom_pwa|chd_admin|qortal_files_bridge|qortal_talk_bridge|nuqloud_scrum' || true)" nc_apps_paths="$(nextcloud_apps_paths_from_config "${nextcloud_path}" 2>/dev/null || true)" nc_selected_apps_path="$(nextcloud_select_apps_path "${nextcloud_path}" 2>/dev/null || true)" fi docker_cli_present="0" if command -v docker >/dev/null 2>&1; then docker_cli_present="1" fi ensure_docker_best_effort || true docker_ps_summary="" docker_qortal_related="" docker_networks="" if [[ "${docker_daemon_reachable}" == "1" ]]; then docker_ps_summary="$(run_docker_cmd "ps --format '{{.Names}}\t{{.Image}}\t{{.Ports}}'" 2>/dev/null || true)" docker_qortal_related="$(printf '%s\n' "${docker_ps_summary}" | grep -Ei 'qortal|broker|external_auth|nextcloud|collabora|talk|caddy' || true)" docker_networks="$(run_docker_cmd "network ls --format '{{.Name}}'" 2>/dev/null || true)" fi topology_id="unknown" topology_reason="Unable to classify runtime yet." recommended_target="manual-analysis" recommended_next_step="./scripts/analyze-existing-nextcloud-instance.sh --text" if [[ "${occ_found}" == "1" && "${docker_daemon_reachable}" == "1" ]]; then topology_id="host-nextcloud-plus-docker" topology_reason="Detected host-level Nextcloud OCC and a reachable Docker daemon." recommended_target="host-nextcloud-hybrid" recommended_next_step="./scripts/install-host-nextcloud-hybrid.sh --occ-path ${occ_path}" elif [[ "${occ_found}" == "1" ]]; then topology_id="host-nextcloud-no-docker" topology_reason="Detected host-level Nextcloud OCC without usable Docker access." recommended_target="app-only-or-external-services" recommended_next_step="Provision broker/external-auth/Qortal services separately, then wire Nextcloud with OCC." elif [[ "${docker_daemon_reachable}" == "1" && -n "${docker_qortal_related}" ]]; then topology_id="docker-runtime-no-host-occ" topology_reason="Detected Docker daemon and Nextcloud/Qortal sidecar containers, but no host-level OCC path." recommended_target="repo-devprod-or-custom-compose" recommended_next_step="./scripts/topology/discover-cloud-topology.sh --text" fi report_text() { cat <} kernel: ${kernel_name:-} user: ${current_user:-} php: ${php_path:-} php_version: ${php_version:-} Host services: apache2: ${apache_state:-} httpd: ${httpd_state:-} nginx: ${nginx_state:-} caddy: ${caddy_state:-} docker: ${docker_state:-} mariadb: ${mariadb_state:-} mysql: ${mysql_state:-} postgresql: ${postgres_state:-} redis-server: ${redis_state:-} Nextcloud host detection: occ_found: ${occ_found} occ_path: ${occ_path:-} nextcloud_path: ${nextcloud_path:-} occ_status_ok: ${occ_status_ok} installed: ${nc_installed:-} version: ${nc_version:-} maintenance: ${nc_maintenance:-} needsDbUpgrade: ${nc_needs_db_upgrade:-} overwrite.cli.url: ${nc_overwrite_url:-} trusted_domains[0]: ${nc_trusted_domain_0:-} dbtype: ${nc_dbtype:-} dbhost: ${nc_dbhost:-} datadirectory: ${nc_datadirectory:-} Apps paths: $(if [[ -n "${nc_apps_paths}" ]]; then printf '%s\n' "${nc_apps_paths}" | while IFS=$'\t' read -r path url writable; do [[ -n "${path}" ]] || continue printf ' - path=%s url=%s writable=%s\n' "${path}" "${url:-}" "$( [[ "${writable}" == "1" ]] && echo true || echo false )" done; else echo " "; fi) selected_apps_path: ${nc_selected_apps_path:-} Apps of interest: $(if [[ -n "${nc_apps_of_interest}" ]]; then printf '%s\n' "${nc_apps_of_interest}" | sed 's/^/ /'; else echo " "; fi) Docker: cli_present: ${docker_cli_present} daemon_reachable: ${docker_daemon_reachable} access_mode: ${docker_access_mode} Qortal/Nextcloud-related containers: $(if [[ -n "${docker_qortal_related}" ]]; then printf '%s\n' "${docker_qortal_related}" | sed 's/^/ /'; else echo " "; fi) Docker networks: $(if [[ -n "${docker_networks}" ]]; then printf '%s\n' "${docker_networks}" | sed 's/^/ /'; else echo " "; fi) Listening ports snapshot: $(if [[ -n "${listening_ports}" ]]; then printf '%s\n' "${listening_ports}" | sed 's/^/ /'; else echo " "; fi) Recommendation: topology_id: ${topology_id} reason: ${topology_reason} target_profile: ${recommended_target} next_step: ${recommended_next_step} Suggested rollout stages: 1. Run this analyzer on the target host and save the output. 2. Decide whether the target is: - full NuQloud sidecar stack on an existing Nextcloud host - hybrid host-Nextcloud plus Docker sidecars - app-only/manual external services 3. Reuse existing scripts where possible: - scripts/topology/discover-cloud-topology.sh - scripts/topology/apply-topology-profile.sh - scripts/nextcloud-vm-install.sh - scripts/finish-initial-setup.sh - scripts/ensure-*.sh helpers for URL, Talk, service auth, CustomPWA, qortal runtime 4. Use the host-hybrid installer and lifecycle wrappers after confirming the target topology. EOF } report_json() { printf '{\n' printf ' "ok": true,\n' printf ' "generatedAt": "%s",\n' "$(json_escape "$(date -u +%Y-%m-%dT%H:%M:%SZ)")" printf ' "host": {\n' printf ' "hostname": "%s",\n' "$(json_escape "${host_name}")" printf ' "kernel": "%s",\n' "$(json_escape "${kernel_name}")" printf ' "user": "%s",\n' "$(json_escape "${current_user}")" printf ' "phpPath": "%s",\n' "$(json_escape "${php_path}")" printf ' "phpVersion": "%s"\n' "$(json_escape "${php_version}")" printf ' },\n' printf ' "services": {\n' printf ' "apache2": "%s",\n' "$(json_escape "${apache_state}")" printf ' "httpd": "%s",\n' "$(json_escape "${httpd_state}")" printf ' "nginx": "%s",\n' "$(json_escape "${nginx_state}")" printf ' "caddy": "%s",\n' "$(json_escape "${caddy_state}")" printf ' "docker": "%s",\n' "$(json_escape "${docker_state}")" printf ' "mariadb": "%s",\n' "$(json_escape "${mariadb_state}")" printf ' "mysql": "%s",\n' "$(json_escape "${mysql_state}")" printf ' "postgresql": "%s",\n' "$(json_escape "${postgres_state}")" printf ' "redisServer": "%s"\n' "$(json_escape "${redis_state}")" printf ' },\n' printf ' "nextcloud": {\n' printf ' "occFound": %s,\n' "$(bool_json "${occ_found}")" printf ' "occPath": "%s",\n' "$(json_escape "${occ_path}")" printf ' "nextcloudPath": "%s",\n' "$(json_escape "${nextcloud_path}")" printf ' "occStatusOk": %s,\n' "$(bool_json "${occ_status_ok}")" printf ' "installed": "%s",\n' "$(json_escape "${nc_installed}")" printf ' "version": "%s",\n' "$(json_escape "${nc_version}")" printf ' "maintenance": "%s",\n' "$(json_escape "${nc_maintenance}")" printf ' "needsDbUpgrade": "%s",\n' "$(json_escape "${nc_needs_db_upgrade}")" printf ' "overwriteCliUrl": "%s",\n' "$(json_escape "${nc_overwrite_url}")" printf ' "trustedDomain0": "%s",\n' "$(json_escape "${nc_trusted_domain_0}")" printf ' "dbtype": "%s",\n' "$(json_escape "${nc_dbtype}")" printf ' "dbhost": "%s",\n' "$(json_escape "${nc_dbhost}")" printf ' "datadirectory": "%s",\n' "$(json_escape "${nc_datadirectory}")" printf ' "appsPaths": [%s],\n' "$(apps_paths_json "${nc_apps_paths}")" printf ' "selectedAppsPath": "%s",\n' "$(json_escape "${nc_selected_apps_path}")" printf ' "appsOfInterest": [%s]\n' "$(join_lines_json "${nc_apps_of_interest}")" printf ' },\n' printf ' "docker": {\n' printf ' "cliPresent": %s,\n' "$(bool_json "${docker_cli_present}")" printf ' "daemonReachable": %s,\n' "$(bool_json "${docker_daemon_reachable}")" printf ' "accessMode": "%s",\n' "$(json_escape "${docker_access_mode}")" printf ' "qortalRelatedContainers": [%s],\n' "$(join_lines_json "${docker_qortal_related}")" printf ' "networks": [%s]\n' "$(join_lines_json "${docker_networks}")" printf ' },\n' printf ' "listeningPorts": [%s],\n' "$(join_lines_json "${listening_ports}")" printf ' "recommendation": {\n' printf ' "topologyId": "%s",\n' "$(json_escape "${topology_id}")" printf ' "reason": "%s",\n' "$(json_escape "${topology_reason}")" printf ' "targetProfile": "%s",\n' "$(json_escape "${recommended_target}")" printf ' "nextStep": "%s"\n' "$(json_escape "${recommended_next_step}")" printf ' }\n' printf '}\n' } rendered_output="" if [[ "${output_mode}" == "json" ]]; then rendered_output="$(report_json)" else rendered_output="$(report_text)" fi if [[ -n "${output_file}" ]]; then mkdir -p "$(dirname "${output_file}")" printf '%s\n' "${rendered_output}" > "${output_file}" else printf '%s\n' "${rendered_output}" fi