@anmiles/downloader 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.js ADDED
@@ -0,0 +1,93 @@
1
+ module.exports = {
2
+ root : true,
3
+ extends : [
4
+ 'eslint:recommended',
5
+ 'plugin:jest/recommended',
6
+ ],
7
+ parser : '@typescript-eslint/parser',
8
+ parserOptions : {
9
+ ecmaVersion : 2019,
10
+ sourceType : 'module',
11
+ },
12
+ plugins : [
13
+ '@typescript-eslint',
14
+ 'align-assignments',
15
+ 'import',
16
+ 'jest',
17
+ ],
18
+ env : {
19
+ node : true,
20
+ jest : true,
21
+ },
22
+ ignorePatterns : [
23
+ '**/node_modules/',
24
+ 'coverage/',
25
+ 'dist/',
26
+ ],
27
+ rules : {
28
+ 'no-unused-vars' : [ 'off' ],
29
+ '@typescript-eslint/no-redeclare' : [ 'error' ],
30
+ '@typescript-eslint/no-unused-vars' : [ 'error' ],
31
+ 'align-assignments/align-assignments' : [ 'error' ],
32
+ 'array-bracket-spacing' : [ 'error', 'always' ],
33
+ 'arrow-body-style' : [ 'error' ],
34
+ 'arrow-parens' : [ 'error' ],
35
+ 'arrow-spacing' : [ 'error' ],
36
+ 'block-spacing' : [ 'error' ],
37
+ 'brace-style' : [ 'error' ],
38
+ 'camelcase' : [ 'error' ],
39
+ 'comma-dangle' : [ 'error', 'always-multiline' ],
40
+ 'comma-spacing' : [ 'error' ],
41
+ 'comma-style' : [ 'error' ],
42
+ 'complexity' : [ 'error' ],
43
+ 'computed-property-spacing' : [ 'error', 'never' ],
44
+ 'curly' : [ 'error' ],
45
+ 'dot-location' : [ 'error', 'property' ],
46
+ 'eol-last' : [ 'error' ],
47
+ 'func-call-spacing' : [ 'error' ],
48
+ 'func-style' : [ 'error', 'declaration', { allowArrowFunctions : true } ],
49
+ 'generator-star-spacing' : [ 'error', 'neither' ],
50
+ 'import/order' : [ 'error', { groups : [ 'builtin', 'external', 'unknown', 'internal', 'parent', 'sibling', 'index' ] } ],
51
+ 'indent' : [ 'error', 'tab', { SwitchCase : 1 } ],
52
+ 'jest/no-standalone-expect' : [ 'error' ],
53
+ 'key-spacing' : [ 'error', { beforeColon : true, afterColon : true, align : 'colon' } ],
54
+ 'keyword-spacing' : [ 'error' ],
55
+ 'linebreak-style' : [ 'error', 'unix' ],
56
+ 'max-params' : [ 'error', { max : 5 } ],
57
+ 'new-parens' : [ 'error' ],
58
+ 'no-eval' : [ 'error' ],
59
+ 'no-extra-bind' : [ 'error' ],
60
+ 'no-floating-decimal' : [ 'error' ],
61
+ 'no-implied-eval' : [ 'error' ],
62
+ 'no-loop-func' : [ 'error' ],
63
+ 'no-mixed-spaces-and-tabs' : [ 'error', 'smart-tabs' ],
64
+ 'no-multiple-empty-lines' : [ 'error', { max : 1, maxEOF : 1, maxBOF : 0 } ],
65
+ 'no-redeclare' : [ 'off' ],
66
+ 'no-return-await' : [ 'error' ],
67
+ 'no-trailing-spaces' : [ 'error' ],
68
+ 'no-useless-rename' : [ 'error' ],
69
+ 'no-var' : [ 'error' ],
70
+ 'no-whitespace-before-property' : [ 'error' ],
71
+ 'object-curly-spacing' : [ 'error', 'always' ],
72
+ 'object-property-newline' : [ 'error', { allowMultiplePropertiesPerLine : true } ],
73
+ 'object-shorthand' : [ 'error' ],
74
+ 'operator-linebreak' : [ 'error', 'before' ],
75
+ 'prefer-const' : [ 'error' ],
76
+ 'prefer-numeric-literals' : [ 'error' ],
77
+ 'prefer-spread' : [ 'error' ],
78
+ 'prefer-template' : [ 'error' ],
79
+ 'quote-props' : [ 'error', 'consistent-as-needed' ],
80
+ 'quotes' : [ 'error', 'single', { avoidEscape : true } ],
81
+ 'semi-spacing' : [ 'error' ],
82
+ 'semi' : [ 'error' ],
83
+ 'space-before-blocks' : [ 'error' ],
84
+ 'space-before-function-paren' : [ 'error', { anonymous : 'never', named : 'never', asyncArrow : 'always' } ],
85
+ 'space-in-parens' : [ 'error' ],
86
+ 'space-infix-ops' : [ 'error' ],
87
+ 'space-unary-ops' : [ 'error' ],
88
+ 'spaced-comment' : [ 'error' ],
89
+ 'template-curly-spacing' : [ 'error' ],
90
+ 'yield-star-spacing' : [ 'error' ],
91
+ 'yoda' : [ 'error' ],
92
+ },
93
+ };
package/.gitlab-ci.yml ADDED
@@ -0,0 +1,117 @@
1
+ default:
2
+ image: node:18.14
3
+
4
+ stages:
5
+ - setup
6
+ - build
7
+ - test
8
+ - coverage
9
+ - deploy
10
+
11
+ install:
12
+ stage: setup
13
+ cache:
14
+ key:
15
+ files:
16
+ - package-lock.json
17
+ prefix: ${CI_COMMIT_REF_SLUG}
18
+ paths:
19
+ - node_modules
20
+ policy: pull-push
21
+ script:
22
+ - npm ci
23
+ only:
24
+ - main
25
+ - merge_requests
26
+
27
+ build:
28
+ stage: build
29
+ cache:
30
+ key:
31
+ files:
32
+ - package-lock.json
33
+ prefix: ${CI_COMMIT_REF_SLUG}
34
+ paths:
35
+ - node_modules
36
+ policy: pull
37
+ script:
38
+ - npm run build
39
+ artifacts:
40
+ paths:
41
+ - dist/
42
+ only:
43
+ - main
44
+ - merge_requests
45
+
46
+ lint:
47
+ stage: test
48
+ cache:
49
+ key:
50
+ files:
51
+ - package-lock.json
52
+ prefix: ${CI_COMMIT_REF_SLUG}
53
+ paths:
54
+ - node_modules
55
+ policy: pull
56
+ script:
57
+ - npm run lint
58
+ only:
59
+ - main
60
+ - merge_requests
61
+
62
+ test:
63
+ stage: test
64
+ cache:
65
+ - key:
66
+ files:
67
+ - package-lock.json
68
+ prefix: ${CI_COMMIT_REF_SLUG}
69
+ paths:
70
+ - node_modules
71
+ policy: pull
72
+ - key: ${CI_COMMIT_REF_SLUG}_coverage
73
+ paths:
74
+ - coverage/**/*
75
+ policy: push
76
+ script:
77
+ - npm run test:ci
78
+ only:
79
+ - main
80
+ - merge_requests
81
+
82
+ coverage:
83
+ stage: coverage
84
+ cache:
85
+ - key:
86
+ files:
87
+ - package-lock.json
88
+ prefix: ${CI_COMMIT_REF_SLUG}
89
+ paths:
90
+ - node_modules
91
+ policy: pull
92
+ - key: ${CI_COMMIT_REF_SLUG}_coverage
93
+ paths:
94
+ - coverage/**/*
95
+ policy: pull
96
+ script:
97
+ - npm run test:report:coverage
98
+ only:
99
+ - main
100
+ - merge_requests
101
+
102
+ publish:
103
+ stage: deploy
104
+ cache:
105
+ key:
106
+ files:
107
+ - package-lock.json
108
+ prefix: ${CI_COMMIT_REF_SLUG}
109
+ paths:
110
+ - node_modules
111
+ policy: pull
112
+ script:
113
+ - npm publish
114
+ when: manual
115
+ only:
116
+ - main
117
+ - merge_requests
@@ -0,0 +1,10 @@
1
+ {
2
+ "cSpell.words": [
3
+ "anmiles",
4
+ "cicd",
5
+ "colorette",
6
+ "KHTML",
7
+ "linebreak",
8
+ "nycrc"
9
+ ]
10
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0](../../tags/v1.0.0) - 2023-05-03
9
+ ### Changed
10
+ - Ready to release
11
+
12
+ ## 0.0.1 - 2023-05-03
13
+ ### Added
14
+ - Initial commit
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Anatoliy Oblaukhov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @anmiles/lib/downloader
2
+
3
+ Wrapper for downloading data as string, buffer or complex types
4
+
5
+ ----
6
+
7
+ ## Installation
8
+
9
+ 1. Install dependencies
10
+ `npm install`
11
+ 1. Build
12
+ `npm run build`
13
+ 1. Test everything
14
+ `npm test`
15
+
16
+ ## Usage examples
17
+
18
+ ```js
19
+ import { download } from '@anmiles/downloader';
20
+ const buffer = await download('http://url/page');
21
+ ```
22
+
23
+ ```js
24
+ import { download } from '@anmiles/downloader';
25
+ await download('http://url/file', '/path/to/file');
26
+ ```
27
+
28
+ ```js
29
+ import { downloadString } from '@anmiles/downloader';
30
+ const str = downloadString('http://url/string');
31
+ ```
32
+
33
+ ```js
34
+ import { downloadString } from '@anmiles/downloader';
35
+ const str = downloadString('http://url/base64-string', 'base64');
36
+ ```
37
+
38
+ ```js
39
+ import { downloadJSON } from '@anmiles/downloader';
40
+ const str = downloadJSON('http://url/json');
41
+ ```
42
+
43
+ ```js
44
+ import { downloadJSON } from '@anmiles/downloader';
45
+ const str = downloadJSON('http://url/base64-encoded-json', 'base64');
46
+ ```
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ 'check-coverage' : true,
3
+ 'statements' : 100,
4
+ 'branches' : 100,
5
+ 'lines' : 100,
6
+ 'functions' : 100,
7
+ 'reporter' : [ 'html', 'text' ],
8
+ };
@@ -0,0 +1 @@
1
+ export { download, downloadString, downloadJSON } from './lib/downloader';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.downloadJSON = exports.downloadString = exports.download = void 0;
4
+ var downloader_1 = require("./lib/downloader");
5
+ Object.defineProperty(exports, "download", { enumerable: true, get: function () { return downloader_1.download; } });
6
+ Object.defineProperty(exports, "downloadString", { enumerable: true, get: function () { return downloader_1.downloadString; } });
7
+ Object.defineProperty(exports, "downloadJSON", { enumerable: true, get: function () { return downloader_1.downloadJSON; } });
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,+CAA0E;AAAjE,sGAAA,QAAQ,OAAA;AAAE,4GAAA,cAAc,OAAA;AAAE,0GAAA,YAAY,OAAA"}
@@ -0,0 +1,12 @@
1
+ /// <reference types="node" />
2
+ export { download, downloadString, downloadJSON };
3
+ declare const _default: {
4
+ download: typeof download;
5
+ downloadString: typeof downloadString;
6
+ downloadJSON: typeof downloadJSON;
7
+ };
8
+ export default _default;
9
+ declare function download(url: string): Promise<Buffer>;
10
+ declare function download(url: string, file: string): Promise<void>;
11
+ declare function downloadString(url: string, encoding?: Parameters<Buffer['toString']>[0]): Promise<string>;
12
+ declare function downloadJSON(url: string, encoding?: Parameters<Buffer['toString']>[0]): Promise<any>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.downloadJSON = exports.downloadString = exports.download = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const https_1 = __importDefault(require("https"));
10
+ const iconv_lite_1 = __importDefault(require("iconv-lite"));
11
+ const downloader_1 = __importDefault(require("./downloader"));
12
+ exports.default = { download, downloadString, downloadJSON };
13
+ function download(url, file) {
14
+ return new Promise((resolve, reject) => {
15
+ let protocol;
16
+ if (url.startsWith('https://')) {
17
+ protocol = https_1.default;
18
+ }
19
+ else if (url.startsWith('http://')) {
20
+ protocol = http_1.default;
21
+ }
22
+ else {
23
+ throw `Unknown protocol in url ${url}, expected one of "http" or "https"`;
24
+ }
25
+ const options = {
26
+ headers: {
27
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
28
+ },
29
+ };
30
+ protocol.get(url, options, function (res) {
31
+ if (res.statusCode !== 200) {
32
+ reject(`Request to ${url} returned with status code: ${res.statusCode}`);
33
+ res.resume();
34
+ }
35
+ const chunks = [];
36
+ if (typeof file === 'undefined') {
37
+ res.on('data', function (chunk) {
38
+ chunks.push(chunk);
39
+ });
40
+ res.on('end', function () {
41
+ resolve(Buffer.concat(chunks));
42
+ });
43
+ }
44
+ else {
45
+ res.pipe(fs_1.default.createWriteStream(file));
46
+ res.on('end', function () {
47
+ resolve();
48
+ });
49
+ }
50
+ }).on('error', (e) => {
51
+ reject(`Request to ${url} failed with error: ${e.message}`);
52
+ });
53
+ });
54
+ }
55
+ exports.download = download;
56
+ async function downloadString(url, encoding = 'utf8') {
57
+ if (!Buffer.isEncoding(encoding)) {
58
+ throw `Unknown encoding ${encoding}`;
59
+ }
60
+ const buffer = await downloader_1.default.download(url);
61
+ return iconv_lite_1.default.decode(buffer, encoding);
62
+ }
63
+ exports.downloadString = downloadString;
64
+ async function downloadJSON(url, encoding = 'utf8') {
65
+ const json = await downloader_1.default.downloadString(url, encoding);
66
+ return JSON.parse(json);
67
+ }
68
+ exports.downloadJSON = downloadJSON;
69
+ //# sourceMappingURL=downloader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"downloader.js","sourceRoot":"","sources":["../../src/lib/downloader.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,gDAAwB;AACxB,kDAA0B;AAC1B,4DAA+B;AAE/B,8DAAsC;AAGtC,kBAAe,EAAE,QAAQ,EAAE,cAAc,EAAE,YAAY,EAAE,CAAC;AAI1D,SAAS,QAAQ,CAAC,GAAW,EAAE,IAAa;IAC3C,OAAO,IAAI,OAAO,CAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrD,IAAI,QAAqC,CAAC;QAE1C,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE;YAC/B,QAAQ,GAAG,eAAK,CAAC;SACjB;aAAM,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE;YACrC,QAAQ,GAAG,cAAI,CAAC;SAChB;aAAM;YACN,MAAM,2BAA2B,GAAG,qCAAqC,CAAC;SAC1E;QAED,MAAM,OAAO,GAAG;YACf,OAAO,EAAG;gBACT,YAAY,EAAG,iHAAiH;aAChI;SACD,CAAC;QAEF,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,UAAS,GAAG;YACtC,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG,EAAE;gBAC3B,MAAM,CAAC,cAAc,GAAG,+BAA+B,GAAG,CAAC,UAAU,EAAE,CAAC,CAAC;gBACzE,GAAG,CAAC,MAAM,EAAE,CAAC;aACb;YAED,MAAM,MAAM,GAAiB,EAAE,CAAC;YAEhC,IAAI,OAAO,IAAI,KAAK,WAAW,EAAE;gBAChC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,UAAS,KAAK;oBAC5B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACpB,CAAC,CAAC,CAAC;gBAEH,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE;oBACb,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;gBAChC,CAAC,CAAC,CAAC;aACH;iBAAM;gBACN,GAAG,CAAC,IAAI,CAAC,YAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;gBAErC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE;oBACb,OAAO,EAAE,CAAC;gBACX,CAAC,CAAC,CAAC;aACH;QACF,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE;YACpB,MAAM,CAAC,cAAc,GAAG,uBAAuB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,CAAC;AACJ,CAAC;AAlDQ,4BAAQ;AAoDjB,KAAK,UAAU,cAAc,CAAC,GAAW,EAAE,WAA8C,MAAM;IAC9F,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE;QACjC,MAAM,oBAAoB,QAAQ,EAAE,CAAC;KACrC;IAED,MAAM,MAAM,GAAG,MAAM,oBAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC9C,OAAO,oBAAK,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AACvC,CAAC;AA3DkB,wCAAc;AA6DjC,KAAK,UAAU,YAAY,CAAC,GAAW,EAAE,WAA8C,MAAM;IAC5F,MAAM,IAAI,GAAG,MAAM,oBAAU,CAAC,cAAc,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC5D,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AAhEkC,oCAAY"}
package/file ADDED
File without changes
package/jest.config.js ADDED
@@ -0,0 +1,22 @@
1
+ module.exports = {
2
+ preset : 'ts-jest',
3
+ clearMocks : true,
4
+
5
+ roots : [ '<rootDir>/src' ],
6
+ testMatch : [ '<rootDir>/src/**/__tests__/*.test.ts' ],
7
+ transform : {
8
+ '^.+\\.ts$' : 'ts-jest',
9
+ },
10
+ collectCoverageFrom : [
11
+ '<rootDir>/src/**/*.ts',
12
+ '!<rootDir>/src/**/*.d.ts',
13
+ '!<rootDir>/src/*.ts',
14
+ '!<rootDir>/src/types/*.ts',
15
+
16
+ '!**/node_modules/**',
17
+ '!**/__tests__/**',
18
+
19
+ '!<rootDir>/coverage/**',
20
+ '!<rootDir>/dist/**',
21
+ ],
22
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@anmiles/downloader",
3
+ "version": "2.0.0",
4
+ "description": "Wrapper for downloading data as string, buffer or complex types",
5
+ "keywords": [
6
+ "download",
7
+ "http",
8
+ "request"
9
+ ],
10
+ "author": "Anatoliy Oblaukhov",
11
+ "homepage": "https://gitlab.com/anmiles/lib/downloader",
12
+ "repository": "gitlab:anmiles/lib/downloader",
13
+ "license": "MIT",
14
+ "engines": {
15
+ "node": ">=18.14.2"
16
+ },
17
+ "main": "dist/index.js",
18
+ "scripts": {
19
+ "build": "rimraf dist && tsc",
20
+ "lint": "eslint --ext .js,.ts .",
21
+ "lint:fix": "npm run lint -- --fix",
22
+ "test": "jest --verbose",
23
+ "test:coverage": "npm test -- --coverage",
24
+ "test:ci": "npm test -- --ci --coverage",
25
+ "test:watch": "npm test -- --watch",
26
+ "test:watch:coverage": "npm test -- --watch --coverage",
27
+ "test:report:coverage": "nyc report --nycrc-path ./coverage.config.js -t ./coverage --report-dir ./coverage"
28
+ },
29
+ "dependencies": {
30
+ "colorette": "^2.0.19",
31
+ "iconv-lite": "^0.6.3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/jest": "^29.5.1",
35
+ "@typescript-eslint/eslint-plugin": "^5.59.2",
36
+ "@typescript-eslint/parser": "^5.59.2",
37
+ "eslint": "^8.39.0",
38
+ "eslint-plugin-align-assignments": "^1.1.2",
39
+ "eslint-plugin-import": "^2.27.5",
40
+ "eslint-plugin-jest": "^27.2.1",
41
+ "jest": "^29.5.0",
42
+ "nyc": "^15.1.0",
43
+ "rimraf": "^5.0.0",
44
+ "ts-jest": "^29.1.0",
45
+ "typescript": "^5.0.4"
46
+ }
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { download, downloadString, downloadJSON } from './lib/downloader';
@@ -0,0 +1,177 @@
1
+ import fs from 'fs';
2
+ import http from 'http';
3
+ import https from 'https';
4
+ import iconv from 'iconv-lite';
5
+
6
+ import downloader from '../downloader';
7
+ const original = jest.requireActual('../downloader').default as typeof downloader;
8
+
9
+ jest.mock<Partial<typeof downloader>>('../downloader', () => ({
10
+ download : jest.fn().mockImplementation(() => downloaded),
11
+ downloadString : jest.fn().mockImplementation((...args: Parameters<typeof original.downloadString>) => original.downloadString(...args)),
12
+ }));
13
+
14
+ const request = {} as http.ClientRequest;
15
+ request.on = jest.fn().mockImplementation((ev: string, listener: () => void) => {
16
+ switch (ev) {
17
+ case 'error':
18
+ requestError = listener;
19
+ break;
20
+ }
21
+ });
22
+
23
+ const response = {} as http.IncomingMessage;
24
+
25
+ let data: (data: Uint8Array) => void = () => {};
26
+ let end: () => void = () => {};
27
+ let requestError: (e: Error) => void = () => {};
28
+
29
+ function get(url: string | URL, options: https.RequestOptions, callback?: ((res: http.IncomingMessage) => void) | undefined): http.ClientRequest {
30
+ if (callback) {
31
+ callback(response);
32
+ }
33
+
34
+ return request;
35
+ }
36
+
37
+ beforeEach(() => {
38
+ response.statusCode = 200;
39
+ response.on = jest.fn().mockImplementation((ev: string, listener: () => void) => {
40
+ switch (ev) {
41
+ case 'data':
42
+ data = listener;
43
+ break;
44
+ case 'end':
45
+ end = listener;
46
+ break;
47
+ }
48
+ });
49
+ response.pipe = jest.fn();
50
+ response.resume = jest.fn();
51
+ });
52
+
53
+ let httpGetSpy: jest.SpyInstance;
54
+ let httpsGetSpy: jest.SpyInstance;
55
+ let downloaded: Buffer;
56
+
57
+ beforeAll(() => {
58
+ httpGetSpy = jest.spyOn(http, 'get');
59
+ httpsGetSpy = jest.spyOn(https, 'get');
60
+ });
61
+
62
+ beforeEach(() => {
63
+ httpGetSpy.mockImplementation(get);
64
+ httpsGetSpy.mockImplementation(get);
65
+ });
66
+
67
+ afterAll(() => {
68
+ httpGetSpy.mockRestore();
69
+ });
70
+
71
+ describe('src/lib/downloader', () => {
72
+ describe('download', () => {
73
+ it('should throw if url protocol is not supported', async () => {
74
+ await expect(() => original.download('ftp://url')).rejects.toEqual('Unknown protocol in url ftp://url, expected one of "http" or "https"');
75
+ });
76
+
77
+ it('should call http.get if url protocol is http', async () => {
78
+ const promise = original.download('http://url');
79
+ end();
80
+ await promise;
81
+ expect(httpGetSpy.mock.calls[0][0]).toEqual('http://url');
82
+ });
83
+
84
+ it('should call https.get if url protocol is https', async () => {
85
+ const promise = original.download('https://url');
86
+ end();
87
+ await promise;
88
+ expect(httpsGetSpy.mock.calls[0][0]).toEqual('https://url');
89
+ });
90
+
91
+ it('should pass user-agent in options', async () => {
92
+ const promise = original.download('http://url');
93
+ end();
94
+ await promise;
95
+ expect(httpGetSpy.mock.calls[0][1]).toEqual(expect.objectContaining({
96
+ headers : {
97
+ 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
98
+ },
99
+ }));
100
+ });
101
+
102
+ it('should reject and resume response if status code is not 200', async () => {
103
+ response.statusCode = 404;
104
+ await expect(() => original.download('http://url')).rejects.toEqual('Request to http://url returned with status code: 404');
105
+ expect(response.resume).toHaveBeenCalled();
106
+ });
107
+
108
+ it('should reject if response errored', async () => {
109
+ const promise = original.download('http://url');
110
+ requestError(new Error('request error'));
111
+ await expect(() => promise).rejects.toEqual('Request to http://url failed with error: request error');
112
+ });
113
+
114
+ it('should concat and resolve received data if no file specified', async () => {
115
+ const promise = original.download('http://url');
116
+ data(new Uint8Array([ 10, 11, 12 ]));
117
+ data(new Uint8Array([ 20, 21, 22 ]));
118
+ data(new Uint8Array([ 30, 31, 32 ]));
119
+ end();
120
+ const result = await promise;
121
+ expect(result).toEqual(Buffer.from([ 10, 11, 12, 20, 21, 22, 30, 31, 32 ]));
122
+ });
123
+
124
+ it('should pipe response stream to file if specified', async () => {
125
+ const stream = {} as fs.WriteStream;
126
+ const createWriteStreamSpy = jest.spyOn(fs, 'createWriteStream').mockReturnValue(stream);
127
+ const promise = original.download('http://url', 'file');
128
+ data(new Uint8Array([ 10, 11, 12 ]));
129
+ data(new Uint8Array([ 20, 21, 22 ]));
130
+ data(new Uint8Array([ 30, 31, 32 ]));
131
+ end();
132
+ const result = await promise;
133
+ expect(createWriteStreamSpy).toHaveBeenCalledWith('file');
134
+ expect(response.pipe).toHaveBeenCalledWith(stream);
135
+ expect(result).toEqual(undefined);
136
+ createWriteStreamSpy.mockRestore();
137
+ });
138
+ });
139
+
140
+ describe('downloadString', () => {
141
+ it('should throw if unknown encoding specified', async () => {
142
+ await expect(() => original.downloadString('http://url', 'wrong_encoding' as any)).rejects.toEqual('Unknown encoding wrong_encoding');
143
+ });
144
+
145
+ it('should return string decoded with utf8', async () => {
146
+ downloaded = iconv.encode('test', 'utf8');
147
+ const result = await original.downloadString('http://url');
148
+ expect(downloader.download).toHaveBeenCalledWith('http://url');
149
+ expect(result).toEqual('test');
150
+ });
151
+
152
+ it('should return string decoded with specified encoding', async () => {
153
+ downloaded = iconv.encode('test', 'base64');
154
+ const result = await original.downloadString('http://url', 'base64');
155
+ expect(downloader.download).toHaveBeenCalledWith('http://url');
156
+ expect(result).toEqual('test');
157
+ });
158
+ });
159
+
160
+ describe('downloadJSON', () => {
161
+ it('should JSON.parse downloaded string decoded with utf8', async () => {
162
+ downloaded = iconv.encode('{"key1": "value", "key2": 5}', 'utf8');
163
+ const result = await original.downloadJSON('http://url');
164
+ expect(downloader.download).toHaveBeenCalledWith('http://url');
165
+ expect(downloader.downloadString).toHaveBeenCalledWith('http://url', 'utf8');
166
+ expect(result).toEqual({ key1 : 'value', key2 : 5 });
167
+ });
168
+
169
+ it('should JSON.parse downloaded string decoded with specified encoding', async () => {
170
+ downloaded = iconv.encode('{"key1": "value", "key2": 5}', 'ucs2');
171
+ const result = await original.downloadJSON('http://url', 'ucs2');
172
+ expect(downloader.download).toHaveBeenCalledWith('http://url');
173
+ expect(downloader.downloadString).toHaveBeenCalledWith('http://url', 'ucs2');
174
+ expect(result).toEqual({ key1 : 'value', key2 : 5 });
175
+ });
176
+ });
177
+ });
@@ -0,0 +1,72 @@
1
+ import fs from 'fs';
2
+ import http from 'http';
3
+ import https from 'https';
4
+ import iconv from 'iconv-lite';
5
+
6
+ import downloader from './downloader';
7
+
8
+ export { download, downloadString, downloadJSON };
9
+ export default { download, downloadString, downloadJSON };
10
+
11
+ function download(url: string): Promise<Buffer>;
12
+ function download(url: string, file: string): Promise<void>;
13
+ function download(url: string, file?: string): Promise<Buffer | void> {
14
+ return new Promise<Buffer | void>((resolve, reject) => {
15
+ let protocol : typeof https | typeof http;
16
+
17
+ if (url.startsWith('https://')) {
18
+ protocol = https;
19
+ } else if (url.startsWith('http://')) {
20
+ protocol = http;
21
+ } else {
22
+ throw `Unknown protocol in url ${url}, expected one of "http" or "https"`;
23
+ }
24
+
25
+ const options = {
26
+ headers : {
27
+ 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36',
28
+ },
29
+ };
30
+
31
+ protocol.get(url, options, function(res) {
32
+ if (res.statusCode !== 200) {
33
+ reject(`Request to ${url} returned with status code: ${res.statusCode}`);
34
+ res.resume();
35
+ }
36
+
37
+ const chunks: Uint8Array[] = [];
38
+
39
+ if (typeof file === 'undefined') {
40
+ res.on('data', function(chunk) {
41
+ chunks.push(chunk);
42
+ });
43
+
44
+ res.on('end', function() {
45
+ resolve(Buffer.concat(chunks));
46
+ });
47
+ } else {
48
+ res.pipe(fs.createWriteStream(file));
49
+
50
+ res.on('end', function() {
51
+ resolve();
52
+ });
53
+ }
54
+ }).on('error', (e) => {
55
+ reject(`Request to ${url} failed with error: ${e.message}`);
56
+ });
57
+ });
58
+ }
59
+
60
+ async function downloadString(url: string, encoding: Parameters<Buffer['toString']>[0] = 'utf8') {
61
+ if (!Buffer.isEncoding(encoding)) {
62
+ throw `Unknown encoding ${encoding}`;
63
+ }
64
+
65
+ const buffer = await downloader.download(url);
66
+ return iconv.decode(buffer, encoding);
67
+ }
68
+
69
+ async function downloadJSON(url: string, encoding: Parameters<Buffer['toString']>[0] = 'utf8') {
70
+ const json = await downloader.downloadString(url, encoding);
71
+ return JSON.parse(json);
72
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "CommonJS",
4
+ "target": "ES2019",
5
+
6
+ "moduleResolution": "node",
7
+ "resolveJsonModule": true,
8
+
9
+ "rootDir": "./src",
10
+ "outDir": "./dist",
11
+
12
+ "allowJs": false,
13
+ "sourceMap": true,
14
+ "declaration": true,
15
+ "noImplicitAny": true,
16
+ "esModuleInterop": true,
17
+ "strictNullChecks": true,
18
+ "allowSyntheticDefaultImports": true,
19
+ },
20
+ "exclude": [
21
+ "**/node_modules/",
22
+ "**/__tests__/",
23
+ "coverage/",
24
+ "dist/",
25
+ ],
26
+ }