Client.js 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.Client = void 0;
  4. const fs_1 = require("fs");
  5. const path_1 = require("path");
  6. const tls_1 = require("tls");
  7. const util_1 = require("util");
  8. const FtpContext_1 = require("./FtpContext");
  9. const parseList_1 = require("./parseList");
  10. const ProgressTracker_1 = require("./ProgressTracker");
  11. const StringWriter_1 = require("./StringWriter");
  12. const parseListMLSD_1 = require("./parseListMLSD");
  13. const netUtils_1 = require("./netUtils");
  14. const transfer_1 = require("./transfer");
  15. const parseControlResponse_1 = require("./parseControlResponse");
  16. // Use promisify to keep the library compatible with Node 8.
  17. const fsReadDir = (0, util_1.promisify)(fs_1.readdir);
  18. const fsMkDir = (0, util_1.promisify)(fs_1.mkdir);
  19. const fsStat = (0, util_1.promisify)(fs_1.stat);
  20. const fsOpen = (0, util_1.promisify)(fs_1.open);
  21. const fsClose = (0, util_1.promisify)(fs_1.close);
  22. const fsUnlink = (0, util_1.promisify)(fs_1.unlink);
  23. const LIST_COMMANDS_DEFAULT = () => ["LIST -a", "LIST"];
  24. const LIST_COMMANDS_MLSD = () => ["MLSD", "LIST -a", "LIST"];
  25. /**
  26. * High-level API to interact with an FTP server.
  27. */
  28. class Client {
  29. /**
  30. * Instantiate an FTP client.
  31. *
  32. * @param timeout Timeout in milliseconds, use 0 for no timeout. Optional, default is 30 seconds.
  33. */
  34. constructor(timeout = 30000) {
  35. this.availableListCommands = LIST_COMMANDS_DEFAULT();
  36. this.ftp = new FtpContext_1.FTPContext(timeout);
  37. this.prepareTransfer = this._enterFirstCompatibleMode([transfer_1.enterPassiveModeIPv6, transfer_1.enterPassiveModeIPv4]);
  38. this.parseList = parseList_1.parseList;
  39. this._progressTracker = new ProgressTracker_1.ProgressTracker();
  40. }
  41. /**
  42. * Close the client and all open socket connections.
  43. *
  44. * Close the client and all open socket connections. The client can’t be used anymore after calling this method,
  45. * you have to either reconnect with `access` or `connect` or instantiate a new instance to continue any work.
  46. * A client is also closed automatically if any timeout or connection error occurs.
  47. */
  48. close() {
  49. this.ftp.close();
  50. this._progressTracker.stop();
  51. }
  52. /**
  53. * Returns true if the client is closed and can't be used anymore.
  54. */
  55. get closed() {
  56. return this.ftp.closed;
  57. }
  58. /**
  59. * Connect (or reconnect) to an FTP server.
  60. *
  61. * This is an instance method and thus can be called multiple times during the lifecycle of a `Client`
  62. * instance. Whenever you do, the client is reset with a new control connection. This also implies that
  63. * you can reopen a `Client` instance that has been closed due to an error when reconnecting with this
  64. * method. In fact, reconnecting is the only way to continue using a closed `Client`.
  65. *
  66. * @param host Host the client should connect to. Optional, default is "localhost".
  67. * @param port Port the client should connect to. Optional, default is 21.
  68. */
  69. connect(host = "localhost", port = 21) {
  70. this.ftp.reset();
  71. this.ftp.socket.connect({
  72. host,
  73. port,
  74. family: this.ftp.ipFamily
  75. }, () => this.ftp.log(`Connected to ${(0, netUtils_1.describeAddress)(this.ftp.socket)} (${(0, netUtils_1.describeTLS)(this.ftp.socket)})`));
  76. return this._handleConnectResponse();
  77. }
  78. /**
  79. * As `connect` but using implicit TLS. Implicit TLS is not an FTP standard and has been replaced by
  80. * explicit TLS. There are still FTP servers that support only implicit TLS, though.
  81. */
  82. connectImplicitTLS(host = "localhost", port = 21, tlsOptions = {}) {
  83. this.ftp.reset();
  84. 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)})`));
  85. this.ftp.tlsOptions = tlsOptions;
  86. return this._handleConnectResponse();
  87. }
  88. /**
  89. * Handles the first reponse by an FTP server after the socket connection has been established.
  90. */
  91. _handleConnectResponse() {
  92. return this.ftp.handle(undefined, (res, task) => {
  93. if (res instanceof Error) {
  94. // The connection has been destroyed by the FTPContext at this point.
  95. task.reject(res);
  96. }
  97. else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) {
  98. task.resolve(res);
  99. }
  100. // Reject all other codes, including 120 "Service ready in nnn minutes".
  101. else {
  102. // Don't stay connected but don't replace the socket yet by using reset()
  103. // so the user can inspect properties of this instance.
  104. task.reject(new FtpContext_1.FTPError(res));
  105. }
  106. });
  107. }
  108. /**
  109. * Send an FTP command and handle the first response.
  110. */
  111. send(command, ignoreErrorCodesDEPRECATED = false) {
  112. if (ignoreErrorCodesDEPRECATED) { // Deprecated starting from 3.9.0
  113. this.ftp.log("Deprecated call using send(command, flag) with boolean flag to ignore errors. Use sendIgnoringError(command).");
  114. return this.sendIgnoringError(command);
  115. }
  116. return this.ftp.request(command);
  117. }
  118. /**
  119. * Send an FTP command and ignore an FTP error response. Any other kind of error or timeout will still reject the Promise.
  120. *
  121. * @param command
  122. */
  123. sendIgnoringError(command) {
  124. return this.ftp.handle(command, (res, task) => {
  125. if (res instanceof FtpContext_1.FTPError) {
  126. task.resolve({ code: res.code, message: res.message });
  127. }
  128. else if (res instanceof Error) {
  129. task.reject(res);
  130. }
  131. else {
  132. task.resolve(res);
  133. }
  134. });
  135. }
  136. /**
  137. * Upgrade the current socket connection to TLS.
  138. *
  139. * @param options TLS options as in `tls.connect(options)`, optional.
  140. * @param command Set the authentication command. Optional, default is "AUTH TLS".
  141. */
  142. async useTLS(options = {}, command = "AUTH TLS") {
  143. const ret = await this.send(command);
  144. this.ftp.socket = await (0, netUtils_1.upgradeSocket)(this.ftp.socket, options);
  145. this.ftp.tlsOptions = options; // Keep the TLS options for later data connections that should use the same options.
  146. this.ftp.log(`Control socket is using: ${(0, netUtils_1.describeTLS)(this.ftp.socket)}`);
  147. return ret;
  148. }
  149. /**
  150. * Login a user with a password.
  151. *
  152. * @param user Username to use for login. Optional, default is "anonymous".
  153. * @param password Password to use for login. Optional, default is "guest".
  154. */
  155. login(user = "anonymous", password = "guest") {
  156. this.ftp.log(`Login security: ${(0, netUtils_1.describeTLS)(this.ftp.socket)}`);
  157. return this.ftp.handle("USER " + user, (res, task) => {
  158. if (res instanceof Error) {
  159. task.reject(res);
  160. }
  161. else if ((0, parseControlResponse_1.positiveCompletion)(res.code)) { // User logged in proceed OR Command superfluous
  162. task.resolve(res);
  163. }
  164. else if (res.code === 331) { // User name okay, need password
  165. this.ftp.send("PASS " + password);
  166. }
  167. else { // Also report error on 332 (Need account)
  168. task.reject(new FtpContext_1.FTPError(res));
  169. }
  170. });
  171. }
  172. /**
  173. * Set the usual default settings.
  174. *
  175. * Settings used:
  176. * * Binary mode (TYPE I)
  177. * * File structure (STRU F)
  178. * * Additional settings for FTPS (PBSZ 0, PROT P)
  179. */
  180. async useDefaultSettings() {
  181. const features = await this.features();
  182. // Use MLSD directory listing if possible. See https://tools.ietf.org/html/rfc3659#section-7.8:
  183. // "The presence of the MLST feature indicates that both MLST and MLSD are supported."
  184. const supportsMLSD = features.has("MLST");
  185. this.availableListCommands = supportsMLSD ? LIST_COMMANDS_MLSD() : LIST_COMMANDS_DEFAULT();
  186. await this.send("TYPE I"); // Binary mode
  187. await this.sendIgnoringError("STRU F"); // Use file structure
  188. await this.sendIgnoringError("OPTS UTF8 ON"); // Some servers expect UTF-8 to be enabled explicitly and setting before login might not have worked.
  189. if (supportsMLSD) {
  190. 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
  191. }
  192. if (this.ftp.hasTLS) {
  193. await this.sendIgnoringError("PBSZ 0"); // Set to 0 for TLS
  194. await this.sendIgnoringError("PROT P"); // Protect channel (also for data connections)
  195. }
  196. }
  197. /**
  198. * Convenience method that calls `connect`, `useTLS`, `login` and `useDefaultSettings`.
  199. *
  200. * This is an instance method and thus can be called multiple times during the lifecycle of a `Client`
  201. * instance. Whenever you do, the client is reset with a new control connection. This also implies that
  202. * you can reopen a `Client` instance that has been closed due to an error when reconnecting with this
  203. * method. In fact, reconnecting is the only way to continue using a closed `Client`.
  204. */
  205. async access(options = {}) {
  206. var _a, _b;
  207. const useExplicitTLS = options.secure === true;
  208. const useImplicitTLS = options.secure === "implicit";
  209. let welcome;
  210. if (useImplicitTLS) {
  211. welcome = await this.connectImplicitTLS(options.host, options.port, options.secureOptions);
  212. }
  213. else {
  214. welcome = await this.connect(options.host, options.port);
  215. }
  216. if (useExplicitTLS) {
  217. // Fixes https://github.com/patrickjuchli/basic-ftp/issues/166 by making sure
  218. // host is set for any future data connection as well.
  219. const secureOptions = (_a = options.secureOptions) !== null && _a !== void 0 ? _a : {};
  220. secureOptions.host = (_b = secureOptions.host) !== null && _b !== void 0 ? _b : options.host;
  221. await this.useTLS(secureOptions);
  222. }
  223. // Set UTF-8 on before login in case there are non-ascii characters in user or password.
  224. // Note that this might not work before login depending on server.
  225. await this.sendIgnoringError("OPTS UTF8 ON");
  226. await this.login(options.user, options.password);
  227. await this.useDefaultSettings();
  228. return welcome;
  229. }
  230. /**
  231. * Get the current working directory.
  232. */
  233. async pwd() {
  234. const res = await this.send("PWD");
  235. // The directory is part of the return message, for example:
  236. // 257 "/this/that" is current directory.
  237. const parsed = res.message.match(/"(.+)"/);
  238. if (parsed === null || parsed[1] === undefined) {
  239. throw new Error(`Can't parse response to command 'PWD': ${res.message}`);
  240. }
  241. return parsed[1];
  242. }
  243. /**
  244. * Get a description of supported features.
  245. *
  246. * This sends the FEAT command and parses the result into a Map where keys correspond to available commands
  247. * and values hold further information. Be aware that your FTP servers might not support this
  248. * command in which case this method will not throw an exception but just return an empty Map.
  249. */
  250. async features() {
  251. const res = await this.sendIgnoringError("FEAT");
  252. const features = new Map();
  253. // Not supporting any special features will be reported with a single line.
  254. if (res.code < 400 && (0, parseControlResponse_1.isMultiline)(res.message)) {
  255. // The first and last line wrap the multiline response, ignore them.
  256. res.message.split("\n").slice(1, -1).forEach(line => {
  257. // A typical lines looks like: " REST STREAM" or " MDTM".
  258. // Servers might not use an indentation though.
  259. const entry = line.trim().split(" ");
  260. features.set(entry[0], entry[1] || "");
  261. });
  262. }
  263. return features;
  264. }
  265. /**
  266. * Set the working directory.
  267. */
  268. async cd(path) {
  269. const validPath = await this.protectWhitespace(path);
  270. return this.send("CWD " + validPath);
  271. }
  272. /**
  273. * Switch to the parent directory of the working directory.
  274. */
  275. async cdup() {
  276. return this.send("CDUP");
  277. }
  278. /**
  279. * Get the last modified time of a file. This is not supported by every FTP server, in which case
  280. * calling this method will throw an exception.
  281. */
  282. async lastMod(path) {
  283. const validPath = await this.protectWhitespace(path);
  284. const res = await this.send(`MDTM ${validPath}`);
  285. const date = res.message.slice(4);
  286. return (0, parseListMLSD_1.parseMLSxDate)(date);
  287. }
  288. /**
  289. * Get the size of a file.
  290. */
  291. async size(path) {
  292. const validPath = await this.protectWhitespace(path);
  293. const command = `SIZE ${validPath}`;
  294. const res = await this.send(command);
  295. // The size is part of the response message, for example: "213 555555". It's
  296. // possible that there is a commmentary appended like "213 5555, some commentary".
  297. const size = parseInt(res.message.slice(4), 10);
  298. if (Number.isNaN(size)) {
  299. throw new Error(`Can't parse response to command '${command}' as a numerical value: ${res.message}`);
  300. }
  301. return size;
  302. }
  303. /**
  304. * Rename a file.
  305. *
  306. * Depending on the FTP server this might also be used to move a file from one
  307. * directory to another by providing full paths.
  308. */
  309. async rename(srcPath, destPath) {
  310. const validSrc = await this.protectWhitespace(srcPath);
  311. const validDest = await this.protectWhitespace(destPath);
  312. await this.send("RNFR " + validSrc);
  313. return this.send("RNTO " + validDest);
  314. }
  315. /**
  316. * Remove a file from the current working directory.
  317. *
  318. * You can ignore FTP error return codes which won't throw an exception if e.g.
  319. * the file doesn't exist.
  320. */
  321. async remove(path, ignoreErrorCodes = false) {
  322. const validPath = await this.protectWhitespace(path);
  323. if (ignoreErrorCodes) {
  324. return this.sendIgnoringError(`DELE ${validPath}`);
  325. }
  326. return this.send(`DELE ${validPath}`);
  327. }
  328. /**
  329. * Report transfer progress for any upload or download to a given handler.
  330. *
  331. * This will also reset the overall transfer counter that can be used for multiple transfers. You can
  332. * also call the function without a handler to stop reporting to an earlier one.
  333. *
  334. * @param handler Handler function to call on transfer progress.
  335. */
  336. trackProgress(handler) {
  337. this._progressTracker.bytesOverall = 0;
  338. this._progressTracker.reportTo(handler);
  339. }
  340. /**
  341. * Upload data from a readable stream or a local file to a remote file.
  342. *
  343. * @param source Readable stream or path to a local file.
  344. * @param toRemotePath Path to a remote file to write to.
  345. */
  346. async uploadFrom(source, toRemotePath, options = {}) {
  347. return this._uploadWithCommand(source, toRemotePath, "STOR", options);
  348. }
  349. /**
  350. * Upload data from a readable stream or a local file by appending it to an existing file. If the file doesn't
  351. * exist the FTP server should create it.
  352. *
  353. * @param source Readable stream or path to a local file.
  354. * @param toRemotePath Path to a remote file to write to.
  355. */
  356. async appendFrom(source, toRemotePath, options = {}) {
  357. return this._uploadWithCommand(source, toRemotePath, "APPE", options);
  358. }
  359. /**
  360. * @protected
  361. */
  362. async _uploadWithCommand(source, remotePath, command, options) {
  363. if (typeof source === "string") {
  364. return this._uploadLocalFile(source, remotePath, command, options);
  365. }
  366. return this._uploadFromStream(source, remotePath, command);
  367. }
  368. /**
  369. * @protected
  370. */
  371. async _uploadLocalFile(localPath, remotePath, command, options) {
  372. const fd = await fsOpen(localPath, "r");
  373. const source = (0, fs_1.createReadStream)("", {
  374. fd,
  375. start: options.localStart,
  376. end: options.localEndInclusive,
  377. autoClose: false
  378. });
  379. try {
  380. return await this._uploadFromStream(source, remotePath, command);
  381. }
  382. finally {
  383. await ignoreError(() => fsClose(fd));
  384. }
  385. }
  386. /**
  387. * @protected
  388. */
  389. async _uploadFromStream(source, remotePath, command) {
  390. const onError = (err) => this.ftp.closeWithError(err);
  391. source.once("error", onError);
  392. try {
  393. const validPath = await this.protectWhitespace(remotePath);
  394. await this.prepareTransfer(this.ftp);
  395. // Keep the keyword `await` or the `finally` clause below runs too early
  396. // and removes the event listener for the source stream too early.
  397. return await (0, transfer_1.uploadFrom)(source, {
  398. ftp: this.ftp,
  399. tracker: this._progressTracker,
  400. command,
  401. remotePath: validPath,
  402. type: "upload"
  403. });
  404. }
  405. finally {
  406. source.removeListener("error", onError);
  407. }
  408. }
  409. /**
  410. * Download a remote file and pipe its data to a writable stream or to a local file.
  411. *
  412. * You can optionally define at which position of the remote file you'd like to start
  413. * downloading. If the destination you provide is a file, the offset will be applied
  414. * to it as well. For example: To resume a failed download, you'd request the size of
  415. * the local, partially downloaded file and use that as the offset. Assuming the size
  416. * is 23, you'd download the rest using `downloadTo("local.txt", "remote.txt", 23)`.
  417. *
  418. * @param destination Stream or path for a local file to write to.
  419. * @param fromRemotePath Path of the remote file to read from.
  420. * @param startAt Position within the remote file to start downloading at. If the destination is a file, this offset is also applied to it.
  421. */
  422. async downloadTo(destination, fromRemotePath, startAt = 0) {
  423. if (typeof destination === "string") {
  424. return this._downloadToFile(destination, fromRemotePath, startAt);
  425. }
  426. return this._downloadToStream(destination, fromRemotePath, startAt);
  427. }
  428. /**
  429. * @protected
  430. */
  431. async _downloadToFile(localPath, remotePath, startAt) {
  432. const appendingToLocalFile = startAt > 0;
  433. const fileSystemFlags = appendingToLocalFile ? "r+" : "w";
  434. const fd = await fsOpen(localPath, fileSystemFlags);
  435. const destination = (0, fs_1.createWriteStream)("", {
  436. fd,
  437. start: startAt,
  438. autoClose: false
  439. });
  440. try {
  441. return await this._downloadToStream(destination, remotePath, startAt);
  442. }
  443. catch (err) {
  444. const localFileStats = await ignoreError(() => fsStat(localPath));
  445. const hasDownloadedData = localFileStats && localFileStats.size > 0;
  446. const shouldRemoveLocalFile = !appendingToLocalFile && !hasDownloadedData;
  447. if (shouldRemoveLocalFile) {
  448. await ignoreError(() => fsUnlink(localPath));
  449. }
  450. throw err;
  451. }
  452. finally {
  453. await ignoreError(() => fsClose(fd));
  454. }
  455. }
  456. /**
  457. * @protected
  458. */
  459. async _downloadToStream(destination, remotePath, startAt) {
  460. const onError = (err) => this.ftp.closeWithError(err);
  461. destination.once("error", onError);
  462. try {
  463. const validPath = await this.protectWhitespace(remotePath);
  464. await this.prepareTransfer(this.ftp);
  465. // Keep the keyword `await` or the `finally` clause below runs too early
  466. // and removes the event listener for the source stream too early.
  467. return await (0, transfer_1.downloadTo)(destination, {
  468. ftp: this.ftp,
  469. tracker: this._progressTracker,
  470. command: startAt > 0 ? `REST ${startAt}` : `RETR ${validPath}`,
  471. remotePath: validPath,
  472. type: "download"
  473. });
  474. }
  475. finally {
  476. destination.removeListener("error", onError);
  477. destination.end();
  478. }
  479. }
  480. /**
  481. * List files and directories in the current working directory, or from `path` if specified.
  482. *
  483. * @param [path] Path to remote file or directory.
  484. */
  485. async list(path = "") {
  486. const validPath = await this.protectWhitespace(path);
  487. let lastError;
  488. for (const candidate of this.availableListCommands) {
  489. const command = validPath === "" ? candidate : `${candidate} ${validPath}`;
  490. await this.prepareTransfer(this.ftp);
  491. try {
  492. const parsedList = await this._requestListWithCommand(command);
  493. // Use successful candidate for all subsequent requests.
  494. this.availableListCommands = [candidate];
  495. return parsedList;
  496. }
  497. catch (err) {
  498. const shouldTryNext = err instanceof FtpContext_1.FTPError;
  499. if (!shouldTryNext) {
  500. throw err;
  501. }
  502. lastError = err;
  503. }
  504. }
  505. throw lastError;
  506. }
  507. /**
  508. * @protected
  509. */
  510. async _requestListWithCommand(command) {
  511. const buffer = new StringWriter_1.StringWriter();
  512. await (0, transfer_1.downloadTo)(buffer, {
  513. ftp: this.ftp,
  514. tracker: this._progressTracker,
  515. command,
  516. remotePath: "",
  517. type: "list"
  518. });
  519. const text = buffer.getText(this.ftp.encoding);
  520. this.ftp.log(text);
  521. return this.parseList(text);
  522. }
  523. /**
  524. * Remove a directory and all of its content.
  525. *
  526. * @param remoteDirPath The path of the remote directory to delete.
  527. * @example client.removeDir("foo") // Remove directory 'foo' using a relative path.
  528. * @example client.removeDir("foo/bar") // Remove directory 'bar' using a relative path.
  529. * @example client.removeDir("/foo/bar") // Remove directory 'bar' using an absolute path.
  530. * @example client.removeDir("/") // Remove everything.
  531. */
  532. async removeDir(remoteDirPath) {
  533. return this._exitAtCurrentDirectory(async () => {
  534. await this.cd(remoteDirPath);
  535. // Get the absolute path of the target because remoteDirPath might be a relative path, even `../` is possible.
  536. const absoluteDirPath = await this.pwd();
  537. await this.clearWorkingDir();
  538. const dirIsRoot = absoluteDirPath === "/";
  539. if (!dirIsRoot) {
  540. await this.cdup();
  541. await this.removeEmptyDir(absoluteDirPath);
  542. }
  543. });
  544. }
  545. /**
  546. * Remove all files and directories in the working directory without removing
  547. * the working directory itself.
  548. */
  549. async clearWorkingDir() {
  550. for (const file of await this.list()) {
  551. if (file.isDirectory) {
  552. await this.cd(file.name);
  553. await this.clearWorkingDir();
  554. await this.cdup();
  555. await this.removeEmptyDir(file.name);
  556. }
  557. else {
  558. await this.remove(file.name);
  559. }
  560. }
  561. }
  562. /**
  563. * Upload the contents of a local directory to the remote working directory.
  564. *
  565. * This will overwrite existing files with the same names and reuse existing directories.
  566. * Unrelated files and directories will remain untouched. You can optionally provide a `remoteDirPath`
  567. * to put the contents inside a directory which will be created if necessary including all
  568. * intermediate directories. If you did provide a remoteDirPath the working directory will stay
  569. * the same as before calling this method.
  570. *
  571. * @param localDirPath Local path, e.g. "foo/bar" or "../test"
  572. * @param [remoteDirPath] Remote path of a directory to upload to. Working directory if undefined.
  573. */
  574. async uploadFromDir(localDirPath, remoteDirPath) {
  575. return this._exitAtCurrentDirectory(async () => {
  576. if (remoteDirPath) {
  577. await this.ensureDir(remoteDirPath);
  578. }
  579. return await this._uploadToWorkingDir(localDirPath);
  580. });
  581. }
  582. /**
  583. * @protected
  584. */
  585. async _uploadToWorkingDir(localDirPath) {
  586. const files = await fsReadDir(localDirPath);
  587. for (const file of files) {
  588. const fullPath = (0, path_1.join)(localDirPath, file);
  589. const stats = await fsStat(fullPath);
  590. if (stats.isFile()) {
  591. await this.uploadFrom(fullPath, file);
  592. }
  593. else if (stats.isDirectory()) {
  594. await this._openDir(file);
  595. await this._uploadToWorkingDir(fullPath);
  596. await this.cdup();
  597. }
  598. }
  599. }
  600. /**
  601. * Download all files and directories of the working directory to a local directory.
  602. *
  603. * @param localDirPath The local directory to download to.
  604. * @param remoteDirPath Remote directory to download. Current working directory if not specified.
  605. */
  606. async downloadToDir(localDirPath, remoteDirPath) {
  607. return this._exitAtCurrentDirectory(async () => {
  608. if (remoteDirPath) {
  609. await this.cd(remoteDirPath);
  610. }
  611. return await this._downloadFromWorkingDir(localDirPath);
  612. });
  613. }
  614. /**
  615. * @protected
  616. */
  617. async _downloadFromWorkingDir(localDirPath) {
  618. await ensureLocalDirectory(localDirPath);
  619. for (const file of await this.list()) {
  620. const localPath = (0, path_1.join)(localDirPath, file.name);
  621. if (file.isDirectory) {
  622. await this.cd(file.name);
  623. await this._downloadFromWorkingDir(localPath);
  624. await this.cdup();
  625. }
  626. else if (file.isFile) {
  627. await this.downloadTo(localPath, file.name);
  628. }
  629. }
  630. }
  631. /**
  632. * Make sure a given remote path exists, creating all directories as necessary.
  633. * This function also changes the current working directory to the given path.
  634. */
  635. async ensureDir(remoteDirPath) {
  636. // If the remoteDirPath was absolute go to root directory.
  637. if (remoteDirPath.startsWith("/")) {
  638. await this.cd("/");
  639. }
  640. const names = remoteDirPath.split("/").filter(name => name !== "");
  641. for (const name of names) {
  642. await this._openDir(name);
  643. }
  644. }
  645. /**
  646. * Try to create a directory and enter it. This will not raise an exception if the directory
  647. * couldn't be created if for example it already exists.
  648. * @protected
  649. */
  650. async _openDir(dirName) {
  651. await this.sendIgnoringError("MKD " + dirName);
  652. await this.cd(dirName);
  653. }
  654. /**
  655. * Remove an empty directory, will fail if not empty.
  656. */
  657. async removeEmptyDir(path) {
  658. const validPath = await this.protectWhitespace(path);
  659. return this.send(`RMD ${validPath}`);
  660. }
  661. /**
  662. * FTP servers can't handle filenames that have leading whitespace. This method transforms
  663. * a given path to fix that issue for most cases.
  664. */
  665. async protectWhitespace(path) {
  666. if (!path.startsWith(" ")) {
  667. return path;
  668. }
  669. // Handle leading whitespace by prepending the absolute path:
  670. // " test.txt" while being in the root directory becomes "/ test.txt".
  671. const pwd = await this.pwd();
  672. const absolutePathPrefix = pwd.endsWith("/") ? pwd : pwd + "/";
  673. return absolutePathPrefix + path;
  674. }
  675. async _exitAtCurrentDirectory(func) {
  676. const userDir = await this.pwd();
  677. try {
  678. return await func();
  679. }
  680. finally {
  681. if (!this.closed) {
  682. await ignoreError(() => this.cd(userDir));
  683. }
  684. }
  685. }
  686. /**
  687. * Try all available transfer strategies and pick the first one that works. Update `client` to
  688. * use the working strategy for all successive transfer requests.
  689. *
  690. * @returns a function that will try the provided strategies.
  691. */
  692. _enterFirstCompatibleMode(strategies) {
  693. return async (ftp) => {
  694. ftp.log("Trying to find optimal transfer strategy...");
  695. let lastError = undefined;
  696. for (const strategy of strategies) {
  697. try {
  698. const res = await strategy(ftp);
  699. ftp.log("Optimal transfer strategy found.");
  700. this.prepareTransfer = strategy; // eslint-disable-line require-atomic-updates
  701. return res;
  702. }
  703. catch (err) {
  704. // Try the next candidate no matter the exact error. It's possible that a server
  705. // answered incorrectly to a strategy, for example a PASV answer to an EPSV.
  706. lastError = err;
  707. }
  708. }
  709. throw new Error(`None of the available transfer strategies work. Last error response was '${lastError}'.`);
  710. };
  711. }
  712. /**
  713. * DEPRECATED, use `uploadFrom`.
  714. * @deprecated
  715. */
  716. async upload(source, toRemotePath, options = {}) {
  717. this.ftp.log("Warning: upload() has been deprecated, use uploadFrom().");
  718. return this.uploadFrom(source, toRemotePath, options);
  719. }
  720. /**
  721. * DEPRECATED, use `appendFrom`.
  722. * @deprecated
  723. */
  724. async append(source, toRemotePath, options = {}) {
  725. this.ftp.log("Warning: append() has been deprecated, use appendFrom().");
  726. return this.appendFrom(source, toRemotePath, options);
  727. }
  728. /**
  729. * DEPRECATED, use `downloadTo`.
  730. * @deprecated
  731. */
  732. async download(destination, fromRemotePath, startAt = 0) {
  733. this.ftp.log("Warning: download() has been deprecated, use downloadTo().");
  734. return this.downloadTo(destination, fromRemotePath, startAt);
  735. }
  736. /**
  737. * DEPRECATED, use `uploadFromDir`.
  738. * @deprecated
  739. */
  740. async uploadDir(localDirPath, remoteDirPath) {
  741. this.ftp.log("Warning: uploadDir() has been deprecated, use uploadFromDir().");
  742. return this.uploadFromDir(localDirPath, remoteDirPath);
  743. }
  744. /**
  745. * DEPRECATED, use `downloadToDir`.
  746. * @deprecated
  747. */
  748. async downloadDir(localDirPath) {
  749. this.ftp.log("Warning: downloadDir() has been deprecated, use downloadToDir().");
  750. return this.downloadToDir(localDirPath);
  751. }
  752. }
  753. exports.Client = Client;
  754. async function ensureLocalDirectory(path) {
  755. try {
  756. await fsStat(path);
  757. }
  758. catch (err) {
  759. await fsMkDir(path, { recursive: true });
  760. }
  761. }
  762. async function ignoreError(func) {
  763. try {
  764. return await func();
  765. }
  766. catch (err) {
  767. // Ignore
  768. return undefined;
  769. }
  770. }