1
mirror of https://github.com/jakejarvis/careful-downloader.git synced 2025-04-25 15:25:26 -04:00

122 lines
4.4 KiB
JavaScript

import path from "path";
import fs from "fs-extra";
import { temporaryDirectory } 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) => {
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
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 = temporaryDirectory();
debug(`Temp dir generated: '${tempDir}'`);
try {
// 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();
// 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 && await fs.access(options.destDir)) {
debug(`Deleting contents of '${options.destDir}'`);
await fs.emptyDir(options.destDir);
}
// ensure the target directory exists
debug(`Ensuring target '${options.destDir}' exists`);
await fs.ensureDir(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);
}
};