Files
nextcloud-collabora-nodered/lib/webdav.js
T
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

236 lines
7.9 KiB
JavaScript

'use strict';
const { requestBuffer } = require('./http-client');
const { normalizeNcPath, filenameFromPath } = require('./utils');
function authHeader(config) {
const token = Buffer.from(`${config.username}:${config.password}`).toString('base64');
return `Basic ${token}`;
}
function webDavBase(config) {
const base = config.nextcloudUrl.replace(/\/$/, '');
return `${base}/remote.php/dav/files/${encodeURIComponent(config.username)}`;
}
function encodePathSegments(ncPath) {
const normalized = normalizeNcPath(ncPath);
return normalized
.split('/')
.filter(Boolean)
.map((segment) => encodeURIComponent(segment))
.join('/');
}
function webDavUrl(config, ncPath) {
const rel = encodePathSegments(ncPath);
return rel ? `${webDavBase(config)}/${rel}` : webDavBase(config);
}
function tlsOptions(config) {
return { rejectUnauthorized: config.verifyTls !== false && config.verifyTls !== 'false' };
}
async function webdavGet(config, ncPath) {
const url = webDavUrl(config, ncPath);
const res = await requestBuffer({
url,
method: 'GET',
headers: { Authorization: authHeader(config) },
rejectUnauthorized: tlsOptions(config).rejectUnauthorized
});
if (res.statusCode === 404) {
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`);
}
if (res.statusCode < 200 || res.statusCode >= 300) {
throw new Error(`WebDAV GET misslyckades (${res.statusCode}) för ${normalizeNcPath(ncPath)}`);
}
return {
buffer: res.body,
filename: filenameFromPath(ncPath),
ncPath: normalizeNcPath(ncPath),
contentType: res.headers['content-type']
};
}
async function ensureFolder(config, folderPath) {
const normalized = normalizeNcPath(folderPath);
if (!normalized || normalized === '/') {
return;
}
const parts = normalized.split('/').filter(Boolean);
let current = '';
for (const part of parts) {
current += '/' + part;
const url = webDavUrl(config, current);
const res = await requestBuffer({
url,
method: 'MKCOL',
headers: { Authorization: authHeader(config) },
rejectUnauthorized: tlsOptions(config).rejectUnauthorized
});
if (res.statusCode !== 201 && res.statusCode !== 405 && res.statusCode !== 301) {
if (res.statusCode >= 400) {
throw new Error(`WebDAV MKCOL misslyckades (${res.statusCode}) för ${current}`);
}
}
}
}
async function webdavPut(config, ncPath, buffer, contentType) {
const dest = normalizeNcPath(ncPath);
const folder = dest.substring(0, dest.lastIndexOf('/')) || '/';
await ensureFolder(config, folder);
const url = webDavUrl(config, dest);
const res = await requestBuffer({
url,
method: 'PUT',
headers: {
Authorization: authHeader(config),
'Content-Type': contentType || 'application/octet-stream',
'Content-Length': buffer.length
},
body: buffer,
rejectUnauthorized: tlsOptions(config).rejectUnauthorized
});
if (res.statusCode === 401 || res.statusCode === 403) {
throw new Error(`WebDAV ${res.statusCode}: kontrollera app-lösenord`);
}
if (res.statusCode < 200 || res.statusCode >= 300) {
throw new Error(`WebDAV PUT misslyckades (${res.statusCode}) för ${dest}`);
}
return {
statusCode: res.statusCode,
destNcPath: dest,
filename: filenameFromPath(dest)
};
}
async function webdavPropfindFileId(config, ncPath) {
const url = webDavUrl(config, ncPath);
const body = `<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop><oc:fileid/></d:prop>
</d:propfind>`;
const res = await requestBuffer({
url,
method: 'PROPFIND',
headers: {
Authorization: authHeader(config),
Depth: '0',
'Content-Type': 'application/xml; charset=utf-8'
},
body,
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)}`);
}
const text = res.body.toString('utf8');
const match = text.match(/<oc:fileid>(\d+)<\/oc:fileid>/i) || text.match(/<nc:fileid>(\d+)<\/nc:fileid>/i);
if (!match) {
throw new Error(`Kunde inte hitta fileId för ${normalizeNcPath(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: '<?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 = {
webDavUrl,
webdavGet,
webdavPut,
webdavPropfindFileId,
ensureFolder,
listUserExamples
};