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/cdn/index.js
ADDED
package/cdn/refresh.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const $CDN = require('@alicloud/cdn20180510');
|
|
2
|
+
const $OpenApi = require('@alicloud/openapi-client');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const cdnConfig = require('../config/config')[process.env.NODE_ENV];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 使用凭据初始化账号Client
|
|
8
|
+
* @return Client
|
|
9
|
+
* @throws Exception
|
|
10
|
+
*/
|
|
11
|
+
const createClient = () => {
|
|
12
|
+
let config = new $OpenApi.Config({
|
|
13
|
+
// AccessKey ID
|
|
14
|
+
accessKeyId: cdnConfig.cdn.accessKeyId,
|
|
15
|
+
// AccessKey Secret
|
|
16
|
+
accessKeySecret: cdnConfig.cdn.accessKeySecret,
|
|
17
|
+
endpoint: cdnConfig.cdn.endpoint,
|
|
18
|
+
});
|
|
19
|
+
return new $CDN.default(config);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
*
|
|
24
|
+
* @param list
|
|
25
|
+
* @returns {Promise<void>}
|
|
26
|
+
*/
|
|
27
|
+
const run = async (list = []) => {
|
|
28
|
+
logger.warn('开始刷新CDN...')
|
|
29
|
+
let client = createClient();
|
|
30
|
+
for (const item of list) {
|
|
31
|
+
let req = new $CDN.RefreshObjectCachesRequest({
|
|
32
|
+
objectType: item.type,
|
|
33
|
+
objectPath: item.url
|
|
34
|
+
});
|
|
35
|
+
let resp = await client.refreshObjectCaches(req).catch((err) => {
|
|
36
|
+
logger.error("❌刷新CDN缓存失败");
|
|
37
|
+
logger.error(err);
|
|
38
|
+
});
|
|
39
|
+
logger.success("✅ 刷新CDN缓存成功--------------------\n");
|
|
40
|
+
logger.warn(`url:${item.url}\n`);
|
|
41
|
+
logger.info({ statusCode: resp.statusCode, body: resp.body, date: resp.headers.date });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = run;
|
package/config/config.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const { _config, coreConfig } = require('./index');
|
|
2
|
+
|
|
3
|
+
const ossConfigCdn = () => {
|
|
4
|
+
if (!_config[process.env.NODE_ENV].cdn) return {};
|
|
5
|
+
return {
|
|
6
|
+
cdn: {
|
|
7
|
+
accessKeyId: coreConfig.cdn.accessKeyId,
|
|
8
|
+
accessKeySecret: coreConfig.cdn.accessKeySecret,
|
|
9
|
+
endpoint: coreConfig.cdn.endpoint,
|
|
10
|
+
list: _config[process.env.NODE_ENV].cdn,
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// 服务器配置信息
|
|
16
|
+
module.exports = {
|
|
17
|
+
test: {
|
|
18
|
+
localPath: _config.test.localPath,
|
|
19
|
+
host: coreConfig.test.host,
|
|
20
|
+
port: coreConfig.test.port,
|
|
21
|
+
username: coreConfig.test.username,
|
|
22
|
+
// 私钥路径
|
|
23
|
+
privateKey: coreConfig.test.privateKey,
|
|
24
|
+
// 项目旧版本代码保存最近3个月备份
|
|
25
|
+
backUpSeconds: _config.test.backUpSeconds,
|
|
26
|
+
// 如果私钥有密码,需要提供
|
|
27
|
+
// passphrase: 'your-passphrase',
|
|
28
|
+
// 服务器上的部署路径
|
|
29
|
+
path: _config.test.path,
|
|
30
|
+
// 是否备份
|
|
31
|
+
backUp: _config.test.backUp,
|
|
32
|
+
},
|
|
33
|
+
pro: {
|
|
34
|
+
localPath: _config.pro.localPath,
|
|
35
|
+
host: coreConfig.pro.host,
|
|
36
|
+
port: coreConfig.pro.port,
|
|
37
|
+
username: coreConfig.pro.username,
|
|
38
|
+
// 私钥路径
|
|
39
|
+
privateKey: coreConfig.pro.privateKey,
|
|
40
|
+
// 项目旧版本代码保存最近3个月备份
|
|
41
|
+
backUpSeconds: _config.pro.backUpSeconds,
|
|
42
|
+
// 如果私钥有密码,需要提供
|
|
43
|
+
// passphrase: 'your-passphrase',
|
|
44
|
+
// 服务器上的部署路径
|
|
45
|
+
path: _config.pro.path,
|
|
46
|
+
// 是否备份
|
|
47
|
+
backUp: _config.pro.backUp,
|
|
48
|
+
// cdn 配置
|
|
49
|
+
...ossConfigCdn(),
|
|
50
|
+
},
|
|
51
|
+
// 生产oss配置
|
|
52
|
+
oss: {
|
|
53
|
+
accessKeyId: coreConfig.oss.accessKeyId,
|
|
54
|
+
accessKeySecret: coreConfig.oss.accessKeySecret,
|
|
55
|
+
region: coreConfig.oss.region,
|
|
56
|
+
bucket: coreConfig.oss.bucket,
|
|
57
|
+
// 本地打包后的文件夹路径
|
|
58
|
+
localDir: _config.oss.localDir,
|
|
59
|
+
// 上传到OSS的目标路径,默认为根目录
|
|
60
|
+
targetDir: _config.oss.targetDir,
|
|
61
|
+
|
|
62
|
+
// 项目旧版本代码保存最近3个月备份
|
|
63
|
+
backUpSeconds: _config.oss.backUpSeconds,
|
|
64
|
+
// 是否备份
|
|
65
|
+
backUp: _config.pro.backUp,
|
|
66
|
+
// cdn 配置
|
|
67
|
+
...ossConfigCdn(),
|
|
68
|
+
}
|
|
69
|
+
};
|
package/config/index.js
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
let { _config, coreConfig } = require('./config/index')
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
initConfig: (config, _coreConfig) => {
|
|
5
|
+
_config = config;
|
|
6
|
+
coreConfig = _coreConfig;
|
|
7
|
+
},
|
|
8
|
+
clear: () => {
|
|
9
|
+
|
|
10
|
+
},
|
|
11
|
+
upload: () => {
|
|
12
|
+
return require('./upload/index')();
|
|
13
|
+
},
|
|
14
|
+
refresh: () => {
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ql-publish",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"scripts": {},
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@alicloud/cdn20180510": "^7.0.1",
|
|
7
|
+
"@alicloud/openapi-client": "^0.4.14",
|
|
8
|
+
"ali-oss": "^6.23.0",
|
|
9
|
+
"cross-env": "^10.0.0",
|
|
10
|
+
"dayjs": "^1.11.11",
|
|
11
|
+
"readline": "^1.3.0",
|
|
12
|
+
"scp2": "^0.5.0",
|
|
13
|
+
"ssh2": "^1.16.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {},
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"keywords": [
|
|
18
|
+
"auto Publish",
|
|
19
|
+
"ql"
|
|
20
|
+
],
|
|
21
|
+
"author": "Jresun",
|
|
22
|
+
"license": "ISC",
|
|
23
|
+
"description": ""
|
|
24
|
+
}
|
package/upload/clear.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const config = require('../config/config.js')[process.env.NODE_ENV];
|
|
2
|
+
const logger = require('../utils/logger');
|
|
3
|
+
const { Client } = require('ssh2');
|
|
4
|
+
const readline = require("readline");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
|
|
7
|
+
const readStreamByFile = async (sftp, remoteFilePath) => {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
console.log(`准备读取文件: ${remoteFilePath}`);
|
|
10
|
+
|
|
11
|
+
// 创建读取流
|
|
12
|
+
const readStream = sftp.createReadStream(remoteFilePath, {
|
|
13
|
+
encoding: 'utf8' // 以UTF-8编码读取
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
let fileContent = '';
|
|
17
|
+
|
|
18
|
+
// 收集文件内容
|
|
19
|
+
readStream.on('data', (chunk) => {
|
|
20
|
+
fileContent += chunk;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 读取完成处理
|
|
24
|
+
readStream.on('end', () => {
|
|
25
|
+
try {
|
|
26
|
+
// 解析JSON数据
|
|
27
|
+
const jsonData = JSON.parse(fileContent);
|
|
28
|
+
console.log('文件内容解析成功:');
|
|
29
|
+
resolve(jsonData)
|
|
30
|
+
} catch (parseErr) {
|
|
31
|
+
logger.error('JSON解析失败:', parseErr);
|
|
32
|
+
console.log('原始文件内容:', fileContent);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// 处理读取错误
|
|
38
|
+
readStream.on('error', (err) => {
|
|
39
|
+
logger.error('读取文件时发生错误:', err);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
});
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isHasDir = (sftp, dirPath) => {
|
|
46
|
+
return new Promise(resolve => {
|
|
47
|
+
sftp.stat(dirPath, (err) => {
|
|
48
|
+
resolve(!err);
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const authProjectName = () => {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
// 创建readline接口
|
|
56
|
+
const rl = readline.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// 向用户提问
|
|
62
|
+
rl.question(`${process.env.NODE_ENV}环境:是否确认删除map映射文件 ${config.path} 项目? (输入 y 继续) `, (answer) => {
|
|
63
|
+
// 检查用户输入是否为'y'(不区分大小写)
|
|
64
|
+
if (answer.trim().toLowerCase() === 'y') {
|
|
65
|
+
resolve(true);
|
|
66
|
+
} else {
|
|
67
|
+
logger.error('❌ 发布程序终止');
|
|
68
|
+
resolve(false);
|
|
69
|
+
}
|
|
70
|
+
// 关闭接口
|
|
71
|
+
rl.close();
|
|
72
|
+
});
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const deleteFileByMapJson = async (sftp, currentSourceDir, mtime) => {
|
|
77
|
+
// 获取源目录中的所有项目
|
|
78
|
+
const items = await new Promise((resolve, reject) => {
|
|
79
|
+
sftp.readdir(currentSourceDir, (err, list) => {
|
|
80
|
+
if (err) {
|
|
81
|
+
reject(new Error(`读取目录失败 ${currentSourceDir}: ${err.message}`));
|
|
82
|
+
} else {
|
|
83
|
+
resolve(list);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// 处理每个项目
|
|
89
|
+
for (const item of items) {
|
|
90
|
+
const sourcePath = path.posix.join(currentSourceDir, item.filename);
|
|
91
|
+
|
|
92
|
+
if (item.attrs.isDirectory()) {
|
|
93
|
+
// 如果是目录,递归处理
|
|
94
|
+
await deleteFileByMapJson(sftp, sourcePath, mtime);
|
|
95
|
+
} else {
|
|
96
|
+
// 删除文件
|
|
97
|
+
if (mtime >= item.attrs.mtime) {
|
|
98
|
+
// console.log(`删除文件: ${sourcePath}`,);
|
|
99
|
+
await sftp.unlink(sourcePath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function run() {
|
|
106
|
+
if (!await authProjectName()) {
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let conn, sftp;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
logger.warn('连接到服务器...');
|
|
114
|
+
// 连接到服务器
|
|
115
|
+
conn = new Client();
|
|
116
|
+
await new Promise((resolve, reject) => {
|
|
117
|
+
conn.on('ready', resolve);
|
|
118
|
+
conn.on('error', (err) => reject(new Error(`❌ 服务器连接失败: ${err.message}`)));
|
|
119
|
+
conn.connect(config);
|
|
120
|
+
});
|
|
121
|
+
logger.info('✅ 已成功连接到服务器');
|
|
122
|
+
|
|
123
|
+
// 初始化SFTP
|
|
124
|
+
sftp = await new Promise((resolve, reject) => {
|
|
125
|
+
conn.sftp((err, sftpClient) => {
|
|
126
|
+
if (err) {
|
|
127
|
+
reject(new Error(`SFTP初始化失败: ${err.message}`));
|
|
128
|
+
} else {
|
|
129
|
+
resolve(sftpClient);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// 没有项目目录,直接返回
|
|
135
|
+
if (!await isHasDir(sftp, config.path)) {
|
|
136
|
+
logger.warn('✅ 新项目,无需清理旧文件')
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
// 没有map文件,直接返回
|
|
140
|
+
if (!await isHasDir(sftp, `${config.path}/fileMap.json`)) {
|
|
141
|
+
logger.warn('✅ 没有map映射文件,无需清理旧文件')
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const { mtime } = await readStreamByFile(sftp, `${config.path}/fileMap.json`);
|
|
145
|
+
logger.warn('正在删除文件...')
|
|
146
|
+
await deleteFileByMapJson(sftp, config.path, mtime);
|
|
147
|
+
await deleteFileByMapJson(sftp, config.path, mtime);
|
|
148
|
+
logger.success(`✅ ${process.env.NODE_ENV}环境,根据map删除映射文件成功!`)
|
|
149
|
+
} catch (err) {
|
|
150
|
+
logger.error('❌ 过程出错:', err.message);
|
|
151
|
+
} finally {
|
|
152
|
+
// 关闭连接
|
|
153
|
+
if (sftp) sftp.end();
|
|
154
|
+
if (conn) conn.end();
|
|
155
|
+
// console.log('连接已关闭');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
run();
|
package/upload/index.js
ADDED
package/upload/oss.js
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const config = require('../config/config.js')[process.env.NODE_ENV];
|
|
2
|
+
const OSS = require('ali-oss');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const dayjs = require('dayjs');
|
|
5
|
+
const logger = require('../utils/logger');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const refreshCdn = require('../cdn/refresh')
|
|
9
|
+
|
|
10
|
+
// 初始化OSS客户端
|
|
11
|
+
const client = new OSS(config);
|
|
12
|
+
|
|
13
|
+
const reduceLog = () => {
|
|
14
|
+
return Math.random() * 2 > 1.98
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 复制OSS中的文件夹到指定目录
|
|
19
|
+
* @param {string} sourcePrefix - 源文件夹前缀(如: 'source-folder/')
|
|
20
|
+
* @param {string} targetBucket - 目标Bucket名称(可以与源Bucket相同)
|
|
21
|
+
* @param {string} targetPrefix - 目标文件夹前缀(如: 'target-folder/')
|
|
22
|
+
* @param {string} [delimiter=''] - 分隔符,用于列出文件夹下的文件
|
|
23
|
+
*/
|
|
24
|
+
async function copyFolder(sourcePrefix, targetBucket, targetPrefix, delimiter = '') {
|
|
25
|
+
try {
|
|
26
|
+
let marker = '';
|
|
27
|
+
const maxKeys = 100; // 每次最多获取100个文件,可根据需要调整
|
|
28
|
+
|
|
29
|
+
do {
|
|
30
|
+
// 列出源文件夹下的文件
|
|
31
|
+
const result = await client.list({
|
|
32
|
+
prefix: sourcePrefix,
|
|
33
|
+
marker,
|
|
34
|
+
maxKeys,
|
|
35
|
+
delimiter
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// 复制文件
|
|
39
|
+
if (result.objects && result.objects.length > 0) {
|
|
40
|
+
for (let file of result.objects) {
|
|
41
|
+
// 构建目标文件路径
|
|
42
|
+
const targetPath = targetPrefix + file.name.replace(sourcePrefix, '');
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// 复制文件
|
|
46
|
+
await client.copy(targetPath, file.name);
|
|
47
|
+
reduceLog() && logger.log(`✅ 已复制: ${file.name} -> ${targetPath}`);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
logger.error(`❌ 复制失败 ${file.name}:`, err.message);
|
|
50
|
+
throw new Error(err.message)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 处理子目录(如果有)
|
|
56
|
+
if (result.prefixes && result.prefixes.length > 0) {
|
|
57
|
+
for (const prefix of result.prefixes) {
|
|
58
|
+
// 递归复制子目录
|
|
59
|
+
await copyFolder(prefix, targetBucket, targetPrefix, delimiter);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
marker = result.nextMarker;
|
|
64
|
+
} while (marker);
|
|
65
|
+
|
|
66
|
+
logger.warn('🎉 文件夹复制完成');
|
|
67
|
+
return true;
|
|
68
|
+
} catch (err) {
|
|
69
|
+
logger.error('❌ 复制文件夹时发生错误:', err);
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 删除OSS中的文件夹及其所有内容
|
|
76
|
+
* @param {string} folderPath - 要删除的文件夹路径,例如: 'test-folder/'
|
|
77
|
+
*/
|
|
78
|
+
async function deleteFolder(folderPath) {
|
|
79
|
+
try {
|
|
80
|
+
let nextMarker = null;
|
|
81
|
+
let deletedCount = 0;
|
|
82
|
+
|
|
83
|
+
do {
|
|
84
|
+
// 列出文件夹下的所有文件
|
|
85
|
+
const result = await client.list({
|
|
86
|
+
prefix: folderPath, // 只列出指定前缀的文件
|
|
87
|
+
marker: nextMarker, // 分页标记
|
|
88
|
+
maxKeys: 100 // 每次最多列出100个文件
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 添加检查:确保 result.objects 存在且为数组
|
|
92
|
+
if (!result.objects || result.objects.length === 0) {
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 准备要删除的文件列表
|
|
97
|
+
const objects = result.objects.filter(obj => obj && obj.name).map(obj => obj.name);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// 批量删除文件
|
|
101
|
+
await client.deleteMulti(objects, { quiet: false });
|
|
102
|
+
} catch (e) {
|
|
103
|
+
throw new Error(e.message)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
deletedCount += objects.length;
|
|
107
|
+
nextMarker = result.nextMarker;
|
|
108
|
+
|
|
109
|
+
} while (nextMarker);
|
|
110
|
+
|
|
111
|
+
logger.warn(`✅ 成功删除文件夹 "${folderPath}" 及其 ${deletedCount} 个文件`);
|
|
112
|
+
return true;
|
|
113
|
+
|
|
114
|
+
} catch (err) {
|
|
115
|
+
logger.error('❌ 删除文件夹失败:', err);
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 递归读取目录并上传文件
|
|
121
|
+
async function uploadDir(localPath, targetPath = '') {
|
|
122
|
+
const files = fs.readdirSync(localPath);
|
|
123
|
+
|
|
124
|
+
for (const file of files) {
|
|
125
|
+
const localFilePath = path.join(localPath, file);
|
|
126
|
+
const stats = fs.statSync(localFilePath);
|
|
127
|
+
|
|
128
|
+
// 处理目标路径
|
|
129
|
+
const ossFilePath = targetPath
|
|
130
|
+
? `${targetPath}/${file}`
|
|
131
|
+
: file;
|
|
132
|
+
|
|
133
|
+
if (stats.isDirectory()) {
|
|
134
|
+
// 如果是目录,递归处理
|
|
135
|
+
await uploadDir(localFilePath, ossFilePath);
|
|
136
|
+
} else {
|
|
137
|
+
// 如果是文件,上传到OSS
|
|
138
|
+
try {
|
|
139
|
+
await client.put(ossFilePath, localFilePath);
|
|
140
|
+
reduceLog() && logger.log(`✅ 上传成功: ${ossFilePath}`);
|
|
141
|
+
} catch (e) {
|
|
142
|
+
logger.error(`❌ 上传失败: ${ossFilePath}`, e);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const deleteOldBackupDir = async () => {
|
|
149
|
+
try {
|
|
150
|
+
const folderPath = `${config.targetDir}_old/`;
|
|
151
|
+
let nextMarker = null;
|
|
152
|
+
|
|
153
|
+
do {
|
|
154
|
+
// 列出文件夹下的所有文件
|
|
155
|
+
const result = await client.list({
|
|
156
|
+
prefix: folderPath, // 只列出指定前缀的文件
|
|
157
|
+
marker: nextMarker, // 分页标记
|
|
158
|
+
maxKeys: 100 // 每次最多列出100个文件
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 添加检查:确保 result.objects 存在且为数组
|
|
162
|
+
if (!result.objects || result.objects.length === 0) {
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 准备要删除的文件列表
|
|
167
|
+
const objects = result.objects.filter(obj => {
|
|
168
|
+
if (obj && obj.name) {
|
|
169
|
+
let fileDate = obj.name.split('/')[1].split('_');
|
|
170
|
+
fileDate.length = 3
|
|
171
|
+
if (!fileDate.length) return false;
|
|
172
|
+
fileDate = fileDate.join('-');
|
|
173
|
+
return dayjs().diff(dayjs(`${fileDate} 00:00:00`), 'seconds') > config.backUpSeconds;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}).map(obj => obj.name);
|
|
177
|
+
|
|
178
|
+
if (objects.length !== 0) {
|
|
179
|
+
try {
|
|
180
|
+
// 批量删除文件
|
|
181
|
+
await client.deleteMulti(objects, { quiet: false });
|
|
182
|
+
} catch (e) {
|
|
183
|
+
throw new Error(e.message)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
nextMarker = result.nextMarker;
|
|
187
|
+
} while (nextMarker);
|
|
188
|
+
|
|
189
|
+
logger.warn(`✅ 成功删除3个月内备份`);
|
|
190
|
+
return true;
|
|
191
|
+
|
|
192
|
+
} catch (err) {
|
|
193
|
+
logger.error('❌ 删除3个月内备份文件夹失败:', err);
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const backupDir = async () => {
|
|
199
|
+
const sourceFolder = `${config.targetDir}/`;
|
|
200
|
+
// 目标Bucket(可以与源Bucket相同)
|
|
201
|
+
const targetBucket = config.bucket;
|
|
202
|
+
// 目标文件夹(注意末尾的斜杠)
|
|
203
|
+
const targetFolder = `${config.targetDir}_old/${dayjs().format("YYYY_MM_DD_HH_mm_ss")}/`;
|
|
204
|
+
logger.info(`开始备份旧代码到 ${targetFolder} 目录`);
|
|
205
|
+
return await copyFolder(sourceFolder, targetBucket, targetFolder);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const authProjectName = () => {
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
// 创建readline接口
|
|
211
|
+
const rl = readline.createInterface({
|
|
212
|
+
input: process.stdin,
|
|
213
|
+
output: process.stdout
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// 向用户提问
|
|
217
|
+
rl.question(`是否确认发布 ${config.targetDir} 项目? (输入 y 继续) `, (answer) => {
|
|
218
|
+
// 检查用户输入是否为'y'(不区分大小写)
|
|
219
|
+
if (answer.trim().toLowerCase() === 'y') {
|
|
220
|
+
resolve(true);
|
|
221
|
+
} else {
|
|
222
|
+
logger.error('❌ 发布程序终止');
|
|
223
|
+
resolve(false);
|
|
224
|
+
}
|
|
225
|
+
// 关闭接口
|
|
226
|
+
rl.close();
|
|
227
|
+
});
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 执行上传
|
|
232
|
+
async function run() {
|
|
233
|
+
if (!await authProjectName()) {
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
logger.info('开始上传到阿里云OSS...');
|
|
238
|
+
|
|
239
|
+
// 删除3个月之前的备份
|
|
240
|
+
if (!await deleteOldBackupDir()) {
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 备份上一次发版的代码
|
|
245
|
+
if (!await backupDir()) {
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 删除目标文件夹代码
|
|
250
|
+
if (!await deleteFolder(`${config.targetDir}/`)) {
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!fs.existsSync(config.localDir)) {
|
|
255
|
+
logger.error(`错误: 目录 ${config.localDir} 不存在,请先执行打包命令`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
logger.info('开始上传项目文件:')
|
|
261
|
+
await uploadDir(config.localDir, config.targetDir);
|
|
262
|
+
logger.success('🎉 生产环境:所有文件上传完成!');
|
|
263
|
+
|
|
264
|
+
config.cdn && config.cdn.list && refreshCdn(config.cdn.list);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
logger.error('❌ 生产环境:上传过程中发生错误:');
|
|
267
|
+
logger.error(error);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = run;
|