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/reset.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const config = require('../config/config.js')[process.env.NODE_ENV];
|
|
2
|
+
const inquirer = require('inquirer');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
const refreshCdn = require('../cdn/refresh')
|
|
6
|
+
const { Client } = require("ssh2");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { PassThrough } = require("stream");
|
|
9
|
+
const dayjs = require("dayjs");
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 确保目录存在,如果不存在则创建
|
|
14
|
+
* @param {Object} sftp - SFTP客户端实例
|
|
15
|
+
* @param {string} dirPath - 目录路径
|
|
16
|
+
*/
|
|
17
|
+
function ensureDirectoryExists(sftp, dirPath) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
sftp.stat(dirPath, (err) => {
|
|
20
|
+
if (!err) {
|
|
21
|
+
// 目录已存在
|
|
22
|
+
resolve();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 递归创建父目录
|
|
27
|
+
const parentDir = path.posix.dirname(dirPath);
|
|
28
|
+
if (parentDir === dirPath) {
|
|
29
|
+
// 已经是根目录
|
|
30
|
+
reject(new Error(`无法创建目录: ${dirPath}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 先确保父目录存在,再创建当前目录
|
|
35
|
+
ensureDirectoryExists(sftp, parentDir)
|
|
36
|
+
.then(() => {
|
|
37
|
+
sftp.mkdir(dirPath, (err) => {
|
|
38
|
+
if (err) {
|
|
39
|
+
reject(new Error(`创建目录失败 ${dirPath}: ${err.message}`));
|
|
40
|
+
} else {
|
|
41
|
+
// console.log(`已创建目录: ${dirPath}`);
|
|
42
|
+
resolve();
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
})
|
|
46
|
+
.catch(reject);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const reduceLog = () => {
|
|
52
|
+
return Math.random() * 2 > 1.98
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 复制单个文件
|
|
58
|
+
* @param {Object} sftp - SFTP客户端实例
|
|
59
|
+
* @param {string} sourcePath - 源文件路径
|
|
60
|
+
* @param {string} targetPath - 目标文件路径
|
|
61
|
+
*/
|
|
62
|
+
function copyFile(sftp, sourcePath, targetPath) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const readStream = sftp.createReadStream(sourcePath);
|
|
65
|
+
const writeStream = sftp.createWriteStream(targetPath);
|
|
66
|
+
const transferStream = new PassThrough();
|
|
67
|
+
|
|
68
|
+
// 错误处理
|
|
69
|
+
readStream.on('error', (err) => {
|
|
70
|
+
console.error(`读取文件失败 ${sourcePath}: ${err.message}`);
|
|
71
|
+
resolve(false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
writeStream.on('error', (err) => {
|
|
75
|
+
console.error(`写入文件失败 ${targetPath}: ${err.message}`);
|
|
76
|
+
resolve(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
writeStream.on('close', () => {
|
|
80
|
+
// console.log(`写入流已关闭: ${targetPath}`);
|
|
81
|
+
reduceLog(1.9) && logger.log(`✅ 已复制到: ${targetPath}`);
|
|
82
|
+
resolve(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 完成处理
|
|
86
|
+
writeStream.on('finish', () => {
|
|
87
|
+
// console.log(`已复制: ${sourcePath}`);
|
|
88
|
+
resolve(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 管道传输
|
|
92
|
+
readStream.pipe(transferStream).pipe(writeStream);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 递归复制目录内容
|
|
98
|
+
* @param {Object} sftp - SFTP客户端实例
|
|
99
|
+
* @param {string} currentSourceDir - 当前源目录
|
|
100
|
+
* @param {string} currentTargetDir - 当前目标目录
|
|
101
|
+
*/
|
|
102
|
+
async function recursiveCopy(sftp, currentSourceDir, currentTargetDir) {
|
|
103
|
+
// 确保目标目录存在
|
|
104
|
+
await ensureDirectoryExists(sftp, currentTargetDir);
|
|
105
|
+
|
|
106
|
+
// 获取源目录中的所有项目
|
|
107
|
+
const items = await new Promise((resolve, reject) => {
|
|
108
|
+
sftp.readdir(currentSourceDir, (err, list) => {
|
|
109
|
+
if (err) {
|
|
110
|
+
reject(new Error(`读取目录失败 ${currentSourceDir}: ${err.message}`));
|
|
111
|
+
} else {
|
|
112
|
+
resolve(list);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// 处理每个项目
|
|
118
|
+
for (const item of items) {
|
|
119
|
+
const sourcePath = path.posix.join(currentSourceDir, item.filename);
|
|
120
|
+
const targetPath = path.posix.join(currentTargetDir, item.filename);
|
|
121
|
+
|
|
122
|
+
if (item.attrs.isDirectory()) {
|
|
123
|
+
// 如果是目录,递归处理
|
|
124
|
+
await recursiveCopy(sftp, sourcePath, targetPath);
|
|
125
|
+
} else {
|
|
126
|
+
// 如果是文件,直接复制
|
|
127
|
+
await copyFile(sftp, sourcePath, targetPath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const resetDir = async (sftp, resetName) => {
|
|
133
|
+
logger.warn(`开始回滚代码`, `从${config.path}_old/${resetName}/,到${config.path}/`);
|
|
134
|
+
const oldDir = `${config.path}_old/${resetName}/`;
|
|
135
|
+
// 备份上一次的代码
|
|
136
|
+
await recursiveCopy(sftp, oldDir, `${config.path}/`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const authProjectName = (name) => {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
// 创建readline接口
|
|
142
|
+
const rl = readline.createInterface({
|
|
143
|
+
input: process.stdin,
|
|
144
|
+
output: process.stdout
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 向用户提问
|
|
148
|
+
rl.question(`${process.env.NODE_ENV}环境:是否确认回滚 ${name} 项目? (输入 y 继续) `, (answer) => {
|
|
149
|
+
// 检查用户输入是否为'y'(不区分大小写)
|
|
150
|
+
if (answer.trim().toLowerCase() === 'y') {
|
|
151
|
+
resolve(true);
|
|
152
|
+
} else {
|
|
153
|
+
logger.error('❌ 发布程序终止');
|
|
154
|
+
resolve(false);
|
|
155
|
+
}
|
|
156
|
+
// 关闭接口
|
|
157
|
+
rl.close();
|
|
158
|
+
});
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 获取文件列表
|
|
164
|
+
*/
|
|
165
|
+
const getFileList = () => {
|
|
166
|
+
return new Promise(async resolve => {
|
|
167
|
+
const { sftp, close } = await getSftp();
|
|
168
|
+
const authDirectoryByName = (name) => {
|
|
169
|
+
return new Promise(r => {
|
|
170
|
+
// 检查目录是否存在
|
|
171
|
+
sftp.stat(name, (err, stats) => {
|
|
172
|
+
if (!err && stats.isDirectory()) {
|
|
173
|
+
r(true)
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// 目录不存在,创建目录
|
|
177
|
+
sftp.mkdir(name, (err) => {
|
|
178
|
+
if (err) {
|
|
179
|
+
console.error(`❌ 创建目录 ${name} 失败:`, err);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
r(false)
|
|
182
|
+
} else {
|
|
183
|
+
logger.log(`✅ 目录 ${name} 创建成功`);
|
|
184
|
+
r(true)
|
|
185
|
+
}
|
|
186
|
+
close();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!await authDirectoryByName(`${config.path}_old`)) {
|
|
193
|
+
process.exit(1);
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 读取目录内容
|
|
198
|
+
sftp.readdir(`${config.path}_old`, (err, list) => {
|
|
199
|
+
if (err) {
|
|
200
|
+
console.error('读取目录失败:', err);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 遍历并显示目录内容
|
|
206
|
+
resolve(list.map(item => {
|
|
207
|
+
return item.filename
|
|
208
|
+
}).sort((a, b) => {
|
|
209
|
+
let _a = a.split('_');
|
|
210
|
+
_a = `${_a[0]}-${_a[1]}-${_a[2]} ${_a[3]}:${_a[4]}:${_a[5]}`;
|
|
211
|
+
|
|
212
|
+
let _b = b.split('_');
|
|
213
|
+
_b = `${_b[0]}-${_b[1]}-${_b[2]} ${_b[3]}:${_b[4]}:${_b[5]}`;
|
|
214
|
+
|
|
215
|
+
return dayjs(_b).diff(dayjs(_a));
|
|
216
|
+
}));
|
|
217
|
+
});
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const sftpReaddir = async (sftp, pathStr) => {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
// 读取目录内容
|
|
224
|
+
sftp.readdir(pathStr, (err, entries) => {
|
|
225
|
+
if (err) {
|
|
226
|
+
logger.error(`❌ 读取目录失败 ${pathStr}: ${err.message}`);
|
|
227
|
+
resolve([]);
|
|
228
|
+
} else {
|
|
229
|
+
resolve(entries);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 递归删除文件夹及其内容
|
|
236
|
+
async function deleteRemoteDirectory(sftp, pathStr) {
|
|
237
|
+
try {
|
|
238
|
+
// 获取目录下的所有条目
|
|
239
|
+
const entries = await sftpReaddir(sftp, pathStr);
|
|
240
|
+
// 先删除目录中的所有内容
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
const fullPath = `${pathStr}/${entry.filename}`;
|
|
243
|
+
// 判断是文件还是目录
|
|
244
|
+
if (entry.attrs.isDirectory()) {
|
|
245
|
+
// 递归删除子目录
|
|
246
|
+
await deleteRemoteDirectory(sftp, fullPath);
|
|
247
|
+
} else {
|
|
248
|
+
// 删除文件
|
|
249
|
+
await sftp.unlink(fullPath);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// 最后删除空目录
|
|
253
|
+
await sftp.rmdir(pathStr);
|
|
254
|
+
logger.log(`✅ 已删除目录: ${pathStr}`);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
logger.error(`❌ 删除过程中出错: ${err.message}`);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const getSftp = () => {
|
|
262
|
+
return new Promise(async resolve => {
|
|
263
|
+
let conn, sftp;
|
|
264
|
+
try {
|
|
265
|
+
logger.warn('连接到服务器...');
|
|
266
|
+
// 连接到服务器
|
|
267
|
+
conn = new Client();
|
|
268
|
+
await new Promise((r, reject) => {
|
|
269
|
+
conn.on('ready', r);
|
|
270
|
+
conn.on('error', (err) => reject(new Error(`❌ 服务器连接失败: ${err.message}`)));
|
|
271
|
+
conn.connect(config);
|
|
272
|
+
});
|
|
273
|
+
logger.info('✅ 已成功连接到服务器');
|
|
274
|
+
|
|
275
|
+
// 初始化SFTP
|
|
276
|
+
sftp = await new Promise((r, reject) => {
|
|
277
|
+
conn.sftp((err, sftpClient) => {
|
|
278
|
+
if (err) {
|
|
279
|
+
reject(new Error(`SFTP初始化失败: ${err.message}`));
|
|
280
|
+
} else {
|
|
281
|
+
r(sftpClient);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
resolve({
|
|
286
|
+
sftp,
|
|
287
|
+
conn,
|
|
288
|
+
close: () => {
|
|
289
|
+
if (sftp) sftp.end();
|
|
290
|
+
if (conn) conn.end();
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
} catch (err) {
|
|
294
|
+
logger.error('❌ 过程出错:', err.message);
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function run() {
|
|
301
|
+
if (!await authProjectName(config.path)) {
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
let folderNames = await getFileList();
|
|
306
|
+
|
|
307
|
+
if (!folderNames || folderNames.length === 0) {
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 创建交互式问题
|
|
312
|
+
const questions = [
|
|
313
|
+
{
|
|
314
|
+
type: 'list',
|
|
315
|
+
name: 'selectedOption',
|
|
316
|
+
message: '请使用上下键选择一个版本进行回滚,按回车确认:',
|
|
317
|
+
choices: folderNames,
|
|
318
|
+
// 可选:设置默认选中的索引
|
|
319
|
+
default: 0
|
|
320
|
+
}
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
// 执行交互
|
|
324
|
+
inquirer.prompt(questions).then(async answers => {
|
|
325
|
+
logger.warn(`回滚版本号: ${answers.selectedOption}`);
|
|
326
|
+
if (!await authProjectName(answers.selectedOption)) {
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
const { sftp, close } = await getSftp();
|
|
330
|
+
// 删除目标文件夹代码
|
|
331
|
+
await deleteRemoteDirectory(sftp, config.path);
|
|
332
|
+
|
|
333
|
+
// 恢复目标日期代码
|
|
334
|
+
await resetDir(sftp, answers.selectedOption)
|
|
335
|
+
close();
|
|
336
|
+
logger.success(`🎉 ${process.env.NODE_ENV}环境:项目回滚完成!`);
|
|
337
|
+
config.cdn && config.cdn.list && await refreshCdn(config.cdn.list);
|
|
338
|
+
process.exit(0);
|
|
339
|
+
}).catch(error => {
|
|
340
|
+
if (error.isTtyError) {
|
|
341
|
+
logger.error('❌ 终端不支持交互模式');
|
|
342
|
+
} else {
|
|
343
|
+
logger.error(`❌ ${process.env.NODE_ENV}环境:回滚过程中发生错误:`);
|
|
344
|
+
logger.error(error);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
} catch (err) {
|
|
349
|
+
logger.error('❌ 过程出错:', err.message);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
run();
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const config = require('../config/config.js')[process.env.NODE_ENV];
|
|
2
|
+
const OSS = require('ali-oss');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const refreshCdn = require('../cdn/refresh')
|
|
7
|
+
|
|
8
|
+
// 初始化OSS客户端
|
|
9
|
+
const client = new OSS(config);
|
|
10
|
+
|
|
11
|
+
const reduceLog = () => {
|
|
12
|
+
return Math.random() * 2 > 1.98
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 复制OSS中的文件夹到指定目录
|
|
17
|
+
* @param {string} sourcePrefix - 源文件夹前缀(如: 'source-folder/')
|
|
18
|
+
* @param {string} targetBucket - 目标Bucket名称(可以与源Bucket相同)
|
|
19
|
+
* @param {string} targetPrefix - 目标文件夹前缀(如: 'target-folder/')
|
|
20
|
+
* @param {string} [delimiter=''] - 分隔符,用于列出文件夹下的文件
|
|
21
|
+
*/
|
|
22
|
+
async function copyFolder(sourcePrefix, targetBucket, targetPrefix, delimiter = '') {
|
|
23
|
+
try {
|
|
24
|
+
let marker = '';
|
|
25
|
+
const maxKeys = 100; // 每次最多获取100个文件,可根据需要调整
|
|
26
|
+
|
|
27
|
+
do {
|
|
28
|
+
// 列出源文件夹下的文件
|
|
29
|
+
const result = await client.list({
|
|
30
|
+
prefix: sourcePrefix,
|
|
31
|
+
marker,
|
|
32
|
+
maxKeys,
|
|
33
|
+
delimiter
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 复制文件
|
|
37
|
+
if (result.objects && result.objects.length > 0) {
|
|
38
|
+
for (let file of result.objects) {
|
|
39
|
+
// 构建目标文件路径
|
|
40
|
+
const targetPath = targetPrefix + file.name.replace(sourcePrefix, '');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// 复制文件
|
|
44
|
+
await client.copy(targetPath, file.name);
|
|
45
|
+
reduceLog() && logger.log(`✅ 已复制: ${file.name} -> ${targetPath}`);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
logger.error(`❌ 复制失败 ${file.name}:`, err.message);
|
|
48
|
+
throw new Error(err.message)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 处理子目录(如果有)
|
|
54
|
+
if (result.prefixes && result.prefixes.length > 0) {
|
|
55
|
+
for (const prefix of result.prefixes) {
|
|
56
|
+
// 递归复制子目录
|
|
57
|
+
await copyFolder(prefix, targetBucket, targetPrefix, delimiter);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
marker = result.nextMarker;
|
|
62
|
+
} while (marker);
|
|
63
|
+
|
|
64
|
+
logger.warn('🎉 文件夹复制完成');
|
|
65
|
+
return true;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.error('❌ 复制文件夹时发生错误:', err);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 删除OSS中的文件夹及其所有内容
|
|
74
|
+
* @param {string} folderPath - 要删除的文件夹路径,例如: 'test-folder/'
|
|
75
|
+
*/
|
|
76
|
+
async function deleteFolder(folderPath) {
|
|
77
|
+
try {
|
|
78
|
+
let nextMarker = null;
|
|
79
|
+
let deletedCount = 0;
|
|
80
|
+
|
|
81
|
+
do {
|
|
82
|
+
// 列出文件夹下的所有文件
|
|
83
|
+
const result = await client.list({
|
|
84
|
+
prefix: folderPath, // 只列出指定前缀的文件
|
|
85
|
+
marker: nextMarker, // 分页标记
|
|
86
|
+
maxKeys: 100 // 每次最多列出100个文件
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 添加检查:确保 result.objects 存在且为数组
|
|
90
|
+
if (!result.objects || result.objects.length === 0) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 准备要删除的文件列表
|
|
95
|
+
const objects = result.objects.filter(obj => obj && obj.name).map(obj => obj.name);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// 批量删除文件
|
|
99
|
+
await client.deleteMulti(objects, { quiet: false });
|
|
100
|
+
} catch (e) {
|
|
101
|
+
throw new Error(e.message)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
deletedCount += objects.length;
|
|
105
|
+
nextMarker = result.nextMarker;
|
|
106
|
+
|
|
107
|
+
} while (nextMarker);
|
|
108
|
+
|
|
109
|
+
logger.warn(`✅ 成功删除文件夹 "${folderPath}" 及其 ${deletedCount} 个文件`);
|
|
110
|
+
return true;
|
|
111
|
+
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger.error('❌ 删除文件夹失败:', err);
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const resetDir = async (resetName) => {
|
|
119
|
+
const sourceFolder = `${config.targetDir}_old/${resetName}/`;
|
|
120
|
+
// 目标Bucket(可以与源Bucket相同)
|
|
121
|
+
const targetBucket = config.bucket;
|
|
122
|
+
// 目标文件夹(注意末尾的斜杠)
|
|
123
|
+
const targetFolder = `${config.targetDir}/`;
|
|
124
|
+
logger.info(`开始回滚版本 ${sourceFolder} 到 ${targetFolder} 目录`);
|
|
125
|
+
return await copyFolder(sourceFolder, targetBucket, targetFolder);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const authProjectName = (name) => {
|
|
129
|
+
return new Promise((resolve) => {
|
|
130
|
+
// 创建readline接口
|
|
131
|
+
const rl = readline.createInterface({
|
|
132
|
+
input: process.stdin,
|
|
133
|
+
output: process.stdout
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 向用户提问
|
|
137
|
+
rl.question(`是否确认回滚 ${name || config.targetDir} 项目? (输入 y 继续) `, (answer) => {
|
|
138
|
+
// 检查用户输入是否为'y'(不区分大小写)
|
|
139
|
+
if (answer.trim().toLowerCase() === 'y') {
|
|
140
|
+
resolve(true);
|
|
141
|
+
} else {
|
|
142
|
+
logger.error('❌ 发布程序终止');
|
|
143
|
+
resolve(false);
|
|
144
|
+
}
|
|
145
|
+
// 关闭接口
|
|
146
|
+
rl.close();
|
|
147
|
+
});
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const getDirList = async () => {
|
|
152
|
+
try {
|
|
153
|
+
const folderPath = `${config.targetDir}_old/`;
|
|
154
|
+
let nextMarker = null;
|
|
155
|
+
let folderNames = [];
|
|
156
|
+
logger.info(`查询可回滚版本中...`);
|
|
157
|
+
do {
|
|
158
|
+
// 列出文件夹下的所有文件
|
|
159
|
+
const result = await client.list({
|
|
160
|
+
prefix: folderPath, // 只列出指定前缀的文件
|
|
161
|
+
marker: nextMarker, // 分页标记
|
|
162
|
+
maxKeys: 1000 // 每次最多列出100个文件
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 添加检查:确保 result.objects 存在且为数组
|
|
166
|
+
if (!result.objects || result.objects.length === 0) {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
folderNames = [...folderNames, ...result.objects.filter(obj => obj && obj.name)
|
|
171
|
+
.map(obj => obj.name.split('/')[1])];
|
|
172
|
+
|
|
173
|
+
nextMarker = result.nextMarker;
|
|
174
|
+
} while (nextMarker);
|
|
175
|
+
logger.warn(`✅ 项目可回滚目录查询成功`);
|
|
176
|
+
return [...new Set(folderNames)];
|
|
177
|
+
|
|
178
|
+
} catch (err) {
|
|
179
|
+
logger.error('❌ 项目可回滚目录查询失败:', err);
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function run() {
|
|
185
|
+
if (!await authProjectName()) {
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const folderNames = await getDirList();
|
|
190
|
+
|
|
191
|
+
if (!folderNames || folderNames.length === 0) {
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 创建交互式问题
|
|
196
|
+
const questions = [
|
|
197
|
+
{
|
|
198
|
+
type: 'list',
|
|
199
|
+
name: 'selectedOption',
|
|
200
|
+
message: '请使用上下键选择一个版本进行回滚,按回车确认:',
|
|
201
|
+
choices: folderNames,
|
|
202
|
+
// 可选:设置默认选中的索引
|
|
203
|
+
default: 0
|
|
204
|
+
}
|
|
205
|
+
];
|
|
206
|
+
|
|
207
|
+
// 执行交互
|
|
208
|
+
inquirer.prompt(questions).then(async answers => {
|
|
209
|
+
logger.warn(`回滚版本号: ${answers.selectedOption}`);
|
|
210
|
+
if (!await authProjectName(answers.selectedOption)) {
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
// 删除目标文件夹代码
|
|
214
|
+
if (!await deleteFolder(`${config.targetDir}/`)) {
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
// 备份上一次发版的代码
|
|
218
|
+
if (!await resetDir(answers.selectedOption)) {
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
logger.success('🎉 生产环境:项目回滚完成!');
|
|
222
|
+
config.cdn && config.cdn.list && refreshCdn(config.cdn.list);
|
|
223
|
+
}).catch(error => {
|
|
224
|
+
if (error.isTtyError) {
|
|
225
|
+
logger.error('❌ 终端不支持交互模式');
|
|
226
|
+
} else {
|
|
227
|
+
logger.error('❌ 生产环境:回滚过程中发生错误:');
|
|
228
|
+
logger.error(error);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
run();
|