careful-downloader
Advanced tools
Comparing version 1.4.0 to 2.0.0
export interface Options { | ||
/** | ||
* Absolute URL to a checksums file, usually just a `.txt` containing | ||
* filenames and hashes. | ||
* | ||
* Either this option or `options.checksumHash` is required. | ||
*/ | ||
readonly checksumUrl?: string; | ||
/** | ||
* A single hash for the given downloaded file. | ||
* e.g. `abcd1234abcd1234abcd1234...`. | ||
* | ||
* Either this option or `options.checksumUrl` is required. | ||
*/ | ||
readonly checksumHash?: string; | ||
/** | ||
* Manually set the filename of the download, helpful if the one provided by | ||
@@ -48,7 +64,6 @@ * the server doesn't match the filename listed in the checksum file. | ||
/** | ||
* Tell the file stream to read the download as a binary, UTF-8 text file, | ||
* base64, etc. | ||
* Tell the file stream to read the checksums as a string (hex) or binary. | ||
* | ||
* @default "binary" | ||
*/ | ||
* @default "hex" | ||
*/ | ||
readonly encoding?: BufferEncoding; | ||
@@ -58,4 +73,5 @@ } | ||
/** | ||
* Download a file and validate it with its corresponding checksum file. | ||
* Download a file and validate it against a checksum hash. Returns the path to | ||
* the validated file or folder (if it's safe). | ||
*/ | ||
export default function downloader(downloadUrl: string, checksumUrl: string, options: Options): Promise<string>; | ||
export default function downloader(downloadUrl: string, options: Options): Promise<string>; |
158
index.js
import path from "path"; | ||
import stream from "stream"; | ||
import { promisify } from "util"; | ||
import createDebug from "debug"; | ||
import fs from "fs-extra"; | ||
import tempy from "tempy"; | ||
import got from "got"; | ||
import sumchecker from "sumchecker"; | ||
import decompress from "decompress"; | ||
import urlParse from "url-parse"; | ||
import isPathInCwd from "is-path-in-cwd"; | ||
import isPathInside from "is-path-inside"; | ||
// set DEBUG=careful-downloader in environment to enable detailed logging | ||
const debug = new createDebug("careful-downloader"); | ||
import debug from "./lib/debug.js"; | ||
import download from "./lib/download.js"; | ||
import { checksumViaFile, checksumViaString } from "./lib/checksum.js"; | ||
export default async function downloader(downloadUrl, checksumUrl, options = {}) { | ||
export default async (downloadUrl, options) => { | ||
if (!options) { | ||
throw new Error("Missing the options object. See README for details."); | ||
} | ||
debug(`User-provided config: ${JSON.stringify(options)}`); | ||
// figure out which method we're using to validate (via a checksum file or straight-up hash) | ||
let checksumMethod; | ||
let checksumKey; | ||
if (options.checksumUrl) { | ||
// download and use checksum text file to parse and check | ||
checksumMethod = "file"; | ||
checksumKey = options.checksumUrl; | ||
} else if (options.checksumHash) { | ||
// simply compare hash of file to provided string | ||
checksumMethod = "string"; | ||
checksumKey = options.checksumHash; | ||
} else { | ||
throw new Error("Must provide either checksumUrl or checksumHash."); | ||
} | ||
debug(`Provided a ${checksumMethod} to validate against: '${checksumKey}'`); | ||
// normalize options and set defaults | ||
debug(`User-provided config: ${JSON.stringify(options)}`); | ||
options = { | ||
filename: options.filename || urlParse(downloadUrl).pathname.split("/").pop(), | ||
filename: options.filename || new URL(downloadUrl).pathname.split("/").pop(), | ||
extract: !!options.extract, | ||
@@ -25,3 +40,3 @@ destDir: options.destDir ? path.resolve(process.cwd(), options.destDir) : path.resolve(process.cwd(), "downloads"), | ||
algorithm: options.algorithm || "sha256", | ||
encoding: options.encoding || "binary", | ||
encoding: options.encoding || "hex", | ||
}; | ||
@@ -31,3 +46,3 @@ debug(`Normalized config with defaults: ${JSON.stringify(options)}`); | ||
// throw an error if destDir is outside of the module to prevent path traversal for security reasons | ||
if (!isPathInCwd(options.destDir)) { | ||
if (!isPathInside(options.destDir, process.cwd())) { | ||
throw new Error(`destDir must be located within '${process.cwd()}', it's currently set to '${options.destDir}'.`); | ||
@@ -41,34 +56,64 @@ } | ||
try { | ||
// simultaneously download the desired file and its checksums | ||
await Promise.all([ | ||
downloadFile(downloadUrl, path.join(tempDir, options.filename)), | ||
downloadFile(checksumUrl, path.join(tempDir, "checksums.txt")), | ||
]); | ||
// validate the checksum of the download | ||
if (await checkChecksum(tempDir, options.filename, "checksums.txt", options.algorithm, options.encoding)) { | ||
// optionally clear the target directory of existing files | ||
if (options.cleanDestDir && fs.existsSync(options.destDir)) { | ||
debug(`Deleting contents of '${options.destDir}'`); | ||
await fs.remove(options.destDir); | ||
} | ||
let validated = false; | ||
// ensure the target directory exists | ||
debug(`Ensuring target '${options.destDir}' exists`); | ||
await fs.mkdirp(options.destDir); | ||
if (checksumMethod === "file") { | ||
debug("Using a downloaded checksum file to validate..."); | ||
if (options.extract) { | ||
// decompress download and move resulting files to final destination | ||
debug(`Extracting '${options.filename}' to '${options.destDir}'`); | ||
await decompress(path.join(tempDir, options.filename), options.destDir); | ||
return options.destDir; | ||
} else { | ||
// move verified download to final destination as-is | ||
debug(`Not told to extract; copying '${options.filename}' as-is to '${path.join(options.destDir, options.filename)}'`); | ||
await fs.copy(path.join(tempDir, options.filename), path.join(options.destDir, options.filename)); | ||
return path.join(options.destDir, options.filename); | ||
} | ||
} else { | ||
const checksumFilename = new URL(checksumKey).pathname.split("/").pop(); | ||
// simultaneously download the desired file and its checksums | ||
await Promise.all([ | ||
download(downloadUrl, path.join(tempDir, options.filename)), | ||
download(checksumKey, path.join(tempDir, checksumFilename)), | ||
]); | ||
// finally do the calculations | ||
validated = await checksumViaFile( | ||
path.join(tempDir, options.filename), | ||
path.join(tempDir, checksumFilename), | ||
options.algorithm, | ||
options.encoding, | ||
); | ||
} else if (checksumMethod === "string") { | ||
debug("Using a provided hash to validate..."); | ||
// get the desired file | ||
await download(downloadUrl, path.join(tempDir, options.filename)); | ||
// finally do the calculations | ||
validated = await checksumViaString( | ||
path.join(tempDir, options.filename), | ||
checksumKey, | ||
options.algorithm, | ||
options.encoding, | ||
); | ||
} | ||
// stop here if the checksum wasn't validated by either method | ||
if (!validated) { | ||
throw new Error(`Invalid checksum for '${options.filename}'.`); | ||
} | ||
// optionally clear the target directory of existing files | ||
if (options.cleanDestDir && fs.existsSync(options.destDir)) { | ||
debug(`Deleting contents of '${options.destDir}'`); | ||
await fs.remove(options.destDir); | ||
} | ||
// ensure the target directory exists | ||
debug(`Ensuring target '${options.destDir}' exists`); | ||
await fs.mkdirp(options.destDir); | ||
if (options.extract) { | ||
// decompress download and move resulting files to final destination | ||
debug(`Extracting '${options.filename}' to '${options.destDir}'`); | ||
await decompress(path.join(tempDir, options.filename), options.destDir); | ||
return options.destDir; | ||
} else { | ||
// move verified download to final destination as-is | ||
debug(`Not told to extract; copying '${options.filename}' as-is to '${path.join(options.destDir, options.filename)}'`); | ||
await fs.copy(path.join(tempDir, options.filename), path.join(options.destDir, options.filename)); | ||
return path.join(options.destDir, options.filename); | ||
} | ||
} finally { | ||
@@ -79,31 +124,2 @@ // delete temporary directory | ||
} | ||
} | ||
// Download any file to any destination. Returns a promise. | ||
async function downloadFile(url, dest) { | ||
debug(`Downloading '${url}' to '${dest}'`); | ||
// get remote file and write locally | ||
const pipeline = promisify(stream.pipeline); | ||
const download = await pipeline( | ||
got.stream(url, { followRedirect: true }), // GitHub releases redirect to unpredictable URLs | ||
fs.createWriteStream(dest), | ||
); | ||
return download; | ||
} | ||
// Check da checksum. | ||
async function checkChecksum(baseDir, downloadFile, checksumFile, algorithm, encoding) { | ||
debug(`Validating checksum of '${downloadFile}' (hash: '${algorithm}', encoding: '${encoding}')`); | ||
// instantiate checksum validator | ||
const checker = new sumchecker.ChecksumValidator(algorithm, path.join(baseDir, checksumFile), { | ||
defaultTextEncoding: encoding, | ||
}); | ||
// finally test the file | ||
const valid = await checker.validate(baseDir, downloadFile); | ||
return valid; | ||
} | ||
}; |
{ | ||
"name": "careful-downloader", | ||
"version": "1.4.0", | ||
"description": "🕵️♀️ Downloads a file and its checksums to a temporary directory, validates the hash, and optionally extracts it if safe.", | ||
"version": "2.0.0", | ||
"description": "🕵️♀️ Downloads a file and its checksums, validates the hash, and optionally extracts it if safe.", | ||
"license": "MIT", | ||
@@ -21,3 +21,4 @@ "homepage": "https://github.com/jakejarvis/careful-downloader", | ||
"index.js", | ||
"index.d.ts" | ||
"index.d.ts", | ||
"lib" | ||
], | ||
@@ -32,6 +33,4 @@ "scripts": { | ||
"got": "^11.8.2", | ||
"is-path-in-cwd": "^4.0.0", | ||
"sumchecker": "^3.0.1", | ||
"tempy": "^2.0.0", | ||
"url-parse": "^1.5.3" | ||
"is-path-inside": "^4.0.0", | ||
"tempy": "^2.0.0" | ||
}, | ||
@@ -43,6 +42,5 @@ "devDependencies": { | ||
"@types/fs-extra": "^9.0.13", | ||
"@types/url-parse": "^1.4.4", | ||
"chai": "^4.3.4", | ||
"eslint": "^7.32.0", | ||
"mocha": "^9.1.2" | ||
"eslint": "^8.0.1", | ||
"mocha": "^9.1.3" | ||
}, | ||
@@ -49,0 +47,0 @@ "engines": { |
@@ -7,3 +7,3 @@ # 🕵️♀️ careful-downloader | ||
Downloads a file and its checksums to a temporary directory, validates the hash, and optionally extracts it if safe. A headache-averting wrapper around [`got`](https://github.com/sindresorhus/got), [`sumchecker`](https://github.com/malept/sumchecker), and [`decompress`](https://github.com/kevva/decompress). | ||
Downloads a file and its checksums to a temporary directory, validates the hash, and optionally extracts it if safe. | ||
@@ -25,7 +25,6 @@ ## Install | ||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_extended_0.88.1_Windows-64bit.zip", | ||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt", | ||
{ | ||
destDir: "./vendor", | ||
checksumUrl: "https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt", | ||
destDir: "vendor", // relative to process.cwd() | ||
algorithm: "sha256", | ||
encoding: "binary", | ||
extract: true, | ||
@@ -37,5 +36,22 @@ }, | ||
Instead of a `checksumUrl`, you can also simply provide a hash as a string via `checksumHash`: | ||
```js | ||
import downloader from "careful-downloader"; | ||
await downloader( | ||
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_extended_0.88.1_Windows-64bit.zip", | ||
{ | ||
checksumHash: "aaa20e258cd668cff66400d365d73ddc375e44487692d49a5285b56330f6e6b2", | ||
destDir: "vendor", | ||
algorithm: "sha256", | ||
extract: false, // the default | ||
}, | ||
); | ||
//=> '/Users/jake/src/carefully-downloaded/vendor/hugo_extended_0.88.1_Windows-64bit.zip' | ||
``` | ||
## API | ||
### downloader(downloadUrl, checksumUrl, options?) | ||
### downloader(downloadUrl, options) | ||
@@ -48,4 +64,8 @@ #### downloadUrl | ||
#### checksumUrl | ||
#### options | ||
Type: `object` | ||
##### checksumUrl | ||
Type: `string` | ||
@@ -61,6 +81,12 @@ | ||
#### options | ||
**Either this option or `checksumHash` is required.** | ||
Type: `object` | ||
##### checksumHash | ||
Type: `string` | ||
A single hash for the given downloaded file, e.g. `abcd1234abcd1234abcd1234...`. | ||
**Either this option or `checksumUrl` is required.** | ||
##### filename | ||
@@ -106,8 +132,6 @@ | ||
Type: `string`\ | ||
Default: `"binary"` | ||
Default: `"hex"` | ||
Tell the file stream to read the download as a binary, UTF-8 text file, base64, etc. | ||
## License | ||
MIT |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17325
6
7
8
263
132
3
1
+ Addedis-path-inside@^4.0.0
- Removedis-path-in-cwd@^4.0.0
- Removedsumchecker@^3.0.1
- Removedurl-parse@^1.5.3
- Removedis-path-in-cwd@4.0.0(transitive)
- Removedquerystringify@2.2.0(transitive)
- Removedrequires-port@1.0.0(transitive)
- Removedsumchecker@3.0.1(transitive)
- Removedurl-parse@1.5.10(transitive)