Files
dev 5ee179539e 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.
2026-08-22 22:50:47 +02:00

207 lines
9.7 KiB
JavaScript

'use strict';
const { getConfig, setError, setSuccess, clearStatus, pickMsgField } = require('../lib/utils');
const { ocsRequest } = require('../lib/ocs');
const bridge = require('../lib/bridge');
const { resolveFileId } = bridge;
const { webdavGet, webdavPut, ensureFolder, webdavPropfindFileId } = require('../lib/webdav');
const { convertTo } = require('../lib/collabora');
const {
resolveDestNcPath,
assertSafeDestination,
contentTypeForFormat,
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);
const node = this;
node.configNode = RED.nodes.getNode(config.config);
node.operation = config.operation || 'templates:list';
node.on('input', async (msg, send, done) => {
send = send || function () { node.send.apply(node, arguments); };
done = done || function (err) { if (err) { node.error(err, msg); } };
clearStatus(node);
try {
const cfg = getConfig(node);
const op = msg.operation || node.operation;
let result;
switch (op) {
case 'templates:list': {
const type = pickMsgField(msg, 'templateType') || 'document';
result = await ocsRequest(cfg, {
method: 'GET',
path: `/ocs/v2.php/apps/richdocuments/api/v1/templates/${type}`
});
msg.payload = result.data;
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: {
path,
template: Number(template)
}
});
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': {
const fileId = await resolveFileId(cfg, msg);
result = await ocsRequest(cfg, {
method: 'GET',
path: `/ocs/v2.php/apps/richdocuments/api/v1/template/fields/extract/${fileId}`
});
msg.fileId = fileId;
msg.payload = result.data;
break;
}
case 'template:fillFields': {
const fileId = await resolveFileId(cfg, msg);
const fields = msg.fields || msg.transform;
const body = { fields };
if (msg.destNcPath) {
body.destination = normalizeNcPath(msg.destNcPath).replace(/^\//, '');
}
result = await ocsRequest(cfg, {
method: 'POST',
path: `/ocs/v2.php/apps/richdocuments/api/v1/template/fields/fill/${fileId}`,
body
});
msg.fileId = fileId;
msg.payload = result.data;
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), fileid: Number(fileId) }
});
msg.fileId = fileId;
msg.editorUrl = result.data?.url || result.data;
msg.payload = { url: msg.editorUrl, fileId };
break;
}
case 'document:public': {
if (!msg.shareToken) {
throw new Error('document:public kräver msg.shareToken');
}
result = await ocsRequest(cfg, {
method: 'POST',
path: '/ocs/v2.php/apps/richdocuments/api/v1/document',
body: { shareToken: msg.shareToken }
});
msg.editorUrl = result.data?.url || result.data;
msg.payload = { url: msg.editorUrl };
break;
}
case 'files:convert': {
const format = (pickMsgField(msg, 'format') || 'pdf').toLowerCase();
const source = msg.ncPath
? await webdavGet(cfg, msg.ncPath)
: { buffer: Buffer.from(msg.payload), filename: msg.filename || 'file.bin' };
const converted = await convertTo(cfg, {
buffer: source.buffer,
filename: source.filename,
format
});
const dest = assertSafeDestination(resolveDestNcPath(msg, msg.ncPath, format), msg);
const put = await webdavPut(cfg, dest, converted.buffer, contentTypeForFormat(format));
msg.destNcPath = put.destNcPath;
msg.payload = converted.buffer;
msg.format = format;
break;
}
case 'assets:create': {
const link = await bridge.createAssetLink(cfg, msg);
msg.assetUrl = link.assetUrl;
msg.payload = { assetUrl: link.assetUrl, ncPath: link.ncPath };
break;
}
default:
throw new Error(`Okänd nc-office operation: ${op}`);
}
setSuccess(node, msg, op);
send(msg);
done();
} catch (err) {
setError(node, msg, err);
send(msg);
done(err);
}
});
}
RED.nodes.registerType('nc-office', NcOfficeNode);
};