UNPKG

node-red-contrib-lftp

Version:

A node-red node that supports FTP(s) and SFTP file transfer

348 lines (314 loc) 12.2 kB
module.exports = function(RED) { "use strict"; var fs = require("fs"); var FTPS = require("ftps"); var Parser = require("parse-listing"); var tmp = require("tmp"); var utils = require("./utils"); function LftpConfigNode(n) { RED.nodes.createNode(this, n); this.options = { host: n.host || "localhost", // required protocol: n.protocol || "ftp", // Optional, values : 'ftp', 'sftp', 'ftps', ... default: 'ftp' // protocol is added on beginning of host, ex : sftp://domain.com in this case port: n.port || 21, // Optional // port is added to the end of the host, ex: sftp://domain.com:22 in this case escape: n.escape || true, // optional, used for escaping shell characters (space, $, etc.), default: true retries: n.retries || 2, // Optional, defaults to 1 (1 = no retries, 0 = unlimited retries) timeout: n.timeout || 10, // Optional, Time before failing a connection attempt. Defaults to 10 retryInterval: n.retryInterval || 5, // Optional, Time in seconds between attempts. Defaults to 5 retryMultiplier: n.retryMultiplier || 1, // Optional, Multiplier by which retryInterval is multiplied each time new attempt fails. Defaults to 1 requiresPassword: n.requiresPassword || false, // Optional, defaults to true autoConfirm: false, // Optional, is used to auto confirm ssl questions on sftp or fish protocols, defaults to false cwd: "", // Optional, defaults to the directory from where the script is executed additionalLftpCommands: n.additionalLftpCommands || "", // Additional commands to pass to lftp, splitted by ';' requireSSHKey: false, // Optional, defaults to false, This option for SFTP Protocol with ssh key authentication sshKeyPath: "/path/id_dsa" // Required if requireSSHKey: true , defaults to empty string, This option for SFTP Protocol with ssh key authentication }; this.options.username = ""; this.options.password = ""; if (this.credentials && this.credentials.hasOwnProperty("username")) { this.options.username = this.credentials.username; } if (this.credentials && this.credentials.hasOwnProperty("password")) { this.options.password = this.credentials.password; } } RED.nodes.registerType("lftp-config", LftpConfigNode, { credentials: { username: { type: "text" }, password: { type: "password" } } }); function LftpCommandNode(n) { RED.nodes.createNode(this, n); var node = this; this.server = n.server; this.operation = n.operation; this.filename = n.filename; this.localFilename = n.localFilename; this.workdir = n.workdir; this.savedir = n.savedir; this.serverConfig = RED.nodes.getNode(this.server); var statuses = { active: { fill: "blue", shape: "dot", text: "executing" }, error: { fill: "red", shape: "dot", text: "error" }, blank: {} }; /** * Returns true if the reponse is an error, false otherwise * * @param {*} err * @param {*} res */ var responseErrorHandler = function(err, res, msg) { let message = null; try { if (err) { message = err; } else if (res.error && res.error.toLowerCase().includes("error")) { // When disk is readonly, the output from lftp goes to stderr. // so lets filter this by checking if the string contains // any kind of error. // try catch in the case that res.error is not a string. message = res.error; } } catch (e) { // silently fail and continue } if (message) { node.error(message, msg); node.status(statuses.error); return true; } else { return false; } }; this.commands = {}; this.commands.list = function(event, msg) { var conn = new FTPS(node.serverConfig.options); conn .cd(event.workdir) .ls() .exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { Parser.parseEntries(res.data, function(err, data) { if (err) { node.error(statuses.error); } msg.workdir = event.workdir; msg.payload = data; node.send(msg); node.status(statuses.blank); }); } }); }; this.commands.get = function(event, msg) { var filename = utils.addTrailingSlash(event.workdir) + event.filename; var targetFilename = event.localFilename; var conn = new FTPS(node.serverConfig.options); conn.cat(filename).exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { msg.workdir = event.workdir; msg.payload = {}; msg.payload.filedata = res.data; msg.payload.filename = event.filename; msg.payload.filepath = filename; if (targetFilename) { fs.writeFile(targetFilename, res.data, function(err) { if (err) { throw err; } node.send(msg); node.status(statuses.blank); }); } else { node.send(msg); node.status(statuses.blank); } } }); }; this.commands.put = function(event, msg) { if (!event.filename.length > 0) { var d = new Date(); var guid = d.getTime().toString(); if (event.fileExtension === "") { event.fileExtension = ".txt"; } event.filename = guid + node.fileExtension; } var filename = utils.addTrailingSlash(event.workdir) + event.filename; var filedata = msg.payload.filedata || JSON.stringify(msg.payload); var sourceFilename = event.localFilename; if (sourceFilename) { node.debug("putting " + sourceFilename + " directly"); // If we have a sourcefile instead of file data, we can just // directly give this file to FPTS var conn = new FTPS(node.serverConfig.options); conn.put(sourceFilename, filename).exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { msg.workdir = event.workdir; msg.payload = {}; msg.payload.filename = event.filename; msg.payload.filepath = filename; node.send(msg); node.status(statuses.blank); } }); } else if (filedata) { node.debug("putting " + filedata + " temporarily"); // If we don't have a sourcefile, we will have to make a // temporary file because lftp can't stream data directly. // This file is temporarily written to disk, put, then // deleted locally. tmp.file(function(err, path, fd, cleanupCallback) { if (err) throw err; fs.writeFile(path, filedata, function(err) { if (err) { cleanupCallback(); throw err; } var conn = new FTPS(node.serverConfig.options); conn.put(path, filename).exec(function(err, res) { cleanupCallback(); if (!responseErrorHandler(err, res, msg)) { msg.workdir = event.workdir; msg.payload = {}; msg.payload.filename = event.filename; msg.payload.filepath = filename; node.send(msg); node.status(statuses.blank); } }); }); }); } else { // Nothing to write! node.error("nothing to write", msg); node.status(statuses.error); } }; this.commands.delete = function(event, msg) { var filename = utils.addTrailingSlash(event.workdir) + event.filename; var conn = new FTPS(node.serverConfig.options); conn.rm(filename).exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { msg.workdir = event.workdir; msg.payload = {}; msg.payload.filename = event.filename; msg.payload.filepath = filename; node.send(msg); node.status(statuses.blank); } }); }; this.commands.rmdir = function(event, msg) { var filename = utils.addTrailingSlash(event.workdir) + event.filename; var conn = new FTPS(node.serverConfig.options); conn.rmdir(filename).exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { msg.workdir = event.workdir; msg.payload = {}; msg.payload.filename = event.filename; msg.payload.filepath = filename; node.send(msg); node.status(statuses.blank); } }); }; this.commands.rmrf = function(event, msg) { var filename = utils.addTrailingSlash(event.workdir) + event.filename; var conn = new FTPS(node.serverConfig.options); conn.raw("rm -r -f " + conn._escapeshell(filename)); conn.exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { msg.workdir = event.workdir; msg.payload = {}; msg.payload.filename = event.filename; msg.payload.filepath = filename; node.send(msg); node.status(statuses.blank); } }); }; this.commands.move = function(event, msg) { var filename = utils.addTrailingSlash(event.workdir) + event.filename; var targetFilename = utils.addTrailingSlash(event.workdir) + event.targetFilename; var conn = new FTPS(node.serverConfig.options); conn.mv(filename, targetFilename).exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { msg.workdir = event.workdir; msg.payload = {}; msg.payload.filename = event.targetFilename; msg.payload.filepath = targetFilename; node.send(msg); node.status(statuses.blank); } }); }; this.commands.raw = function(event, msg) { var conn = new FTPS(node.serverConfig.options); if (Array.isArray(msg.payload)) { for (var i = 0, len = msg.payload.length; i < len; i++) { conn.raw(msg.payload[i]); } } else { conn.raw(msg.payload); } conn.exec(function(err, res) { if (!responseErrorHandler(err, res, msg)) { msg.payload = res.data; node.send(msg); node.status(statuses.blank); } }); }; if (this.server) { node.on("input", function(msg) { try { /** * flag status immediately */ node.status(statuses.active); /** * need to ensure all event values can be set via node or msg * to facilitate the per msg operation functionality */ var event = {}; event.operation = node.operation || msg.operation || ""; event.workdir = node.workdir || msg.workdir || ""; event.filename = node.filename || msg.payload.filename || ""; event.targetFilename = node.targetFilename || msg.payload.targetFilename || ""; event.savedir = node.savedir || msg.savedir || ""; event.localFilename = node.localFilename || msg.localFilename || msg.payload.localFilename || ""; /** * set this across the board so downstream processing has the * canonical last operation */ msg.operation = event.operation; if (event.operation && node.commands[event.operation]) { node.commands[event.operation](event, msg); } else { node.error("invalid operation: " + event.operation, msg); node.status(statuses.error); } } catch (error) { node.error(error, msg); node.status(statuses.blank); } }); } else { node.error("missing server configuration"); node.status(statuses.blank); } } RED.nodes.registerType("lftp-command", LftpCommandNode); };