#!/usr/bin/env bash # integrate-into-nodered.sh — lägg Collabora-noder i BEFINTLIG nextcloud-node-red (:1880) # Anropas av install.sh / bootstrap-install.sh (standardläge). set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PKG_SRC="${PKG_SRC:-$(cd "${SCRIPT_DIR}/.." && pwd)}" NODERED_DIR="${NODERED_DIR:-/home/ncdeploy/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.domain.name}" MARKER="nextcloud-collabora-nodered" log() { printf '[integrate] %s\n' "$*"; } warn() { printf '[integrate] VARNING: %s\n' "$*" >&2; } die() { printf '[integrate] FEL: %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 krävs för: $*" fi } runtime_user() { if [ -n "${INSTALL_USER:-}" ]; then echo "${INSTALL_USER}" elif id ncdeploy >/dev/null 2>&1; then echo "ncdeploy" 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 "Skapar .dockerignore" touch "${di}" fi if ! grep -q "${DOCKERIGNORE_MARKER}" "${di}" 2>/dev/null; then log "Patchar .dockerignore (exkluderar oläsbara lokala artefakter)" { 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} (oläsbar)" 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 "Fixar ägande och läsrättigheter för ${NODERED_DIR} (innehåll orört)" 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 "Verifierar 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-fel:" 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 "Tvingade nodes/odds-scraper/odds-scraper in i .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 oläsbar efter .dockerignore — se ${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 "Kunde inte läsa bridge-IP — använder COLLABORA_IP=${COLLABORA_IP}" return 0 fi die "CODE bridge-IP saknas — sätt COLLABORA_IP eller kontrollera att ${CODE_CONTAINER} kör på 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-katalog från compose-label: ${NODERED_DIR}" return 0 fi for candidate in \ "/home/ncdeploy/nextcloud-node-red" \ "/opt/nextcloud-node-red"; do if [ -f "${candidate}/Dockerfile" ]; then NODERED_DIR="${candidate}" log "Node-RED-katalog hittad: ${NODERED_DIR}" return 0 fi done die "NODERED_DIR saknas eller ogiltig — sätt sökväg till befintlig 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/ncdeploy/nextcloud-node-red" ]; then NODERED_DIR="${NODERED_DIR:-/home/ncdeploy/nextcloud-node-red}" PKG_SRC="${PKG_SRC:-/home/ncdeploy/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 "Stoppar felaktig sidecar-container nextcloud-collabora-nodered (skulle inte skapas)" docker stop nextcloud-collabora-nodered 2>/dev/null || true docker rm nextcloud-collabora-nodered 2>/dev/null || true fi } preflight() { [ -d "${PKG_SRC}" ] || die "Paketkälla saknas: ${PKG_SRC}" [ -f "${PKG_SRC}/package.json" ] || die "package.json saknas i ${PKG_SRC}" [ -d "${NODERED_DIR}" ] || die "nextcloud-node-red saknas: ${NODERED_DIR}" [ -f "${NODERED_DIR}/Dockerfile" ] || die "Dockerfile saknas i ${NODERED_DIR}" [ -f "${NODERED_DIR}/entrypoint.sh" ] || die "entrypoint.sh saknas i ${NODERED_DIR}" if ! docker ps --format '{{.Names}}' | grep -qx "${NODERED_CONTAINER}"; then die "Container '${NODERED_CONTAINER}' körs inte — starta befintlig nextcloud-node-red först" fi if ! docker ps --format '{{.Names}}' | grep -qx "${CODE_CONTAINER}"; then die "CODE-container '${CODE_CONTAINER}' körs inte" fi } assert_package_layout() { local dest="$1" local missing=0 if [ ! -f "${dest}/package.json" ]; then warn "Saknas: ${dest}/package.json" missing=1 fi if [ ! -d "${dest}/lib" ] || [ -z "$(ls -A "${dest}/lib" 2>/dev/null)" ]; then warn "Saknas eller tom: ${dest}/lib/" missing=1 fi if [ ! -d "${dest}/nodes" ] || [ -z "$(ls -A "${dest}/nodes" 2>/dev/null)" ]; then warn "Saknas eller tom: ${dest}/nodes/" missing=1 fi if [ "${missing}" -ne 0 ]; then die "Paketlayout fel efter kopiering — förväntat package.json + lib/ + nodes/ under ${dest}" fi } copy_node_package() { local dest="${NODERED_DIR}/nodes/nextcloud-collabora" log "Kopierar paket → ${dest} (lib/ + nodes/, inte platt)" 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 "Paketlayout OK: ${dest}/{package.json,lib/,nodes/}" } patch_dockerfile() { local df="${NODERED_DIR}/Dockerfile" if grep -q "${MARKER}" "${df}"; then log "Dockerfile redan patchad" return 0 fi log "Patchar 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 "Rensar trasig collabora-patch i 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 redan patchad (Collabora install_nodes)" return 0 fi log "Patchar entrypoint.sh (install_nodes för 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 "Ansluter ${NODERED_CONTAINER} till Docker bridge (CODE)" if docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}' 2>/dev/null | grep -q "${NODERED_CONTAINER}"; then log "Redan ansluten till bridge" else docker network connect bridge "${NODERED_CONTAINER}" 2>/dev/null || warn "Kunde inte ansluta bridge — kontrollera manuellt" fi } rebuild_nodered() { log "Bygger om ${NODERED_CONTAINER} (samma instans, 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 "Väntar på 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 svarar på http://127.0.0.1:${NODERED_PORT}" return 0 fi sleep 2 done warn "Node-RED svarade inte inom 60 s" } verify_code_from_nodered() { local code_ip code_ip="$(code_bridge_ip)" [ -n "${code_ip}" ] || code_ip="${COLLABORA_IP}" # CODE lyssnar på HTTPS internt (HTTP ger tom svar) local url="https://${code_ip}:9980/hosting/capabilities" log "Testar CODE från ${NODERED_CONTAINER}: ${url}" if docker exec "${NODERED_CONTAINER}" curl -sfk "${url}" >/dev/null 2>&1; then log "OK: CODE nås från befintlig Node-RED-container" else warn "CODE nås inte — kör: docker network connect bridge ${NODERED_CONTAINER}" warn "nc-collab-config collaboraUrl=https://${code_ip}:9980 (verifyTls=false)" fi } main() { resolve_paths log "Integrerar Collabora-noder i befintlig nextcloud-node-red" log "Paket: ${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.domain.name}" cat <