Initial release: generic CLI agent nodes for Nextcloud Node-RED

This commit is contained in:
n3wk
2026-08-24 19:06:08 +02:00
commit b9249dd9ca
16 changed files with 1437 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
'use strict';
const fs = require('fs').promises;
const path = require('path');
module.exports = function (RED) {
function AgentFileNode(config) {
RED.nodes.createNode(this, config);
const node = this;
this.configNode = RED.nodes.getNode(config.agentConfig);
this.operation = config.operation || 'read';
this.filePath = config.filePath || '';
this.encoding = config.encoding || 'utf8';
this.createDirs = config.createDirs !== false;
if (!this.configNode) {
node.error('No agent config node selected');
return;
}
node.on('input', async function (msg) {
const cfg = node.configNode;
const workspaceDir = cfg.workspaceDir || '/data/agent-workspace';
const operation = msg.operation || node.operation;
const filePath = msg.filePath || node.filePath;
const encoding = msg.encoding || node.encoding;
if (!filePath) {
node.error('No file path specified', msg);
return;
}
const fullPath = path.isAbsolute(filePath)
? filePath
: path.join(workspaceDir, filePath);
const resolvedWorkspace = path.resolve(workspaceDir);
const resolvedPath = path.resolve(fullPath);
if (!resolvedPath.startsWith(resolvedWorkspace)) {
node.error('Path traversal attempt blocked', msg);
return;
}
node.status({ fill: 'blue', shape: 'dot', text: operation });
try {
let result;
switch (operation) {
case 'read':
result = await fs.readFile(resolvedPath, encoding);
break;
case 'write':
const content = msg.payload;
if (node.createDirs) {
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
}
await fs.writeFile(resolvedPath, content, encoding);
result = { written: true, path: filePath, bytes: Buffer.byteLength(content, encoding) };
break;
case 'append':
const appendContent = msg.payload;
if (node.createDirs) {
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
}
await fs.appendFile(resolvedPath, appendContent, encoding);
result = { appended: true, path: filePath, bytes: Buffer.byteLength(appendContent, encoding) };
break;
case 'delete':
await fs.unlink(resolvedPath);
result = { deleted: true, path: filePath };
break;
case 'exists':
try {
await fs.access(resolvedPath);
result = { exists: true, path: filePath };
} catch {
result = { exists: false, path: filePath };
}
break;
case 'list':
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
result = entries.map(e => ({
name: e.name,
path: path.join(filePath, e.name),
isDirectory: e.isDirectory(),
isFile: e.isFile()
}));
break;
case 'stat':
const stats = await fs.stat(resolvedPath);
result = {
path: filePath,
size: stats.size,
isDirectory: stats.isDirectory(),
isFile: stats.isFile(),
created: stats.birthtime,
modified: stats.mtime
};
break;
case 'mkdir':
await fs.mkdir(resolvedPath, { recursive: true });
result = { created: true, path: filePath };
break;
default:
throw new Error(`Unknown operation: ${operation}`);
}
node.status({ fill: 'green', shape: 'dot', text: 'Done' });
msg.payload = result;
msg.filePath = filePath;
msg.operation = operation;
node.send(msg);
} catch (err) {
node.status({ fill: 'red', shape: 'ring', text: 'Error' });
msg.error = err.message;
msg.filePath = filePath;
msg.operation = operation;
node.error(`File ${operation} failed: ${err.message}`, msg);
}
});
}
RED.nodes.registerType('agent-file', AgentFileNode);
};