mirror of
https://github.com/jakejarvis/careful-downloader.git
synced 2025-04-26 03:05:23 -04:00
107 lines
4.0 KiB
JavaScript
107 lines
4.0 KiB
JavaScript
import path from "path";
|
|
import fs from "fs-extra";
|
|
import tempy from "tempy";
|
|
import decompress from "decompress";
|
|
import isPathInside from "is-path-inside";
|
|
|
|
import debug from "./lib/debug.js";
|
|
import download from "./lib/download.js";
|
|
import { checksumViaFile, checksumViaString } from "./lib/checksum.js";
|
|
|
|
export default async (downloadUrl, options = {}) => {
|
|
debug(`User-provided config: ${JSON.stringify(options)}`);
|
|
|
|
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 either provide checksumUrl or checksumHash.");
|
|
}
|
|
debug(`Provided a ${checksumMethod} to validate against: ${checksumKey}`);
|
|
|
|
// normalize options and set defaults
|
|
options = {
|
|
filename: options.filename || new URL(downloadUrl).pathname.split("/").pop(),
|
|
extract: !!options.extract,
|
|
destDir: options.destDir ? path.resolve(process.cwd(), options.destDir) : path.resolve(process.cwd(), "downloads"),
|
|
cleanDestDir: !!options.cleanDestDir,
|
|
algorithm: options.algorithm || "sha256",
|
|
encoding: options.encoding || "hex",
|
|
};
|
|
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 (!isPathInside(options.destDir, process.cwd())) {
|
|
throw new Error(`destDir must be located within '${process.cwd()}', it's currently set to '${options.destDir}'.`);
|
|
}
|
|
|
|
// initialize temporary directory
|
|
const tempDir = tempy.directory();
|
|
debug(`Temp dir generated: '${tempDir}'`);
|
|
|
|
try {
|
|
// get the desired file
|
|
await download(downloadUrl, path.join(tempDir, options.filename));
|
|
|
|
// validate the checksum of the download
|
|
let validated = false;
|
|
|
|
if (checksumMethod === "file") {
|
|
debug("Using a downloaded checksum file to validate...");
|
|
|
|
const checksumFilename = new URL(checksumKey).pathname.split("/").pop();
|
|
await download(checksumKey, path.join(tempDir, checksumFilename));
|
|
|
|
// eslint-disable-next-line max-len
|
|
if (await checksumViaFile(path.join(tempDir, options.filename), path.join(tempDir, checksumFilename), options.algorithm, options.encoding)) {
|
|
validated = true;
|
|
}
|
|
} else if (checksumMethod === "string") {
|
|
debug("Using a provided hash to validate...");
|
|
|
|
// eslint-disable-next-line max-len
|
|
if (await checksumViaString(path.join(tempDir, options.filename), checksumKey, options.algorithm, options.encoding)) {
|
|
validated = true;
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
// delete temporary directory
|
|
debug(`Deleting temp dir: '${tempDir}'`);
|
|
await fs.remove(tempDir);
|
|
}
|
|
};
|