New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

basic-ftp

Package Overview
Dependencies
Maintainers
1
Versions
112
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

basic-ftp - npm Package Compare versions

Comparing version 2.8.3 to 2.9.0

lib/FtpContext.js

6

CHANGELOG.md
# Changelog
## 2.9.0
- Added: Report transfer progress with client.trackProgress().
- Added: Error return codes can be ignored when removing a single file.
- Fixed: Timeout behaviour of control and data socket.
## 2.8.3

@@ -4,0 +10,0 @@

380

lib/ftp.js

@@ -7,6 +7,9 @@ "use strict";

const path = require("path");
const EventEmitter = require("events");
const promisify = require("util").promisify;
const defaultParseList = require("./parseList");
const nullObject = require("./nullObject");
const FTPContext = require("./FtpContext");
const FileInfo = require("./FileInfo");
const StringWriter = require("./StringWriter");
const ProgressTracker = require("./ProgressTracker");

@@ -17,225 +20,17 @@ const fsReadDir = promisify(fs.readdir);

const LF = "\n";
/**
* @typedef {Object} PositiveResponse
* @property {number} code The FTP return code parsed from the FTP return message.
* @property {string} message The whole unparsed FTP return message.
*/
/**
* FTPContext holds the state of an FTP client – its control and data connections – and provides a
* simplified way to interact with an FTP server, handle responses, errors and timeouts.
* @typedef {Object} NegativeResponse
* @property {Object|string} error The error description.
*
* It doesn't implement or use any FTP commands. It's only a foundation to make writing an FTP
* client as easy as possible. You won't usually instantiate this, but use `Client` below.
* Negative responses are usually thrown as exceptions, not returned as values.
*/
class FTPContext {
/**
* Instantiate an FTP context.
*
* @param {number} [timeout=0] Timeout in milliseconds to apply to control and data connections. Use 0 for no timeout.
* @param {string} [encoding="utf8"] Encoding to use for control connection. UTF-8 by default. Use "latin1" for older servers.
*/
constructor(timeout = 0, encoding = "utf8") {
// Timeout applied to all connections.
this._timeout = timeout;
// Current task to be resolved or rejected.
this._task = undefined;
// Function that handles incoming messages and resolves or rejects a task.
this._handler = undefined;
// A multiline response might be received as multiple chunks.
this._partialResponse = "";
// The encoding used when reading from and writing on the control socket.
this.encoding = encoding;
// Options for TLS connections.
this.tlsOptions = {};
// The client can log every outgoing and incoming message.
this.verbose = false;
// The control connection to the FTP server.
this.socket = new Socket();
// The data connection to the FTP server.
this.dataSocket = undefined;
}
/**
* Close control and data connections.
*/
close() {
this.log("Closing sockets.");
this._closeSocket(this._socket);
this._closeSocket(this._dataSocket);
}
/** @type {Socket} */
get socket() {
return this._socket;
}
/**
* Set the socket for the control connection. This will *not* close the former control socket automatically.
*
* @type {Socket}
*/
set socket(socket) {
if (this._socket) {
// Don't close the existing control socket automatically.
// The setter might have been called to upgrade an existing connection.
this._socket.removeAllListeners();
}
this._socket = this._setupSocket(socket);
if (this._socket) {
this._socket.setKeepAlive(true);
this._socket.on("data", data => this._onControlSocketData(data));
}
}
/** @type {Socket} */
get dataSocket() {
return this._dataSocket;
}
/**
* Set the socket for the data connection. This will automatically close the former data socket.
*
* @type {Socket}
**/
set dataSocket(socket) {
this._closeSocket(this._dataSocket);
this._dataSocket = this._setupSocket(socket);
}
/**
* Return true if the control socket is using TLS. This does not mean that a session
* has already been negotiated.
*
* @returns {boolean}
*/
get hasTLS() {
return this._socket && this._socket.encrypted === true;
}
/**
* Send an FTP command and handle any response until the new task is resolved. This returns a Promise that
* will hold whatever the handler passed on when resolving/rejecting its task.
*
* @param {string} command
* @param {HandlerCallback} handler
* @returns {Promise<any>}
*/
handle(command, handler) {
return new Promise((resolvePromise, rejectPromise) => {
this._handler = handler;
this._task = {
// When resolving or rejecting we also want the handler
// to no longer receive any responses or errors.
resolve: (...args) => {
this._handler = undefined;
resolvePromise(...args);
},
reject: (...args) => {
this._handler = undefined;
rejectPromise(...args);
}
};
if (command !== undefined) {
this.send(command);
}
});
}
/**
* Send an FTP command without waiting for or handling the result.
*
* @param {string} command
*/
send(command) {
// Don't log passwords.
const message = command.startsWith("PASS") ? "> PASS ###" : `> ${command}`;
this.log(message);
this._socket.write(command + "\r\n", this._encoding);
}
/**
* Log message if set to be verbose.
*
* @param {string} message
*/
log(message) {
if (this.verbose) {
console.log(message);
}
}
/**
* Handle incoming data on the control socket.
*
* @private
* @param {Buffer} data
*/
_onControlSocketData(data) {
let response = data.toString(this._encoding).trim();
this.log(`< ${response}`);
// This response might complete an earlier partial response.
response = this._partialResponse + response;
const parsed = parseControlResponse(response);
// Remember any incomplete remainder.
this._partialResponse = parsed.rest;
// Each response group is passed along individually.
for (const message of parsed.messages) {
const code = parseInt(message.substr(0, 3), 10);
this._respond({ code, message });
}
}
/**
* Send the current handler a payload. This is usually a control socket response
* or a socket event, like an error or timeout.
*
* @private
* @param {Object} payload
*/
_respond(payload) {
if (this._handler) {
this._handler(payload, this._task);
}
}
/**
* Configure socket properties common to both control and data socket connections.
*
* @private
* @param {Socket} socket
*/
_setupSocket(socket) {
if (socket) {
// All sockets share the same timeout.
socket.setTimeout(this._timeout);
// Reroute any events to the single communication channel with the currently responsible handler.
// In case of an error, the following will happen:
// 1. The current handler will receive a response with the error description.
// 2. The handler should then handle the error by at least rejecting the associated task.
// 3. This rejection will then reject the Promise associated with the task.
// 4. This rejected promise will then lead to an exception in the user's application code.
socket.once("error", error => this._respond({ error })); // An error will automatically close a socket.
// Report timeouts as errors.
socket.once("timeout", () => {
socket.destroy(); // A timeout does not automatically close a socket.
this._respond({ error: "Timeout" });
});
}
return socket;
}
/**
* Close a socket.
*
* @private
* @param {Socket} socket
*/
_closeSocket(socket) {
if (socket) {
socket.removeAllListeners();
socket.destroy();
}
}
}
/**
* An FTP client.
* FTP client using an FTPContext.
*/

@@ -252,3 +47,4 @@ class Client {

this.prepareTransfer = enterPassiveModeIPv4;
this.parseList = defaultParseList;
this.parseList = defaultParseList;
this._progressTracker = new ProgressTracker();
}

@@ -261,18 +57,6 @@

this.ftp.close();
this._progressTracker.stop();
}
/**
* @typedef {Object} PositiveResponse
* @property {number} code The FTP return code parsed from the FTP return message.
* @property {string} message The whole unparsed FTP return message.
*/
/**
* @typedef {Object} NegativeResponse
* @property {Object|string} error The error description.
*
* Negative responses are usually thrown as exceptions, not returned as values.
*/
/**
* Connect to an FTP server.

@@ -302,3 +86,3 @@ *

* @param {string} command
* @param {boolean} ignoreError
* @param {boolean} ignoreErrorCodes
* @return {Promise<PositiveResponse>}

@@ -329,3 +113,3 @@ */

this.ftp.tlsOptions = options; // Keep the TLS options for later data connections that should use the same options.
this.ftp.log("Control socket is using " + this.ftp.socket.getProtocol());
this.ftp.log("Control socket is using " + this.ftp.socket.getProtocol());
return ret;

@@ -396,3 +180,3 @@ }

*
* @returns {Map<string, string>}
* @returns {Promise<Map<string, string>>}
*/

@@ -405,3 +189,3 @@ async features() {

// The first and last line wrap the multiline response, ignore them.
res.message.split(LF).slice(1, -1).forEach(line => {
res.message.split("\n").slice(1, -1).forEach(line => {
// A typical lines looks like: " REST STREAM" or " MDTM".

@@ -432,10 +216,31 @@ // Servers might not use an indentation though.

*
* @param {string} filename
* @param {string} filename
* @param {boolean} ignoreErrorCodes
* @returns {Promise<PositiveResponse>}
*/
remove(filename) {
return this.send("DELE " + filename);
remove(filename, ignoreErrorCodes = false) {
return this.send("DELE " + filename, ignoreErrorCodes);
}
/**
* @typedef {Object} ProgressInfo
* @property {string} name A name describing this info, e.g. the filename of the transfer.
* @property {string} type The type of transfer, typically "upload" or "download".
* @property {number} bytes Transferred bytes in current transfer.
* @property {number} bytesOverall Transferred bytes since last counter reset. Useful for tracking multiple transfers.
*/
/**
* Start reporting transfer progress for any upload or download. This will also reset the
* overall transfer counter that can be used for multiple transfers. Progress information
* will be reported to the given handler. You may use `undefined` to stop any reporting.
*
* @param {((info: ProgressInfo) => void)} [handler] Handler function to call on transfer progress.
*/
trackProgress(handler) {
this._progressTracker.bytesOverall = 0;
this._progressTracker.reportTo(handler);
}
/**
* Upload data from a readable stream and store it as a file with

@@ -450,3 +255,3 @@ * a given filename in the current working directory.

await this.prepareTransfer(this.ftp);
return upload(this.ftp, readableStream, remoteFilename);
return upload(this.ftp, this._progressTracker, readableStream, remoteFilename);
}

@@ -467,3 +272,3 @@

const command = startAt > 0 ? `REST ${startAt}` : `RETR ${remoteFilename}`;
return download(this.ftp, writableStream, command, remoteFilename);
return download(this.ftp, this._progressTracker, writableStream, command, remoteFilename);
}

@@ -479,3 +284,3 @@

const writable = new StringWriter(this.ftp.encoding);
await download(this.ftp, writable, "LIST");
await download(this.ftp, nullObject(), writable, "LIST");
this.ftp.log(writable.text);

@@ -635,6 +440,5 @@ return this.parseList(writable.text);

FileInfo,
// Useful for custom extensions.
// Expose some utilities for custom extensions:
utils: {
upgradeSocket,
parseControlResponse,
parseIPv4PasvResponse,

@@ -650,3 +454,3 @@ TransferResolver

* @param {number} code
* @param {boolean}
* @return {boolean}
*/

@@ -657,6 +461,2 @@ function positiveCompletion(code) {

function isSingle(line) {
return /^\d\d\d /.test(line);
}
function isMultiline(line) {

@@ -674,42 +474,2 @@ return /^\d\d\d-/.test(line);

/**
* Parse an FTP control response as a collection of messages. A message is a complete
* single- or multiline response. A response can also contain multiple multiline responses
* that will each be represented by a message. A response can also be incomplete
* and be completed on the next incoming data chunk for which case this function also
* describes a `rest`. This function converts all CRLF to LF.
*
* @param {string} text
* @returns {{messages: string[], rest: string}}
*/
function parseControlResponse(text) {
const lines = text.split(/\r?\n/);
const messages = [];
let startAt = 0;
let token = "";
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// No group has been opened.
if (token === "") {
if (isMultiline(line)) {
// Open a group by setting an expected token.
token = line.substr(0, 3) + " ";
startAt = i;
}
else if (isSingle(line)) {
// Single lines can be grouped immediately.
messages.push(line);
}
}
// Group has been opened, expect closing token.
else if (line.startsWith(token)) {
token = "";
messages.push(lines.slice(startAt, i + 1).join(LF));
}
}
// The last group might not have been closed, report it as a rest.
const rest = token !== "" ? lines.slice(startAt).join(LF) + LF : "";
return { messages, rest };
}
/**
* Upgrade a socket connection with TLS.

@@ -719,3 +479,3 @@ *

* @param {Object} options Same options as in `tls.connect(options)`
* @returns {Promise<TLSSocket>}
* @returns {Promise<tls.TLSSocket>}
*/

@@ -747,3 +507,3 @@ function upgradeSocket(socket, options) {

*
* @param {FTP} ftp
* @param {FTPContext} ftp
* @returns {Promise<PositiveResponse>}

@@ -817,3 +577,3 @@ */

*
* @param {FTP} ftp
* @param {FTPContext} ftp
* @param {stream.Readable} readableStream

@@ -823,3 +583,3 @@ * @param {string} remoteFilename

*/
function upload(ftp, readableStream, remoteFilename) {
function upload(ftp, progress, readableStream, remoteFilename) {
const resolver = new TransferResolver(ftp);

@@ -834,5 +594,6 @@ const command = "STOR " + remoteFilename;

ftp.log(`Uploading (${describeTLS(ftp.dataSocket)})`);
progress.start(ftp.dataSocket, remoteFilename, "upload");
readableStream.pipe(ftp.dataSocket).once("finish", () => {
// Explicitly close/destroy the socket to signal the end.
ftp.dataSocket.destroy();
ftp.dataSocket.destroy(); // Explicitly close/destroy the socket to signal the end.
progress.updateAndStop();
resolver.confirm(task);

@@ -846,2 +607,3 @@ });

else if (res.code >= 400 || res.error) {
progress.updateAndStop();
resolver.reject(task, res);

@@ -855,9 +617,9 @@ }

*
* @param {FTP} ftp
* @param {FTPContext} ftp
* @param {stream.Writable} writableStream
* @param {string} command
* @param {filename} [remoteFilename]
* @param {string} [remoteFilename]
* @returns {Promise<PositiveResponse>}
*/
function download(ftp, writableStream, command, remoteFilename = "") {
function download(ftp, progress, writableStream, command, remoteFilename = "") {
// It's possible that data transmission begins before the control socket

@@ -870,2 +632,3 @@ // receives the announcement. Start listening for data immediately.

ftp.log(`Downloading (${describeTLS(ftp.dataSocket)})`);
progress.start(ftp.dataSocket, remoteFilename, "download");
// Confirm the transfer as soon as the data socket transmission ended.

@@ -875,3 +638,6 @@ // It's possible, though, that the data transmission is complete before

// Check if the data socket is not already closed.
conditionOrEvent(ftp.dataSocket.destroyed, ftp.dataSocket, "end", () => resolver.confirm(task));
conditionOrEvent(ftp.dataSocket.destroyed, ftp.dataSocket, "end", () => {
progress.updateAndStop();
resolver.confirm(task);
});
}

@@ -885,2 +651,3 @@ else if (res.code === 350) { // Restarting at startAt.

else if (res.code >= 400 || res.error) {
progress.updateAndStop();
resolver.reject(task, res);

@@ -909,17 +676,2 @@ }

class StringWriter extends EventEmitter {
constructor(encoding) {
super();
this.encoding = encoding;
this.text = "";
this.write = this.end = this.append;
}
append(chunk) {
if (chunk) {
this.text += chunk.toString(this.encoding);
}
}
}
/**

@@ -926,0 +678,0 @@ * Upload the contents of a local directory to the working directory. This will overwrite

@@ -14,3 +14,3 @@ "use strict";

*/
module.exports = function(rawList) {
module.exports = function parseList(rawList) {
const lines = rawList.split(/\r?\n/)

@@ -17,0 +17,0 @@ // Strip possible multiline prefix

{
"name": "basic-ftp",
"version": "2.8.3",
"version": "2.9.0",
"description": "FTP client for Node.js with support for explicit FTPS over TLS.",

@@ -5,0 +5,0 @@ "main": "./lib/ftp",

@@ -5,9 +5,9 @@ # Basic FTP

This is an FTP client for Node.js, it supports explicit FTPS over TLS.
This is an FTP client for Node.js. It supports explicit FTPS over TLS, has a Promise-based API and offers methods to operate on whole directories.
## Goals
Provide a foundation that covers the usual needs and make it possible to extend functionality.
Provide a foundation that covers the usual needs.
FTP is an old protocol, there are many features, quirks and server implementations. It's not a goal to support all of them. Instead, make it possible to extend behaviour without requiring a change in the library itself.
FTP is an old protocol, there are many features, quirks and server implementations. It's not a goal to support all of them. Instead, the library should focus on being easy to read, tinker with and extend.

@@ -20,3 +20,3 @@ ## Dependencies

`Client` provides an API to interact with an FTP server. The following example shows how to connect, upgrade to TLS, login, get a directory listing and upload a file.
`Client` provides an API to interact with an FTP server. The following example shows how to connect, upgrade to TLS, login, get a directory listing and upload a file. Be aware that the FTP protocol doesn't allow multiple requests in parallel.

@@ -53,11 +53,2 @@ ```js

You can always use Promises instead of async/await. Be aware that the FTP protocol doesn't allow multiple parallel requests.
```js
client.ensureDir("my/remote/path")
.then(() => client.clearWorkingDir())
.then(() => client.uploadDir("my/local/path"))
.catch(err => console.log("Oh no!", err))
```
If you encounter a problem, it can be helpful to let the client log out all communication with the FTP server.

@@ -151,2 +142,35 @@

`trackProgress(handler)`
Report any transfer progress using the given handler function. See the next section for more details.
## Transfer Progress
You can set a callback function with `client.trackProgress` to track the progress of all uploads and downloads. To disable progress reporting, call `trackProgress` with an undefined handler.
```
// Log progress for any transfer from now on.
client.trackProgress(info => {
console.log("File", info.name);
console.log("Type", info.type);
console.log("Transferred", info.bytes);
console.log("Transferred Overall", info.bytesOverall);
});
// Transfer some data
await client.upload(someStream, "test.txt");
await client.upload(someOtherStream, "test2.txt");
// Reset overall counter
client.trackProgress(info => console.log(info.bytesOverall));
await client.downloadDir("local/path");
// Stop logging
client.trackProgress();
```
For each transfer, the callback function will receive a name, the transfer type (upload/download) and the number of bytes transferred so far. The function will be called at a regular interval during a transfer.
In addition to that, there is also a counter for all bytes transferred since the last time `trackProgress` was called. This is useful when downloading a directory with multiple files where you want to show the total bytes downloaded so far.
## Error Handling

@@ -153,0 +177,0 @@

@@ -25,2 +25,3 @@ const assert = require("assert");

let client;
beforeEach(function() {

@@ -33,2 +34,6 @@ client = new Client();

afterEach(function() {
client.close();
});
/**

@@ -141,2 +146,8 @@ * Testing simple convenience functions follows the same pattern:

});
it("resets overall bytes of progress tracker on trackProgress()", function() {
client._progressTracker.bytesOverall = 5;
client.trackProgress();
assert.equal(client._progressTracker.bytesOverall, 0, "bytesOverall after reset");
});
});

@@ -31,2 +31,6 @@ const assert = require("assert");

afterEach(function() {
client.close();
});
function requestListAndVerify(done) {

@@ -33,0 +37,0 @@ client.list().then(result => {

@@ -18,6 +18,12 @@ const assert = require("assert");

const old = ftp.socket;
ftp.socket = undefined;
ftp.socket = new SocketMock();
assert.equal(old.destroyed, false, "Socket not destroyed.");
});
it("Setting control socket to undefined destroys current", function() {
const old = ftp.socket;
ftp.socket = undefined;
assert.equal(old.destroyed, true, "Socket destroyed.");
});
it("Setting new data socket destroys current", function() {

@@ -31,3 +37,3 @@ const old = ftp.dataSocket;

ftp.handle(undefined, (res, task) => {
assert.deepEqual(res, { error: "Timeout" });
assert.deepEqual(res, { error: "Timeout control socket" });
done();

@@ -48,3 +54,3 @@ });

ftp.handle(undefined, (res, task) => {
assert.deepEqual(res, { error: "Timeout" });
assert.deepEqual(res, { error: "Timeout data socket" });
done();

@@ -51,0 +57,0 @@ });

const assert = require("assert");
const parseControlResponse = require("../lib/ftp").utils.parseControlResponse;
const parseControlResponse = require("../lib/parseControlResponse");

@@ -4,0 +4,0 @@ const CRLF = "\r\n";

@@ -7,2 +7,4 @@ const EventEmitter = require("events");

this.destroyed = false;
this.bytesWritten = 0;
this.bytesRead = 0;
}

@@ -9,0 +11,0 @@ removeAllListeners() {

@@ -19,2 +19,6 @@ const assert = require("assert");

afterEach(function() {
client.close();
});
it("sends the correct command", function(done) {

@@ -21,0 +25,0 @@ client.ftp.socket.once("didSend", buf => {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc