basic-ftp
Advanced tools
Comparing version 1.1.1 to 1.2.0
# Changelog | ||
## 1.2.0 | ||
- Add functions to upload, download and remove whole directories. | ||
- Add function to ensure a given remote path, creating all directories as necessary. | ||
## 1.1.1 | ||
@@ -4,0 +9,0 @@ |
157
lib/ftp.js
@@ -7,2 +7,9 @@ "use strict"; | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
const promisify = require("util").promisify; | ||
const fsReadDir = promisify(fs.readdir); | ||
const fsMkDir = promisify(fs.mkdir); | ||
const fsStat = promisify(fs.stat); | ||
/** | ||
@@ -179,2 +186,6 @@ * Minimal requirements for an FTP client. | ||
pwd, | ||
uploadDir, | ||
downloadDir, | ||
removeDir, | ||
ensureDir, | ||
// Useful for custom extensions | ||
@@ -379,3 +390,3 @@ parseIPV4PasvResponse, | ||
} | ||
else if (res.code > 400 || res.error) { | ||
else if (res.code >= 400 || res.error) { | ||
client.dataSocket = undefined; | ||
@@ -409,3 +420,3 @@ task.reject(res); | ||
} | ||
else if (res.code > 400 || res.error) { | ||
else if (res.code >= 400 || res.error) { | ||
client.dataSocket = undefined; | ||
@@ -441,3 +452,3 @@ task.reject(res); | ||
} | ||
else if (res.code > 400 || res.error) { | ||
else if (res.code >= 400 || res.error) { | ||
client.dataSocket = undefined; | ||
@@ -500,1 +511,141 @@ task.reject(res); | ||
/** | ||
* Remove a directory of the current working directory. This will remove all files and directories recursively. | ||
* | ||
* @param {Client} client | ||
* @param {string} remoteDirPath | ||
*/ | ||
async function removeDir(client, remoteDirPath) { | ||
await send(client, "CWD " + remoteDirPath); | ||
await clearDir(client); | ||
// Remove the directory itself if we're not already on root. | ||
const workingDir = await pwd(client); | ||
if (workingDir !== "/") { | ||
await send(client, "CDUP"); | ||
await send(client, "RMD " + remoteDirPath); | ||
} | ||
} | ||
/** | ||
* Clear all contents of the current working directory. This does not remove | ||
* the working directory itself. | ||
* | ||
* @param {Client} client | ||
*/ | ||
async function clearDir(client) { | ||
await enterPassiveMode(client); | ||
for (const file of await list(client)) { | ||
if (file.isDirectory) { | ||
await cd(client, file.name); | ||
await clearDir(client); | ||
await send(client, "CDUP"); | ||
await send(client, "RMD " + file.name); | ||
} | ||
else { | ||
await send(client, "DELE " + file.name); | ||
} | ||
} | ||
} | ||
/** | ||
* Uploads a local directory to the current working directory. | ||
* | ||
* @param {Client} client | ||
* @param {string} localDirPath A local path, e.g. "foo/bar" or "../test" | ||
* @param {string?} remoteDirName (Optional) The name of the remote directory. If undefined, directory contents will be uploaded to the working directory. | ||
*/ | ||
async function uploadDir(client, localDirPath, remoteDirName = undefined) { | ||
// If a remote directory name has been provided, create it and cd into it. | ||
if (remoteDirName !== undefined) { | ||
if (remoteDirName.indexOf("/") !== -1) { | ||
throw new Error(`Path provided '${remoteDirName}' instead of single directory name.`); | ||
} | ||
await send(client, "MKD " + remoteDirName); | ||
await cd(client, remoteDirName); | ||
} | ||
await uploadDirContents(client, localDirPath); | ||
// The working directory should stay the same after this operation. | ||
if (remoteDirName !== undefined) { | ||
await send(client, "CDUP"); | ||
} | ||
} | ||
/** | ||
* Upload all contents of a local directory to the current working directory. | ||
* | ||
* @param {Client} client | ||
* @param {string} localDirPath | ||
*/ | ||
async function uploadDirContents(client, localDirPath) { | ||
const files = await fsReadDir(localDirPath); | ||
for (const file of files) { | ||
const fullPath = path.join(localDirPath, file); | ||
const stats = await fsStat(fullPath); | ||
if (stats.isFile()) { | ||
await enterPassiveMode(client); | ||
await upload(client, fs.createReadStream(fullPath), file); | ||
} | ||
else if (stats.isDirectory()) { | ||
await send(client, "MKD " + file); | ||
await cd(client, file); | ||
await uploadDirContents(client, fullPath); | ||
await send(client, "CDUP"); | ||
} | ||
} | ||
} | ||
/** | ||
* Download all contents of the current working directory to a local directory. | ||
* | ||
* @param {string} localDirPath | ||
*/ | ||
async function downloadDir(client, localDirPath) { | ||
await ensureLocalDirectory(localDirPath); | ||
await enterPassiveMode(client); | ||
for (const file of await list(client)) { | ||
const localPath = path.join(localDirPath, file.name); | ||
if (file.isDirectory) { | ||
await cd(client, file.name); | ||
await downloadDir(client, localPath); | ||
await send(client, "CDUP"); | ||
} | ||
else { | ||
await enterPassiveMode(client); | ||
const writable = fs.createWriteStream(localPath); | ||
await download(client, writable, file.name); | ||
} | ||
} | ||
} | ||
async function ensureLocalDirectory(path) { | ||
try { | ||
await fsStat(path); | ||
} | ||
catch(err) { | ||
await fsMkDir(path); | ||
} | ||
} | ||
/** | ||
* Make sure a given remote path exists, creating all directories as necessary. This | ||
* function also changes the current working directory to the given path. | ||
* | ||
* @param {Client} client | ||
* @param {string} remoteDirPath | ||
*/ | ||
async function ensureDir(client, remoteDirPath) { | ||
const names = remoteDirPath.split("/").filter(name => name !== ""); | ||
// If the remoteDirPath was absolute go to root directory. | ||
if (remoteDirPath.startsWith("/")) { | ||
await cd(client, "/"); | ||
} | ||
for (const name of names) { | ||
// Check first if the directory exists. Just calling MKD might | ||
// result in a permission error for intermediate directories. | ||
const res = await send(client, "CWD " + name, true); | ||
if (res.code >= 400) { | ||
await send(client, "MKD " + name); | ||
await cd(client, name); | ||
} | ||
} | ||
} |
{ | ||
"name": "basic-ftp", | ||
"version": "1.1.1", | ||
"version": "1.2.0", | ||
"description": "FTP/FTPS client library", | ||
@@ -5,0 +5,0 @@ "main": "./lib/ftp", |
@@ -51,6 +51,4 @@ # Basic FTP | ||
If you're thinking that the example could be written with fewer lines, you're right! I bet you already have an idea how this would look like. Go ahead and write some convenience wrappers however you see fit. | ||
The example also sets the client to be `verbose`. This will log out every communication detail, making it easier to spot an issue and address it. It's also great to learn about FTP. | ||
Note the verbosity setting for the client. Enabling it will log out every communication detail, making it easier to spot an issue and address it. It's also great to learn about FTP. | ||
The next example removes all files and directories of the current working directory recursively. It demonstrates how simple it is to write (and read) more complex operations. | ||
@@ -112,3 +110,3 @@ | ||
The following functions could've been written by you using the Basic API above. They're part of the library because they are convenient shortcuts for frequent tasks. | ||
The following functions are written using the Basic API above. They are convenient shortcuts for frequent tasks. | ||
@@ -129,4 +127,20 @@ `login(client, user, password)` | ||
Returns the current working directory. | ||
Returns the path of the current working directory. | ||
`removeDir(client, remoteDirPath)` | ||
Removes a directory at a given path, including all of its files and directories. | ||
`uploadDir(client, localDirPath, remoteDirName = undefined)` | ||
Uploads all files and directories of a local directory to the current working directory. If you specify a `remoteDirName` it will place the uploads inside a directory of the given name. | ||
`downloadDir(client, localDirPath)` | ||
Downloads all files and directories of the current working directory to a given local directory. | ||
`ensureDir(client, remoteDirPath)` | ||
Makes sure that the given `remoteDirPath` exists on the server, creating all directories as necessary. | ||
## Extending the library | ||
@@ -133,0 +147,0 @@ |
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
69897
877
202
1