Skip to content

Commit 17e47aa

Browse files
committed
initial commit
0 parents  commit 17e47aa

File tree

10 files changed

+1349
-0
lines changed

10 files changed

+1349
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
## coding-generic
2+
用于推送 generic 类型制品到 coding 制品库
3+
4+
## 安装
5+
```
6+
npm install coding-generic -g
7+
```
8+
9+
## 使用
10+
```
11+
coding-generic --username=<USERNAME> --path=<FILE.EXT> --registry=<REGISTRY>
12+
```

bin/index.js

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
#!/usr/bin/env node
2+
3+
const fs = require('fs');
4+
const SparkMD5 = require('spark-md5');
5+
const chalk = require('chalk');
6+
const prompts = require('prompts');
7+
const path = require('path');
8+
const FormData = require('form-data');
9+
require('winston-daily-rotate-file');
10+
const logger = require('../lib/log');
11+
const ProgressBar = require('progress');
12+
const { CHUNK_SIZE } = require('../lib/constants');
13+
const { generateAuthorization, getRegistryInfo } = require('../lib/utils');
14+
const { getExistChunks: _getExistChunks, uploadChunk: _uploadChunk, uploadSuccess: _uploadSuccess } = require('../lib/request');
15+
16+
const argv = require('../lib/argv');
17+
const { requestUrl, version } = getRegistryInfo(argv.registry);
18+
19+
let Authorization = '';
20+
let md5 = '';
21+
let uploadId = '';
22+
let fileSize = 0;
23+
24+
process.on('uncaughtException', error => {
25+
console.log(chalk.red('\n程序发生了一些异常,请稍后重试\n'));
26+
logger.error(error.stack);
27+
})
28+
29+
const upload = async (filePath, parts = []) => {
30+
const totalChunk = Math.ceil(fileSize / CHUNK_SIZE);
31+
32+
const bar = new ProgressBar(':bar [:current/:total] :percent', { total: totalChunk });
33+
const uploadChunk = async (currentChunk, currentChunkIndex, parts, isRetry) => {
34+
if (parts.some(({ partNumber, size }) => partNumber === currentChunkIndex && size === currentChunk.length)) {
35+
bar.tick();
36+
return Promise.resolve();
37+
}
38+
39+
const form = new FormData();
40+
form.append('chunk', currentChunk, {
41+
filename: requestUrl.replace(/^http(s)?:\/\/.+?\/.+?\/.+?\//, '')
42+
});
43+
try {
44+
await _uploadChunk(requestUrl, {
45+
uploadId,
46+
version,
47+
partNumber: currentChunkIndex,
48+
size: currentChunk.length,
49+
form
50+
}, {
51+
headers: form.getHeaders(),
52+
Authorization
53+
});
54+
bar.tick();
55+
} catch (error) {
56+
logger.error(error.message);
57+
logger.error(error.stack);
58+
if (['ECONNREFUSED', 'ECONNRESET', 'ENOENT'].includes(error.code)) {
59+
// 没有重试过就重试一次
60+
if (!isRetry) {
61+
logger.warn('retry')
62+
logger.warn(error.code);
63+
await uploadChunk(currentChunk, currentChunkIndex, parts, true);
64+
} else {
65+
console.log(chalk.red('网络连接异常,请重新执行命令继续上传'));
66+
process.exit(1);
67+
}
68+
} else {
69+
console.log(chalk.red((error.response && error.response.data) || error.message));
70+
process.exit(1);
71+
}
72+
}
73+
}
74+
75+
console.log(`\n开始上传\n`)
76+
logger.info('开始上传')
77+
78+
try {
79+
for (let currentChunkIndex = 1; currentChunkIndex <= totalChunk; currentChunkIndex++) {
80+
const start = (currentChunkIndex - 1) * CHUNK_SIZE;
81+
const end = ((start + CHUNK_SIZE) >= fileSize) ? fileSize : start + CHUNK_SIZE - 1;
82+
const stream = fs.createReadStream(filePath, { start, end })
83+
let buf = [];
84+
await new Promise((resolve) => {
85+
stream.on('data', data => {
86+
buf.push(data)
87+
})
88+
stream.on('error', error => {
89+
reject('读取文件分片异常,请重新执行命令继续上传');
90+
})
91+
stream.on('end', async () => {
92+
await uploadChunk(Buffer.concat(buf), currentChunkIndex, parts);
93+
buf = null;
94+
resolve();
95+
})
96+
}).catch(error => {
97+
throw Error(error)
98+
})
99+
}
100+
} catch (error) {
101+
logger.error(error.message);
102+
logger.error(error.stack);
103+
console.log(chalk(error.message));
104+
return;
105+
}
106+
107+
try {
108+
const res = await _uploadSuccess(requestUrl, {
109+
version,
110+
uploadId,
111+
fileSize,
112+
fileTag: md5
113+
}, {
114+
Authorization
115+
});
116+
if (res.code) {
117+
throw (res.message);
118+
}
119+
} catch (error) {
120+
logger.error(error.message);
121+
logger.error(error.stack);
122+
console.log(chalk.red((error.response && error.response.data) || error.message));
123+
return;
124+
}
125+
126+
console.log(chalk.green(`\n上传完毕\n`))
127+
logger.info('************************ 上传完毕 ************************')
128+
}
129+
130+
const getFileMD5Success = async (filePath) => {
131+
try {
132+
const res = await _getExistChunks(requestUrl, {
133+
version,
134+
fileTag: md5
135+
}, {
136+
Authorization
137+
});
138+
if (res.code) {
139+
throw (res.message);
140+
}
141+
uploadId = res.data.uploadId;
142+
143+
// 上传过一部分
144+
if (Array.isArray(res.data.parts)) {
145+
await upload(filePath, res.data.parts);
146+
} else {
147+
// 未上传过
148+
await upload(filePath);
149+
}
150+
} catch (error) {
151+
logger.error(error.message);
152+
logger.error(error.stack);
153+
console.log(chalk.red((error.response && error.response.data) || error.message));
154+
return;
155+
}
156+
}
157+
158+
const getFileMD5 = async (filePath) => {
159+
const totalChunk = Math.ceil(fileSize / CHUNK_SIZE);
160+
const spark = new SparkMD5.ArrayBuffer();
161+
try {
162+
console.log(`\n开始计算 MD5\n`)
163+
logger.info('开始计算 MD5')
164+
165+
const bar = new ProgressBar(':bar [:current/:total] :percent', { total: totalChunk });
166+
await new Promise(resolve => {
167+
stream = fs.createReadStream(filePath, { highWaterMark: CHUNK_SIZE })
168+
stream.on('data', chunk => {
169+
bar.tick();
170+
spark.append(chunk)
171+
})
172+
stream.on('error', error => {
173+
reject('读取文件分片异常,请重新执行命令继续上传');
174+
})
175+
stream.on('end', async () => {
176+
md5 = spark.end();
177+
spark.destroy();
178+
console.log(`\n文件 MD5:${md5}\n`)
179+
await getFileMD5Success(filePath);
180+
resolve();
181+
})
182+
}).catch(error => {
183+
throw Error(error);
184+
})
185+
} catch (error) {
186+
console.log(chalk.red((error.response && error.response.data) || error.message));
187+
logger.error(error.message);
188+
logger.error(error.stack);
189+
return;
190+
}
191+
}
192+
193+
const beforeUpload = async (filePath) => {
194+
try {
195+
const stat = fs.lstatSync(filePath);
196+
if (stat.isDirectory()) {
197+
console.log(chalk.red(`\n${filePath}不合法,需指定一个文件\n`))
198+
return ;
199+
}
200+
fileSize = stat.size;
201+
} catch (error) {
202+
if (error.code === 'ENOENT') {
203+
console.log(chalk.red(`未找到 ${filePath}`));
204+
} else {
205+
logger.error(error.message);
206+
logger.error(error.stack);
207+
console.log(chalk.red((error.response && error.response.data) || error.message));
208+
}
209+
process.exitCode = 1;
210+
return;
211+
}
212+
await getFileMD5(filePath);
213+
}
214+
215+
const onUpload = (_username, _password) => {
216+
Authorization = generateAuthorization(_username, _password);
217+
218+
logger.info('************************ 准备上传 ************************')
219+
220+
if (path.isAbsolute(argv.path)) {
221+
beforeUpload(argv.path);
222+
} else {
223+
beforeUpload(path.join(process.cwd(), argv.path))
224+
}
225+
}
226+
227+
const [username, password] = argv.username.split(':');
228+
229+
if (username && password) {
230+
onUpload(username, password);
231+
} else {
232+
prompts([
233+
{
234+
type: 'password',
235+
name: 'password',
236+
message: '请输入登陆密码:',
237+
}
238+
], {
239+
onCancel: () => { }
240+
}
241+
).then(async (answers) => {
242+
if (!answers.password) {
243+
return;
244+
}
245+
onUpload(argv.username, answers.password);
246+
})
247+
}

lib/argv.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const argv = require('yargs')
2+
.usage('用法: coding-generic --username=<USERNAME> --path=<FILE.EXT> --registry=<REGISTRY>')
3+
.options({
4+
username: {
5+
alias: 'u',
6+
describe: '用户名',
7+
demandOption: true
8+
},
9+
path: {
10+
alias: 'p',
11+
describe: '需要上传的文件路径',
12+
demandOption: true
13+
},
14+
registry: {
15+
alias: 'r',
16+
describe: '仓库路径',
17+
demandOption: true
18+
}
19+
})
20+
.alias('version', 'v')
21+
.help('h')
22+
.alias('h', 'help')
23+
.example('coding-generic [email protected] --path=./test.txt --registry="https://codingcorp-generic.pkg.coding.net/project/generic-repo/test.txt?version=latest"')
24+
.argv;
25+
26+
module.exports = argv;

lib/constants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
CHUNK_SIZE: 1024 * 1024 * 5
3+
}

lib/log.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const { createLogger, format, transports } = require('winston');
2+
const { combine, timestamp, printf } = format;
3+
4+
const userHome = process.env.HOME || process.env.USERPROFILE;
5+
6+
const formatLog = printf(({ level, message, timestamp }) => `${timestamp} ${level}: ${JSON.stringify(message)}`);
7+
const transport = new (transports.DailyRotateFile)({
8+
filename: `${userHome}/.coding/log/coding-generic/%DATE%.log`,
9+
zippedArchive: true,
10+
maxSize: '20m',
11+
maxFiles: '14d'
12+
});
13+
14+
const logger = createLogger({
15+
format: combine(
16+
timestamp(),
17+
formatLog
18+
),
19+
'transports': [
20+
transport
21+
]
22+
});
23+
24+
module.exports = logger;

lib/request.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const axios = require('axios');
2+
3+
/**
4+
* 获取已经上传完成的分片信息
5+
* @param {string} requestUrl
6+
* @param {string} version
7+
* @param {string} fileTag
8+
* @param {string} Authorization
9+
*/
10+
const getExistChunks = (requestUrl, {
11+
version,
12+
fileTag
13+
}, {
14+
Authorization
15+
}) => {
16+
return axios.post(`${requestUrl}?version=${version}&fileTag=${fileTag}&action=part-init`, {}, {
17+
headers: { Authorization }
18+
})
19+
}
20+
21+
/**
22+
* 单个分片上传
23+
* @param {string} requestUrl
24+
* @param {string} uploadId
25+
* @param {string} version
26+
* @param {number} partNumber 从 1 开始
27+
* @param {number} size 分片大小
28+
* @param {string} form
29+
* @param {string} headers
30+
* @param {string} Authorization
31+
*/
32+
const uploadChunk = (requestUrl, {
33+
uploadId,
34+
version,
35+
partNumber,
36+
size,
37+
form,
38+
}, {
39+
headers,
40+
Authorization
41+
}) => {
42+
return axios.post(`${requestUrl}?version=${version}&uploadId=${uploadId}&partNumber=${partNumber}&size=${size}&action=part-upload`, form,
43+
{
44+
headers: { Authorization, ...headers }
45+
}
46+
)
47+
}
48+
49+
/**
50+
* 完成分片上传
51+
* @param {string} requestUrl
52+
* @param {string} version
53+
* @param {string} uploadId
54+
* @param {string} fileTag
55+
* @param {number} fileSize
56+
* @param {string} Authorization
57+
*/
58+
const uploadSuccess = (requestUrl, {
59+
version,
60+
uploadId,
61+
fileTag,
62+
fileSize
63+
}, {
64+
Authorization
65+
}) => {
66+
return axios.post(`${requestUrl}?version=${version}&uploadId=${uploadId}&fileTag=${fileTag}&size=${fileSize}&action=part-complete`, {}, {
67+
headers: { Authorization }
68+
})
69+
}
70+
71+
module.exports = {
72+
getExistChunks,
73+
uploadChunk,
74+
uploadSuccess
75+
}

0 commit comments

Comments
 (0)