Example-flows + noder: defaults pekar bara på filer som finns

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.
This commit is contained in:
dev
2026-08-22 22:50:47 +02:00
parent 5ca7b4d4e0
commit 5ee179539e
11 changed files with 801 additions and 69 deletions
+1 -1
View File
@@ -176,7 +176,7 @@ Intern CODE körs med TLS och self-signed-cert (`ssl.enable=true`) — därför
### createAssetLink (assets) ### createAssetLink (assets)
`collabora-bridge createAssetLink` / `nc-office assets:create` POSTar form-urlencoded `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
`<nextcloudUrl>/apps/richdocuments/assets`. `200 {url}` = asset-URL skapad; `<nextcloudUrl>/apps/richdocuments/assets`. `200 {url}` = asset-URL skapad;
`404` = filen finns inte i användarens rot (eller fel `nextcloudUrl`) — felet innehåller status + svarskropp. `404` = filen finns inte i användarens rot (eller fel `nextcloudUrl`) — felet innehåller status + svarskropp.
+512 -49
View File
@@ -4,7 +4,7 @@
"type": "tab", "type": "tab",
"label": "NC Collabora — examples", "label": "NC Collabora — examples",
"disabled": false, "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", "id": "nc_collab_cfg",
@@ -12,21 +12,22 @@
"name": "cloud.domain.name", "name": "cloud.domain.name",
"nextcloudUrl": "https://cloud.domain.name", "nextcloudUrl": "https://cloud.domain.name",
"collaboraUrl": "https://code:9980", "collaboraUrl": "https://code:9980",
"collaboraPublicUrl": "https://cloud.domain.name", "collaboraPublicUrl": "https://docs.domain.name",
"username": "FILL_IN_USER", "username": "FILL_IN_USER",
"verifyTls": false "verifyTls": false
}, },
{ {
"id": "inj_capabilities", "id": "inj_capabilities",
"type": "inject", "type": "inject",
"z": "collab_tab", "z": "collab_tab",
"name": "1. CODE capabilities", "name": "1a. CODE capabilities",
"props": [], "props": [],
"repeat": "", "repeat": "",
"crontab": "", "crontab": "",
"once": false, "once": false,
"topic": "", "topic": "",
"x": 150, "x": 160,
"y": 80, "y": 80,
"wires": [["wopi_cap"]] "wires": [["wopi_cap"]]
}, },
@@ -37,7 +38,7 @@
"name": "capabilities", "name": "capabilities",
"config": "nc_collab_cfg", "config": "nc_collab_cfg",
"operation": "capabilities", "operation": "capabilities",
"x": 380, "x": 400,
"y": 80, "y": 80,
"wires": [["dbg_cap"]] "wires": [["dbg_cap"]]
}, },
@@ -51,28 +52,66 @@
"console": false, "console": false,
"tostatus": false, "tostatus": false,
"complete": "payload", "complete": "payload",
"x": 590, "x": 620,
"y": 80, "y": 80,
"wires": [] "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", "id": "inj_pdf",
"type": "inject", "type": "inject",
"z": "collab_tab", "z": "collab_tab",
"name": "2. NC-fil → PDF", "name": "2. Example.md → PDF",
"props": [ "props": [
{ { "p": "ncPath", "v": "/Dokument/Example.md", "vt": "str" },
"p": "ncPath", { "p": "destName", "v": "Example.pdf", "vt": "str" }
"v": "/Dokument/test.docx",
"vt": "str"
}
], ],
"repeat": "", "repeat": "",
"crontab": "", "crontab": "",
"once": false, "once": false,
"topic": "", "topic": "",
"x": 150, "x": 160,
"y": 160, "y": 220,
"wires": [["bridge_pdf"]] "wires": [["bridge_pdf"]]
}, },
{ {
@@ -82,8 +121,8 @@
"name": "ncFileToPdf", "name": "ncFileToPdf",
"config": "nc_collab_cfg", "config": "nc_collab_cfg",
"operation": "ncFileToPdf", "operation": "ncFileToPdf",
"x": 380, "x": 400,
"y": 160, "y": 220,
"wires": [["dbg_pdf"]] "wires": [["dbg_pdf"]]
}, },
{ {
@@ -96,28 +135,91 @@
"console": false, "console": false,
"tostatus": false, "tostatus": false,
"complete": "destNcPath", "complete": "destNcPath",
"x": 590, "x": 620,
"y": 160, "y": 220,
"wires": [] "wires": []
}, },
{ {
"id": "inj_editor", "id": "inj_png_asset",
"type": "inject", "type": "inject",
"z": "collab_tab", "z": "collab_tab",
"name": "3. Open in Office", "name": "3. exempel.odt → PNG → asset-länk",
"props": [ "props": [
{ { "p": "ncPath", "v": "/Dokument/exempel.odt", "vt": "str" },
"p": "ncPath", { "p": "destName", "v": "demo-asset.png", "vt": "str" }
"v": "/Dokument/test.docx",
"vt": "str"
}
], ],
"repeat": "", "repeat": "",
"crontab": "", "crontab": "",
"once": false, "once": false,
"topic": "", "topic": "",
"x": 150, "x": 190,
"y": 240, "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"]] "wires": [["office_direct"]]
}, },
{ {
@@ -127,8 +229,8 @@
"name": "document:direct", "name": "document:direct",
"config": "nc_collab_cfg", "config": "nc_collab_cfg",
"operation": "document:direct", "operation": "document:direct",
"x": 380, "x": 420,
"y": 240, "y": 380,
"wires": [["dbg_editor"]] "wires": [["dbg_editor"]]
}, },
{ {
@@ -141,53 +243,414 @@
"console": false, "console": false,
"tostatus": false, "tostatus": false,
"complete": "editorUrl", "complete": "editorUrl",
"x": 590, "x": 640,
"y": 240, "y": 380,
"wires": [] "wires": []
}, },
{ {
"id": "inj_extract", "id": "inj_html",
"type": "inject", "type": "inject",
"z": "collab_tab", "z": "collab_tab",
"name": "4. Template fields", "name": "5. HTML → PDF",
"props": [ "props": [
{ { "p": "payload", "v": "<!doctype html><html><body><h1>Rapport</h1><p>Skapad av Node-RED mot Collabora CODE.</p></body></html>", "vt": "str" },
"p": "ncPath", { "p": "filename", "v": "rapport.html", "vt": "str" },
"v": "/Mallar/avtal.docx", { "p": "format", "v": "pdf", "vt": "str" },
"vt": "str" { "p": "destName", "v": "rapport-demo.pdf", "vt": "str" }
}
], ],
"repeat": "", "repeat": "",
"crontab": "", "crontab": "",
"once": false, "once": false,
"topic": "", "topic": "",
"x": 150, "x": 150,
"y": 320, "y": 460,
"wires": [["bridge_extract"]] "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", "id": "bridge_extract",
"type": "collabora-bridge", "type": "collabora-bridge",
"z": "collab_tab", "z": "collab_tab",
"name": "extractToPayload", "name": "extract struktur",
"config": "nc_collab_cfg", "config": "nc_collab_cfg",
"operation": "extractToPayload", "operation": "extractToPayload",
"x": 390, "x": 1330,
"y": 320, "y": 560,
"wires": [["dbg_fields"]] "wires": [["dbg_structure"]]
}, },
{ {
"id": "dbg_fields", "id": "dbg_structure",
"type": "debug", "type": "debug",
"z": "collab_tab", "z": "collab_tab",
"name": "fields / structure", "name": "structure",
"active": true, "active": true,
"tosidebar": true, "tosidebar": true,
"console": false, "console": false,
"tostatus": false, "tostatus": false,
"complete": "fields", "complete": "structure",
"x": 610, "x": 1550,
"y": 320, "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": [] "wires": []
} }
] ]
+23 -1
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
const { webdavGet, webdavPut, webdavPropfindFileId } = require('./webdav'); const { webdavGet, webdavPut, webdavPropfindFileId, listUserExamples } = require('./webdav');
const { ocsRequest } = require('./ocs'); const { ocsRequest } = require('./ocs');
const { requestBuffer } = require('./http-client'); const { requestBuffer } = require('./http-client');
const { convertTo, extractStructure, transformStructure } = require('./collabora'); const { convertTo, extractStructure, transformStructure } = require('./collabora');
@@ -50,6 +50,27 @@ async function loadSourceBuffer(config, msg) {
return file; 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) { async function saveResult(config, msg, buffer, format, sourceNcPath) {
const destNcPath = assertSafeDestination( const destNcPath = assertSafeDestination(
resolveDestNcPath(msg, sourceNcPath, format), resolveDestNcPath(msg, sourceNcPath, format),
@@ -266,6 +287,7 @@ module.exports = {
compareVersionsPdf, compareVersionsPdf,
extractToPayload, extractToPayload,
createAssetLink, createAssetLink,
pickDemoSource,
loadSourceBuffer, loadSourceBuffer,
saveResult, saveResult,
resolveFileId resolveFileId
+17 -5
View File
@@ -24,10 +24,13 @@ function parseOcsBody(bodyBuffer) {
const json = JSON.parse(text); const json = JSON.parse(text);
const ocs = json.ocs || json; const ocs = json.ocs || json;
const meta = ocs.meta || {}; const meta = ocs.meta || {};
const statuscode = meta.statuscode ?? meta.statusCode ?? 200; const statuscode = Number(meta.statuscode ?? meta.statusCode ?? 200);
if (statuscode >= 400) { if (statuscode >= 400) {
const message = meta.message || meta.status || text.slice(0, 200); // 996 = intern serverfel, 998 = not found m.m. — inkludera alltid
throw new Error(`OCS ${statuscode}: ${message}`); // 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 { return {
statusCode: statuscode, statusCode: statuscode,
@@ -38,7 +41,7 @@ function parseOcsBody(bodyBuffer) {
if (err.message.startsWith('OCS ')) { if (err.message.startsWith('OCS ')) {
throw err; 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`); 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 || '<tom body>'}`);
}
return { return {
httpStatus: res.statusCode, httpStatus: res.statusCode,
...parseOcsBody(res.body) ...parsed
}; };
} }
+6 -1
View File
@@ -60,7 +60,12 @@ function resolveDestNcPath(msg, sourceNcPath, format) {
if (msg.destFolder || msg.destName) { if (msg.destFolder || msg.destName) {
const folder = normalizeNcPath(msg.destFolder || DEFAULT_OUTPUT_FOLDER); const folder = normalizeNcPath(msg.destFolder || DEFAULT_OUTPUT_FOLDER);
const folderWithSlash = folder.endsWith('/') ? folder : 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 normalizeNcPath(folderWithSlash + name);
} }
return defaultDestNcPath(sourceNcPath, format); return defaultDestNcPath(sourceNcPath, format);
+82 -2
View File
@@ -41,7 +41,11 @@ async function webdavGet(config, ncPath) {
}); });
if (res.statusCode === 404) { 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) { if (res.statusCode === 401 || res.statusCode === 403) {
throw new Error(`WebDAV ${res.statusCode}: kontrollera app-lösenord och användare`); 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 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) { if (res.statusCode < 200 || res.statusCode >= 300) {
throw new Error(`PROPFIND misslyckades (${res.statusCode}) för ${normalizeNcPath(ncPath)}`); 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); 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: '<?xml version="1.0" encoding="UTF-8"?><d:propfind xmlns:d="DAV:"/>',
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('<d:response>').slice(1);
for (const chunk of responses) {
const hrefMatch = chunk.match(/<d:href>([^<]+)<\/d:href>/i);
if (!hrefMatch) {
continue;
}
let href;
try {
href = decodeURIComponent(hrefMatch[1]);
} catch {
href = hrefMatch[1];
}
// href: /remote.php/dav/files/<user>/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 = /<d:collection\s*\/?>/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 = { module.exports = {
webDavUrl, webDavUrl,
webdavGet, webdavGet,
webdavPut, webdavPut,
webdavPropfindFileId, webdavPropfindFileId,
ensureFolder ensureFolder,
listUserExamples
}; };
+2 -1
View File
@@ -11,7 +11,8 @@ const OPERATIONS = {
fillTemplateSave: bridge.fillTemplateSave, fillTemplateSave: bridge.fillTemplateSave,
compareVersionsPdf: bridge.compareVersionsPdf, compareVersionsPdf: bridge.compareVersionsPdf,
extractToPayload: bridge.extractToPayload, extractToPayload: bridge.extractToPayload,
createAssetLink: bridge.createAssetLink createAssetLink: bridge.createAssetLink,
pickDemoSource: bridge.pickDemoSource
}; };
module.exports = function (RED) { module.exports = function (RED) {
+20
View File
@@ -4,6 +4,24 @@ const { getConfig, setError, setSuccess, clearStatus } = require('../lib/utils')
const { getCapabilities, getDiscovery } = require('../lib/collabora'); const { getCapabilities, getDiscovery } = require('../lib/collabora');
const { requestBuffer } = require('../lib/http-client'); 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) { module.exports = function (RED) {
function CollaboraWopiNode(config) { function CollaboraWopiNode(config) {
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
@@ -24,6 +42,8 @@ module.exports = function (RED) {
msg.payload = await getCapabilities(cfg); msg.payload = await getCapabilities(cfg);
} else if (op === 'discovery') { } else if (op === 'discovery') {
msg.payload = await getDiscovery(cfg); msg.payload = await getDiscovery(cfg);
const publicBase = cfg.collaboraPublicUrl || '';
msg.payload = rewriteDiscoveryHosts(msg.payload, cfg.collaboraUrl, publicBase);
} else if (op === 'buildEditor') { } else if (op === 'buildEditor') {
const publicBase = cfg.collaboraPublicUrl || cfg.collaboraUrl; const publicBase = cfg.collaboraPublicUrl || cfg.collaboraUrl;
const wopiSrc = encodeURIComponent(msg.wopiSrc || msg.WOPISrc || ''); const wopiSrc = encodeURIComponent(msg.wopiSrc || msg.WOPISrc || '');
+67 -7
View File
@@ -4,15 +4,20 @@ const { getConfig, setError, setSuccess, clearStatus, pickMsgField } = require('
const { ocsRequest } = require('../lib/ocs'); const { ocsRequest } = require('../lib/ocs');
const bridge = require('../lib/bridge'); const bridge = require('../lib/bridge');
const { resolveFileId } = bridge; const { resolveFileId } = bridge;
const { webdavGet, webdavPut } = require('../lib/webdav'); const { webdavGet, webdavPut, ensureFolder, webdavPropfindFileId } = require('../lib/webdav');
const { convertTo } = require('../lib/collabora'); const { convertTo } = require('../lib/collabora');
const { const {
resolveDestNcPath, resolveDestNcPath,
assertSafeDestination, assertSafeDestination,
contentTypeForFormat, contentTypeForFormat,
normalizeNcPath normalizeNcPath,
DEFAULT_OUTPUT_FOLDER
} = require('../lib/utils'); } = require('../lib/utils');
function hasExtension(p) {
return /\.[^/.]+$/.test(String(p).replace(/\/+$/, ''));
}
module.exports = function (RED) { module.exports = function (RED) {
function NcOfficeNode(config) { function NcOfficeNode(config) {
RED.nodes.createNode(this, config); RED.nodes.createNode(this, config);
@@ -41,16 +46,69 @@ module.exports = function (RED) {
break; break;
} }
case 'templates:create': { 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, { result = await ocsRequest(cfg, {
method: 'POST', method: 'POST',
path: '/ocs/v2.php/apps/richdocuments/api/v1/templates/new', path: '/ocs/v2.php/apps/richdocuments/api/v1/templates/new',
body: { body: {
templateId: msg.templateId, path,
fileName: msg.fileName || msg.destName, template: Number(template)
templatePath: msg.templatePath
} }
}); });
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; break;
} }
case 'template:extractFields': { case 'template:extractFields': {
@@ -80,11 +138,13 @@ module.exports = function (RED) {
break; break;
} }
case 'document:direct': { case 'document:direct': {
// richdocuments OCS#createDirect($fileId) binder parametern
// per metodnamn — skicka både fileId och fileid (Number).
const fileId = await resolveFileId(cfg, msg); const fileId = await resolveFileId(cfg, msg);
result = await ocsRequest(cfg, { result = await ocsRequest(cfg, {
method: 'POST', method: 'POST',
path: '/ocs/v2.php/apps/richdocuments/api/v1/document', path: '/ocs/v2.php/apps/richdocuments/api/v1/document',
body: { fileid: Number(fileId) } body: { fileId: Number(fileId), fileid: Number(fileId) }
}); });
msg.fileId = fileId; msg.fileId = fileId;
msg.editorUrl = result.data?.url || result.data; msg.editorUrl = result.data?.url || result.data;
+68
View File
@@ -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);
});
+3 -2
View File
@@ -550,11 +550,12 @@ Integrating done — same Node-RED on port ${NODERED_PORT}
Create config first (gear → nc-collab-config): Create config first (gear → nc-collab-config):
Nextcloud: ${nc_url} Nextcloud: ${nc_url}
Collabora: ${COLLABORA_URL} (verifyTls=false — self-signed internt) 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 Auth: username + app password
Try: collabora-wopi (capabilities) → collabora-bridge (ncFileToPdf) 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 EOF
} }