d3d3f33ec6
README, NODES.md, install scripts, palette help text, and example flows — node .js logic unchanged. Co-authored-by: Cursor <cursoragent@cursor.com>
408 lines
14 KiB
Bash
Executable File
408 lines
14 KiB
Bash
Executable File
#!/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 <<EOF
|
|
|
|
Integrating done — same Node-RED on port ${NODERED_PORT}
|
|
UI: http://${host_ip}:${NODERED_PORT}
|
|
Palette: "nextcloud collabora" (6 nodes + config)
|
|
|
|
Create config first (gear → nc-collab-config):
|
|
Nextcloud: ${nc_url}
|
|
Collabora: https://${code_ip}:9980 (verifyTls=false)
|
|
Public WOPI: ${nc_url}
|
|
Auth: username + app password
|
|
|
|
Try: collabora-wopi (capabilities) → collabora-bridge (ncFileToPdf)
|
|
Example flow: flows/example-flows.json
|
|
|
|
EOF
|
|
}
|
|
|
|
main "$@"
|