123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770 |
- "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;
- }
- }
|