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:
commit
bd8b8047d7
17
.editorconfig
Normal file
17
.editorconfig
Normal 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
17
.eslintrc.json
Normal 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
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Set default behavior to automatically normalize line endings.
|
||||
* text=auto eol=lf
|
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal 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
20
.github/workflows/ci.yml
vendored
Normal 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
22
.github/workflows/release.yml
vendored
Normal 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
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
test/out/
|
19
LICENSE
Normal file
19
LICENSE
Normal 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
9
README.md
Normal 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
59
index.d.ts
vendored
Normal 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
71
index.js
Normal 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
56
package.json
Normal 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
48
test/index.js
Normal 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);
|
||||
});
|
||||
*/
|
Loading…
x
Reference in New Issue
Block a user