@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 +93 -0
- package/.gitlab-ci.yml +117 -0
- package/.vscode/settings.json +10 -0
- package/CHANGELOG.md +14 -0
- package/LICENSE.md +21 -0
- package/README.md +46 -0
- package/coverage.config.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/downloader.d.ts +12 -0
- package/dist/lib/downloader.js +69 -0
- package/dist/lib/downloader.js.map +1 -0
- package/file +0 -0
- package/jest.config.js +22 -0
- package/package.json +47 -0
- package/src/index.ts +1 -0
- package/src/lib/__tests__/downloader.test.ts +177 -0
- package/src/lib/downloader.ts +72 -0
- package/tsconfig.json +26 -0
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
|
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
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|