#!/usr/bin/env bash set -euo pipefail APP_NAME="gono-cloud" INSTALL_URL="${GONO_CLOUD_INSTALL_URL:-https://run.gono.cloud}" INSTALL_SOURCE="${GONO_CLOUD_INSTALL_SOURCE:-auto}" LOCAL_BUILD="${GONO_CLOUD_LOCAL_BUILD:-auto}" LOCAL_BUILD_PROFILE="${GONO_CLOUD_BUILD_PROFILE:-release}" LOCAL_BIN="${GONO_CLOUD_BIN:-${GONO_CLOUD_LOCAL_BIN:-}}" COMMAND="${GONO_CLOUD_COMMAND:-install}" ASSUME_YES="${GONO_CLOUD_YES:-0}" PURGE="${GONO_CLOUD_PURGE:-0}" SCRIPT_PATH="" SCRIPT_DIR="" REPO_ROOT="" PLATFORM="" PACKAGE_MANAGER="" SERVICE_NAME="" INSTALL_DIR="" BIN_DIR="" BIN_PATH="" CONFIG_DIR="" CONFIG_FILE="" STATE_DIR="" DATA_DIR="" DB_PATH="" TLS_DIR="" LOG_DIR="" PLIST_PATH="" RUN_USER="" RUN_GROUP="" DOMAIN="${GONO_CLOUD_DOMAIN:-gono.cloud}" BASE_URL="${GONO_CLOUD_BASE_URL:-https://${DOMAIN}}" BASE_URL_EXPLICIT=0 if [[ -n "${GONO_CLOUD_BASE_URL+x}" ]]; then BASE_URL_EXPLICIT=1 fi BIND="${GONO_CLOUD_BIND:-127.0.0.1:16102}" XATTR_NS="${GONO_CLOUD_XATTR_NS:-user.nc}" AUTH_REALM="${GONO_CLOUD_AUTH_REALM:-Nextcloud}" MAX_CONNECTIONS="${GONO_CLOUD_DB_MAX_CONNECTIONS:-5}" LOG_FORMAT="${GONO_CLOUD_LOG_FORMAT:-text}" RUST_LOG_VALUE="${RUST_LOG:-info}" INSECURE_HTTP="${GONO_CLOUD_INSECURE_HTTP:-1}" RELEASE_REPO="${GONO_CLOUD_RELEASE_REPO:-Gono-Dev/cloud.server}" RELEASE_BASE="${GONO_CLOUD_RELEASE_BASE:-https://github.com/${RELEASE_REPO}/releases}" VERSION="${GONO_CLOUD_VERSION:-latest}" BIN_URL="${GONO_CLOUD_BIN_URL:-}" HEALTH_URL="${GONO_CLOUD_HEALTH_URL:-}" LOG_STDOUT_OFFSET=0 LOG_STDERR_OFFSET=0 log() { printf '[gono-cloud] %s\n' "$*" } warn() { printf '[gono-cloud] warning: %s\n' "$*" >&2 } die() { printf '[gono-cloud] error: %s\n' "$*" >&2 exit 1 } require_cmd() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1" } usage_name() { if [[ -n "${SCRIPT_PATH}" && -n "${REPO_ROOT}" && "${SCRIPT_PATH}" == "${REPO_ROOT}/"* ]]; then printf '%s\n' "${SCRIPT_PATH#"${REPO_ROOT}/"}" elif [[ -n "${SCRIPT_PATH}" ]]; then printf '%s\n' "${SCRIPT_PATH}" else printf '%s\n' "bash <(curl -sL ${INSTALL_URL})" fi } show_usage() { cat <&2 die "unknown option: $1" ;; *) show_usage >&2 die "unknown argument: $1" ;; esac done } resolve_script_context() { local source source="${BASH_SOURCE[0]:-$0}" case "${source}" in ""|-|bash|/dev/fd/*|/proc/self/fd/*) return ;; esac [[ -e "${source}" ]] || return case "${source}" in /*) SCRIPT_PATH="${source}" ;; *) SCRIPT_PATH="$(cd "$(dirname "${source}")" && pwd -P)/$(basename "${source}")" ;; esac SCRIPT_DIR="$(cd "$(dirname "${SCRIPT_PATH}")" && pwd -P)" if [[ -f "${SCRIPT_DIR}/../Cargo.toml" && -d "${SCRIPT_DIR}/../src" ]]; then REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)" fi } detect_platform() { case "$(uname -s)" in Linux) echo "linux" ;; Darwin) echo "macos" ;; *) die "unsupported OS kernel: $(uname -s). Supported: macOS, Debian, Ubuntu, CentOS/RHEL compatible" ;; esac } detect_linux_package_manager() { [[ -r /etc/os-release ]] || die "cannot detect Linux distribution: /etc/os-release is missing" # shellcheck disable=SC1091 . /etc/os-release case " ${ID:-} ${ID_LIKE:-} " in *" debian "*|*" ubuntu "*) echo "apt" ;; *" rhel "*|*" centos "*|*" fedora "*) if command -v dnf >/dev/null 2>&1; then echo "dnf" elif command -v yum >/dev/null 2>&1; then echo "yum" else die "dnf or yum is required on CentOS/RHEL systems" fi ;; *) die "unsupported Linux distribution '${ID:-unknown}'. Supported: Debian, Ubuntu, CentOS/RHEL compatible" ;; esac } set_platform_defaults() { PLATFORM="$(detect_platform)" case "${PLATFORM}" in linux) PACKAGE_MANAGER="$(detect_linux_package_manager)" SERVICE_NAME="${GONO_CLOUD_SERVICE_NAME:-gono-cloud}" INSTALL_DIR="${GONO_CLOUD_INSTALL_DIR:-/opt/gono-cloud}" CONFIG_DIR="${GONO_CLOUD_CONFIG_DIR:-/etc/gono-cloud}" STATE_DIR="${GONO_CLOUD_STATE_DIR:-/var/lib/gono-cloud}" LOG_DIR="${GONO_CLOUD_LOG_DIR:-/var/log/gono-cloud}" RUN_USER="${GONO_CLOUD_USER:-gono-cloud}" RUN_GROUP="${GONO_CLOUD_GROUP:-gono-cloud}" ;; macos) PACKAGE_MANAGER="none" SERVICE_NAME="${GONO_CLOUD_SERVICE_NAME:-cloud.gono.gono-cloud}" INSTALL_DIR="${GONO_CLOUD_INSTALL_DIR:-/opt/gono-cloud}" CONFIG_DIR="${GONO_CLOUD_CONFIG_DIR:-/Library/Application Support/Gono Cloud}" STATE_DIR="${GONO_CLOUD_STATE_DIR:-/Library/Application Support/Gono Cloud}" LOG_DIR="${GONO_CLOUD_LOG_DIR:-/Library/Logs/Gono Cloud}" RUN_USER="${GONO_CLOUD_USER:-root}" RUN_GROUP="${GONO_CLOUD_GROUP:-wheel}" ;; *) die "unsupported platform: ${PLATFORM}" ;; esac BIN_DIR="${INSTALL_DIR}/bin" BIN_PATH="${GONO_CLOUD_BIN_PATH:-${BIN_DIR}/${APP_NAME}}" CONFIG_FILE="${GONO_CLOUD_CONFIG:-${CONFIG_DIR}/config.toml}" DATA_DIR="${GONO_CLOUD_DATA_DIR:-${STATE_DIR}/data}" DB_PATH="${GONO_CLOUD_DB_PATH:-${STATE_DIR}/gono-cloud.db}" TLS_DIR="${GONO_CLOUD_TLS_DIR:-${CONFIG_DIR}/tls}" PLIST_PATH="${GONO_CLOUD_PLIST_PATH:-/Library/LaunchDaemons/${SERVICE_NAME}.plist}" } require_root() { if [[ "${EUID}" -eq 0 ]]; then return fi if command -v sudo >/dev/null 2>&1; then log "re-running installer with sudo" if [[ -n "${SCRIPT_PATH}" && -r "${SCRIPT_PATH}" ]]; then sudo -E bash "${SCRIPT_PATH}" "$@" else curl -fsSL "${INSTALL_URL}" | sudo -E bash -s -- "$@" fi exit $? fi die "please run as root or install sudo" } install_packages() { case "${PLATFORM}:${PACKAGE_MANAGER}" in linux:apt) export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install -y ca-certificates curl tar gzip coreutils findutils passwd systemd ;; linux:dnf) dnf install -y ca-certificates curl tar gzip coreutils findutils shadow-utils systemd ;; linux:yum) yum install -y ca-certificates curl tar gzip coreutils findutils shadow-utils systemd ;; macos:none) log "macOS detected; using built-in curl, tar, launchd, and system tools" ;; *) die "unknown package manager: ${PACKAGE_MANAGER}" ;; esac } require_platform_commands() { require_cmd curl require_cmd tar require_cmd find require_cmd install require_cmd sed require_cmd tail case "${PLATFORM}" in linux) require_cmd getent require_cmd useradd require_cmd groupadd require_cmd systemctl require_cmd journalctl ;; macos) require_cmd launchctl require_cmd id ;; esac } target_arch() { local machine machine="${GONO_CLOUD_ARCH:-$(uname -m)}" case "${machine}" in x86_64|amd64) echo "x86_64" ;; aarch64|arm64) echo "aarch64" ;; armv8l|armv7l|armv7|armv7hf|armhf) if [[ "${PLATFORM}" == "macos" ]]; then die "unsupported macOS ARM architecture: ${machine}. macOS ARM builds must be aarch64/arm64." fi echo "armv7" ;; armv6l|armv6) if [[ "${PLATFORM}" == "macos" ]]; then die "unsupported macOS ARM architecture: ${machine}. macOS ARM builds must be aarch64/arm64." fi echo "armv6" ;; *) die "unsupported architecture: ${machine}. Supported: x86_64/amd64, aarch64/arm64, Linux armv7, Linux armv6" ;; esac } target_os() { case "${PLATFORM}" in linux) echo "linux" ;; macos) echo "macos" ;; *) die "unsupported platform: ${PLATFORM}" ;; esac } release_tag() { local version="$1" case "${version}" in v*) echo "${version}" ;; *) echo "v${version}" ;; esac } release_asset_version() { local version="$1" echo "${version#v}" } artifact_url() { local arch os target tag asset_version arch="$(target_arch)" os="$(target_os)" target="${os}-${arch}" if [[ -n "${BIN_URL}" ]]; then echo "${BIN_URL}" elif [[ "${VERSION}" == "latest" ]]; then echo "${RELEASE_BASE}/latest/download/${APP_NAME}-${target}.tar.gz" else tag="$(release_tag "${VERSION}")" asset_version="$(release_asset_version "${VERSION}")" echo "${RELEASE_BASE}/download/${tag}/${APP_NAME}-${asset_version}-${target}.tar.gz" fi } use_local_source() { case "${INSTALL_SOURCE}" in local) [[ -n "${LOCAL_BIN}" || -n "${REPO_ROOT}" ]] \ || die "GONO_CLOUD_INSTALL_SOURCE=local requires a local repo or GONO_CLOUD_BIN" return 0 ;; release) return 1 ;; auto) [[ -z "${BIN_URL}" && -n "${REPO_ROOT}" ]] ;; *) die "unsupported GONO_CLOUD_INSTALL_SOURCE='${INSTALL_SOURCE}'. Use auto, local, or release." ;; esac } target_dir() { if [[ -n "${CARGO_TARGET_DIR:-}" ]]; then case "${CARGO_TARGET_DIR}" in /*) printf '%s\n' "${CARGO_TARGET_DIR}" ;; *) printf '%s/%s\n' "$(pwd -P)" "${CARGO_TARGET_DIR}" ;; esac else printf '%s\n' "${REPO_ROOT}/target" fi } local_binary_candidate() { local profile_dir case "${LOCAL_BUILD_PROFILE}" in release) profile_dir="release" ;; debug|dev) profile_dir="debug" ;; *) die "unsupported GONO_CLOUD_BUILD_PROFILE='${LOCAL_BUILD_PROFILE}'. Use release or debug." ;; esac printf '%s/%s/%s\n' "$(target_dir)" "${profile_dir}" "${APP_NAME}" } should_build_local_binary() { case "${LOCAL_BUILD}" in 1|true|yes) return 0 ;; 0|false|no) return 1 ;; auto) command -v cargo >/dev/null 2>&1 ;; *) die "unsupported GONO_CLOUD_LOCAL_BUILD='${LOCAL_BUILD}'. Use auto, 1, or 0." ;; esac } build_local_binary() { [[ -n "${REPO_ROOT}" ]] || die "cannot build local binary: repository root was not detected" require_cmd cargo case "${LOCAL_BUILD_PROFILE}" in release) log "building local release binary from ${REPO_ROOT}" >&2 cargo build --locked --release --manifest-path "${REPO_ROOT}/Cargo.toml" ;; debug|dev) log "building local debug binary from ${REPO_ROOT}" >&2 cargo build --locked --manifest-path "${REPO_ROOT}/Cargo.toml" ;; *) die "unsupported GONO_CLOUD_BUILD_PROFILE='${LOCAL_BUILD_PROFILE}'. Use release or debug." ;; esac } normalize_path() { local path="$1" case "${path}" in /*) printf '%s\n' "${path}" ;; *) printf '%s/%s\n' "$(pwd -P)" "${path}" ;; esac } resolve_local_binary() { local candidate if [[ -n "${LOCAL_BIN}" ]]; then candidate="$(normalize_path "${LOCAL_BIN}")" else candidate="$(local_binary_candidate)" if should_build_local_binary; then build_local_binary fi fi [[ -x "${candidate}" ]] || die "local binary is not executable: ${candidate}. Set GONO_CLOUD_BIN or allow GONO_CLOUD_LOCAL_BUILD=auto." printf '%s\n' "${candidate}" } prepare_local_binary_before_sudo() { if [[ "${EUID}" -eq 0 ]]; then return fi if ! use_local_source; then return fi if [[ -n "${LOCAL_BIN}" ]]; then LOCAL_BIN="$(normalize_path "${LOCAL_BIN}")" else if should_build_local_binary; then build_local_binary LOCAL_BIN="$(local_binary_candidate)" else LOCAL_BIN="$(local_binary_candidate)" fi fi [[ -x "${LOCAL_BIN}" ]] || die "local binary is not executable: ${LOCAL_BIN}" export GONO_CLOUD_BIN="${LOCAL_BIN}" export GONO_CLOUD_INSTALL_SOURCE="local" export GONO_CLOUD_LOCAL_BUILD="0" INSTALL_SOURCE="local" LOCAL_BUILD="0" } verify_sha256() { local expected="$1" local file="$2" local actual if command -v sha256sum >/dev/null 2>&1; then printf '%s %s\n' "${expected}" "${file}" | sha256sum -c - elif command -v shasum >/dev/null 2>&1; then actual="$(shasum -a 256 "${file}" | while read -r hash _; do echo "${hash}"; done)" [[ "${actual}" == "${expected}" ]] || die "sha256 mismatch: expected ${expected}, got ${actual}" else die "GONO_CLOUD_SHA256 was set but neither sha256sum nor shasum is available" fi } sha256_from_sidecar() { local file="$1" sed -n '1s/^\([0-9a-fA-F]\{64\}\).*/\1/p' "${file}" } verify_downloaded_artifact() { local url="$1" local artifact="$2" local sidecar sidecar_url expected if [[ -n "${GONO_CLOUD_SHA256:-}" ]]; then verify_sha256 "${GONO_CLOUD_SHA256}" "${artifact}" return fi sidecar="${TMP_DIR}/artifact.sha256" sidecar_url="${url%%\?*}.sha256" if curl -fsL --retry 3 --retry-delay 2 -o "${sidecar}" "${sidecar_url}"; then expected="$(sha256_from_sidecar "${sidecar}")" [[ -n "${expected}" ]] || die "sha256 sidecar does not contain a hash: ${sidecar_url}" verify_sha256 "${expected}" "${artifact}" else warn "sha256 sidecar was not found at ${sidecar_url}; continuing without checksum verification" fi } download_binary() { local url artifact extract_dir candidate url="$(artifact_url)" artifact="${TMP_DIR}/artifact" extract_dir="${TMP_DIR}/extract" log "downloading ${url}" if ! curl -fL --retry 3 --retry-delay 2 -o "${artifact}" "${url}"; then die "failed to download release artifact from ${url}. Check the GitHub Release assets or set GONO_CLOUD_BIN_URL" fi verify_downloaded_artifact "${url}" "${artifact}" mkdir -p "${extract_dir}" case "${url%%\?*}" in *.tar.gz|*.tgz) tar -xzf "${artifact}" -C "${extract_dir}" candidate="$(find "${extract_dir}" -type f -name "${APP_NAME}" | head -n 1)" [[ -n "${candidate}" ]] || die "archive does not contain ${APP_NAME}" install -m 0755 "${candidate}" "${BIN_PATH}" ;; *) install -m 0755 "${artifact}" "${BIN_PATH}" ;; esac } install_local_binary() { local source_binary source_binary="$(resolve_local_binary)" log "installing local binary ${source_binary}" install -m 0755 "${source_binary}" "${BIN_PATH}" } install_binary() { if use_local_source; then install_local_binary else download_binary fi } nologin_shell() { if [[ -x /usr/sbin/nologin ]]; then echo /usr/sbin/nologin elif [[ -x /sbin/nologin ]]; then echo /sbin/nologin else echo /bin/false fi } ensure_linux_user() { if ! getent group "${RUN_GROUP}" >/dev/null 2>&1; then groupadd --system "${RUN_GROUP}" fi if ! id -u "${RUN_USER}" >/dev/null 2>&1; then useradd \ --system \ --gid "${RUN_GROUP}" \ --home-dir "${STATE_DIR}" \ --create-home \ --shell "$(nologin_shell)" \ "${RUN_USER}" fi } ensure_macos_user() { if ! id -u "${RUN_USER}" >/dev/null 2>&1; then die "macOS run user '${RUN_USER}' does not exist. Use GONO_CLOUD_USER=root or create the user first." fi } ensure_run_identity() { case "${PLATFORM}" in linux) ensure_linux_user ;; macos) ensure_macos_user ;; esac } prepare_directories() { mkdir -p "${BIN_DIR}" "${CONFIG_DIR}" "${TLS_DIR}" "${STATE_DIR}" "${DATA_DIR}" "$(dirname "${DB_PATH}")" "${LOG_DIR}" chown -R "${RUN_USER}:${RUN_GROUP}" "${STATE_DIR}" "${LOG_DIR}" chmod 0750 "${STATE_DIR}" "${DATA_DIR}" chmod 0755 "${LOG_DIR}" } write_config() { if [[ -f "${CONFIG_FILE}" ]]; then log "keeping existing config ${CONFIG_FILE}" else log "writing ${CONFIG_FILE}" cat >"${CONFIG_FILE}" <"${unit}" <"${PLIST_PATH}" < Label $(xml_escape "${SERVICE_NAME}") ProgramArguments $(xml_escape "${BIN_PATH}") WorkingDirectory $(xml_escape "${STATE_DIR}") UserName $(xml_escape "${RUN_USER}") GroupName $(xml_escape "${RUN_GROUP}") EnvironmentVariables NC_DAV_CONFIG $(xml_escape "${CONFIG_FILE}") NC_DAV_INSECURE_HTTP $(xml_escape "${INSECURE_HTTP}") NC_DAV_LOG_FORMAT $(xml_escape "${LOG_FORMAT}") RUST_LOG $(xml_escape "${RUST_LOG_VALUE}") StandardOutPath $(xml_escape "${LOG_DIR}/stdout.log") StandardErrorPath $(xml_escape "${LOG_DIR}/stderr.log") RunAtLoad KeepAlive EOF chown root:wheel "${PLIST_PATH}" chmod 0644 "${PLIST_PATH}" } write_service_definition() { case "${PLATFORM}" in linux) write_systemd_unit ;; macos) write_launchd_plist ;; esac } health_url() { if [[ -n "${HEALTH_URL}" ]]; then echo "${HEALTH_URL}" return fi local port="${BIND##*:}" echo "http://127.0.0.1:${port}/status.php" } show_service_logs() { case "${PLATFORM}" in linux) journalctl -u "${SERVICE_NAME}" -n 80 --no-pager >&2 || true ;; macos) launchctl print "system/${SERVICE_NAME}" >&2 2>/dev/null || true tail -n 80 "${LOG_DIR}/stderr.log" >&2 2>/dev/null || true tail -n 80 "${LOG_DIR}/stdout.log" >&2 2>/dev/null || true ;; esac } service_is_active() { case "${PLATFORM}" in linux) systemctl is-active --quiet "${SERVICE_NAME}" ;; macos) launchctl print "system/${SERVICE_NAME}" >/dev/null 2>&1 ;; esac } wait_for_service() { local url="$1" for _ in $(seq 1 60); do if curl -fsS "${url}" >/dev/null 2>&1; then return 0 fi if ! service_is_active; then show_service_logs die "${SERVICE_NAME} failed to start" fi sleep 1 done show_service_logs die "service did not become healthy at ${url}" } log_size() { local file="$1" if [[ -f "${file}" ]]; then wc -c <"${file}" | tr -d '[:space:]' else echo 0 fi } capture_macos_log_offsets() { LOG_STDOUT_OFFSET="$(log_size "${LOG_DIR}/stdout.log")" LOG_STDERR_OFFSET="$(log_size "${LOG_DIR}/stderr.log")" } latest_generated_password() { local since="$1" case "${PLATFORM}" in linux) journalctl -u "${SERVICE_NAME}" --since "${since}" --no-pager -o cat \ | sed -n 's/.*Generated app password for gono: //p' \ | tail -n 1 \ | sed 's/[",}].*$//' ;; macos) { tail -c +"$((LOG_STDOUT_OFFSET + 1))" "${LOG_DIR}/stdout.log" 2>/dev/null || true tail -c +"$((LOG_STDERR_OFFSET + 1))" "${LOG_DIR}/stderr.log" 2>/dev/null || true } \ | sed -n 's/.*Generated app password for gono: //p' \ | tail -n 1 \ | sed 's/[",}].*$//' ;; esac } start_linux_service() { systemctl daemon-reload systemctl enable "${SERVICE_NAME}" >/dev/null systemctl restart "${SERVICE_NAME}" } start_macos_service() { touch "${LOG_DIR}/stdout.log" "${LOG_DIR}/stderr.log" chown "${RUN_USER}:${RUN_GROUP}" "${LOG_DIR}/stdout.log" "${LOG_DIR}/stderr.log" capture_macos_log_offsets launchctl bootout system "${PLIST_PATH}" >/dev/null 2>&1 || true if ! launchctl bootstrap system "${PLIST_PATH}"; then warn "launchctl bootstrap failed; trying legacy launchctl load" launchctl unload "${PLIST_PATH}" >/dev/null 2>&1 || true launchctl load -w "${PLIST_PATH}" fi launchctl enable "system/${SERVICE_NAME}" >/dev/null 2>&1 || true launchctl kickstart -k "system/${SERVICE_NAME}" >/dev/null 2>&1 || true } start_service() { case "${PLATFORM}" in linux) start_linux_service ;; macos) start_macos_service ;; esac } service_status_hint() { case "${PLATFORM}" in linux) echo "systemctl status ${SERVICE_NAME}" ;; macos) echo "launchctl print system/${SERVICE_NAME}" ;; esac } service_log_hint() { case "${PLATFORM}" in linux) echo "journalctl -u ${SERVICE_NAME} --no-pager" ;; macos) echo "tail -f '${LOG_DIR}/stdout.log' '${LOG_DIR}/stderr.log'" ;; esac } show_service_status() { case "${PLATFORM}" in linux) require_cmd systemctl systemctl status "${SERVICE_NAME}" --no-pager ;; macos) require_cmd launchctl launchctl print "system/${SERVICE_NAME}" ;; esac } follow_service_logs() { case "${PLATFORM}" in linux) require_cmd journalctl journalctl -u "${SERVICE_NAME}" -f --no-pager ;; macos) require_cmd tail touch "${LOG_DIR}/stdout.log" "${LOG_DIR}/stderr.log" tail -f "${LOG_DIR}/stdout.log" "${LOG_DIR}/stderr.log" ;; esac } restart_existing_service() { local url case "${PLATFORM}" in linux) require_cmd systemctl start_linux_service ;; macos) require_cmd launchctl start_macos_service ;; esac url="$(health_url)" wait_for_service "${url}" log "restarted ${SERVICE_NAME}" log "local health: ${url}" } stop_existing_service() { case "${PLATFORM}" in linux) if command -v systemctl >/dev/null 2>&1; then systemctl disable --now "${SERVICE_NAME}" >/dev/null 2>&1 || true systemctl daemon-reload >/dev/null 2>&1 || true fi ;; macos) if command -v launchctl >/dev/null 2>&1; then launchctl bootout system "${PLIST_PATH}" >/dev/null 2>&1 || true fi ;; esac } confirm_uninstall() { local input if [[ "${ASSUME_YES}" == "1" || "${ASSUME_YES}" == "true" || "${ASSUME_YES}" == "yes" ]]; then return fi warn "uninstall will stop ${SERVICE_NAME} and remove service files plus ${BIN_PATH}" if [[ "${PURGE}" == "1" || "${PURGE}" == "true" || "${PURGE}" == "yes" ]]; then warn "purge is enabled; config, data, and logs will also be removed" else warn "config, data, and logs will be preserved; pass --purge to remove them" fi printf "Continue? [y/N] " read -r input case "${input}" in [yY]|[yY][eE][sS]) ;; *) log "uninstall cancelled" exit 0 ;; esac } uninstall_service() { confirm_uninstall stop_existing_service case "${PLATFORM}" in linux) rm -f "/etc/systemd/system/${SERVICE_NAME}.service" if command -v systemctl >/dev/null 2>&1; then systemctl daemon-reload >/dev/null 2>&1 || true fi ;; macos) rm -f "${PLIST_PATH}" ;; esac rm -f "${BIN_PATH}" rmdir "${BIN_DIR}" "${INSTALL_DIR}" >/dev/null 2>&1 || true if [[ "${PURGE}" == "1" || "${PURGE}" == "true" || "${PURGE}" == "yes" ]]; then rm -rf "${CONFIG_DIR}" "${STATE_DIR}" "${LOG_DIR}" fi log "uninstalled ${APP_NAME}" if [[ "${PURGE}" != "1" && "${PURGE}" != "true" && "${PURGE}" != "yes" ]]; then log "preserved config: ${CONFIG_DIR}" log "preserved state: ${STATE_DIR}" log "preserved logs: ${LOG_DIR}" fi } install_service() { local start_time url password log "installing for ${PLATFORM}" install_packages require_platform_commands ensure_run_identity prepare_directories install_binary write_config write_service_definition if [[ "${INSECURE_HTTP}" == "1" && "${BIND}" != 127.* && "${BIND}" != localhost:* ]]; then warn "NC_DAV_INSECURE_HTTP=1 with non-loopback bind '${BIND}'. Put this behind trusted network controls or enable TLS." fi start_time="$(date '+%Y-%m-%d %H:%M:%S')" start_service url="$(health_url)" wait_for_service "${url}" password="$(latest_generated_password "${start_time}" || true)" log "installed ${APP_NAME}" log "service: $(service_status_hint)" log "logs: $(service_log_hint)" log "local health: ${url}" log "public base URL: ${BASE_URL}" log "webdav URL: ${BASE_URL}/remote.php/dav" if [[ -n "${password}" ]]; then log "bootstrap user: gono" log "bootstrap app password: ${password}" warn "save this password now; normal restarts will not print it again" else log "no new bootstrap password was printed; existing database/password was preserved" fi log "configure HTTPS reverse proxy, including WebSocket upgrade for ${BASE_URL}/push/ws, to forward to http://${BIND}" } dispatch_command() { case "${COMMAND}" in install) prepare_local_binary_before_sudo require_root "$@" install_service ;; status) show_service_status ;; logs) follow_service_logs ;; restart) require_root "$@" restart_existing_service ;; uninstall) require_root "$@" uninstall_service ;; *) show_usage >&2 die "unknown command: ${COMMAND}" ;; esac } main() { require_cmd uname resolve_script_context parse_args "$@" set_platform_defaults dispatch_command "$@" } TMP_DIR="$(mktemp -d)" cleanup() { rm -rf "${TMP_DIR}" } trap cleanup EXIT main "$@"