#!/usr/bin/env bash # integrate-into-nodered.sh — add Collabora nodes to EXISTING nextcloud-node-red (:1880) # Called by install.sh / bootstrap-install.sh (default mode). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PKG_SRC="${PKG_SRC:-$(cd "${SCRIPT_DIR}/.." && pwd)}" NODERED_DIR="${NODERED_DIR:-/home/ncadmin/nextcloud-node-red}" NODERED_CONTAINER="${NODERED_CONTAINER:-nextcloud-node-red}" NODERED_PORT="${NODERED_PORT:-1880}" CODE_CONTAINER="${CODE_CONTAINER:-code}" COLLABORA_IP="${COLLABORA_IP:-}" NEXTCLOUD_URL="${NEXTCLOUD_URL:-https://cloud.hannesahlin.se}" MARKER="nextcloud-collabora-nodered" log() { printf '[integrate] %s\n' "$*"; } warn() { printf '[integrate] WARNING: %s\n' "$*" >&2; } die() { printf '[integrate] ERROR: %s\n' "$*" >&2; exit 1; } run_as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif command -v sudo >/dev/null 2>&1; then sudo "$@" else die "sudo/root required for: $*" fi } runtime_user() { if [ -n "${INSTALL_USER:-}" ]; then echo "${INSTALL_USER}" elif id ncadmin >/dev/null 2>&1; then echo "ncadmin" else id -un fi } # Docker build context — .dockerignore + rättigheter (undviker permission denied) DOCKERIGNORE_MARKER="nextcloud-collabora-nodered" patch_dockerignore() { local di="${NODERED_DIR}/.dockerignore" local snippet="${PKG_SRC}/integrate/dockerignore.snippet" if [ ! -f "${di}" ]; then log "Creating .dockerignore" touch "${di}" fi if ! grep -q "${DOCKERIGNORE_MARKER}" "${di}" 2>/dev/null; then log "Patching .dockerignore (exclude unreadable local artifacts)" { echo "" echo "# ${DOCKERIGNORE_MARKER}" if [ -f "${snippet}" ]; then cat "${snippet}" else echo "nodes/odds-scraper/odds-scraper" echo ".git" echo ".vscode" echo "**/*.deb" echo "winboat*" echo "**/node_modules" fi } >> "${di}" fi # Dynamiskt: alla filer under nodes/ som inte ens root kan läsa → ignorera local unreadable while IFS= read -r relpath; do [ -n "${relpath}" ] || continue if ! grep -qxF "${relpath}" "${di}" 2>/dev/null; then log " + .dockerignore: ${relpath} (unreadable)" echo "${relpath}" >> "${di}" fi done < <(run_as_root find "${NODERED_DIR}/nodes" -type f ! -readable -printf 'nodes/%P\n' 2>/dev/null || true) } fix_nodered_build_permissions() { local user group user="$(runtime_user)" group="$(id -gn "${user}" 2>/dev/null || echo "${user}")" if [ ! -d "${NODERED_DIR}/nodes" ]; then return 0 fi log "Fixing ownership and read permissions for ${NODERED_DIR} (content unchanged)" run_as_root chown -R "${user}:${group}" "${NODERED_DIR}" run_as_root find "${NODERED_DIR}/nodes" -type d -exec chmod u+rwx,go+rx {} + 2>/dev/null || true run_as_root find "${NODERED_DIR}/nodes" -type f -exec chmod u+rw,go+r {} + 2>/dev/null || true # Binärer som ska vara körbara run_as_root find "${NODERED_DIR}/nodes" -type f \( -name "*.sh" -o -name "odds-scraper" \) -exec chmod u+rx,go+rx {} + 2>/dev/null || true } # Simulera Docker build context — måste lyckas innan build verify_build_context() { local di="${NODERED_DIR}/.dockerignore" local errfile errfile="$(mktemp)" log "Verifying Docker build context..." patch_dockerignore if ! run_as_root tar -cf /dev/null \ --exclude-from="${di}" \ -C "${NODERED_DIR}" . 2>"${errfile}"; then warn "Build context error:" sed 's/^/[integrate] /' "${errfile}" >&2 || true # Sista utväg: tvinga bort kända problemfil if [ -f "${NODERED_DIR}/nodes/odds-scraper/odds-scraper" ]; then echo "nodes/odds-scraper/odds-scraper" >> "${di}" log "Forced nodes/odds-scraper/odds-scraper into .dockerignore" fi if ! run_as_root tar -cf /dev/null \ --exclude-from="${di}" \ -C "${NODERED_DIR}" . 2>"${errfile}"; then rm -f "${errfile}" die "Build context still unreadable after .dockerignore — see ${di}" fi fi rm -f "${errfile}" log "Build context OK" } compose_cmd() { if docker compose version >/dev/null 2>&1; then echo "docker compose" else echo "docker-compose" fi } # Alltid root för compose build — samma användare som kan läsa hela trädet + .dockerignore compose_run() { local compose compose="$(compose_cmd)" cd "${NODERED_DIR}" run_as_root env DOCKER_BUILDKIT=1 ${compose} "$@" } code_bridge_ip() { docker inspect -f '{{range $k,$v := .NetworkSettings.Networks}}{{if eq $k "bridge"}}{{$v.IPAddress}}{{end}}{{end}}' "${CODE_CONTAINER}" 2>/dev/null } detect_code_ip() { local ip ip="$(code_bridge_ip)" if [ -n "${ip}" ]; then COLLABORA_IP="${ip}" log "CODE bridge-IP: ${COLLABORA_IP} (container ${CODE_CONTAINER})" return 0 fi if [ -n "${COLLABORA_IP}" ]; then warn "Could not read bridge IP — using COLLABORA_IP=${COLLABORA_IP}" return 0 fi die "CODE bridge IP missing — set COLLABORA_IP or ensure ${CODE_CONTAINER} runs on Docker bridge" } detect_nodered_dir() { if [ -d "${NODERED_DIR}" ] && [ -f "${NODERED_DIR}/Dockerfile" ]; then return 0 fi local from_compose from_compose="$(docker inspect -f '{{index .Config.Labels "com.docker.compose.project.working_dir"}}' "${NODERED_CONTAINER}" 2>/dev/null || true)" if [ -n "${from_compose}" ] && [ -f "${from_compose}/Dockerfile" ]; then NODERED_DIR="${from_compose}" log "Node-RED directory from compose label: ${NODERED_DIR}" return 0 fi for candidate in \ "/home/ncadmin/nextcloud-node-red" \ "/opt/nextcloud-node-red"; do if [ -f "${candidate}/Dockerfile" ]; then NODERED_DIR="${candidate}" log "Node-RED directory found: ${NODERED_DIR}" return 0 fi done die "NODERED_DIR missing or invalid — set path to existing nextcloud-node-red (Dockerfile + entrypoint.sh)" } resolve_paths() { if [ -f "${PKG_SRC}/.env" ]; then set -a # shellcheck disable=SC1091 . "${PKG_SRC}/.env" set +a fi if [ "$(id -u)" -eq 0 ] && [ -d "/home/ncadmin/nextcloud-node-red" ]; then NODERED_DIR="${NODERED_DIR:-/home/ncadmin/nextcloud-node-red}" PKG_SRC="${PKG_SRC:-/home/ncadmin/nextcloud-collabora-nodered}" fi detect_nodered_dir detect_code_ip } stop_sidecar_if_any() { if docker ps -a --format '{{.Names}}' | grep -qx "nextcloud-collabora-nodered"; then warn "Stopping stray sidecar container nextcloud-collabora-nodered (should not exist)" docker stop nextcloud-collabora-nodered 2>/dev/null || true docker rm nextcloud-collabora-nodered 2>/dev/null || true fi } preflight() { [ -d "${PKG_SRC}" ] || die "Package source missing: ${PKG_SRC}" [ -f "${PKG_SRC}/package.json" ] || die "package.json missing in ${PKG_SRC}" [ -d "${NODERED_DIR}" ] || die "nextcloud-node-red missing: ${NODERED_DIR}" [ -f "${NODERED_DIR}/Dockerfile" ] || die "Dockerfile missing in ${NODERED_DIR}" [ -f "${NODERED_DIR}/entrypoint.sh" ] || die "entrypoint.sh missing in ${NODERED_DIR}" if ! docker ps --format '{{.Names}}' | grep -qx "${NODERED_CONTAINER}"; then die "Container '${NODERED_CONTAINER}' is not running — start existing nextcloud-node-red first" fi if ! docker ps --format '{{.Names}}' | grep -qx "${CODE_CONTAINER}"; then die "CODE container '${CODE_CONTAINER}' is not running" fi } assert_package_layout() { local dest="$1" local missing=0 if [ ! -f "${dest}/package.json" ]; then warn "Missing: ${dest}/package.json" missing=1 fi if [ ! -d "${dest}/lib" ] || [ -z "$(ls -A "${dest}/lib" 2>/dev/null)" ]; then warn "Missing or empty: ${dest}/lib/" missing=1 fi if [ ! -d "${dest}/nodes" ] || [ -z "$(ls -A "${dest}/nodes" 2>/dev/null)" ]; then warn "Missing or empty: ${dest}/nodes/" missing=1 fi if [ "${missing}" -ne 0 ]; then die "Wrong package layout after copy — expected package.json + lib/ + nodes/ under ${dest}" fi } copy_node_package() { local dest="${NODERED_DIR}/nodes/nextcloud-collabora" log "Copying package → ${dest} (lib/ + nodes/, not flat)" rm -rf "${dest}" mkdir -p "${dest}/lib" "${dest}/nodes" cp -a "${PKG_SRC}/package.json" "${dest}/" cp -a "${PKG_SRC}/lib/." "${dest}/lib/" cp -a "${PKG_SRC}/nodes/." "${dest}/nodes/" if [ -d "${PKG_SRC}/locales" ] && [ -n "$(ls -A "${PKG_SRC}/locales" 2>/dev/null)" ]; then mkdir -p "${dest}/locales" cp -a "${PKG_SRC}/locales/." "${dest}/locales/" fi assert_package_layout "${dest}" log "Package layout OK: ${dest}/{package.json,lib/,nodes/}" } patch_dockerfile() { local df="${NODERED_DIR}/Dockerfile" if grep -q "${MARKER}" "${df}"; then log "Dockerfile already patched" return 0 fi log "Patching Dockerfile" cat >> "${df}" <<'EOF' # nextcloud-collabora-nodered — Collabora/Office-noder (WebDAV + CODE REST) RUN mkdir -p /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora COPY nodes/nextcloud-collabora/package.json /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora/ COPY nodes/nextcloud-collabora/lib /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora/lib COPY nodes/nextcloud-collabora/nodes /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora/nodes RUN cd /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora && npm install --omit=dev EOF } patch_entrypoint() { local ep="${NODERED_DIR}/entrypoint.sh" # Rensa trasig tidigare patch (rå cp-rader utan install_nodes) if grep -q 'cp -a /opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora' "${ep}"; then log "Removing broken collabora patch from entrypoint.sh" sed -i '/cp -a \/opt\/nextcloud-nodes\/node-red-contrib-nextcloud-collabora/d' "${ep}" fi if grep -q 'install_nodes.*Nextcloud Collabora' "${ep}"; then log "entrypoint.sh already patched (Collabora install_nodes)" return 0 fi log "Patching entrypoint.sh (install_nodes for Collabora)" if grep -q 'install_nodes.*Nextcloud OCS' "${ep}"; then sed -i '/install_nodes.*Nextcloud OCS/a\ install_nodes "/opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora" "/data/node_modules/node-red-contrib-nextcloud-collabora" "Nextcloud Collabora"' "${ep}" elif grep -q 'install_nodes.*Odds Scraper' "${ep}"; then sed -i '/install_nodes.*Odds Scraper/a\ install_nodes "/opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora" "/data/node_modules/node-red-contrib-nextcloud-collabora" "Nextcloud Collabora"' "${ep}" else cat >> "${ep}" <<'EOF' # nextcloud-collabora-nodered install_nodes "/opt/nextcloud-nodes/node-red-contrib-nextcloud-collabora" "/data/node_modules/node-red-contrib-nextcloud-collabora" "Nextcloud Collabora" EOF fi } connect_code_network() { log "Connecting ${NODERED_CONTAINER} to Docker bridge (CODE)" if docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | grep -q "${NODERED_CONTAINER}"; then log "Already connected to bridge" else docker network connect bridge "${NODERED_CONTAINER}" 2>/dev/null || warn "Could not connect bridge — check manually" fi } rebuild_nodered() { log "Rebuilding ${NODERED_CONTAINER} (same instance, port ${NODERED_PORT})..." fix_nodered_build_permissions verify_build_context if [ "${FORCE_REBUILD:-0}" = "1" ]; then compose_run build --no-cache else compose_run build fi compose_run up -d } wait_nodered() { log "Waiting for Node-RED :${NODERED_PORT}..." for i in $(seq 1 30); do if curl -sf "http://127.0.0.1:${NODERED_PORT}" >/dev/null 2>&1; then log "Node-RED responding at http://127.0.0.1:${NODERED_PORT}" return 0 fi sleep 2 done warn "Node-RED did not respond within 60 s" } verify_code_from_nodered() { local code_ip code_ip="$(code_bridge_ip)" [ -n "${code_ip}" ] || code_ip="${COLLABORA_IP}" # CODE listens on HTTPS internally (HTTP returns empty reply) local url="https://${code_ip}:9980/hosting/capabilities" log "Testing CODE from ${NODERED_CONTAINER}: ${url}" if docker exec "${NODERED_CONTAINER}" curl -sfk "${url}" >/dev/null 2>&1; then log "OK: CODE reachable from Node-RED container" else warn "CODE not reachable — run: docker network connect bridge ${NODERED_CONTAINER}" warn "nc-collab-config collaboraUrl=https://${code_ip}:9980 (verifyTls=false)" fi } main() { resolve_paths log "Integrating Collabora nodes into existing nextcloud-node-red" log "Package: ${PKG_SRC}" log "Node-RED: ${NODERED_DIR} (container ${NODERED_CONTAINER})" stop_sidecar_if_any preflight copy_node_package patch_dockerfile patch_entrypoint rebuild_nodered connect_code_network wait_nodered verify_code_from_nodered local host_ip code_ip nc_url host_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1")" code_ip="$(code_bridge_ip)" [ -n "${code_ip}" ] || code_ip="${COLLABORA_IP}" nc_url="${NEXTCLOUD_URL:-https://cloud.hannesahlin.se}" cat <