| "use strict"; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.Client = void 0; |
| const fs_1 = require("fs"); |
| const path_1 = require("path"); |
| const tls_1 = require("tls"); |
| const util_1 = require("util"); |
| const FtpContext_1 = require("./FtpContext"); |
| const parseList_1 = require("./parseList"); |
| const ProgressTracker_1 = require("./ProgressTracker"); |
| const StringWriter_1 = require("./StringWriter"); |
| const parseListMLSD_1 = require("./parseListMLSD"); |
| const netUtils_1 = require("./netUtils"); |
| const transfer_1 = require("./transfer"); |
| const parseControlResponse_1 = require("./parseControlResponse"); |
| // Use promisify to keep the library compatible with Node 8. |
| const fsReadDir = (0, util_1.promisify)(fs_1.readdir); |
| const fsMkDir = (0, util_1.promisify)(fs_1.mkdir); |
| const fsStat = (0, util_1.promisify)(fs_1.stat); |
| const fsOpen = (0, util_1.promisify)(fs_1.open); |
| const fsClose = (0, util_1.promisify)(fs_1.close); |
| const fsUnlink = (0, util_1.promisify)(fs_1.unlink); |
| const LIST_COMMANDS_DEFAULT = ["LIST -a", "LIST"]; |
| const LIST_COMMANDS_MLSD = ["MLSD", "LIST -a", "LIST"]; |
| /** |
| * High-level API to interact with an FTP server. |
| */ |
| class Client { |
| /** |
| * Instantiate an FTP client. |
| * |
| * @param timeout Timeout in milliseconds, use 0 for no timeout. Optional, default is 30 seconds. |
| */ |
| constructor(timeout = 30000) { |
| this.availableListCommands = LIST_COMMANDS_DEFAULT; |
| this.ftp = new FtpContext_1.FTPContext(timeout); |
| this.prepareTransfer = this._enterFirstCompatibleMode([transfer_1.enterPassiveModeIPv6, transfer_1.enterPassiveModeIPv4]); |
| this.parseList = parseList_1.parseList; |
| this._progressTracker = new ProgressTracker_1.ProgressTracker(); |
| } |
| /** |
| * Close the client and all open socket connections. |
| * |
| * Close the client and all open socket connections. The client can’t be used anymore after calling this method, |
| * you have to either reconnect with `access` or `connect` or instantiate a new instance to continue any work. |
| * A client is also closed automatically if any timeout or connection error occurs. |
| */ |
| close() { |
| this.ftp.close(); |
| this._progressTracker.stop(); |
| } |
| /** |
| * Returns true if the client is closed and can't be used anymore. |
| */ |
| get closed() { |
| return this.ftp.closed; |
| } |
| /** |
| * Connect (or reconnect) to an FTP server. |
| * |
| * This is an instance method and thus can be called multiple times during the lifecycle of a `Client` |
| * instance. Whenever you do, the client is reset with a new control connection. This also implies that |
| * you can reopen a `Client` instance that has been closed due to an error when reconnecting with this |
| * method. In fact, reconnecting is the only way to continue using a closed `Client`. |
| * |
| * @param host Host the client should connect to. Optional, default is "localhost". |
| * @param port Port the client should connect to. Optional, default is 21. |
| */ |
| connect(host = "localhost", port = 21) { |
| this.ftp.reset(); |
| this.ftp.socket.connect({ |
| host, |
| port, |
| family: this.ftp.ipFamily |
| }, () => this.ftp.log(`Connected to ${(0, netUtils_1.describeAddress)(this.ftp.socket)} (${(0, netUtils_1.describeTLS)(this.ftp.socket)})`)); |
| return this._handleConnectResponse(); |
| } |
| /** |
| * As `connect` but using implicit TLS. Implicit TLS is not an FTP standard and has been replaced by |
| * explicit TLS. There are still FTP servers that support only implicit TLS, though. |
| */ |
| connectImplicitTLS(host = "localhost", port = 21, tlsOptions = {}) { |
| this.ftp.reset(); |
| this.ftp.socket = (0, tls_1.connect)(port, host, tlsOptions, () => this.ftp.log(`Connected to ${(0, netUtils_1.describeAddress)(this.ftp.socket)} (${(0, netUtils_1.describeTLS)(this.ftp.socket)})`)); |
| this.ftp.tlsOptions = tlsOptions; |
| return this._handleConnectResponse(); |
| } |
| /** |
| * Handles the first reponse by an FTP server after the socket connection has been established. |
| */ |
| _handleConnectResponse() { |
| return this.ftp.handle(undefined, (res, task) => { |
| if (res instanceof Error) { |
| // The connection has been destroyed by the FTPContext at this point. |
| task.reject(res); |
| } |
| else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) { |
| task.resolve(res); |
| } |
| // Reject all other codes, including 120 "Service ready in nnn minutes". |
| else { |
| // Don't stay connected but don't replace the socket yet by using reset() |
| // so the user can inspect properties of this instance. |
| task.reject(new FtpContext_1.FTPError(res)); |
| } |
| }); |
| } |
| /** |
| * Send an FTP command and handle the first response. |
| */ |
| send(command, ignoreErrorCodesDEPRECATED = false) { |
| if (ignoreErrorCodesDEPRECATED) { // Deprecated starting from 3.9.0 |
| this.ftp.log("Deprecated call using send(command, flag) with boolean flag to ignore errors. Use sendIgnoringError(command)."); |
| return this.sendIgnoringError(command); |
| } |
| return this.ftp.request(command); |
| } |
| /** |
| * Send an FTP command and ignore an FTP error response. Any other kind of error or timeout will still reject the Promise. |
| * |
| * @param command |
| */ |
| sendIgnoringError(command) { |
| return this.ftp.handle(command, (res, task) => { |
| if (res instanceof FtpContext_1.FTPError) { |
| task.resolve({ code: res.code, message: res.message }); |
| } |
| else if (res instanceof Error) { |
| task.reject(res); |
| } |
| else { |
| task.resolve(res); |
| } |
| }); |
| } |
| /** |
| * Upgrade the current socket connection to TLS. |
| * |
| * @param options TLS options as in `tls.connect(options)`, optional. |
| * @param command Set the authentication command. Optional, default is "AUTH TLS". |
| */ |
| async useTLS(options = {}, command = "AUTH TLS") { |
| const ret = await this.send(command); |
| this.ftp.socket = await (0, netUtils_1.upgradeSocket)(this.ftp.socket, options); |
| 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: ${(0, netUtils_1.describeTLS)(this.ftp.socket)}`); |
| return ret; |
| } |
| /** |
| * Login a user with a password. |
| * |
| * @param user Username to use for login. Optional, default is "anonymous". |
| * @param password Password to use for login. Optional, default is "guest". |
| */ |
| login(user = "anonymous", password = "guest") { |
| this.ftp.log(`Login security: ${(0, netUtils_1.describeTLS)(this.ftp.socket)}`); |
| return this.ftp.handle("USER " + user, (res, task) => { |
| if (res instanceof Error) { |
| task.reject(res); |
| } |
| else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) { // User logged in proceed OR Command superfluous |
| task.resolve(res); |
| } |
| else if (res.code === 331) { // User name okay, need password |
| this.ftp.send("PASS " + password); |
| } |
| else { // Also report error on 332 (Need account) |
| task.reject(new FtpContext_1.FTPError(res)); |
| } |
| }); |
| } |
| /** |
| * Set the usual default settings. |
| * |
| * Settings used: |
| * * Binary mode (TYPE I) |
| * * File structure (STRU F) |
| * * Additional settings for FTPS (PBSZ 0, PROT P) |
| */ |
| async useDefaultSettings() { |
| const features = await this.features(); |
| // Use MLSD directory listing if possible. See https://tools.ietf.org/html/rfc3659#section-7.8: |
| // "The presence of the MLST feature indicates that both MLST and MLSD are supported." |
| const supportsMLSD = features.has("MLST"); |
| this.availableListCommands = supportsMLSD ? LIST_COMMANDS_MLSD : LIST_COMMANDS_DEFAULT; |
| await this.send("TYPE I"); // Binary mode |
| await this.sendIgnoringError("STRU F"); // Use file structure |
| await this.sendIgnoringError("OPTS UTF8 ON"); // Some servers expect UTF-8 to be enabled explicitly and setting before login might not have worked. |
| if (supportsMLSD) { |
| await this.sendIgnoringError("OPTS MLST type;size;modify;unique;unix.mode;unix.owner;unix.group;unix.ownername;unix.groupname;"); // Make sure MLSD listings include all we can parse |
| } |
| if (this.ftp.hasTLS) { |
| await this.sendIgnoringError("PBSZ 0"); // Set to 0 for TLS |
| await this.sendIgnoringError("PROT P"); // Protect channel (also for data connections) |
| } |
| } |
| /** |
| * Convenience method that calls `connect`, `useTLS`, `login` and `useDefaultSettings`. |
| * |
| * This is an instance method and thus can be called multiple times during the lifecycle of a `Client` |
| * instance. Whenever you do, the client is reset with a new control connection. This also implies that |
| * you can reopen a `Client` instance that has been closed due to an error when reconnecting with this |
| * method. In fact, reconnecting is the only way to continue using a closed `Client`. |
| */ |
| async access(options = {}) { |
| var _a, _b; |
| const useExplicitTLS = options.secure === true; |
| const useImplicitTLS = options.secure === "implicit"; |
| let welcome; |
| if (useImplicitTLS) { |
| welcome = await this.connectImplicitTLS(options.host, options.port, options.secureOptions); |
| } |
| else { |
| welcome = await this.connect(options.host, options.port); |
| } |
| if (useExplicitTLS) { |
| // Fixes https://github.com/patrickjuchli/basic-ftp/issues/166 by making sure |
| // host is set for any future data connection as well. |
| const secureOptions = (_a = options.secureOptions) !== null && _a !== void 0 ? _a : {}; |
| secureOptions.host = (_b = secureOptions.host) !== null && _b !== void 0 ? _b : options.host; |
| await this.useTLS(secureOptions); |
| } |
| // Set UTF-8 on before login in case there are non-ascii characters in user or password. |
| // Note that this might not work before login depending on server. |
| await this.sendIgnoringError("OPTS UTF8 ON"); |
| await this.login(options.user, options.password); |
| await this.useDefaultSettings(); |
| return welcome; |
| } |
| /** |
| * Get the current working directory. |
| */ |
| async pwd() { |
| const res = await this.send("PWD"); |
| // The directory is part of the return message, for example: |
| // 257 "/this/that" is current directory. |
| const parsed = res.message.match(/"(.+)"/); |
| if (parsed === null || parsed[1] === undefined) { |
| throw new Error(`Can't parse response to command 'PWD': ${res.message}`); |
| } |
| return parsed[1]; |
| } |
| /** |
| * Get a description of supported features. |
| * |
| * This sends the FEAT command and parses the result into a Map where keys correspond to available commands |
| * and values hold further information. Be aware that your FTP servers might not support this |
| * command in which case this method will not throw an exception but just return an empty Map. |
| */ |
| async features() { |
| const res = await this.sendIgnoringError("FEAT"); |
| const features = new Map(); |
| // Not supporting any special features will be reported with a single line. |
| if (res.code < 400 && (0, parseControlResponse_1.isMultiline)(res.message)) { |
| // The first and last line wrap the multiline response, ignore them. |
| res.message.split("\n").slice(1, -1).forEach(line => { |
| // A typical lines looks like: " REST STREAM" or " MDTM". |
| // Servers might not use an indentation though. |
| const entry = line.trim().split(" "); |
| features.set(entry[0], entry[1] || ""); |
| }); |
| } |
| return features; |
| } |
| /** |
| * Set the working directory. |
| */ |
| async cd(path) { |
| const validPath = await this.protectWhitespace(path); |
| return this.send("CWD " + validPath); |
| } |
| /** |
| * Switch to the parent directory of the working directory. |
| */ |
| async cdup() { |
| return this.send("CDUP"); |
| } |
| /** |
| * Get the last modified time of a file. This is not supported by every FTP server, in which case |
| * calling this method will throw an exception. |
| */ |
| async lastMod(path) { |
| const validPath = await this.protectWhitespace(path); |
| const res = await this.send(`MDTM ${validPath}`); |
| const date = res.message.slice(4); |
| return (0, parseListMLSD_1.parseMLSxDate)(date); |
| } |
| /** |
| * Get the size of a file. |
| */ |
| async size(path) { |
| const validPath = await this.protectWhitespace(path); |
| const command = `SIZE ${validPath}`; |
| const res = await this.send(command); |
| // The size is part of the response message, for example: "213 555555". It's |
| // possible that there is a commmentary appended like "213 5555, some commentary". |
| const size = parseInt(res.message.slice(4), 10); |
| if (Number.isNaN(size)) { |
| throw new Error(`Can't parse response to command '${command}' as a numerical value: ${res.message}`); |
| } |
| return size; |
| } |
| /** |
| * Rename a file. |
| * |
| * Depending on the FTP server this might also be used to move a file from one |
| * directory to another by providing full paths. |
| */ |
| async rename(srcPath, destPath) { |
| const validSrc = await this.protectWhitespace(srcPath); |
| const validDest = await this.protectWhitespace(destPath); |
| await this.send("RNFR " + validSrc); |
| return this.send("RNTO " + validDest); |
| } |
| /** |
| * Remove a file from the current working directory. |
| * |
| * You can ignore FTP error return codes which won't throw an exception if e.g. |
| * the file doesn't exist. |
| */ |
| async remove(path, ignoreErrorCodes = false) { |
| const validPath = await this.protectWhitespace(path); |
| if (ignoreErrorCodes) { |
| return this.sendIgnoringError(`DELE ${validPath}`); |
| } |
| return this.send(`DELE ${validPath}`); |
| } |
| /** |
| * Report transfer progress for any upload or download to a given handler. |
| * |
| * This will also reset the overall transfer counter that can be used for multiple transfers. You can |
| * also call the function without a handler to stop reporting to an earlier one. |
| * |
| * @param handler Handler function to call on transfer progress. |
| */ |
| trackProgress(handler) { |
| this._progressTracker.bytesOverall = 0; |
| this._progressTracker.reportTo(handler); |
| } |
| /** |
| * Upload data from a readable stream or a local file to a remote file. |
| * |
| * @param source Readable stream or path to a local file. |
| * @param toRemotePath Path to a remote file to write to. |
| */ |
| async uploadFrom(source, toRemotePath, options = {}) { |
| return this._uploadWithCommand(source, toRemotePath, "STOR", options); |
| } |
| /** |
| * Upload data from a readable stream or a local file by appending it to an existing file. If the file doesn't |
| * exist the FTP server should create it. |
| * |
| * @param source Readable stream or path to a local file. |
| * @param toRemotePath Path to a remote file to write to. |
| */ |
| async appendFrom(source, toRemotePath, options = {}) { |
| return this._uploadWithCommand(source, toRemotePath, "APPE", options); |
| } |
| /** |
| * @protected |
| */ |
| async _uploadWithCommand(source, remotePath, command, options) { |
| if (typeof source === "string") { |
| return this._uploadLocalFile(source, remotePath, command, options); |
| } |
| return this._uploadFromStream(source, remotePath, command); |
| } |
| /** |
| * @protected |
| */ |
| async _uploadLocalFile(localPath, remotePath, command, options) { |
| const fd = await fsOpen(localPath, "r"); |
| const source = (0, fs_1.createReadStream)("", { |
| fd, |
| start: options.localStart, |
| end: options.localEndInclusive, |
| autoClose: false |
| }); |
| try { |
| return await this._uploadFromStream(source, remotePath, command); |
| } |
| finally { |
| await ignoreError(() => fsClose(fd)); |
| } |
| } |
| /** |
| * @protected |
| */ |
| async _uploadFromStream(source, remotePath, command) { |
| const onError = (err) => this.ftp.closeWithError(err); |
| source.once("error", onError); |
| try { |
| const validPath = await this.protectWhitespace(remotePath); |
| await this.prepareTransfer(this.ftp); |
| // Keep the keyword `await` or the `finally` clause below runs too early |
| // and removes the event listener for the source stream too early. |
| return await (0, transfer_1.uploadFrom)(source, { |
| ftp: this.ftp, |
| tracker: this._progressTracker, |
| command, |
| remotePath: validPath, |
| type: "upload" |
| }); |
| } |
| finally { |
| source.removeListener("error", onError); |
| } |
| } |
| /** |
| * Download a remote file and pipe its data to a writable stream or to a local file. |
| * |
| * You can optionally define at which position of the remote file you'd like to start |
| * downloading. If the destination you provide is a file, the offset will be applied |
| * to it as well. For example: To resume a failed download, you'd request the size of |
| * the local, partially downloaded file and use that as the offset. Assuming the size |
| * is 23, you'd download the rest using `downloadTo("local.txt", "remote.txt", 23)`. |
| * |
| * @param destination Stream or path for a local file to write to. |
| * @param fromRemotePath Path of the remote file to read from. |
| * @param startAt Position within the remote file to start downloading at. If the destination is a file, this offset is also applied to it. |
| */ |
| async downloadTo(destination, fromRemotePath, startAt = 0) { |
| if (typeof destination === "string") { |
| return this._downloadToFile(destination, fromRemotePath, startAt); |
| } |
| return this._downloadToStream(destination, fromRemotePath, startAt); |
| } |
| /** |
| * @protected |
| */ |
| async _downloadToFile(localPath, remotePath, startAt) { |
| const appendingToLocalFile = startAt > 0; |
| const fileSystemFlags = appendingToLocalFile ? "r+" : "w"; |
| const fd = await fsOpen(localPath, fileSystemFlags); |
| const destination = (0, fs_1.createWriteStream)("", { |
| fd, |
| start: startAt, |
| autoClose: false |
| }); |
| try { |
| return await this._downloadToStream(destination, remotePath, startAt); |
| } |
| catch (err) { |
| const localFileStats = await ignoreError(() => fsStat(localPath)); |
| const hasDownloadedData = localFileStats && localFileStats.size > 0; |
| const shouldRemoveLocalFile = !appendingToLocalFile && !hasDownloadedData; |
| if (shouldRemoveLocalFile) { |
| await ignoreError(() => fsUnlink(localPath)); |
| } |
| throw err; |
| } |
| finally { |
| await ignoreError(() => fsClose(fd)); |
| } |
| } |
| /** |
| * @protected |
| */ |
| async _downloadToStream(destination, remotePath, startAt) { |
| const onError = (err) => this.ftp.closeWithError(err); |
| destination.once("error", onError); |
| try { |
| const validPath = await this.protectWhitespace(remotePath); |
| await this.prepareTransfer(this.ftp); |
| // Keep the keyword `await` or the `finally` clause below runs too early |
| // and removes the event listener for the source stream too early. |
| return await (0, transfer_1.downloadTo)(destination, { |
| ftp: this.ftp, |
| tracker: this._progressTracker, |
| command: startAt > 0 ? `REST ${startAt}` : `RETR ${validPath}`, |
| remotePath: validPath, |
| type: "download" |
| }); |
| } |
| finally { |
| destination.removeListener("error", onError); |
| destination.end(); |
| } |
| } |
| /** |
| * List files and directories in the current working directory, or from `path` if specified. |
| * |
| * @param [path] Path to remote file or directory. |
| */ |
| async list(path = "") { |
| const validPath = await this.protectWhitespace(path); |
| let lastError; |
| for (const candidate of this.availableListCommands) { |
| const command = validPath === "" ? candidate : `${candidate} ${validPath}`; |
| await this.prepareTransfer(this.ftp); |
| try { |
| const parsedList = await this._requestListWithCommand(command); |
| // Use successful candidate for all subsequent requests. |
| this.availableListCommands = [candidate]; |
| return parsedList; |
| } |
| catch (err) { |
| const shouldTryNext = err instanceof FtpContext_1.FTPError; |
| if (!shouldTryNext) { |
| throw err; |
| } |
| lastError = err; |
| } |
| } |
| throw lastError; |
| } |
| /** |
| * @protected |
| */ |
| async _requestListWithCommand(command) { |
| const buffer = new StringWriter_1.StringWriter(); |
| await (0, transfer_1.downloadTo)(buffer, { |
| ftp: this.ftp, |
| tracker: this._progressTracker, |
| command, |
| remotePath: "", |
| type: "list" |
| }); |
| const text = buffer.getText(this.ftp.encoding); |
| this.ftp.log(text); |
| return this.parseList(text); |
| } |
| /** |
| * Remove a directory and all of its content. |
| * |
| * @param remoteDirPath The path of the remote directory to delete. |
| * @example client.removeDir("foo") // Remove directory 'foo' using a relative path. |
| * @example client.removeDir("foo/bar") // Remove directory 'bar' using a relative path. |
| * @example client.removeDir("/foo/bar") // Remove directory 'bar' using an absolute path. |
| * @example client.removeDir("/") // Remove everything. |
| */ |
| async removeDir(remoteDirPath) { |
| return this._exitAtCurrentDirectory(async () => { |
| await this.cd(remoteDirPath); |
| // Get the absolute path of the target because remoteDirPath might be a relative path, even `../` is possible. |
| const absoluteDirPath = await this.pwd(); |
| await this.clearWorkingDir(); |
| const dirIsRoot = absoluteDirPath === "/"; |
| if (!dirIsRoot) { |
| await this.cdup(); |
| await this.removeEmptyDir(absoluteDirPath); |
| } |
| }); |
| } |
| /** |
| * Remove all files and directories in the working directory without removing |
| * the working directory itself. |
| */ |
| async clearWorkingDir() { |
| for (const file of await this.list()) { |
| if (file.isDirectory) { |
| await this.cd(file.name); |
| await this.clearWorkingDir(); |
| await this.cdup(); |
| await this.removeEmptyDir(file.name); |
| } |
| else { |
| await this.remove(file.name); |
| } |
| } |
| } |
| /** |
| * Upload the contents of a local directory to the remote working directory. |
| * |
| * This will overwrite existing files with the same names and reuse existing directories. |
| * Unrelated files and directories will remain untouched. You can optionally provide a `remoteDirPath` |
| * to put the contents inside a directory which will be created if necessary including all |
| * intermediate directories. If you did provide a remoteDirPath the working directory will stay |
| * the same as before calling this method. |
| * |
| * @param localDirPath Local path, e.g. "foo/bar" or "../test" |
| * @param [remoteDirPath] Remote path of a directory to upload to. Working directory if undefined. |
| */ |
| async uploadFromDir(localDirPath, remoteDirPath) { |
| return this._exitAtCurrentDirectory(async () => { |
| if (remoteDirPath) { |
| await this.ensureDir(remoteDirPath); |
| } |
| return await this._uploadToWorkingDir(localDirPath); |
| }); |
| } |
| /** |
| * @protected |
| */ |
| async _uploadToWorkingDir(localDirPath) { |
| const files = await fsReadDir(localDirPath); |
| for (const file of files) { |
| const fullPath = (0, path_1.join)(localDirPath, file); |
| const stats = await fsStat(fullPath); |
| if (stats.isFile()) { |
| await this.uploadFrom(fullPath, file); |
| } |
| else if (stats.isDirectory()) { |
| await this._openDir(file); |
| await this._uploadToWorkingDir(fullPath); |
| await this.cdup(); |
| } |
| } |
| } |
| /** |
| * Download all files and directories of the working directory to a local directory. |
| * |
| * @param localDirPath The local directory to download to. |
| * @param remoteDirPath Remote directory to download. Current working directory if not specified. |
| */ |
| async downloadToDir(localDirPath, remoteDirPath) { |
| return this._exitAtCurrentDirectory(async () => { |
| if (remoteDirPath) { |
| await this.cd(remoteDirPath); |
| } |
| return await this._downloadFromWorkingDir(localDirPath); |
| }); |
| } |
| /** |
| * @protected |
| */ |
| async _downloadFromWorkingDir(localDirPath) { |
| await ensureLocalDirectory(localDirPath); |
| for (const file of await this.list()) { |
| const localPath = (0, path_1.join)(localDirPath, file.name); |
| if (file.isDirectory) { |
| await this.cd(file.name); |
| await this._downloadFromWorkingDir(localPath); |
| await this.cdup(); |
| } |
| else if (file.isFile) { |
| await this.downloadTo(localPath, file.name); |
| } |
| } |
| } |
| /** |
| * Make sure a given remote path exists, creating all directories as necessary. |
| * This function also changes the current working directory to the given path. |
| */ |
| async ensureDir(remoteDirPath) { |
| // If the remoteDirPath was absolute go to root directory. |
| if (remoteDirPath.startsWith("/")) { |
| await this.cd("/"); |
| } |
| const names = remoteDirPath.split("/").filter(name => name !== ""); |
| for (const name of names) { |
| await this._openDir(name); |
| } |
| } |
| /** |
| * Try to create a directory and enter it. This will not raise an exception if the directory |
| * couldn't be created if for example it already exists. |
| * @protected |
| */ |
| async _openDir(dirName) { |
| await this.sendIgnoringError("MKD " + dirName); |
| await this.cd(dirName); |
| } |
| /** |
| * Remove an empty directory, will fail if not empty. |
| */ |
| async removeEmptyDir(path) { |
| const validPath = await this.protectWhitespace(path); |
| return this.send(`RMD ${validPath}`); |
| } |
| /** |
| * FTP servers can't handle filenames that have leading whitespace. This method transforms |
| * a given path to fix that issue for most cases. |
| */ |
| async protectWhitespace(path) { |
| if (!path.startsWith(" ")) { |
| return path; |
| } |
| // Handle leading whitespace by prepending the absolute path: |
| // " test.txt" while being in the root directory becomes "/ test.txt". |
| const pwd = await this.pwd(); |
| const absolutePathPrefix = pwd.endsWith("/") ? pwd : pwd + "/"; |
| return absolutePathPrefix + path; |
| } |
| async _exitAtCurrentDirectory(func) { |
| const userDir = await this.pwd(); |
| try { |
| return await func(); |
| } |
| finally { |
| if (!this.closed) { |
| await ignoreError(() => this.cd(userDir)); |
| } |
| } |
| } |
| /** |
| * Try all available transfer strategies and pick the first one that works. Update `client` to |
| * use the working strategy for all successive transfer requests. |
| * |
| * @returns a function that will try the provided strategies. |
| */ |
| _enterFirstCompatibleMode(strategies) { |
| return async (ftp) => { |
| ftp.log("Trying to find optimal transfer strategy..."); |
| let lastError = undefined; |
| for (const strategy of strategies) { |
| try { |
| const res = await strategy(ftp); |
| ftp.log("Optimal transfer strategy found."); |
| this.prepareTransfer = strategy; // eslint-disable-line require-atomic-updates |
| return res; |
| } |
| catch (err) { |
| // Try the next candidate no matter the exact error. It's possible that a server |
| // answered incorrectly to a strategy, for example a PASV answer to an EPSV. |
| lastError = err; |
| } |
| } |
| throw new Error(`None of the available transfer strategies work. Last error response was '${lastError}'.`); |
| }; |
| } |
| /** |
| * DEPRECATED, use `uploadFrom`. |
| * @deprecated |
| */ |
| async upload(source, toRemotePath, options = {}) { |
| this.ftp.log("Warning: upload() has been deprecated, use uploadFrom()."); |
| return this.uploadFrom(source, toRemotePath, options); |
| } |
| /** |
| * DEPRECATED, use `appendFrom`. |
| * @deprecated |
| */ |
| async append(source, toRemotePath, options = {}) { |
| this.ftp.log("Warning: append() has been deprecated, use appendFrom()."); |
| return this.appendFrom(source, toRemotePath, options); |
| } |
| /** |
| * DEPRECATED, use `downloadTo`. |
| * @deprecated |
| */ |
| async download(destination, fromRemotePath, startAt = 0) { |
| this.ftp.log("Warning: download() has been deprecated, use downloadTo()."); |
| return this.downloadTo(destination, fromRemotePath, startAt); |
| } |
| /** |
| * DEPRECATED, use `uploadFromDir`. |
| * @deprecated |
| */ |
| async uploadDir(localDirPath, remoteDirPath) { |
| this.ftp.log("Warning: uploadDir() has been deprecated, use uploadFromDir()."); |
| return this.uploadFromDir(localDirPath, remoteDirPath); |
| } |
| /** |
| * DEPRECATED, use `downloadToDir`. |
| * @deprecated |
| */ |
| async downloadDir(localDirPath) { |
| this.ftp.log("Warning: downloadDir() has been deprecated, use downloadToDir()."); |
| return this.downloadToDir(localDirPath); |
| } |
| } |
| exports.Client = Client; |
| async function ensureLocalDirectory(path) { |
| try { |
| await fsStat(path); |
| } |
| catch (err) { |
| await fsMkDir(path, { recursive: true }); |
| } |
| } |
| async function ignoreError(func) { |
| try { |
| return await func(); |
| } |
| catch (err) { |
| // Ignore |
| return undefined; |
| } |
| } |