Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

careful-downloader

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

careful-downloader - npm Package Compare versions

Comparing version 1.4.0 to 2.0.0

lib/checksum.js

28

index.d.ts
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>;
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
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc