@bratel/dgit 0.0.13

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.
@@ -0,0 +1,128 @@
1
+ import { Command } from 'commander';
2
+ import ora, { Ora } from 'ora';
3
+ import ProgressBar from 'progress';
4
+ import dgit from '../dgit';
5
+ import {
6
+ ParseGithubHttpsLink, TextEllipsis, isHttpsLink,
7
+ } from './utils';
8
+ import { CommandInfo } from './type';
9
+
10
+ import { DownloadPrompt, PasswordPrompt } from './prompt';
11
+
12
+ const MAX_TEXT_ELLIPSIS = 30;
13
+
14
+ const DownloadAction = async (
15
+ githubLink: string | undefined,
16
+ cmd: Command & CommandInfo,
17
+ ): Promise<any> => {
18
+ let {
19
+ ref = '',
20
+ dest = '',
21
+ owner = '',
22
+ repoName = '',
23
+ relativePath = '',
24
+ password,
25
+ } = cmd;
26
+
27
+ const {
28
+ exclude = '',
29
+ include = '',
30
+ log = false,
31
+ logPrefix = '[dgit-logger]',
32
+ } = cmd;
33
+
34
+ const {
35
+ parallelLimit = '', username, token,
36
+ } = cmd;
37
+ const { proxy = '' } = cmd;
38
+
39
+ if (githubLink && isHttpsLink(githubLink)) {
40
+ const parseResult = ParseGithubHttpsLink(githubLink);
41
+ ref = parseResult.ref;
42
+ owner = parseResult.owner;
43
+ repoName = parseResult.repoName;
44
+ relativePath = parseResult.relativePath;
45
+ }
46
+
47
+ if (username && !password) {
48
+ const pwdAnswer = await PasswordPrompt();
49
+ password = pwdAnswer.password;
50
+ }
51
+
52
+ const answer = await DownloadPrompt({
53
+ ref,
54
+ dest,
55
+ owner,
56
+ repoName,
57
+ relativePath,
58
+ });
59
+
60
+ ref = answer.ref;
61
+ dest = answer.dest;
62
+ owner = answer.owner;
63
+ repoName = answer.repoName;
64
+ relativePath = answer.relativePath;
65
+
66
+ const excludeList = exclude.split(',').filter(Boolean);
67
+ const includeList = include.split(',').filter(Boolean);
68
+
69
+ const spinner: Ora = ora(' loading remote repo tree...');
70
+ let bar: ProgressBar;
71
+
72
+ try {
73
+ await dgit(
74
+ {
75
+ ref,
76
+ owner,
77
+ repoName,
78
+ relativePath,
79
+ username,
80
+ password,
81
+ token,
82
+ proxy,
83
+ },
84
+ dest,
85
+ {
86
+ log,
87
+ logPrefix,
88
+ parallelLimit: Number(parallelLimit.trim()),
89
+ exclude : excludeList,
90
+ include : includeList,
91
+ },
92
+ {
93
+ beforeLoadTree () {
94
+ spinner.start();
95
+ },
96
+ afterLoadTree () {
97
+ spinner.succeed(' load remote repo tree succeed! ');
98
+ },
99
+ onResolved (status) {
100
+ if (log) return;
101
+ const green = '\u001b[42m \u001b[0m';
102
+ const red = '\u001b[41m \u001b[0m';
103
+ const index = 0;
104
+ bar = new ProgressBar(
105
+ ' DOWNLOAD |:bar| :current/:total :percent elapsed: :elapseds eta: :eta :file, done.',
106
+ {
107
+ total : status.totalCount,
108
+ width : 50,
109
+ complete : green,
110
+ incomplete: red,
111
+ },
112
+ );
113
+ bar.update(index);
114
+ },
115
+ onProgress (_, node) {
116
+ if (log) return;
117
+ bar.tick({ file: TextEllipsis(node.path, MAX_TEXT_ELLIPSIS) });
118
+ },
119
+ },
120
+ );
121
+ spinner.succeed(' download all files succeed!');
122
+ } catch (error) {
123
+ console.error(error);
124
+ spinner.fail(' download files failed!');
125
+ }
126
+ };
127
+
128
+ export default DownloadAction;
@@ -0,0 +1,70 @@
1
+ import program from 'commander';
2
+ import chalk from 'chalk';
3
+
4
+ import { PackageInfo } from './type';
5
+ import { GetPackageInfo } from './utils';
6
+ import DownloadAction from './action';
7
+
8
+ const EXIT_CODE = 1;
9
+
10
+ const Exit = (): void => {
11
+ process.exit(EXIT_CODE);
12
+ };
13
+
14
+ const UnknownCommand = (cmdName: string): void => {
15
+ console.log(`${ chalk.red('Unknown command') } ${ chalk.yellow(cmdName) }.`);
16
+ };
17
+
18
+ const packageInfo: PackageInfo = GetPackageInfo();
19
+
20
+ program.version(packageInfo.version);
21
+
22
+ program
23
+ .command('download [githubLink]')
24
+ .option('--owner <ownerName>', 'git repo author.')
25
+ .option('--repo-name <repoName>', 'git repo name.')
26
+ .option('--ref <refName>', 'git repo branch, commit hash or tagname.')
27
+ .option(
28
+ '--relative-path <relativePath>',
29
+ 'specified repo relative path to download.',
30
+ )
31
+ .option('-d, --dest <destPath>', 'specified dest path.')
32
+ .option(
33
+ '-l, --parallel-limit, <number>',
34
+ 'specified download max parallel limit.',
35
+ )
36
+ .option('-u, --username, <username>', 'specified git account username.')
37
+ .option('-p --password, <password>', 'specified git account password.')
38
+ .option(
39
+ '-t --token, <token>',
40
+ 'specified git account personal access token.',
41
+ )
42
+ .option(
43
+ '-e --exclude, <relativePath,...,relativePath>',
44
+ 'indicates which file paths need to be excluded in the current directory.',
45
+ )
46
+ .option(
47
+ '-i --include, <relativePath,...,relativePath>',
48
+ 'indicates which files need to be included in the exclusion file list.',
49
+ )
50
+ .option('--log', 'output dgit internal log details.')
51
+ .option('--log-prefix, <log>', 'dgit internal log prefix.')
52
+ .option('--proxy, <proxyHttp>', 'dgit proxy download url.')
53
+ .alias('d')
54
+ .description('download the file with the specified path of the remote repo.')
55
+ .action(DownloadAction);
56
+
57
+ program.on('command:*', (cmdObj = []) => {
58
+ const [ cmd ] = cmdObj;
59
+ if (cmd) {
60
+ program.outputHelp();
61
+ UnknownCommand(cmd);
62
+ Exit();
63
+ }
64
+ });
65
+
66
+ if (process.argv.slice(2).length <= 0) {
67
+ program.help();
68
+ }
69
+
70
+ program.parse(process.argv);
@@ -0,0 +1,98 @@
1
+ import inquirer, { Question } from 'inquirer';
2
+ import { DownloadPromptInfo, PasswordPromptInfo } from './type';
3
+
4
+ export const CreatePrompt = (questions: Array<Question>): Promise<any> => inquirer.prompt(questions);
5
+
6
+ export const DownloadPrompt = async (
7
+ currentInfo: DownloadPromptInfo,
8
+ ): Promise<DownloadPromptInfo> => {
9
+ if (
10
+ currentInfo.owner &&
11
+ currentInfo.repoName &&
12
+ currentInfo.ref &&
13
+ currentInfo.relativePath &&
14
+ currentInfo.dest
15
+ ) return currentInfo;
16
+
17
+ const questions = [
18
+ {
19
+ type: 'input',
20
+ name: 'owner',
21
+ when () {
22
+ return !currentInfo.owner;
23
+ },
24
+ validate (input: string) {
25
+ return input && input.length > 0;
26
+ },
27
+ message: 'input github ownername.',
28
+ },
29
+ {
30
+ type: 'input',
31
+ name: 'repoName',
32
+ when () {
33
+ return !currentInfo.repoName;
34
+ },
35
+ validate (input: string) {
36
+ return input && input.length > 0;
37
+ },
38
+ message: 'input github repoName.',
39
+ },
40
+ {
41
+ type: 'input',
42
+ name: 'ref',
43
+ when () {
44
+ return !currentInfo.ref;
45
+ },
46
+ validate (input: string) {
47
+ return input && input.length > 0;
48
+ },
49
+ 'default': 'master',
50
+ message : 'input github branch or commit hash or tagname.',
51
+ },
52
+ {
53
+ type: 'input',
54
+ name: 'relativePath',
55
+ when () {
56
+ return !currentInfo.relativePath;
57
+ },
58
+ validate (input: string) {
59
+ return input && input.length > 0;
60
+ },
61
+ 'default': '.',
62
+ message : 'input github relative path.',
63
+ },
64
+ {
65
+ type: 'input',
66
+ name: 'dest',
67
+ when () {
68
+ return !currentInfo.dest;
69
+ },
70
+ validate (input: string) {
71
+ return input && input.length > 0;
72
+ },
73
+ 'default': '.',
74
+ message : 'input template output dest path.',
75
+ },
76
+ ];
77
+
78
+ const answer = await CreatePrompt(questions);
79
+ return {
80
+ owner : answer.owner || currentInfo.owner,
81
+ dest : answer.dest || currentInfo.dest,
82
+ repoName : answer.repoName || currentInfo.repoName,
83
+ relativePath: answer.relativePath || currentInfo.relativePath,
84
+ ref : answer.ref || currentInfo.ref,
85
+ };
86
+ };
87
+
88
+ export const PasswordPrompt = (): Promise<PasswordPromptInfo> => {
89
+ const question = {
90
+ type: 'password',
91
+ name: 'password',
92
+ validate (input: string) {
93
+ return input && input.length > 0;
94
+ },
95
+ message: 'input github account password.',
96
+ };
97
+ return CreatePrompt([ question ]);
98
+ };
@@ -0,0 +1,41 @@
1
+ export interface PackageInfo {
2
+ version: string;
3
+ name: string;
4
+ }
5
+
6
+ export interface CommandInfo {
7
+ dest?: string;
8
+ owner?: string;
9
+ repoName?: string;
10
+ ref?: string;
11
+ relativePath?: string;
12
+ parallelLimit?: string;
13
+ username?: string;
14
+ password?: string;
15
+ token?: string;
16
+ exclude?: string;
17
+ include?: string;
18
+ log?: boolean;
19
+ logPrefix?: string;
20
+ proxy?: string;
21
+ }
22
+
23
+ export interface DownloadPromptInfo {
24
+ dest: string;
25
+ owner: string;
26
+ repoName: string;
27
+ ref: string;
28
+ relativePath: string;
29
+ }
30
+
31
+ export interface GithubLinkInfo {
32
+ owner: string;
33
+ repoName: string;
34
+ ref: string;
35
+ relativePath: string;
36
+ type: string;
37
+ }
38
+
39
+ export interface PasswordPromptInfo {
40
+ password: string;
41
+ }
@@ -0,0 +1,88 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { PackageInfo, GithubLinkInfo } from './type';
4
+
5
+ export const GetPackageInfo = (): PackageInfo => {
6
+ const buffer = fs.readFileSync(path.resolve(__dirname, '../../package.json'));
7
+ return JSON.parse(buffer.toString());
8
+ };
9
+
10
+ export const GITHUB_ADDRESS = 'https://github.com/';
11
+ export const isHttpsLink = (link: string) => link.trim().startsWith(GITHUB_ADDRESS);
12
+
13
+ export const ParseGithubHttpsLink = (httpsLink: string): GithubLinkInfo => {
14
+ let nextLink = httpsLink.trim().slice(GITHUB_ADDRESS.length);
15
+ let index = nextLink.indexOf('/');
16
+ if (index === -1) throw new Error('invalid github address.');
17
+ const owner = nextLink.slice(0, index);
18
+ nextLink = nextLink.slice(owner.length + 1);
19
+ index = nextLink.indexOf('/');
20
+ let repoName: string;
21
+ if (index === -1) {
22
+ repoName = nextLink.slice(0);
23
+ if (!repoName) throw new Error('invalid github address.');
24
+ return {
25
+ owner,
26
+ repoName,
27
+ ref : 'master',
28
+ relativePath: '',
29
+ type : 'tree',
30
+ };
31
+ }
32
+ repoName = nextLink.slice(0, index);
33
+ nextLink = nextLink.slice(repoName.length + 1);
34
+ index = nextLink.indexOf('/');
35
+ let ref = 'master';
36
+ let relativePath = '';
37
+ let type = 'tree';
38
+ if (index === -1) {
39
+ if (repoName.endsWith('.git')) {
40
+ const lastIndex = -4;
41
+ repoName = repoName.slice(0, lastIndex);
42
+ }
43
+ } else {
44
+ type = nextLink.slice(0, index);
45
+ nextLink = nextLink.slice(type.length + 1);
46
+ index = nextLink.indexOf('/');
47
+ if (index === -1) {
48
+ ref = nextLink.slice(0) || 'master';
49
+ } else {
50
+ ref = nextLink.slice(0, index);
51
+ relativePath = nextLink.slice(ref.length + 1);
52
+ }
53
+ }
54
+
55
+ return {
56
+ owner,
57
+ repoName,
58
+ ref,
59
+ relativePath,
60
+ type,
61
+ };
62
+ };
63
+
64
+ export const TextEllipsis = (text: string, maxLen: number): string => (text.length >= maxLen ? `${ text.slice(0, maxLen) }...` : text);
65
+
66
+ export const MakeDirs = (dirs: string): void => {
67
+ const mkdirs = (dir: string, callback?: ()=> void) => {
68
+ if (fs.existsSync(dir)) {
69
+ callback && callback();
70
+ return;
71
+ }
72
+
73
+ mkdirs(path.dirname(dir), () => {
74
+ fs.mkdirSync(dir);
75
+ callback && callback();
76
+ });
77
+ };
78
+
79
+ if (fs.existsSync(dirs)) return;
80
+ mkdirs(dirs);
81
+ };
82
+
83
+ export const AddExtraRandomQs = (origin: string): string => {
84
+ if (origin.indexOf('?') !== -1) {
85
+ return `${ origin }&_t=${ Math.random() }`;
86
+ }
87
+ return `${ origin }?_t=${ Math.random() }`;
88
+ };
package/src/dgit.ts ADDED
@@ -0,0 +1,256 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import async from 'async';
4
+ import repo from './repo';
5
+ import { createLogger } from './log';
6
+ import { requestGetPromise, requestOnStream } from './request';
7
+ import {
8
+ DgitGlobalOption,
9
+ RepoOptionType,
10
+ RepoTreeNode,
11
+ DgitLifeCycle,
12
+ DgitLoadGitTree,
13
+ } from './type';
14
+ import {
15
+ ParseGithubHttpsLink, isHttpsLink, MakeDirs,
16
+ } from './cmd/utils';
17
+
18
+ const UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36';
19
+ const DEFAULT_PARALLEL_LIMIT = 10;
20
+ const MAX_PARALLEL_LIMIT = 100;
21
+ const JSON_STRINGIFY_PADDING = 2;
22
+
23
+ const dgit = async (
24
+ repoOption: RepoOptionType,
25
+ dPath: string,
26
+ dgitOptions?: DgitGlobalOption,
27
+ hooks?: DgitLifeCycle & DgitLoadGitTree,
28
+ ): Promise<void> => {
29
+ const {
30
+ username, password, token, githubLink, proxy = '',
31
+ } = repoOption;
32
+
33
+ let {
34
+ owner, repoName, ref = 'master', relativePath = '.',
35
+ } = repoOption;
36
+
37
+ if (githubLink && isHttpsLink(githubLink)) {
38
+ const parseResult = ParseGithubHttpsLink(githubLink);
39
+ owner = parseResult.owner;
40
+ repoName = parseResult.repoName;
41
+ ref = parseResult.ref;
42
+ relativePath = parseResult.relativePath;
43
+ }
44
+
45
+ if (!owner || !repoName) {
46
+ throw new Error('invalid repo option.');
47
+ }
48
+
49
+ const logger = createLogger(dgitOptions);
50
+
51
+ const { exclude = [], include = []} = dgitOptions || {};
52
+
53
+ let { parallelLimit = DEFAULT_PARALLEL_LIMIT } = dgitOptions || {};
54
+ if (!parallelLimit || parallelLimit <= 0) {
55
+ logger('parallelLimit value is invalid.');
56
+ parallelLimit = DEFAULT_PARALLEL_LIMIT;
57
+ }
58
+
59
+ parallelLimit > MAX_PARALLEL_LIMIT && (parallelLimit = MAX_PARALLEL_LIMIT);
60
+
61
+ const {
62
+ onSuccess,
63
+ onError,
64
+ onProgress,
65
+ onFinish,
66
+ onRetry,
67
+ onResolved,
68
+ beforeLoadTree,
69
+ afterLoadTree,
70
+ } = hooks || {};
71
+
72
+ let onSuccessResolve: (data?: any)=> void = () => {};
73
+ let onErrorReject: (err?: any)=> void = () => {};
74
+
75
+ const prom: Promise<void> = new Promise((resolve, reject) => {
76
+ onSuccessResolve = resolve;
77
+ onErrorReject = reject;
78
+ });
79
+
80
+ const { getRepoTreeUrl, getDownloadUrl } = repo(owner, repoName, ref, proxy);
81
+ const url = getRepoTreeUrl();
82
+
83
+ const headers = {
84
+ 'User-Agent' : UserAgent,
85
+ Authorization: token ? `token ${ token }` : undefined,
86
+ };
87
+
88
+ const auth = username && password ?
89
+ {
90
+ user : username,
91
+ pass : password,
92
+ sendImmediately: true,
93
+ } :
94
+ undefined;
95
+
96
+ const options = {
97
+ url, headers, auth,
98
+ };
99
+
100
+ const destPath = path.isAbsolute(dPath) ? dPath : path.resolve(process.cwd(), dPath);
101
+
102
+ logger(' request repo tree options.');
103
+ logger(JSON.stringify(options, null, JSON_STRINGIFY_PADDING));
104
+
105
+ try {
106
+ logger(' loading remote repo tree...');
107
+ beforeLoadTree && beforeLoadTree();
108
+ const body = await requestGetPromise(options, dgitOptions || {}, {
109
+ onRetry () {
110
+ logger(` request ${ url } failed. Retrying...`);
111
+ onRetry && onRetry();
112
+ },
113
+ });
114
+
115
+ logger(' loading remote repo tree succeed.');
116
+ afterLoadTree && afterLoadTree();
117
+ const result = JSON.parse(body);
118
+
119
+ if (!result.tree || result.tree.length <= 0) {
120
+ throw new Error('404 repo not found!');
121
+ }
122
+
123
+ const treeNodeList: RepoTreeNode[] = result.tree;
124
+ const includeTreeNodeList = treeNodeList.filter(node => {
125
+ const nPath = path.resolve(__dirname, node.path);
126
+ const rPath = path.resolve(__dirname, relativePath);
127
+ if (!nPath.startsWith(rPath) || node.type !== 'blob') {
128
+ return false;
129
+ }
130
+ if (
131
+ exclude.some(v => nPath.startsWith(path.resolve(rPath, v))) &&
132
+ include.every(v => !nPath.startsWith(path.resolve(rPath, v)))
133
+ ) {
134
+ return false;
135
+ }
136
+ return true;
137
+ });
138
+
139
+ if (includeTreeNodeList.length <= 0) {
140
+ throw new Error(`404 repo ${ relativePath } not found!`);
141
+ }
142
+
143
+ const totalStatus = includeTreeNodeList.reduce(
144
+ (prev, cur) => {
145
+ if (cur.type === 'blob') {
146
+ prev.size += cur.size;
147
+ prev.count++;
148
+ }
149
+ return prev;
150
+ },
151
+ { size: 0, count: 0 },
152
+ );
153
+
154
+ let currentSize = 0;
155
+ let currentCount = 0;
156
+
157
+ onResolved &&
158
+ onResolved({
159
+ currentSize,
160
+ currentCount,
161
+ totalSize : totalStatus.size,
162
+ totalCount: totalStatus.count,
163
+ });
164
+
165
+ logger(' include files resolved.');
166
+ logger(
167
+ '',
168
+ JSON.stringify({
169
+ currentSize,
170
+ currentCount,
171
+ totalSize : totalStatus.size,
172
+ totalCount: totalStatus.count,
173
+ }),
174
+ );
175
+
176
+ async.eachLimit(
177
+ includeTreeNodeList,
178
+ parallelLimit,
179
+ (node, callback) => {
180
+ const downloadUrl = getDownloadUrl(node.path);
181
+
182
+ const rPath = path.resolve(destPath, relativePath);
183
+ const tPath = path.resolve(destPath, node.path);
184
+ const root = path.resolve(destPath, '.');
185
+
186
+ let targetPath: string;
187
+ if (rPath === tPath) {
188
+ targetPath = path.resolve(destPath, path.basename(tPath));
189
+ } else {
190
+ targetPath = tPath.replace(rPath, root);
191
+ }
192
+
193
+ logger('', node.path, relativePath, targetPath);
194
+
195
+ if (!fs.existsSync(path.dirname(targetPath))) {
196
+ MakeDirs(path.dirname(targetPath));
197
+ }
198
+
199
+ const ws = fs.createWriteStream(targetPath);
200
+
201
+ logger(` downloading from ${ downloadUrl }...`);
202
+
203
+ requestOnStream(downloadUrl, ws, dgitOptions || {}, {
204
+ onSuccess () {
205
+ currentCount++;
206
+ currentSize += node.size;
207
+
208
+ logger(` write file ${ node.path } succeed.
209
+ size: [${ currentSize }/${ totalStatus.size }],
210
+ count: [${ currentCount }/${ totalStatus.count }]`);
211
+
212
+ onProgress &&
213
+ onProgress(
214
+ {
215
+ totalCount: totalStatus.count,
216
+ totalSize : totalStatus.size,
217
+ currentSize,
218
+ currentCount,
219
+ },
220
+ node,
221
+ );
222
+
223
+ callback();
224
+ },
225
+ onError (err) {
226
+ logger('', err);
227
+ callback(new Error(` request ${ downloadUrl } failed.`));
228
+ },
229
+ onRetry () {
230
+ logger(` request ${ downloadUrl } failed. Retrying...`);
231
+ onRetry && onRetry();
232
+ },
233
+ });
234
+ },
235
+ err => {
236
+ if (err) {
237
+ onError && onError(err);
238
+ onFinish && onFinish();
239
+ onErrorReject(err);
240
+ } else {
241
+ onSuccess && onSuccess();
242
+ onFinish && onFinish();
243
+ onSuccessResolve();
244
+ }
245
+ },
246
+ );
247
+ } catch (error) {
248
+ onError && onError(error);
249
+ onFinish && onFinish();
250
+ onErrorReject(error);
251
+ }
252
+
253
+ return prom;
254
+ };
255
+
256
+ export default dgit;
package/src/log.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { DgitGlobalOption } from './type';
2
+
3
+ const DEFAULT_PREFIX = '[dgit-logger]';
4
+
5
+ export const createLogger = (option?: DgitGlobalOption) => (...message: any[]) => {
6
+ if (option && option.log) {
7
+ const prefix = option ?
8
+ option.logPrefix || DEFAULT_PREFIX :
9
+ DEFAULT_PREFIX;
10
+ console.log(prefix, ...message, '\n');
11
+ }
12
+ };
package/src/repo.ts ADDED
@@ -0,0 +1,6 @@
1
+ const repoUtils = (owner: string, repoName: string, ref: string, proxy: string) => ({
2
+ getRepoTreeUrl: () => `https://api.github.com/repos/${ owner }/${ repoName }/git/trees/${ ref }?recursive=1`,
3
+ getDownloadUrl: (path: string) => `${ proxy ? `${ proxy }/` : '' }https://raw.githubusercontent.com/${ owner }/${ repoName }/${ ref }/${ path }`,
4
+ });
5
+
6
+ export default repoUtils;