ql-publish 0.0.1
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/cdn/index.js +4 -0
- package/cdn/refresh.js +45 -0
- package/config/config.js +69 -0
- package/config/index.js +7 -0
- package/index.js +17 -0
- package/package.json +24 -0
- package/upload/clear.js +159 -0
- package/upload/index.js +6 -0
- package/upload/oss.js +272 -0
- package/upload/reset.js +353 -0
- package/upload/resetOss.js +234 -0
- package/upload/u.js +535 -0
- package/utils/date.js +30 -0
- package/utils/logger.js +81 -0
package/upload/u.js
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
const config = require('../config/config.js')[process.env.NODE_ENV];
|
|
2
|
+
const scpClient = require('scp2');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
const { Client } = require('ssh2');
|
|
6
|
+
const { PassThrough } = require('stream');
|
|
7
|
+
const dayjs = require("dayjs");
|
|
8
|
+
const refreshCdn = require("../cdn/refresh");
|
|
9
|
+
const readline = require("readline");
|
|
10
|
+
|
|
11
|
+
const deleteFolder = () => {
|
|
12
|
+
return new Promise(resolve => {
|
|
13
|
+
logger.warn('开始连接服务器');
|
|
14
|
+
// 创建 SSH 客户端
|
|
15
|
+
const conn = new Client();
|
|
16
|
+
conn.on('ready', () => {
|
|
17
|
+
logger.log('✅ 已成功连接到服务器');
|
|
18
|
+
// 执行清空文件夹的命令(保留文件夹本身)
|
|
19
|
+
// 命令解释:
|
|
20
|
+
// - 先检查文件夹是否存在
|
|
21
|
+
// - 再删除文件夹内所有文件和子目录
|
|
22
|
+
const clearCommand = `
|
|
23
|
+
if [ -d "${config.path}" ]; then
|
|
24
|
+
find "${config.path}" -mindepth 1 -delete
|
|
25
|
+
echo "文件夹内容已清空"
|
|
26
|
+
else
|
|
27
|
+
echo "错误:文件夹 ${config.path} 不存在"
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
conn.exec(clearCommand, (err, stream) => {
|
|
33
|
+
if (err) throw err;
|
|
34
|
+
|
|
35
|
+
stream.on('close', (code) => {
|
|
36
|
+
if (code === 0 || code === 1) {
|
|
37
|
+
logger.info(`✅ 成功清空文件夹: ${config.path}`);
|
|
38
|
+
resolve(true);
|
|
39
|
+
} else {
|
|
40
|
+
logger.error(`❌ 清空文件夹失败,退出码: ${code}`);
|
|
41
|
+
resolve(null);
|
|
42
|
+
}
|
|
43
|
+
conn.end(); // 关闭连接
|
|
44
|
+
}).on('data', () => {
|
|
45
|
+
// console.log(`输出: ${data}`);
|
|
46
|
+
}).stderr.on('data', (data) => {
|
|
47
|
+
logger.error(`❌ 错误: ${data}`);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}).connect(config);
|
|
51
|
+
|
|
52
|
+
conn.on('error', (err) => {
|
|
53
|
+
logger.error('❌ 连接错误:', err);
|
|
54
|
+
resolve(null);
|
|
55
|
+
});
|
|
56
|
+
conn.on('end', () => {});
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取文件列表
|
|
62
|
+
*/
|
|
63
|
+
const getFileList = (sftp) => {
|
|
64
|
+
return new Promise(async resolve => {
|
|
65
|
+
const authDirectoryByName = (name) => {
|
|
66
|
+
return new Promise(r => {
|
|
67
|
+
// 检查目录是否存在
|
|
68
|
+
sftp.stat(name, (err, stats) => {
|
|
69
|
+
if (!err && stats.isDirectory()) {
|
|
70
|
+
r(true)
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// 目录不存在,创建目录
|
|
74
|
+
sftp.mkdir(name, (err) => {
|
|
75
|
+
if (err) {
|
|
76
|
+
console.error(`❌ 创建目录 ${name} 失败:`, err);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
r(false)
|
|
79
|
+
} else {
|
|
80
|
+
logger.log(`✅ 目录 ${name} 创建成功`);
|
|
81
|
+
r(true)
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!await authDirectoryByName(`${config.path}_old`)) {
|
|
89
|
+
process.exit(1);
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 读取目录内容
|
|
94
|
+
sftp.readdir(`${config.path}_old`, (err, list) => {
|
|
95
|
+
if (err) {
|
|
96
|
+
console.error('读取目录失败:', err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 遍历并显示目录内容
|
|
102
|
+
resolve(list.map(item => {
|
|
103
|
+
return item.filename
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 确保目录存在,如果不存在则创建
|
|
111
|
+
* @param {Object} sftp - SFTP客户端实例
|
|
112
|
+
* @param {string} dirPath - 目录路径
|
|
113
|
+
*/
|
|
114
|
+
function ensureDirectoryExists(sftp, dirPath) {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
sftp.stat(dirPath, (err) => {
|
|
117
|
+
if (!err) {
|
|
118
|
+
// 目录已存在
|
|
119
|
+
resolve();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 递归创建父目录
|
|
124
|
+
const parentDir = path.posix.dirname(dirPath);
|
|
125
|
+
if (parentDir === dirPath) {
|
|
126
|
+
// 已经是根目录
|
|
127
|
+
reject(new Error(`无法创建目录: ${dirPath}`));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 先确保父目录存在,再创建当前目录
|
|
132
|
+
ensureDirectoryExists(sftp, parentDir)
|
|
133
|
+
.then(() => {
|
|
134
|
+
sftp.mkdir(dirPath, (err) => {
|
|
135
|
+
if (err) {
|
|
136
|
+
reject(new Error(`创建目录失败 ${dirPath}: ${err.message}`));
|
|
137
|
+
} else {
|
|
138
|
+
// console.log(`已创建目录: ${dirPath}`);
|
|
139
|
+
resolve();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
.catch(reject);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const reduceLog = (n = 1.98) => {
|
|
149
|
+
return Math.random() * 2 > n
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 复制单个文件
|
|
155
|
+
* @param {Object} sftp - SFTP客户端实例
|
|
156
|
+
* @param {string} sourcePath - 源文件路径
|
|
157
|
+
* @param {string} targetPath - 目标文件路径
|
|
158
|
+
*/
|
|
159
|
+
function copyFile(sftp, sourcePath, targetPath) {
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const readStream = sftp.createReadStream(sourcePath);
|
|
162
|
+
const writeStream = sftp.createWriteStream(targetPath);
|
|
163
|
+
const transferStream = new PassThrough();
|
|
164
|
+
|
|
165
|
+
// 错误处理
|
|
166
|
+
readStream.on('error', (err) => {
|
|
167
|
+
console.error(`读取文件失败 ${sourcePath}: ${err.message}`);
|
|
168
|
+
resolve(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
writeStream.on('error', (err) => {
|
|
172
|
+
console.error(`写入文件失败 ${targetPath}: ${err.message}`);
|
|
173
|
+
resolve(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
writeStream.on('close', () => {
|
|
177
|
+
// console.log(`写入流已关闭: ${targetPath}`);
|
|
178
|
+
reduceLog(1.9) && logger.log(`✅ 已复制: ${targetPath}`);
|
|
179
|
+
resolve(true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 完成处理
|
|
183
|
+
writeStream.on('finish', () => {
|
|
184
|
+
// console.log(`已复制: ${sourcePath}`);
|
|
185
|
+
resolve(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// 管道传输
|
|
189
|
+
readStream.pipe(transferStream).pipe(writeStream);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 递归复制目录内容
|
|
195
|
+
* @param {Object} sftp - SFTP客户端实例
|
|
196
|
+
* @param {string} currentSourceDir - 当前源目录
|
|
197
|
+
* @param {string} currentTargetDir - 当前目标目录
|
|
198
|
+
*/
|
|
199
|
+
async function recursiveCopy(sftp, currentSourceDir, currentTargetDir) {
|
|
200
|
+
// 确保目标目录存在
|
|
201
|
+
await ensureDirectoryExists(sftp, currentTargetDir);
|
|
202
|
+
|
|
203
|
+
// 获取源目录中的所有项目
|
|
204
|
+
const items = await new Promise((resolve, reject) => {
|
|
205
|
+
sftp.readdir(currentSourceDir, (err, list) => {
|
|
206
|
+
if (err) {
|
|
207
|
+
reject(new Error(`读取目录失败 ${currentSourceDir}: ${err.message}`));
|
|
208
|
+
} else {
|
|
209
|
+
resolve(list);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// 处理每个项目
|
|
215
|
+
for (const item of items) {
|
|
216
|
+
const sourcePath = path.posix.join(currentSourceDir, item.filename);
|
|
217
|
+
const targetPath = path.posix.join(currentTargetDir, item.filename);
|
|
218
|
+
|
|
219
|
+
if (item.attrs.isDirectory()) {
|
|
220
|
+
// 如果是目录,递归处理
|
|
221
|
+
await recursiveCopy(sftp, sourcePath, targetPath);
|
|
222
|
+
} else {
|
|
223
|
+
// 如果是文件,直接复制
|
|
224
|
+
await copyFile(sftp, sourcePath, targetPath);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// await readStreamByFile(sftp, `${config.path}/fileMap.json`)
|
|
230
|
+
const readStreamByFile = async (sftp, remoteFilePath) => {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
console.log(`准备读取文件: ${remoteFilePath}`);
|
|
233
|
+
|
|
234
|
+
// 创建读取流
|
|
235
|
+
const readStream = sftp.createReadStream(remoteFilePath, {
|
|
236
|
+
encoding: 'utf8' // 以UTF-8编码读取
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
let fileContent = '';
|
|
240
|
+
|
|
241
|
+
// 收集文件内容
|
|
242
|
+
readStream.on('data', (chunk) => {
|
|
243
|
+
fileContent += chunk;
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// 读取完成处理
|
|
247
|
+
readStream.on('end', () => {
|
|
248
|
+
try {
|
|
249
|
+
// 解析JSON数据
|
|
250
|
+
const jsonData = JSON.parse(fileContent);
|
|
251
|
+
console.log('文件内容解析成功:');
|
|
252
|
+
console.log(jsonData.deleteList);
|
|
253
|
+
} catch (parseErr) {
|
|
254
|
+
logger.error('JSON解析失败:', parseErr);
|
|
255
|
+
console.log('原始文件内容:', fileContent);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// 处理读取错误
|
|
261
|
+
readStream.on('error', (err) => {
|
|
262
|
+
logger.error('读取文件时发生错误:', err);
|
|
263
|
+
process.exit(1);
|
|
264
|
+
});
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const writeStreamToFile = async (sftp, data, filePath) => {
|
|
269
|
+
return new Promise((resolve) => {
|
|
270
|
+
// 使用SFTP创建文件并写入内容
|
|
271
|
+
// 注意:如果文件已存在,会被覆盖
|
|
272
|
+
const writeStream = sftp.createWriteStream(filePath, {
|
|
273
|
+
flags: 'w', // 写入模式,会覆盖现有文件
|
|
274
|
+
encoding: 'utf8', // 编码格式
|
|
275
|
+
mode: 0o644 // 文件权限(可读可写)
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// 写入JS模块内容
|
|
279
|
+
writeStream.end(data);
|
|
280
|
+
|
|
281
|
+
// 处理写入完成事件
|
|
282
|
+
writeStream.on('finish', () => {
|
|
283
|
+
console.log(`文件 ${filePath} 已成功创建并写入数据`);
|
|
284
|
+
resolve();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// 处理写入完成事件
|
|
288
|
+
writeStream.on('close', () => {
|
|
289
|
+
logger.warn(`✅ 旧项目文件索引已成功创建并写入数据,${filePath}`);
|
|
290
|
+
resolve();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// 处理写入错误
|
|
294
|
+
writeStream.on('error', (err) => {
|
|
295
|
+
logger.error('❌ 写入文件时发生错误:', err);
|
|
296
|
+
process.exit(1);
|
|
297
|
+
});
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let fileMapData = {
|
|
302
|
+
mtime: 0
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const creatFileMap = async (sftp, currentSourceDir) => {
|
|
306
|
+
// 获取源目录中的所有项目
|
|
307
|
+
const items = await new Promise((resolve, reject) => {
|
|
308
|
+
sftp.readdir(currentSourceDir, (err, list) => {
|
|
309
|
+
if (err) {
|
|
310
|
+
reject(new Error(`读取目录失败 ${currentSourceDir}: ${err.message}`));
|
|
311
|
+
} else {
|
|
312
|
+
resolve(list);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// 处理每个项目
|
|
318
|
+
for (const item of items) {
|
|
319
|
+
const sourcePath = path.posix.join(currentSourceDir, item.filename);
|
|
320
|
+
|
|
321
|
+
if (item.attrs.isDirectory()) {
|
|
322
|
+
// 如果是目录,递归处理
|
|
323
|
+
await creatFileMap(sftp, sourcePath);
|
|
324
|
+
} else {
|
|
325
|
+
fileMapData.mtime = item.attrs.mtime > fileMapData.mtime ? item.attrs.mtime : fileMapData.mtime;
|
|
326
|
+
|
|
327
|
+
// if (sourcePath.indexOf(`assets`) > -1) {
|
|
328
|
+
// if (sourcePath.indexOf(`assets/_plugin`) === -1) {
|
|
329
|
+
// fileMapData.deleteList.push(sourcePath)
|
|
330
|
+
// } else {
|
|
331
|
+
// fileMapData.unDeleteList.push(sourcePath);
|
|
332
|
+
// }
|
|
333
|
+
// } else {
|
|
334
|
+
// fileMapData.unDeleteList.push(sourcePath);
|
|
335
|
+
// }
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (fileMapData.mtime === 0) {
|
|
340
|
+
logger.error(`❌ 旧项目文件索引创建失败,请检查目录 ${currentSourceDir} 是否存在`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const sftpReaddir = async (sftp, pathStr) => {
|
|
346
|
+
return new Promise((resolve) => {
|
|
347
|
+
// 读取目录内容
|
|
348
|
+
sftp.readdir(pathStr, (err, entries) => {
|
|
349
|
+
if (err) {
|
|
350
|
+
logger.error(`❌ 读取目录失败 ${pathStr}: ${err.message}`);
|
|
351
|
+
resolve([]);
|
|
352
|
+
} else {
|
|
353
|
+
resolve(entries);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 递归删除文件夹及其内容
|
|
360
|
+
async function deleteRemoteDirectory(sftp, pathStr) {
|
|
361
|
+
try {
|
|
362
|
+
// 获取目录下的所有条目
|
|
363
|
+
const entries = await sftpReaddir(sftp, pathStr);
|
|
364
|
+
// 先删除目录中的所有内容
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
const fullPath = `${pathStr}/${entry.filename}`;
|
|
367
|
+
// 判断是文件还是目录
|
|
368
|
+
if (entry.attrs.isDirectory()) {
|
|
369
|
+
// 递归删除子目录
|
|
370
|
+
await deleteRemoteDirectory(sftp, fullPath);
|
|
371
|
+
} else {
|
|
372
|
+
// 删除文件
|
|
373
|
+
await sftp.unlink(fullPath);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// 最后删除空目录
|
|
377
|
+
await sftp.rmdir(pathStr);
|
|
378
|
+
logger.info(`✅ 已删除目录: ${pathStr}`);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
logger.error(`❌ 删除过程中出错: ${err.message}`);
|
|
381
|
+
throw err;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const removeOldDir = async (sftp) => {
|
|
386
|
+
logger.warn('正在删除过期备份');
|
|
387
|
+
let list = await getFileList(sftp);
|
|
388
|
+
list = list.filter(fName => {
|
|
389
|
+
let fileDate = fName.split('_');
|
|
390
|
+
fileDate.length = 3
|
|
391
|
+
if (!fileDate.length) return false;
|
|
392
|
+
fileDate = fileDate.join('-');
|
|
393
|
+
return dayjs().diff(dayjs(`${fileDate} 00:00:00`), 'seconds') > config.backUpSeconds;
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
for (const item of list) {
|
|
397
|
+
// 开始删除操作
|
|
398
|
+
await deleteRemoteDirectory(sftp, `${config.path}_old/${item}`);
|
|
399
|
+
}
|
|
400
|
+
logger.info('✅ 过期备份删除成功');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const isHasDir = (sftp, dirPath) => {
|
|
404
|
+
return new Promise(resolve => {
|
|
405
|
+
sftp.stat(dirPath, (err) => {
|
|
406
|
+
resolve(!err);
|
|
407
|
+
})
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const sshAction = async () => {
|
|
412
|
+
let conn, sftp;
|
|
413
|
+
|
|
414
|
+
try {
|
|
415
|
+
logger.warn('连接到服务器...');
|
|
416
|
+
// 连接到服务器
|
|
417
|
+
conn = new Client();
|
|
418
|
+
await new Promise((resolve, reject) => {
|
|
419
|
+
conn.on('ready', resolve);
|
|
420
|
+
conn.on('error', (err) => reject(new Error(`❌ 服务器连接失败: ${err.message}`)));
|
|
421
|
+
conn.connect(config);
|
|
422
|
+
});
|
|
423
|
+
logger.info('✅ 已成功连接到服务器');
|
|
424
|
+
|
|
425
|
+
// 初始化SFTP
|
|
426
|
+
sftp = await new Promise((resolve, reject) => {
|
|
427
|
+
conn.sftp((err, sftpClient) => {
|
|
428
|
+
if (err) {
|
|
429
|
+
reject(new Error(`SFTP初始化失败: ${err.message}`));
|
|
430
|
+
} else {
|
|
431
|
+
resolve(sftpClient);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
// 没有项目目录,直接返回
|
|
436
|
+
if (!await isHasDir(sftp, config.path)) {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
if (config.backUp) {
|
|
440
|
+
// 删除三个月之前的备份
|
|
441
|
+
await removeOldDir(sftp);
|
|
442
|
+
const oldDir = `${config.path}_old/${dayjs().format("YYYY_MM_DD_HH_mm_ss")}/`;
|
|
443
|
+
logger.warn(`正在备份项目文件到: ${oldDir}`);
|
|
444
|
+
// 备份上一次的代码
|
|
445
|
+
await recursiveCopy(sftp, `${config.path}/`, oldDir);
|
|
446
|
+
logger.info('✅ 项目备份完成!');
|
|
447
|
+
}
|
|
448
|
+
// 生成文件地址路径映像
|
|
449
|
+
await creatFileMap(sftp, `${config.path}/`);
|
|
450
|
+
await writeStreamToFile(sftp, JSON.stringify(fileMapData), `${config.path}/fileMap.json`);
|
|
451
|
+
} catch (err) {
|
|
452
|
+
logger.error('❌ 过程出错:', err.message);
|
|
453
|
+
} finally {
|
|
454
|
+
// 关闭连接
|
|
455
|
+
if (sftp) sftp.end();
|
|
456
|
+
if (conn) conn.end();
|
|
457
|
+
// console.log('连接已关闭');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const scpAction = async () => {
|
|
462
|
+
// 清空目标目录文件
|
|
463
|
+
// if (!await deleteFolder()) {
|
|
464
|
+
// process.exit(1);
|
|
465
|
+
// }
|
|
466
|
+
|
|
467
|
+
logger.warn('开始上传文件到服务器...');
|
|
468
|
+
|
|
469
|
+
let timer = null;
|
|
470
|
+
|
|
471
|
+
const runTimer = () => {
|
|
472
|
+
timer && clearTimeout(timer);
|
|
473
|
+
timer = setTimeout(() => {
|
|
474
|
+
if (!timer) return;
|
|
475
|
+
logger.log('✅ 拼命上传中...');
|
|
476
|
+
runTimer();
|
|
477
|
+
}, Math.random() * 5000 + 1000);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
runTimer();
|
|
481
|
+
// 上传文件
|
|
482
|
+
scpClient.scp(
|
|
483
|
+
config.localPath,
|
|
484
|
+
config,
|
|
485
|
+
async (err) => {
|
|
486
|
+
clearTimeout(timer);
|
|
487
|
+
timer = null;
|
|
488
|
+
if (err) {
|
|
489
|
+
logger.error(`❌ ${process.env.NODE_ENV}环境:上传失败:`);
|
|
490
|
+
logger.error(err);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
} else {
|
|
493
|
+
logger.success(`🎉 ${process.env.NODE_ENV}环境:所有文件上传成功!`);
|
|
494
|
+
// 刷新cdn
|
|
495
|
+
config.cdn && config.cdn.list && await refreshCdn(config.cdn.list);
|
|
496
|
+
logger.warn(`接下来1分钟后,记得清理旧项目版本,请手动执行 yarn clear:${process.env.NODE_ENV}`)
|
|
497
|
+
process.exit(0);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const authProjectName = () => {
|
|
504
|
+
return new Promise((resolve) => {
|
|
505
|
+
// 创建readline接口
|
|
506
|
+
const rl = readline.createInterface({
|
|
507
|
+
input: process.stdin,
|
|
508
|
+
output: process.stdout
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// 向用户提问
|
|
512
|
+
rl.question(`${process.env.NODE_ENV}环境:是否确认发布 ${config.path} 项目? (输入 y 继续) `, (answer) => {
|
|
513
|
+
// 检查用户输入是否为'y'(不区分大小写)
|
|
514
|
+
if (answer.trim().toLowerCase() === 'y') {
|
|
515
|
+
resolve(true);
|
|
516
|
+
} else {
|
|
517
|
+
logger.error('❌ 发布程序终止');
|
|
518
|
+
resolve(false);
|
|
519
|
+
}
|
|
520
|
+
// 关闭接口
|
|
521
|
+
rl.close();
|
|
522
|
+
});
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 上传文件到服务器
|
|
527
|
+
async function run() {
|
|
528
|
+
if (!await authProjectName()) {
|
|
529
|
+
process.exit(1);
|
|
530
|
+
}
|
|
531
|
+
await sshAction();
|
|
532
|
+
await scpAction();
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
module.exports = run;
|
package/utils/date.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 日期格式化函数
|
|
3
|
+
* @param {Date} date - 日期对象,默认当前时间
|
|
4
|
+
* @param {string} format - 格式化字符串,例如:'yyyy-MM-dd HH:mm:ss'
|
|
5
|
+
* @returns {string} 格式化后的日期字符串
|
|
6
|
+
*/
|
|
7
|
+
function formatDate(date = new Date(), format = 'yyyy-MM-dd HH:mm:ss') {
|
|
8
|
+
// 补零函数
|
|
9
|
+
const pad = (num) => num.toString().padStart(2, '0');
|
|
10
|
+
|
|
11
|
+
const year = date.getFullYear();
|
|
12
|
+
const month = pad(date.getMonth() + 1); // 月份从0开始
|
|
13
|
+
const day = pad(date.getDate());
|
|
14
|
+
const hours = pad(date.getHours());
|
|
15
|
+
const minutes = pad(date.getMinutes());
|
|
16
|
+
const seconds = pad(date.getSeconds());
|
|
17
|
+
const milliseconds = date.getMilliseconds().toString().padStart(3, '0');
|
|
18
|
+
|
|
19
|
+
// 替换格式化字符串中的占位符
|
|
20
|
+
return format
|
|
21
|
+
.replace('yyyy', year)
|
|
22
|
+
.replace('MM', month)
|
|
23
|
+
.replace('dd', day)
|
|
24
|
+
.replace('HH', hours)
|
|
25
|
+
.replace('mm', minutes)
|
|
26
|
+
.replace('ss', seconds)
|
|
27
|
+
.replace('SSS', milliseconds);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = { formatDate };
|
package/utils/logger.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { formatDate } = require('./date');
|
|
2
|
+
|
|
3
|
+
// 定义 ANSI 颜色代码
|
|
4
|
+
const colors = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
bright: '\x1b[1m',
|
|
7
|
+
dim: '\x1b[2m',
|
|
8
|
+
underscore: '\x1b[4m',
|
|
9
|
+
blink: '\x1b[5m',
|
|
10
|
+
reverse: '\x1b[7m',
|
|
11
|
+
hidden: '\x1b[8m',
|
|
12
|
+
|
|
13
|
+
// 文本颜色
|
|
14
|
+
black: '\x1b[30m',
|
|
15
|
+
red: '\x1b[31m',
|
|
16
|
+
green: '\x1b[32m',
|
|
17
|
+
yellow: '\x1b[33m',
|
|
18
|
+
blue: '\x1b[34m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
20
|
+
cyan: '\x1b[36m',
|
|
21
|
+
white: '\x1b[37m',
|
|
22
|
+
|
|
23
|
+
// 背景颜色
|
|
24
|
+
bgBlack: '\x1b[40m',
|
|
25
|
+
bgRed: '\x1b[41m',
|
|
26
|
+
bgGreen: '\x1b[42m',
|
|
27
|
+
bgYellow: '\x1b[43m',
|
|
28
|
+
bgBlue: '\x1b[44m',
|
|
29
|
+
bgMagenta: '\x1b[45m',
|
|
30
|
+
bgCyan: '\x1b[46m',
|
|
31
|
+
bgWhite: '\x1b[47m'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// 格式化时间
|
|
35
|
+
function getFormattedTime() {
|
|
36
|
+
return formatDate()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 日志工具
|
|
40
|
+
const logger = {
|
|
41
|
+
// 普通日志 - 白色
|
|
42
|
+
log: (...args) => {
|
|
43
|
+
console.log(`${colors.white}[${getFormattedTime()}]`, ...args, colors.reset);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// 信息日志 - 蓝色
|
|
47
|
+
info: (...args) => {
|
|
48
|
+
console.log(`${colors.blue}[${getFormattedTime()}] [INFO]`, ...args, colors.reset);
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// 成功日志 - 绿色
|
|
52
|
+
success: (...args) => {
|
|
53
|
+
console.log(`${colors.green}[${getFormattedTime()}] [SUCCESS]`, ...args, colors.reset);
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// 警告日志 - 黄色
|
|
57
|
+
warn: (...args) => {
|
|
58
|
+
console.log(`${colors.yellow}[${getFormattedTime()}] [WARN]`, ...args, colors.reset);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// 错误日志 - 红色
|
|
62
|
+
error: (...args) => {
|
|
63
|
+
console.error(`${colors.red}[${getFormattedTime()}] [ERROR]`, ...args, colors.reset);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// 调试日志 - 品红
|
|
67
|
+
debug: (...args) => {
|
|
68
|
+
console.log(`${colors.magenta}[${getFormattedTime()}] [DEBUG]`, ...args, colors.reset);
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// 自定义颜色日志
|
|
72
|
+
custom: (color, ...args) => {
|
|
73
|
+
if (colors[color]) {
|
|
74
|
+
console.log(`${colors[color]}[${getFormattedTime()}]`, ...args, colors.reset);
|
|
75
|
+
} else {
|
|
76
|
+
console.log(`[${getFormattedTime()}]`, ...args);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
module.exports = logger;
|