1
mirror of https://github.com/jakejarvis/careful-downloader.git synced 2025-04-26 06:35:22 -04:00

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);
}
};