1
mirror of https://github.com/jakejarvis/careful-downloader.git synced 2025-04-25 16:35:24 -04:00

initial commit 🎉

This commit is contained in:
Jake Jarvis 2021-10-05 10:42:41 -04:00
commit bd8b8047d7
Signed by: jake
GPG Key ID: 2B0C9CF251E69A39
14 changed files with 2895 additions and 0 deletions

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# http://editorconfig.org
# this file is the top-most editorconfig file
root = true
# all files
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
# site content
[*.md]
trim_trailing_whitespace = false

17
.eslintrc.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": [
"@jakejarvis/eslint-config"
],
"parserOptions": {
"ecmaVersion": 2020,
"sourceType": "module"
},
"env": {
"node": true,
"es6": true,
"browser": false
},
"ignorePatterns": [
"*.d.ts"
]
}

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Set default behavior to automatically normalize line endings.
* text=auto eol=lf

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
versioning-strategy: increase
schedule:
interval: "daily"
commit-message:
prefix: "📦 npm:"

20
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
- run: yarn install --frozen-lockfile
- run: yarn lint
- run: yarn test
- run: yarn build

22
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
npm:
name: Publish to NPM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
yarn install --frozen-lockfile
yarn publish

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
test/out/

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2021 Jake Jarvis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

9
README.md Normal file
View File

@ -0,0 +1,9 @@
# 🕵️‍♀️ careful-download
**🚧 Work in progress!**
A 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.
## License
MIT

59
index.d.ts vendored Normal file
View File

@ -0,0 +1,59 @@
export interface Options {
/**
* Filename of the download, helpful if the one provided by the server doesn't
* match the name listed in the checksum file.
*
* @default Extracted from the download URL.
*/
readonly filename?: string;
/**
* Use decompress to extract the final download to the destination directory.
*
* @default false
*/
readonly extract?: boolean;
/**
* Full path or directory name relative to module to store the validated
* download.
*
* @default "./downloads"
*/
readonly destDir?: string;
/**
* Path to temporary directory for unverified and/or unextracted downloads.
* Automatically generated if not set (recommended).
*
* @default `tempy.directory()`
*/
readonly tempDir?: string;
/**
* The algorithm used by the checksum file. Available options are dependent on
* the version of OpenSSL on the platform. Examples are 'SHA1', 'SHA256',
* 'SHA512', 'MD5', etc.
*
* On recent releases of OpenSSL, `openssl list -digest-algorithms` will
* display the available digest algorithms:
*
* https://nodejs.org/dist/latest-v4.x/docs/api/crypto.html#crypto_crypto_createhash_algorithm
*
* @default "sha256"
*/
readonly algorithm?: string;
/**
* Tell the file stream to read the download as a binary, UTF-8 text file,
* base64, etc.
*
* @default "binary"
*/
readonly encoding?: BufferEncoding;
}
/**
* Download a file and validate it with its corresponding checksum file.
*/
export default function downloadAndCheck(downloadUrl: string, checksumUrl: string, options: Options): Promise<string>;

71
index.js Normal file
View File

@ -0,0 +1,71 @@
import path from "path";
import stream from "stream";
import { promisify } from "util";
import { fileURLToPath } from "url";
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";
// https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#what-do-i-use-instead-of-__dirname-and-__filename
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default async function downloadAndCheck(downloadUrl, checksumUrl, options) {
// normalize options and set defaults
options = {
filename: options.filename ?? urlParse(downloadUrl).pathname.split("/").pop(),
extract: !!options.extract,
destDir: options.destDir ? path.resolve(__dirname, options.destDir) : path.resolve(__dirname, "download"),
tempDir: options.tempDir ? path.resolve(__dirname, options.tempDir) : tempy.directory(),
algorithm: options.algorithm ?? "sha256",
encoding: options.encoding ?? "binary",
};
try {
// simultaneously download the desired file and its checksums
await Promise.all([
downloadFile(downloadUrl, path.join(options.tempDir, options.filename)),
downloadFile(checksumUrl, path.join(options.tempDir, "checksums.txt")),
]);
// validate the checksum of the download
await checkChecksum(options.tempDir, path.join(options.tempDir, "checksums.txt"), options.filename, options.algorithm, options.encoding);
// ensure the target directory exists
await fs.mkdirp(options.destDir);
if (options.extract) {
// decompress download and move resulting files to final destination
await decompress(path.join(options.tempDir, options.filename), options.destDir);
return options.destDir;
} else {
// move verified download to final destination as-is
await fs.copy(path.join(options.tempDir, options.filename), path.join(options.destDir, options.filename));
return path.join(options.destDir, options.filename);
}
} finally {
// delete temporary directory
await fs.remove(options.tempDir);
}
}
// Download any file to any destination. Returns a promise.
async function downloadFile(url, dest) {
const pipeline = promisify(stream.pipeline);
return await pipeline(
got.stream(url, { followRedirect: true }), // GitHub releases redirect to unpredictable URLs
fs.createWriteStream(dest),
);
}
// Check da checksum.
async function checkChecksum(baseDir, checksumFile, downloadFile, algorithm, encoding) {
const checker = new sumchecker.ChecksumValidator(algorithm, checksumFile, {
defaultTextEncoding: encoding,
});
return await checker.validate(baseDir, downloadFile);
}

56
package.json Normal file
View File

@ -0,0 +1,56 @@
{
"name": "careful-download",
"version": "0.0.0",
"description": "🕵️‍♀️ Downloads a file and its checksums to a temporary directory, validates the hash, and optionally extracts it if safe.",
"license": "MIT",
"homepage": "https://github.com/jakejarvis/careful-download",
"author": {
"name": "Jake Jarvis",
"email": "jake@jarv.is",
"url": "https://jarv.is/"
},
"repository": {
"type": "git",
"url": "https://github.com/jakejarvis/careful-download.git"
},
"type": "module",
"module": "./index.js",
"types": "./index.d.ts",
"files": [
"index.js",
"index.d.ts"
],
"scripts": {
"lint": "eslint .",
"test": "mocha"
},
"dependencies": {
"decompress": "^4.2.1",
"fs-extra": "^10.0.0",
"got": "^11.8.2",
"sumchecker": "^3.0.1",
"tempy": "^2.0.0",
"url-parse": "^1.5.3"
},
"devDependencies": {
"@jakejarvis/eslint-config": "*",
"@types/debug": "^4.1.7",
"@types/decompress": "^4.2.4",
"@types/fs-extra": "^9.0.13",
"@types/url-parse": "^1.4.4",
"chai": "^4.3.4",
"eslint": "^7.32.0",
"mocha": "^9.1.2"
},
"keywords": [
"download",
"extract",
"checksum",
"hash",
"file",
"http",
"url",
"security",
"backend"
]
}

48
test/index.js Normal file
View File

@ -0,0 +1,48 @@
/* eslint-env mocha */
import fs from "fs-extra";
import path from "path";
import tempy from "tempy";
import { expect } from "chai";
import downloader from "../index.js";
it("hugo.exe was downloaded and extracted", async () => {
const outDir = path.join(tempy.directory());
await downloader(
"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: outDir,
algorithm: "sha256",
encoding: "binary",
extract: true,
},
);
expect(fs.existsSync(path.join(outDir, "hugo.exe"))).to.be.true;
fs.removeSync(outDir);
});
// TODO: FIX THIS
/*
it("incorrect checksum", async () => {
const outDir = path.join(tempy.directory());
expect(await downloader(
"https://github.com/gohugoio/hugo/releases/download/v0.88.0/hugo_0.88.0_NetBSD-ARM.tar.gz",
"https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_0.88.1_checksums.txt",
{
destDir: outDir,
algorithm: "sha256",
encoding: "binary",
extract: false,
},
)).to.throw(/No checksum/);
// assert(fs.existsSync(path.join(outDir, "hugo.exe")));
// fs.removeSync(outDir);
});
*/

2543
yarn.lock Normal file

File diff suppressed because it is too large Load Diff