Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 --username=coding@coding.com --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)

AltStyle によって変換されたページ (->オリジナル) /