From 5ee179539ebad1c334e9f51df5002923631a530f Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 22 Aug 2026 22:50:47 +0200 Subject: [PATCH] =?UTF-8?q?Example-flows=20+=20noder:=20defaults=20pekar?= =?UTF-8?q?=20bara=20p=C3=A5=20filer=20som=20finns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flöden (flows/example-flows.json): - Kedjor som skapar det de behöver: png→asset, templates:list→create→ extract/fill/transform/extractFields, create A+B→compareVersionsPdf - Defaults /Dokument/Example.md, /Dokument/exempel.odt, dest /Dokument/OfficeOut/, mall-id 230 (Invoice.odt via templates:list) - Ingen referens till /Mallar/avtal.docx, avtal-ny.docx eller logotyp.png - Debug-noder med tostatus=false (undviker TypeError-length i status) Noder: - nc-office templates:create: skickar path+template (richdocuments- kontrakt), ärver extension från mallen, MKCOL av mapp, idempotent (återanvänd existerande fil i stället för 'namn (2)') — fixar OCS 400 - nc-office document:direct: fileId camelCase (+fileid), 996-fel inkluderar nu OCS-body i felmeddelandet - collabora-wopi discovery: rewrite urlsrc/favIconUrl till collaboraPublicUrl så iframe inte går mot CODE bridge-IP - webdavGet/PROPFIND 404: feltext med föreslagen åtgärd - resolveDestNcPath: tvingar rätt filändelse (.pdf/.png/…) - Nytt: listUserExamples (PROPFIND /Dokument+/Documents) + bridge-op pickDemoSource; scripts/discover-examples.js Verifierat mot cloud.domain.name (your-user): capabilities, discovery- rewrite, pdf/png, asset-länk, mall-skapa+extract/fill/transform, compare A/B→pdf, document:direct, htmlToOffice — ALL PASS. --- docs/NODES.md | 2 +- flows/example-flows.json | 561 +++++++++++++++++++++++++++--- lib/bridge.js | 24 +- lib/ocs.js | 22 +- lib/utils.js | 7 +- lib/webdav.js | 84 ++++- nodes/collabora-bridge.js | 3 +- nodes/collabora-wopi.js | 20 ++ nodes/nc-office.js | 74 +++- scripts/discover-examples.js | 68 ++++ scripts/integrate-into-nodered.sh | 5 +- 11 files changed, 801 insertions(+), 69 deletions(-) create mode 100644 scripts/discover-examples.js diff --git a/docs/NODES.md b/docs/NODES.md index ca8da23..d196633 100644 --- a/docs/NODES.md +++ b/docs/NODES.md @@ -176,7 +176,7 @@ Intern CODE körs med TLS och self-signed-cert (`ssl.enable=true`) — därför ### createAssetLink (assets) `collabora-bridge createAssetLink` / `nc-office assets:create` POSTar form-urlencoded -(`path=Dokument/logotyp.png`, relativt användarens filrot) mot +(`path=Dokument/OfficeOut/demo-asset.png`, relativt användarens filrot) mot `/apps/richdocuments/assets`. `200 {url}` = asset-URL skapad; `404` = filen finns inte i användarens rot (eller fel `nextcloudUrl`) — felet innehåller status + svarskropp. diff --git a/flows/example-flows.json b/flows/example-flows.json index f45439b..b352b1b 100644 --- a/flows/example-flows.json +++ b/flows/example-flows.json @@ -4,7 +4,7 @@ "type": "tab", "label": "NC Collabora — examples", "disabled": false, - "info": "Create nc-collab-config first (gear menu). Set FILL_IN_USER and app password.\n\n1. capabilities — is CODE up?\n2. ncFileToPdf — PDF in OfficeOut\n3. document:direct — editorUrl\n\nSee docs/NODES.md" + "info": "Create/edit nc-collab-config (gear): Nextcloud URL, username + app password,\nCollabora intern https://code:9980 (verifyTls av), publik URL t.ex. https://docs.domain.name.\n\nAlla kedjor utgår från filer som FINNS på denna host:\n /Dokument/Example.md, /Dokument/exempel.odt\nMall-id 230 = Invoice.odt (hämtat via templates:list — kör kedjan\n'templates:list → pick → create' om id:n skiljer).\nDestinationer landar i /Dokument/OfficeOut/ (MKCOL sker tyst).\n\nKedjor:\n 1. capabilities + discovery (urlsrc skrivs om till publik bas)\n 2. ncFileToPdf (/Dokument/Example.md)\n 3. ncFileToPng → createAssetLink (png:n skapas först i kedjan)\n 4. document:direct mot befintlig fil (PROPFIND fileId)\n 5. htmlToOffice\n 6. templates:create explicit (msg.template + msg.path)\n 7. templates:list → pick → create → extract/fill/transform/extractFields\n 8. compareVersionsPdf: två kopior skapade först via templates:create\n\nSe docs/NODES.md" }, { "id": "nc_collab_cfg", @@ -12,21 +12,22 @@ "name": "cloud.domain.name", "nextcloudUrl": "https://cloud.domain.name", "collaboraUrl": "https://code:9980", - "collaboraPublicUrl": "https://cloud.domain.name", + "collaboraPublicUrl": "https://docs.domain.name", "username": "FILL_IN_USER", "verifyTls": false }, + { "id": "inj_capabilities", "type": "inject", "z": "collab_tab", - "name": "1. CODE capabilities", + "name": "1a. CODE capabilities", "props": [], "repeat": "", "crontab": "", "once": false, "topic": "", - "x": 150, + "x": 160, "y": 80, "wires": [["wopi_cap"]] }, @@ -37,7 +38,7 @@ "name": "capabilities", "config": "nc_collab_cfg", "operation": "capabilities", - "x": 380, + "x": 400, "y": 80, "wires": [["dbg_cap"]] }, @@ -51,28 +52,66 @@ "console": false, "tostatus": false, "complete": "payload", - "x": 590, + "x": 620, "y": 80, "wires": [] }, + + { + "id": "inj_discovery", + "type": "inject", + "z": "collab_tab", + "name": "1b. discovery (publik rewrite)", + "props": [], + "repeat": "", + "crontab": "", + "once": false, + "topic": "", + "x": 170, + "y": 140, + "wires": [["wopi_disc"]] + }, + { + "id": "wopi_disc", + "type": "collabora-wopi", + "z": "collab_tab", + "name": "discovery", + "config": "nc_collab_cfg", + "operation": "discovery", + "x": 400, + "y": 140, + "wires": [["dbg_disc"]] + }, + { + "id": "dbg_disc", + "type": "debug", + "z": "collab_tab", + "name": "urlsrc ska peka på publik bas", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "x": 640, + "y": 140, + "wires": [] + }, + { "id": "inj_pdf", "type": "inject", "z": "collab_tab", - "name": "2. NC-fil → PDF", + "name": "2. Example.md → PDF", "props": [ - { - "p": "ncPath", - "v": "/Dokument/test.docx", - "vt": "str" - } + { "p": "ncPath", "v": "/Dokument/Example.md", "vt": "str" }, + { "p": "destName", "v": "Example.pdf", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "", - "x": 150, - "y": 160, + "x": 160, + "y": 220, "wires": [["bridge_pdf"]] }, { @@ -82,8 +121,8 @@ "name": "ncFileToPdf", "config": "nc_collab_cfg", "operation": "ncFileToPdf", - "x": 380, - "y": 160, + "x": 400, + "y": 220, "wires": [["dbg_pdf"]] }, { @@ -96,28 +135,91 @@ "console": false, "tostatus": false, "complete": "destNcPath", - "x": 590, - "y": 160, + "x": 620, + "y": 220, "wires": [] }, + { - "id": "inj_editor", + "id": "inj_png_asset", "type": "inject", "z": "collab_tab", - "name": "3. Open in Office", + "name": "3. exempel.odt → PNG → asset-länk", "props": [ - { - "p": "ncPath", - "v": "/Dokument/test.docx", - "vt": "str" - } + { "p": "ncPath", "v": "/Dokument/exempel.odt", "vt": "str" }, + { "p": "destName", "v": "demo-asset.png", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "", - "x": 150, - "y": 240, + "x": 190, + "y": 300, + "wires": [["bridge_png"]] + }, + { + "id": "bridge_png", + "type": "collabora-bridge", + "z": "collab_tab", + "name": "ncFileToPng", + "config": "nc_collab_cfg", + "operation": "ncFileToPng", + "x": 410, + "y": 300, + "wires": [["fn_asset_path"]] + }, + { + "id": "fn_asset_path", + "type": "function", + "z": "collab_tab", + "name": "peka ncPath på nya png:en", + "func": "if (!msg.destNcPath) { return null; }\nmsg.ncPath = msg.destNcPath;\nreturn msg;", + "outputs": 1, + "noerr": 0, + "x": 630, + "y": 300, + "wires": [["bridge_asset"]] + }, + { + "id": "bridge_asset", + "type": "collabora-bridge", + "z": "collab_tab", + "name": "createAssetLink", + "config": "nc_collab_cfg", + "operation": "createAssetLink", + "x": 860, + "y": 300, + "wires": [["dbg_asset"]] + }, + { + "id": "dbg_asset", + "type": "debug", + "z": "collab_tab", + "name": "assetUrl", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "assetUrl", + "x": 1080, + "y": 300, + "wires": [] + }, + + { + "id": "inj_direct", + "type": "inject", + "z": "collab_tab", + "name": "4. document:direct (befintlig fil)", + "props": [ + { "p": "ncPath", "v": "/Dokument/exempel.odt", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": false, + "topic": "", + "x": 190, + "y": 380, "wires": [["office_direct"]] }, { @@ -127,8 +229,8 @@ "name": "document:direct", "config": "nc_collab_cfg", "operation": "document:direct", - "x": 380, - "y": 240, + "x": 420, + "y": 380, "wires": [["dbg_editor"]] }, { @@ -141,53 +243,414 @@ "console": false, "tostatus": false, "complete": "editorUrl", - "x": 590, - "y": 240, + "x": 640, + "y": 380, "wires": [] }, + { - "id": "inj_extract", + "id": "inj_html", "type": "inject", "z": "collab_tab", - "name": "4. Template fields", + "name": "5. HTML → PDF", "props": [ - { - "p": "ncPath", - "v": "/Mallar/avtal.docx", - "vt": "str" - } + { "p": "payload", "v": "

Rapport

Skapad av Node-RED mot Collabora CODE.

", "vt": "str" }, + { "p": "filename", "v": "rapport.html", "vt": "str" }, + { "p": "format", "v": "pdf", "vt": "str" }, + { "p": "destName", "v": "rapport-demo.pdf", "vt": "str" } ], "repeat": "", "crontab": "", "once": false, "topic": "", "x": 150, - "y": 320, - "wires": [["bridge_extract"]] + "y": 460, + "wires": [["bridge_html"]] + }, + { + "id": "bridge_html", + "type": "collabora-bridge", + "z": "collab_tab", + "name": "htmlToOffice", + "config": "nc_collab_cfg", + "operation": "htmlToOffice", + "x": 410, + "y": 460, + "wires": [["dbg_html"]] + }, + { + "id": "dbg_html", + "type": "debug", + "z": "collab_tab", + "name": "destNcPath", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "destNcPath", + "x": 630, + "y": 460, + "wires": [] + }, + + { + "id": "inj_tpl_explicit", + "type": "inject", + "z": "collab_tab", + "name": "6. skapa från mall 230 (Invoice)", + "props": [ + { "p": "template", "v": 230, "vt": "num" }, + { "p": "path", "v": "/Dokument/OfficeOut/fran-mall", "vt": "str" }, + { "p": "templateExtension", "v": "odt", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": false, + "topic": "", + "x": 180, + "y": 540, + "wires": [["office_tpl_create1"]] + }, + { + "id": "office_tpl_create1", + "type": "nc-office", + "z": "collab_tab", + "name": "templates:create (path+template)", + "config": "nc_collab_cfg", + "operation": "templates:create", + "x": 450, + "y": 540, + "wires": [["fn_created_direct"]] + }, + { + "id": "fn_created_direct", + "type": "function", + "z": "collab_tab", + "name": "använd skapad fil", + "func": "if (!msg.createdPath) { return null; }\nmsg.ncPath = msg.createdPath;\nreturn msg;", + "outputs": 1, + "noerr": 0, + "x": 700, + "y": 540, + "wires": [["office_extract_fields"]] + }, + { + "id": "office_extract_fields", + "type": "nc-office", + "z": "collab_tab", + "name": "extractFields (fileId via PROPFIND)", + "config": "nc_collab_cfg", + "operation": "template:extractFields", + "x": 950, + "y": 540, + "wires": [["dbg_tpl_fields"]] + }, + { + "id": "dbg_tpl_fields", + "type": "debug", + "z": "collab_tab", + "name": "fields (tom lista är OK)", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "x": 1200, + "y": 540, + "wires": [] + }, + + { + "id": "inj_tpl_chain", + "type": "inject", + "z": "collab_tab", + "name": "7a. lista mallar", + "props": [], + "repeat": "", + "crontab": "", + "once": false, + "topic": "", + "x": 150, + "y": 640, + "wires": [["office_tpl_list"]] + }, + { + "id": "office_tpl_list", + "type": "nc-office", + "z": "collab_tab", + "name": "templates:list document", + "config": "nc_collab_cfg", + "operation": "templates:list", + "x": 380, + "y": 640, + "wires": [["fn_pick_template"]] + }, + { + "id": "fn_pick_template", + "type": "function", + "z": "collab_tab", + "name": "pick Invoice/Letter (inte Empty)", + "func": "const list = Array.isArray(msg.payload) ? msg.payload : (msg.payload && msg.payload.data) || [];\nconst docs = list.filter(t => t && t.type === 'document' && !/^empty/i.test(String(t.name || '')));\nconst pick = docs.find(t => /invoice/i.test(t.name)) || docs.find(t => /letter/i.test(t.name)) || docs[0];\nif (!pick) {\n throw new Error('Ingen document-mall hittad (forvantade Invoice/Letter, inte Empty) - kor templates:list forst');\n}\n// Extension fran mallens NAMN (Invoice.odt -> odt) speglar verkligt innehall;\n// 'extension'-faltet kan avvika nar NC doc_format=ooxml.\nconst nameExt = (String(pick.name || '').match(/\\.([A-Za-z0-9]+)$/) || [])[1];\nconst ext = nameExt || pick.extension || 'odt';\nmsg.templateList = list;\nmsg.template = pick.id;\nmsg.templateName = pick.name;\nmsg.templateExtension = ext;\nmsg.path = '/Dokument/OfficeOut/demo-mall.' + ext;\nreturn msg;", + "outputs": 1, + "noerr": 0, + "x": 610, + "y": 640, + "wires": [["office_tpl_create2"]] + }, + { + "id": "office_tpl_create2", + "type": "nc-office", + "z": "collab_tab", + "name": "templates:create demo-mall", + "config": "nc_collab_cfg", + "operation": "templates:create", + "x": 850, + "y": 640, + "wires": [["fn_use_created"]] + }, + { + "id": "fn_use_created", + "type": "function", + "z": "collab_tab", + "name": "prep: ncPath/fileId/fields {}", + "func": "if (!msg.createdPath) { return null; }\nmsg.ncPath = msg.createdPath;\nmsg.fileId = undefined;\nmsg.fields = {};\nmsg.transform = {};\nmsg.destNcPath = '/Dokument/OfficeOut/demo-mall-ifylld.' + (msg.templateExtension || 'odt');\nreturn msg;", + "outputs": 1, + "noerr": 0, + "x": 1090, + "y": 640, + "wires": [ + ["bridge_extract"], + ["office_extract2"], + ["bridge_fill"], + ["node_transform"] + ] }, { "id": "bridge_extract", "type": "collabora-bridge", "z": "collab_tab", - "name": "extractToPayload", + "name": "extract struktur", "config": "nc_collab_cfg", "operation": "extractToPayload", - "x": 390, - "y": 320, - "wires": [["dbg_fields"]] + "x": 1330, + "y": 560, + "wires": [["dbg_structure"]] }, { - "id": "dbg_fields", + "id": "dbg_structure", "type": "debug", "z": "collab_tab", - "name": "fields / structure", + "name": "structure", "active": true, "tosidebar": true, "console": false, "tostatus": false, - "complete": "fields", - "x": 610, - "y": 320, + "complete": "structure", + "x": 1550, + "y": 560, + "wires": [] + }, + { + "id": "office_extract2", + "type": "nc-office", + "z": "collab_tab", + "name": "extractFields (OCS)", + "config": "nc_collab_cfg", + "operation": "template:extractFields", + "x": 1320, + "y": 620, + "wires": [["dbg_ocs_fields"]] + }, + { + "id": "dbg_ocs_fields", + "type": "debug", + "z": "collab_tab", + "name": "OCS fields", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "payload", + "x": 1540, + "y": 620, + "wires": [] + }, + { + "id": "bridge_fill", + "type": "collabora-bridge", + "z": "collab_tab", + "name": "fillTemplateSave {}", + "config": "nc_collab_cfg", + "operation": "fillTemplateSave", + "x": 1320, + "y": 680, + "wires": [["dbg_fill"]] + }, + { + "id": "dbg_fill", + "type": "debug", + "z": "collab_tab", + "name": "ifylld destNcPath", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "destNcPath", + "x": 1550, + "y": 680, + "wires": [] + }, + { + "id": "node_transform", + "type": "collabora-transform", + "z": "collab_tab", + "name": "transform {}", + "config": "nc_collab_cfg", + "x": 1310, + "y": 740, + "wires": [["dbg_transform"]] + }, + { + "id": "dbg_transform", + "type": "debug", + "z": "collab_tab", + "name": "transform buffer", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "contentType", + "x": 1540, + "y": 740, + "wires": [] + }, + + { + "id": "inj_compare_a", + "type": "inject", + "z": "collab_tab", + "name": "8a. skapa kopia A", + "props": [ + { "p": "template", "v": 230, "vt": "num" }, + { "p": "path", "v": "/Dokument/OfficeOut/demo-a.odt", "vt": "str" } + ], + "repeat": "", + "crontab": "", + "once": false, + "topic": "", + "x": 160, + "y": 860, + "wires": [["office_cmp_a"]] + }, + { + "id": "office_cmp_a", + "type": "nc-office", + "z": "collab_tab", + "name": "templates:create A", + "config": "nc_collab_cfg", + "operation": "templates:create", + "x": 420, + "y": 860, + "wires": [["fn_to_b"]] + }, + { + "id": "fn_to_b", + "type": "function", + "z": "collab_tab", + "name": "byt path till B", + "func": "if (!msg.createdPath) { return null; }\nmsg.path = '/Dokument/OfficeOut/demo-b.odt';\nreturn msg;", + "outputs": 1, + "noerr": 0, + "x": 640, + "y": 860, + "wires": [["office_cmp_b"]] + }, + { + "id": "office_cmp_b", + "type": "nc-office", + "z": "collab_tab", + "name": "templates:create B", + "config": "nc_collab_cfg", + "operation": "templates:create", + "x": 880, + "y": 860, + "wires": [["fn_compare_setup"]] + }, + { + "id": "fn_compare_setup", + "type": "function", + "z": "collab_tab", + "name": "sätt A/B + dest", + "func": "if (!msg.createdPath) { return null; }\nmsg.ncPath = '/Dokument/OfficeOut/demo-a.odt';\nmsg.compareNcPath = '/Dokument/OfficeOut/demo-b.odt';\nmsg.destNcPath = '/Dokument/OfficeOut/demo-diff.pdf';\nreturn msg;", + "outputs": 1, + "noerr": 0, + "x": 1110, + "y": 860, + "wires": [["bridge_compare"]] + }, + { + "id": "bridge_compare", + "type": "collabora-bridge", + "z": "collab_tab", + "name": "compareVersionsPdf", + "config": "nc_collab_cfg", + "operation": "compareVersionsPdf", + "x": 1350, + "y": 860, + "wires": [["dbg_diff"]] + }, + { + "id": "dbg_diff", + "type": "debug", + "z": "collab_tab", + "name": "diff destNcPath", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "destNcPath", + "x": 1570, + "y": 860, + "wires": [] + }, + + { + "id": "inj_pick_demo", + "type": "inject", + "z": "collab_tab", + "name": "9. hitta exempelfil (pickDemoSource)", + "props": [], + "repeat": "", + "crontab": "", + "once": false, + "topic": "", + "x": 190, + "y": 960, + "wires": [["bridge_pick"]] + }, + { + "id": "bridge_pick", + "type": "collabora-bridge", + "z": "collab_tab", + "name": "pickDemoSource", + "config": "nc_collab_cfg", + "operation": "pickDemoSource", + "x": 430, + "y": 960, + "wires": [["dbg_pick"]] + }, + { + "id": "dbg_pick", + "type": "debug", + "z": "collab_tab", + "name": "kandidater + vald ncPath", + "active": true, + "tosidebar": true, + "console": false, + "tostatus": false, + "complete": "picked", + "x": 670, + "y": 960, "wires": [] } ] diff --git a/lib/bridge.js b/lib/bridge.js index 23fdb02..a54389b 100644 --- a/lib/bridge.js +++ b/lib/bridge.js @@ -1,6 +1,6 @@ 'use strict'; -const { webdavGet, webdavPut, webdavPropfindFileId } = require('./webdav'); +const { webdavGet, webdavPut, webdavPropfindFileId, listUserExamples } = require('./webdav'); const { ocsRequest } = require('./ocs'); const { requestBuffer } = require('./http-client'); const { convertTo, extractStructure, transformStructure } = require('./collabora'); @@ -50,6 +50,27 @@ async function loadSourceBuffer(config, msg) { return file; } +// Bridge-op "pickDemoSource": slå upp en fil som FAKTISKT finns hos användaren +// (/Dokument först, sedan /Documents) och lägg den på msg.ncPath. Endast för +// demo/generator-flöden — ingen silent fallback inne i convert-operationer. +async function pickDemoSource(config, msg) { + const examples = await listUserExamples(config); + if (!examples.length) { + throw new Error( + 'Ingen exempelfil hittad i /Dokument eller /Documents — ladda upp en .md/.odt/.docx ' + + 'eller ange msg.ncPath manuellt.' + ); + } + msg.demoSources = examples.map((e) => e.ncPath); + if (!pickMsgField(msg, 'ncPath')) { + msg.ncPath = examples[0].ncPath; + } + return { + payload: examples, + picked: msg.ncPath + }; +} + async function saveResult(config, msg, buffer, format, sourceNcPath) { const destNcPath = assertSafeDestination( resolveDestNcPath(msg, sourceNcPath, format), @@ -266,6 +287,7 @@ module.exports = { compareVersionsPdf, extractToPayload, createAssetLink, + pickDemoSource, loadSourceBuffer, saveResult, resolveFileId diff --git a/lib/ocs.js b/lib/ocs.js index b792f9e..e46c3b1 100644 --- a/lib/ocs.js +++ b/lib/ocs.js @@ -24,10 +24,13 @@ function parseOcsBody(bodyBuffer) { const json = JSON.parse(text); const ocs = json.ocs || json; const meta = ocs.meta || {}; - const statuscode = meta.statuscode ?? meta.statusCode ?? 200; + const statuscode = Number(meta.statuscode ?? meta.statusCode ?? 200); if (statuscode >= 400) { - const message = meta.message || meta.status || text.slice(0, 200); - throw new Error(`OCS ${statuscode}: ${message}`); + // 996 = intern serverfel, 998 = not found m.m. — inkludera alltid + // OCS-bodyn så felet går att felsöka utan logggrävning. + const message = meta.message || meta.status || ''; + const raw = text.slice(0, 300); + throw new Error(`OCS ${statuscode}: ${message}${message && raw !== message ? ` (${raw})` : raw}`); } return { statusCode: statuscode, @@ -38,7 +41,7 @@ function parseOcsBody(bodyBuffer) { if (err.message.startsWith('OCS ')) { throw err; } - throw new Error(`OCS-svar kunde inte parsas: ${text.slice(0, 200)}`); + throw new Error(`OCS-svar kunde inte parsas (HTTP-body): ${text.slice(0, 200)}`); } } @@ -77,9 +80,18 @@ async function ocsRequest(config, { method = 'GET', path, query, body, headers = throw new Error(`OCS ${res.statusCode}: kontrollera app-lösenord`); } + // OCS-fel (996/998/…) kommer med HTTP 4xx/5xx men har parsbar meta-body — + // försök alltid parsa först så felet innehåller OCS-meddelandet. + const parsed = parseOcsBody(res.body); + + if (res.statusCode < 200 || res.statusCode >= 300) { + const bodyText = res.body.toString('utf8').slice(0, 200); + throw new Error(`OCS-anrop misslyckades (HTTP ${res.statusCode}) för ${path}: ${bodyText || ''}`); + } + return { httpStatus: res.statusCode, - ...parseOcsBody(res.body) + ...parsed }; } diff --git a/lib/utils.js b/lib/utils.js index 33ed7e8..d42de1d 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -60,7 +60,12 @@ function resolveDestNcPath(msg, sourceNcPath, format) { if (msg.destFolder || msg.destName) { const folder = normalizeNcPath(msg.destFolder || DEFAULT_OUTPUT_FOLDER); const folderWithSlash = folder.endsWith('/') ? folder : folder + '/'; - const name = msg.destName || replaceExtension(basenameFromNcPath(sourceNcPath), format || 'bin'); + let name = msg.destName || replaceExtension(basenameFromNcPath(sourceNcPath), format || 'bin'); + // Rätt filändelse tvingas in — annars landar filer utan ändelse i NC. + const fmt = String(format || '').toLowerCase(); + if (fmt && !new RegExp(`\\.${fmt}$`, 'i').test(name)) { + name = replaceExtension(name, fmt); + } return normalizeNcPath(folderWithSlash + name); } return defaultDestNcPath(sourceNcPath, format); diff --git a/lib/webdav.js b/lib/webdav.js index 04d5369..99ed2c8 100644 --- a/lib/webdav.js +++ b/lib/webdav.js @@ -41,7 +41,11 @@ async function webdavGet(config, ncPath) { }); if (res.statusCode === 404) { - throw new Error(`WebDAV 404: filen finns inte: ${normalizeNcPath(ncPath)}`); + throw new Error( + `WebDAV 404: filen finns inte: ${normalizeNcPath(ncPath)} — ` + + 'ange msg.ncPath till en fil som finns; defaults är /Dokument/Example.md ' + + 'eller en mall via templates:list (se bridge-op pickDemoSource).' + ); } if (res.statusCode === 401 || res.statusCode === 403) { throw new Error(`WebDAV ${res.statusCode}: kontrollera app-lösenord och användare`); @@ -134,6 +138,12 @@ async function webdavPropfindFileId(config, ncPath) { rejectUnauthorized: tlsOptions(config).rejectUnauthorized }); + if (res.statusCode === 404) { + throw new Error( + `PROPFIND 404: ${normalizeNcPath(ncPath)} finns inte — ` + + 'ange msg.ncPath till en fil som finns (t.ex. /Dokument/Example.md).' + ); + } if (res.statusCode < 200 || res.statusCode >= 300) { throw new Error(`PROPFIND misslyckades (${res.statusCode}) för ${normalizeNcPath(ncPath)}`); } @@ -146,10 +156,80 @@ async function webdavPropfindFileId(config, ncPath) { return parseInt(match[1], 10); } +// Discovery av riktiga defaults: PROPFIND /Dokument och /Documents (Depth 1), +// returnera kandidater (.md/.odt/.docx) i prioritetsordning. Används BARA av +// example-flow-generator / bridge-op pickDemoSource — aldrig inne i convert. +async function listUserExamples(config, folders) { + const candidates = folders || ['/Dokument', '/Documents']; + const found = []; + const wanted = /\.(md|odt|docx)$/i; + + for (const folder of candidates) { + let res; + try { + res = await requestBuffer({ + url: webDavUrl(config, folder), + method: 'PROPFIND', + headers: { + Authorization: authHeader(config), + Depth: '1', + 'Content-Type': 'application/xml; charset=utf-8' + }, + body: '', + rejectUnauthorized: tlsOptions(config).rejectUnauthorized + }); + } catch (err) { + continue; // mapp finns ej — prova nästa + } + if (res.statusCode < 200 || res.statusCode >= 300) { + continue; + } + + const text = res.body.toString('utf8'); + const responses = text.split('').slice(1); + for (const chunk of responses) { + const hrefMatch = chunk.match(/([^<]+)<\/d:href>/i); + if (!hrefMatch) { + continue; + } + let href; + try { + href = decodeURIComponent(hrefMatch[1]); + } catch { + href = hrefMatch[1]; + } + // href: /remote.php/dav/files//Dokument/exempel.odt → ncPath + const marker = `/files/${config.username}/`; + const idx = href.indexOf(marker); + if (idx === -1) { + continue; + } + const ncPath = normalizeNcPath(href.substring(idx + marker.length)); + if (!wanted.test(ncPath)) { + continue; + } + const isCollection = //i.test(chunk); + if (isCollection) { + continue; + } + found.push({ ncPath, folder: normalizeNcPath(folder), name: filenameFromPath(ncPath) }); + } + } + + const priority = { md: 0, odt: 1, docx: 2 }; + found.sort((a, b) => { + const extA = a.ncPath.split('.').pop().toLowerCase(); + const extB = b.ncPath.split('.').pop().toLowerCase(); + return (priority[extA] ?? 9) - (priority[extB] ?? 9); + }); + return found; +} + module.exports = { webDavUrl, webdavGet, webdavPut, webdavPropfindFileId, - ensureFolder + ensureFolder, + listUserExamples }; diff --git a/nodes/collabora-bridge.js b/nodes/collabora-bridge.js index 73732b4..b8f65aa 100644 --- a/nodes/collabora-bridge.js +++ b/nodes/collabora-bridge.js @@ -11,7 +11,8 @@ const OPERATIONS = { fillTemplateSave: bridge.fillTemplateSave, compareVersionsPdf: bridge.compareVersionsPdf, extractToPayload: bridge.extractToPayload, - createAssetLink: bridge.createAssetLink + createAssetLink: bridge.createAssetLink, + pickDemoSource: bridge.pickDemoSource }; module.exports = function (RED) { diff --git a/nodes/collabora-wopi.js b/nodes/collabora-wopi.js index f9a77a8..e9366e2 100644 --- a/nodes/collabora-wopi.js +++ b/nodes/collabora-wopi.js @@ -4,6 +4,24 @@ const { getConfig, setError, setSuccess, clearStatus } = require('../lib/utils') const { getCapabilities, getDiscovery } = require('../lib/collabora'); const { requestBuffer } = require('../lib/http-client'); +// Discovery urlsrc/favIconUrl pekar ofta på CODE:s interna host (t.ex. +// https://172.17.0.2:9980). Om en publik bas är satt (collaboraPublicUrl) +// skrivs hosten om så att browser/iframes inte går mot bridge-IP:t. +function rewriteDiscoveryHosts(xml, internalBase, publicBase) { + if (!xml || !publicBase || !internalBase) { + return xml; + } + let internalOrigin; + try { + internalOrigin = new URL(internalBase).origin; + } catch { + return xml; + } + const escaped = internalOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const publicOrigin = String(publicBase).replace(/\/+$/, ''); + return xml.replace(new RegExp(escaped, 'g'), publicOrigin); +} + module.exports = function (RED) { function CollaboraWopiNode(config) { RED.nodes.createNode(this, config); @@ -24,6 +42,8 @@ module.exports = function (RED) { msg.payload = await getCapabilities(cfg); } else if (op === 'discovery') { msg.payload = await getDiscovery(cfg); + const publicBase = cfg.collaboraPublicUrl || ''; + msg.payload = rewriteDiscoveryHosts(msg.payload, cfg.collaboraUrl, publicBase); } else if (op === 'buildEditor') { const publicBase = cfg.collaboraPublicUrl || cfg.collaboraUrl; const wopiSrc = encodeURIComponent(msg.wopiSrc || msg.WOPISrc || ''); diff --git a/nodes/nc-office.js b/nodes/nc-office.js index 3450410..7b43a43 100644 --- a/nodes/nc-office.js +++ b/nodes/nc-office.js @@ -4,15 +4,20 @@ const { getConfig, setError, setSuccess, clearStatus, pickMsgField } = require(' const { ocsRequest } = require('../lib/ocs'); const bridge = require('../lib/bridge'); const { resolveFileId } = bridge; -const { webdavGet, webdavPut } = require('../lib/webdav'); +const { webdavGet, webdavPut, ensureFolder, webdavPropfindFileId } = require('../lib/webdav'); const { convertTo } = require('../lib/collabora'); const { resolveDestNcPath, assertSafeDestination, contentTypeForFormat, - normalizeNcPath + normalizeNcPath, + DEFAULT_OUTPUT_FOLDER } = require('../lib/utils'); +function hasExtension(p) { + return /\.[^/.]+$/.test(String(p).replace(/\/+$/, '')); +} + module.exports = function (RED) { function NcOfficeNode(config) { RED.nodes.createNode(this, config); @@ -41,16 +46,69 @@ module.exports = function (RED) { break; } case 'templates:create': { + // richdocuments OCS#createFromTemplate($path, $template) + // POST /ocs/v2.php/apps/richdocuments/api/v1/templates/new + const template = pickMsgField(msg, 'template', 'templateId'); + if (template === undefined || template === null || template === '') { + throw new Error('templates:create kräver msg.template (numeriskt id från templates:list)'); + } + + let path = normalizeNcPath( + pickMsgField(msg, 'path', 'destNcPath') || + msg.templatePath || + undefined + ); + if (!path) { + const name = msg.destName || msg.fileName; + if (!name) { + throw new Error('templates:create kräver msg.path eller msg.destFolder+msg.destName'); + } + const folder = normalizeNcPath(msg.destFolder || DEFAULT_OUTPUT_FOLDER); + path = normalizeNcPath((folder.endsWith('/') ? folder : folder + '/') + String(name).replace(/^\/+/, '')); + } + + // Ärv extension från mallen om målnamnet saknar en. + if (!hasExtension(path)) { + const ext = pickMsgField(msg, 'templateExtension', 'extension'); + if (ext) { + path += '.' + String(ext).replace(/^\./, ''); + } + } + + // Idempotent: finns filen redan → återanvänd (ingen + // "namn (2)"-dubblett via direct editing). + try { + await webdavPropfindFileId(cfg, path); + msg.template = Number(template); + msg.path = path; + msg.createdPath = path; + msg.reused = true; + msg.payload = { path, reused: true }; + setSuccess(node, msg, 'templates:create (återanvänd)'); + send(msg); + done(); + return; + } catch (e) { /* finns inte — skapa nedan */ } + + // Mallifyllning via direct editing kräver att mappen finns. + await ensureFolder(cfg, path.substring(0, path.lastIndexOf('/')) || '/'); + result = await ocsRequest(cfg, { method: 'POST', path: '/ocs/v2.php/apps/richdocuments/api/v1/templates/new', body: { - templateId: msg.templateId, - fileName: msg.fileName || msg.destName, - templatePath: msg.templatePath + path, + template: Number(template) } }); - msg.payload = result.data; + + msg.template = Number(template); + msg.path = path; + msg.createdPath = path; + if (result.data && typeof result.data === 'object' && result.data.url) { + msg.directEditUrl = result.data.url; + } + msg.payload = { ...result.data, path }; break; } case 'template:extractFields': { @@ -80,11 +138,13 @@ module.exports = function (RED) { break; } case 'document:direct': { + // richdocuments OCS#createDirect($fileId) binder parametern + // per metodnamn — skicka både fileId och fileid (Number). const fileId = await resolveFileId(cfg, msg); result = await ocsRequest(cfg, { method: 'POST', path: '/ocs/v2.php/apps/richdocuments/api/v1/document', - body: { fileid: Number(fileId) } + body: { fileId: Number(fileId), fileid: Number(fileId) } }); msg.fileId = fileId; msg.editorUrl = result.data?.url || result.data; diff --git a/scripts/discover-examples.js b/scripts/discover-examples.js new file mode 100644 index 0000000..8a59147 --- /dev/null +++ b/scripts/discover-examples.js @@ -0,0 +1,68 @@ +#!/usr/bin/env node +'use strict'; + +// discover-examples.js — slå upp filer som FAKTISKT finns hos användaren +// och skriv ut förslag på defaults till example-flows. +// +// Usage: +// NC_URL=https://cloud.domain.name NC_USER=your-user NC_PASS=app-password \ +// node scripts/discover-examples.js [COLLABORA_URL] [PUBLIC_URL] +// +// Kör t.ex. inne i Node-RED-containern: +// docker exec nextcloud-node-red sh -c \ +// 'cd /data/node_modules/node-red-contrib-nextcloud-collabora && \ +// NC_URL=... NC_USER=... NC_PASS=... node scripts/discover-examples.js' + +const { listUserExamples, webdavPropfindFileId } = require('../lib/webdav'); +const { getCapabilities } = require('../lib/collabora'); + +async function main() { + const config = { + nextcloudUrl: process.env.NC_URL || 'https://cloud.domain.name', + collaboraUrl: process.argv[2] || process.env.COLLABORA_URL || 'https://code:9980', + collaboraPublicUrl: process.argv[3] || process.env.COLLABORA_PUBLIC_URL || '', + username: process.env.NC_USER, + password: process.env.NC_PASS, + verifyTls: false + }; + if (!config.username || !config.password) { + console.error('Set NC_USER and NC_PASS (app password)'); + process.exit(1); + } + + const examples = await listUserExamples(config); + const primary = examples.find((e) => /\.md$/i.test(e.ncPath)) || examples[0]; + const office = examples.find((e) => /\.odt$/i.test(e.ncPath)) || primary; + + console.log(JSON.stringify({ + sourceCandidates: examples.map((e) => e.ncPath), + suggestedDefaults: { + pdfSourceNcPath: primary ? primary.ncPath : '/Dokument/Example.md', + pngSourceNcPath: office ? office.ncPath : '/Dokument/exempel.odt', + destFolder: '/Dokument/OfficeOut/' + }, + note: 'Mall-id:n hämtas via templates:list (nc-office). Hardkoda aldrig /Mallar/…' + }, null, 2)); + + if (primary) { + try { + const fileId = await webdavPropfindFileId(config, primary.ncPath); + console.log(`# fileId för ${primary.ncPath}: ${fileId} (document:direct kan använda msg.ncPath direkt)`); + } catch (err) { + console.error(`# PROPFIND fileId misslyckades: ${err.message}`); + } + } + + try { + await getCapabilities(config); + console.log('# CODE capabilities: OK via ' + config.collaboraUrl); + } catch (err) { + console.error(`# CODE capabilities MISSLYCKADES via ${config.collaboraUrl}: ${err.message}`); + process.exitCode = 1; + } +} + +main().catch((err) => { + console.error(err.message); + process.exit(1); +}); diff --git a/scripts/integrate-into-nodered.sh b/scripts/integrate-into-nodered.sh index 3d5863d..ceb543c 100755 --- a/scripts/integrate-into-nodered.sh +++ b/scripts/integrate-into-nodered.sh @@ -550,11 +550,12 @@ Integrating done — same Node-RED on port ${NODERED_PORT} Create config first (gear → nc-collab-config): Nextcloud: ${nc_url} Collabora: ${COLLABORA_URL} (verifyTls=false — self-signed internt) - Public WOPI: ${nc_url} + Public WOPI: https://docs.domain.name (collaboraPublicUrl — rewrite av discovery urlsrc) Auth: username + app password Try: collabora-wopi (capabilities) → collabora-bridge (ncFileToPdf) -Example flow: flows/example-flows.json +Example flow: flows/example-flows.json — defaults /Dokument/Example.md, +/Dokument/exempel.odt, dest /Dokument/OfficeOut/, mall-id 230 (Invoice.odt). EOF }