133 lines
5.1 KiB
JavaScript
133 lines
5.1 KiB
JavaScript
'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);
|
|
}; |