Initial release v1.0.0 — Nextcloud Collabora Node-RED sidecar
This commit is contained in:
+258
@@ -0,0 +1,258 @@
|
||||
'use strict';
|
||||
|
||||
const { webdavGet, webdavPut, webdavPropfindFileId } = require('./webdav');
|
||||
const { ocsRequest } = require('./ocs');
|
||||
const { convertTo, extractStructure, transformStructure } = require('./collabora');
|
||||
const {
|
||||
resolveDestNcPath,
|
||||
assertSafeDestination,
|
||||
contentTypeForFormat,
|
||||
replaceExtension,
|
||||
basenameFromNcPath,
|
||||
normalizeNcPath,
|
||||
pickMsgField
|
||||
} = require('./utils');
|
||||
|
||||
async function resolveFileId(config, msg) {
|
||||
if (msg.fileId) {
|
||||
return parseInt(msg.fileId, 10);
|
||||
}
|
||||
const ncPath = pickMsgField(msg, 'ncPath');
|
||||
if (ncPath) {
|
||||
return webdavPropfindFileId(config, ncPath);
|
||||
}
|
||||
throw new Error('Ange msg.fileId eller msg.ncPath');
|
||||
}
|
||||
|
||||
async function loadSourceBuffer(config, msg) {
|
||||
if (msg.payload !== undefined && msg.payload !== null && msg.payload !== '') {
|
||||
if (Buffer.isBuffer(msg.payload)) {
|
||||
return {
|
||||
buffer: msg.payload,
|
||||
filename: msg.filename || 'payload.bin',
|
||||
ncPath: msg.ncPath
|
||||
};
|
||||
}
|
||||
const text = typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload);
|
||||
return {
|
||||
buffer: Buffer.from(text, 'utf8'),
|
||||
filename: msg.filename || 'payload.txt',
|
||||
ncPath: msg.ncPath
|
||||
};
|
||||
}
|
||||
|
||||
const ncPath = pickMsgField(msg, 'ncPath');
|
||||
if (!ncPath) {
|
||||
throw new Error('Ange msg.ncPath eller msg.payload');
|
||||
}
|
||||
const file = await webdavGet(config, ncPath);
|
||||
return file;
|
||||
}
|
||||
|
||||
async function saveResult(config, msg, buffer, format, sourceNcPath) {
|
||||
const destNcPath = assertSafeDestination(
|
||||
resolveDestNcPath(msg, sourceNcPath, format),
|
||||
msg
|
||||
);
|
||||
const contentType = contentTypeForFormat(format);
|
||||
const putResult = await webdavPut(config, destNcPath, buffer, contentType);
|
||||
return {
|
||||
destNcPath: putResult.destNcPath,
|
||||
filename: putResult.filename,
|
||||
contentType,
|
||||
statusCode: putResult.statusCode
|
||||
};
|
||||
}
|
||||
|
||||
async function ncFileConvert(config, msg, format) {
|
||||
const source = await loadSourceBuffer(config, msg);
|
||||
const extraFields = [];
|
||||
|
||||
if (msg.templateNcPath) {
|
||||
const tpl = await webdavGet(config, msg.templateNcPath);
|
||||
extraFields.push({
|
||||
name: 'template',
|
||||
buffer: tpl.buffer,
|
||||
filename: tpl.filename
|
||||
});
|
||||
}
|
||||
|
||||
if (msg.compareNcPath) {
|
||||
const cmp = await webdavGet(config, msg.compareNcPath);
|
||||
extraFields.push({
|
||||
name: 'compare',
|
||||
buffer: cmp.buffer,
|
||||
filename: cmp.filename
|
||||
});
|
||||
}
|
||||
|
||||
const converted = await convertTo(config, {
|
||||
buffer: source.buffer,
|
||||
filename: source.filename,
|
||||
format,
|
||||
extraFields
|
||||
});
|
||||
|
||||
const saved = await saveResult(config, msg, converted.buffer, format, source.ncPath);
|
||||
return {
|
||||
...saved,
|
||||
format,
|
||||
sourceNcPath: source.ncPath,
|
||||
payload: converted.buffer
|
||||
};
|
||||
}
|
||||
|
||||
async function ncFileToPdf(config, msg) {
|
||||
return ncFileConvert(config, msg, 'pdf');
|
||||
}
|
||||
|
||||
async function ncFileToPng(config, msg) {
|
||||
return ncFileConvert(config, msg, 'png');
|
||||
}
|
||||
|
||||
async function ncFileToHtml(config, msg) {
|
||||
return ncFileConvert(config, msg, 'html');
|
||||
}
|
||||
|
||||
async function htmlToOffice(config, msg) {
|
||||
const format = (pickMsgField(msg, 'format') || 'docx').toLowerCase();
|
||||
const source = await loadSourceBuffer(config, msg);
|
||||
let buffer = source.buffer;
|
||||
let filename = source.filename || 'content.html';
|
||||
|
||||
if (!filename.endsWith('.html') && !filename.endsWith('.htm')) {
|
||||
filename = 'content.html';
|
||||
}
|
||||
|
||||
const extraFields = [];
|
||||
if (msg.templateNcPath) {
|
||||
const tpl = await webdavGet(config, msg.templateNcPath);
|
||||
extraFields.push({
|
||||
name: 'template',
|
||||
buffer: tpl.buffer,
|
||||
filename: tpl.filename
|
||||
});
|
||||
}
|
||||
|
||||
const converted = await convertTo(config, {
|
||||
buffer,
|
||||
filename,
|
||||
format,
|
||||
extraFields
|
||||
});
|
||||
|
||||
const saved = await saveResult(config, msg, converted.buffer, format, msg.ncPath || filename);
|
||||
return {
|
||||
...saved,
|
||||
format,
|
||||
payload: converted.buffer
|
||||
};
|
||||
}
|
||||
|
||||
async function fillTemplateSave(config, msg) {
|
||||
const fileId = await resolveFileId(config, msg);
|
||||
const fields = msg.fields || msg.transform;
|
||||
if (!fields) {
|
||||
throw new Error('Ange msg.fields eller msg.transform för mallifyllning');
|
||||
}
|
||||
|
||||
const destination = msg.destNcPath
|
||||
? assertSafeDestination(normalizeNcPath(msg.destNcPath), msg)
|
||||
: undefined;
|
||||
|
||||
const body = { fields };
|
||||
if (destination) {
|
||||
body.destination = destination.startsWith('/') ? destination.slice(1) : destination;
|
||||
}
|
||||
|
||||
await ocsRequest(config, {
|
||||
method: 'POST',
|
||||
path: `/ocs/v2.php/apps/richdocuments/api/v1/template/fields/fill/${fileId}`,
|
||||
body
|
||||
});
|
||||
|
||||
const destNcPath = destination || resolveDestNcPath(msg, msg.ncPath, 'docx');
|
||||
return {
|
||||
fileId,
|
||||
destNcPath: normalizeNcPath('/' + (body.destination || destNcPath.replace(/^\//, ''))),
|
||||
statusCode: 200
|
||||
};
|
||||
}
|
||||
|
||||
async function compareVersionsPdf(config, msg) {
|
||||
if (!msg.compareNcPath) {
|
||||
throw new Error('compareVersionsPdf kräver msg.compareNcPath (äldre version)');
|
||||
}
|
||||
msg.format = 'pdf';
|
||||
return ncFileToPdf(config, msg);
|
||||
}
|
||||
|
||||
async function extractToPayload(config, msg) {
|
||||
if (msg.fileId) {
|
||||
const res = await ocsRequest(config, {
|
||||
method: 'GET',
|
||||
path: `/ocs/v2.php/apps/richdocuments/api/v1/template/fields/extract/${msg.fileId}`
|
||||
});
|
||||
return { fields: res.data, fileId: msg.fileId };
|
||||
}
|
||||
|
||||
const source = await loadSourceBuffer(config, msg);
|
||||
const filter = msg.filter || 'contentcontrol';
|
||||
const structure = await extractStructure(config, {
|
||||
buffer: source.buffer,
|
||||
filename: source.filename,
|
||||
filter
|
||||
});
|
||||
return {
|
||||
structure,
|
||||
sourceNcPath: source.ncPath
|
||||
};
|
||||
}
|
||||
|
||||
async function createAssetLink(config, msg) {
|
||||
const assetPath = pickMsgField(msg, 'ncPath');
|
||||
if (!assetPath) {
|
||||
throw new Error('createAssetLink kräver msg.ncPath');
|
||||
}
|
||||
|
||||
const relPath = normalizeNcPath(assetPath);
|
||||
const { requestBuffer } = require('./http-client');
|
||||
const base = config.nextcloudUrl.replace(/\/$/, '');
|
||||
const token = Buffer.from(`${config.username}:${config.password}`).toString('base64');
|
||||
const res = await requestBuffer({
|
||||
url: `${base}/index.php/apps/richdocuments/assets`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${token}`,
|
||||
'OCS-APIRequest': 'true',
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: Buffer.from(JSON.stringify({ path: relPath }), 'utf8'),
|
||||
rejectUnauthorized: config.verifyTls !== false && config.verifyTls !== 'false'
|
||||
});
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
throw new Error(`assets:create misslyckades (${res.statusCode})`);
|
||||
}
|
||||
|
||||
const json = JSON.parse(res.body.toString('utf8'));
|
||||
return {
|
||||
assetUrl: json.url || json.assetUrl || json,
|
||||
ncPath: relPath
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ncFileToPdf,
|
||||
ncFileToPng,
|
||||
ncFileToHtml,
|
||||
htmlToOffice,
|
||||
fillTemplateSave,
|
||||
compareVersionsPdf,
|
||||
extractToPayload,
|
||||
createAssetLink,
|
||||
loadSourceBuffer,
|
||||
saveResult,
|
||||
resolveFileId
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
'use strict';
|
||||
|
||||
const FormData = require('form-data');
|
||||
const { requestStream } = require('./http-client');
|
||||
|
||||
function collaboraBase(config) {
|
||||
return (config.collaboraUrl || '').replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function tlsOptions(config) {
|
||||
return config.verifyTls !== false && config.verifyTls !== 'false';
|
||||
}
|
||||
|
||||
function buildMultipart(fields) {
|
||||
const form = new FormData();
|
||||
for (const field of fields) {
|
||||
if (field.buffer) {
|
||||
form.append(field.name, field.buffer, {
|
||||
filename: field.filename || 'document.bin',
|
||||
contentType: field.contentType || 'application/octet-stream'
|
||||
});
|
||||
} else if (field.value !== undefined) {
|
||||
form.append(field.name, String(field.value));
|
||||
}
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
async function postMultipart(config, endpoint, fields) {
|
||||
const form = buildMultipart(fields);
|
||||
const url = `${collaboraBase(config)}${endpoint}`;
|
||||
|
||||
const res = await requestStream(
|
||||
{
|
||||
url,
|
||||
method: 'POST',
|
||||
headers: form.getHeaders(),
|
||||
rejectUnauthorized: tlsOptions(config)
|
||||
},
|
||||
(req) => form.pipe(req)
|
||||
);
|
||||
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
const snippet = res.body.toString('utf8').slice(0, 300);
|
||||
throw new Error(`Collabora ${endpoint} misslyckades (${res.statusCode}): ${snippet}`);
|
||||
}
|
||||
|
||||
if (!res.body || res.body.length === 0) {
|
||||
throw new Error(`Collabora ${endpoint} returnerade tomt svar — kontrollera intern URL och post_allow`);
|
||||
}
|
||||
|
||||
return {
|
||||
buffer: res.body,
|
||||
contentType: res.headers['content-type'],
|
||||
statusCode: res.statusCode
|
||||
};
|
||||
}
|
||||
|
||||
async function convertTo(config, { buffer, filename, format, extraFields = [] }) {
|
||||
const fmt = String(format || 'pdf').toLowerCase();
|
||||
const fields = [
|
||||
{ name: 'data', buffer, filename: filename || 'document.bin' },
|
||||
...extraFields
|
||||
];
|
||||
|
||||
try {
|
||||
return await postMultipart(config, `/cool/convert-to/${fmt}`, fields);
|
||||
} catch (err) {
|
||||
fields.push({ name: 'format', value: fmt });
|
||||
return postMultipart(config, '/cool/convert-to', fields);
|
||||
}
|
||||
}
|
||||
|
||||
async function extractStructure(config, { buffer, filename, filter }) {
|
||||
const fields = [{ name: 'data', buffer, filename: filename || 'document.bin' }];
|
||||
if (filter) {
|
||||
fields.push({ name: 'filter', value: typeof filter === 'string' ? filter : JSON.stringify(filter) });
|
||||
}
|
||||
const res = await postMultipart(config, '/cool/extract-document-structure', fields);
|
||||
try {
|
||||
return JSON.parse(res.buffer.toString('utf8'));
|
||||
} catch {
|
||||
return { raw: res.buffer.toString('utf8') };
|
||||
}
|
||||
}
|
||||
|
||||
async function transformStructure(config, { buffer, filename, transform }) {
|
||||
const fields = [
|
||||
{ name: 'data', buffer, filename: filename || 'document.bin' },
|
||||
{ name: 'transform', value: typeof transform === 'string' ? transform : JSON.stringify(transform) }
|
||||
];
|
||||
const res = await postMultipart(config, '/cool/transform-document-structure', fields);
|
||||
return {
|
||||
buffer: res.buffer,
|
||||
contentType: res.contentType
|
||||
};
|
||||
}
|
||||
|
||||
async function getCapabilities(config) {
|
||||
const url = `${collaboraBase(config)}/hosting/capabilities`;
|
||||
const { requestBuffer } = require('./http-client');
|
||||
const res = await requestBuffer({
|
||||
url,
|
||||
method: 'GET',
|
||||
rejectUnauthorized: tlsOptions(config)
|
||||
});
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
throw new Error(`Capabilities misslyckades (${res.statusCode})`);
|
||||
}
|
||||
return JSON.parse(res.body.toString('utf8'));
|
||||
}
|
||||
|
||||
async function getDiscovery(config) {
|
||||
const url = `${collaboraBase(config)}/hosting/discovery`;
|
||||
const { requestBuffer } = require('./http-client');
|
||||
const res = await requestBuffer({
|
||||
url,
|
||||
method: 'GET',
|
||||
rejectUnauthorized: tlsOptions(config)
|
||||
});
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
throw new Error(`Discovery misslyckades (${res.statusCode})`);
|
||||
}
|
||||
return res.body.toString('utf8');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
convertTo,
|
||||
extractStructure,
|
||||
transformStructure,
|
||||
getCapabilities,
|
||||
getDiscovery,
|
||||
postMultipart
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
'use strict';
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const { URL } = require('url');
|
||||
|
||||
function requestBuffer(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(options.url);
|
||||
const transport = url.protocol === 'https:' ? https : http;
|
||||
const reqOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method || 'GET',
|
||||
headers: options.headers || {},
|
||||
rejectUnauthorized: options.rejectUnauthorized !== false
|
||||
};
|
||||
|
||||
const req = transport.request(reqOptions, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
body: Buffer.concat(chunks)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (options.body) {
|
||||
req.write(options.body);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function requestStream(options, writeBody) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(options.url);
|
||||
const transport = url.protocol === 'https:' ? https : http;
|
||||
const reqOptions = {
|
||||
hostname: url.hostname,
|
||||
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||
path: url.pathname + url.search,
|
||||
method: options.method || 'POST',
|
||||
headers: options.headers || {},
|
||||
rejectUnauthorized: options.rejectUnauthorized !== false
|
||||
};
|
||||
|
||||
const req = transport.request(reqOptions, (res) => {
|
||||
const chunks = [];
|
||||
res.on('data', (chunk) => chunks.push(chunk));
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
body: Buffer.concat(chunks)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
if (writeBody) {
|
||||
writeBody(req);
|
||||
} else if (options.body) {
|
||||
req.write(options.body);
|
||||
req.end();
|
||||
} else {
|
||||
req.end();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
requestBuffer,
|
||||
requestStream
|
||||
};
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
'use strict';
|
||||
|
||||
const { requestBuffer } = require('./http-client');
|
||||
|
||||
function authHeader(config) {
|
||||
const token = Buffer.from(`${config.username}:${config.password}`).toString('base64');
|
||||
return `Basic ${token}`;
|
||||
}
|
||||
|
||||
function ocsUrl(config, path, query) {
|
||||
const base = config.nextcloudUrl.replace(/\/$/, '');
|
||||
const q = query || 'format=json';
|
||||
const sep = path.includes('?') ? '&' : '?';
|
||||
return `${base}${path}${sep}${q}`;
|
||||
}
|
||||
|
||||
function tlsOptions(config) {
|
||||
return config.verifyTls !== false && config.verifyTls !== 'false';
|
||||
}
|
||||
|
||||
function parseOcsBody(bodyBuffer) {
|
||||
const text = bodyBuffer.toString('utf8');
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const ocs = json.ocs || json;
|
||||
const meta = ocs.meta || {};
|
||||
const statuscode = meta.statuscode ?? meta.statusCode ?? 200;
|
||||
if (statuscode >= 400) {
|
||||
const message = meta.message || meta.status || text.slice(0, 200);
|
||||
throw new Error(`OCS ${statuscode}: ${message}`);
|
||||
}
|
||||
return {
|
||||
statusCode: statuscode,
|
||||
data: ocs.data,
|
||||
raw: json
|
||||
};
|
||||
} catch (err) {
|
||||
if (err.message.startsWith('OCS ')) {
|
||||
throw err;
|
||||
}
|
||||
throw new Error(`OCS-svar kunde inte parsas: ${text.slice(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function ocsRequest(config, { method = 'GET', path, query, body, headers = {}, contentType }) {
|
||||
const url = ocsUrl(config, path, query);
|
||||
const reqHeaders = {
|
||||
Authorization: authHeader(config),
|
||||
'OCS-APIRequest': 'true',
|
||||
Accept: 'application/json',
|
||||
...headers
|
||||
};
|
||||
|
||||
let payload;
|
||||
if (body !== undefined) {
|
||||
if (Buffer.isBuffer(body)) {
|
||||
payload = body;
|
||||
} else if (typeof body === 'string') {
|
||||
payload = Buffer.from(body, 'utf8');
|
||||
reqHeaders['Content-Type'] = contentType || 'application/json';
|
||||
} else {
|
||||
payload = Buffer.from(JSON.stringify(body), 'utf8');
|
||||
reqHeaders['Content-Type'] = contentType || 'application/json';
|
||||
}
|
||||
reqHeaders['Content-Length'] = payload.length;
|
||||
}
|
||||
|
||||
const res = await requestBuffer({
|
||||
url,
|
||||
method,
|
||||
headers: reqHeaders,
|
||||
body: payload,
|
||||
rejectUnauthorized: tlsOptions(config)
|
||||
});
|
||||
|
||||
if (res.statusCode === 401 || res.statusCode === 403) {
|
||||
throw new Error(`OCS ${res.statusCode}: kontrollera app-lösenord`);
|
||||
}
|
||||
|
||||
return {
|
||||
httpStatus: res.statusCode,
|
||||
...parseOcsBody(res.body)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ocsRequest,
|
||||
ocsUrl
|
||||
};
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const DEFAULT_OUTPUT_FOLDER = '/Dokument/OfficeOut/';
|
||||
|
||||
const MIME_BY_EXT = {
|
||||
pdf: 'application/pdf',
|
||||
png: 'image/png',
|
||||
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
odt: 'application/vnd.oasis.opendocument.text',
|
||||
html: 'text/html',
|
||||
htm: 'text/html',
|
||||
txt: 'text/plain',
|
||||
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
|
||||
};
|
||||
|
||||
function normalizeNcPath(ncPath) {
|
||||
if (!ncPath) {
|
||||
return null;
|
||||
}
|
||||
let p = String(ncPath).trim();
|
||||
if (!p.startsWith('/')) {
|
||||
p = '/' + p;
|
||||
}
|
||||
return p.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
function basenameFromNcPath(ncPath) {
|
||||
const p = normalizeNcPath(ncPath);
|
||||
if (!p) {
|
||||
return 'output.bin';
|
||||
}
|
||||
return path.posix.basename(p);
|
||||
}
|
||||
|
||||
function replaceExtension(filename, newExt) {
|
||||
const base = filename.replace(/\.[^/.]+$/, '');
|
||||
const ext = newExt.startsWith('.') ? newExt.slice(1) : newExt;
|
||||
return `${base}.${ext}`;
|
||||
}
|
||||
|
||||
function contentTypeForFormat(format) {
|
||||
if (!format) {
|
||||
return 'application/octet-stream';
|
||||
}
|
||||
return MIME_BY_EXT[String(format).toLowerCase()] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
function defaultDestNcPath(sourceNcPath, format) {
|
||||
const name = replaceExtension(basenameFromNcPath(sourceNcPath), format || 'bin');
|
||||
return normalizeNcPath(DEFAULT_OUTPUT_FOLDER + name);
|
||||
}
|
||||
|
||||
function resolveDestNcPath(msg, sourceNcPath, format) {
|
||||
if (msg.destNcPath) {
|
||||
return normalizeNcPath(msg.destNcPath);
|
||||
}
|
||||
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');
|
||||
return normalizeNcPath(folderWithSlash + name);
|
||||
}
|
||||
return defaultDestNcPath(sourceNcPath, format);
|
||||
}
|
||||
|
||||
function assertSafeDestination(destNcPath, msg) {
|
||||
const dest = normalizeNcPath(destNcPath);
|
||||
const allowedPrefix = normalizeNcPath(DEFAULT_OUTPUT_FOLDER);
|
||||
if (msg.allowOverwrite === true) {
|
||||
return dest;
|
||||
}
|
||||
if (!dest.startsWith(allowedPrefix)) {
|
||||
throw new Error(
|
||||
`destNcPath "${dest}" ligger utanför ${allowedPrefix}. Sätt msg.allowOverwrite=true för att tillåta.`
|
||||
);
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
function getConfig(node) {
|
||||
const cfg = node.configNode;
|
||||
if (!cfg) {
|
||||
throw new Error('Ingen nc-collab-config kopplad');
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
|
||||
function filenameFromPath(ncPath) {
|
||||
return basenameFromNcPath(ncPath);
|
||||
}
|
||||
|
||||
function pickMsgField(msg, ...keys) {
|
||||
for (const key of keys) {
|
||||
if (msg[key] !== undefined && msg[key] !== null && msg[key] !== '') {
|
||||
return msg[key];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function setError(node, msg, err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
msg.error = error.message;
|
||||
msg.payload = { error: error.message };
|
||||
node.error(error.message, msg);
|
||||
node.status({ fill: 'red', shape: 'ring', text: error.message.slice(0, 40) });
|
||||
return msg;
|
||||
}
|
||||
|
||||
function setSuccess(node, msg, text) {
|
||||
node.status({ fill: 'green', shape: 'dot', text: text || 'ok' });
|
||||
return msg;
|
||||
}
|
||||
|
||||
function clearStatus(node) {
|
||||
node.status({});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
DEFAULT_OUTPUT_FOLDER,
|
||||
MIME_BY_EXT,
|
||||
normalizeNcPath,
|
||||
basenameFromNcPath,
|
||||
replaceExtension,
|
||||
contentTypeForFormat,
|
||||
defaultDestNcPath,
|
||||
resolveDestNcPath,
|
||||
assertSafeDestination,
|
||||
getConfig,
|
||||
filenameFromPath,
|
||||
pickMsgField,
|
||||
setError,
|
||||
setSuccess,
|
||||
clearStatus
|
||||
};
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
'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)}`);
|
||||
}
|
||||
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 < 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);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
webDavUrl,
|
||||
webdavGet,
|
||||
webdavPut,
|
||||
webdavPropfindFileId,
|
||||
ensureFolder
|
||||
};
|
||||
Reference in New Issue
Block a user