@cloudbase/manager-node 5.9.4 → 5.10.0
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/CHANGELOG.md +3 -0
- package/changelog_content_en.md +2 -2
- package/lib/database/index.js +76 -0
- package/lib/deploy/DeployOrchestrator.js +225 -25
- package/lib/deploy/FunctionDeployer.js +5 -2
- package/lib/deploy/StaticDeployer.js +469 -142
- package/lib/deploy/framework.js +4 -3
- package/lib/env/index.js +2 -1
- package/lib/error-types.js +8 -1
- package/lib/function/index.js +2 -2
- package/lib/hosting/index.js +9 -3
- package/lib/platform/index.js +279 -0
- package/lib/storage/index.js +37 -14
- package/lib/utils/cos-upload.js +69 -0
- package/lib/utils/fs.js +86 -0
- package/lib/utils/ignore.js +148 -0
- package/lib/utils/index.js +23 -1
- package/package.json +1 -1
- package/types/database/index.d.ts +55 -1
- package/types/database/type.d.ts +112 -0
- package/types/deploy/DeployOrchestrator.d.ts +107 -1
- package/types/deploy/StaticDeployer.d.ts +199 -35
- package/types/deploy/framework.d.ts +4 -3
- package/types/error-types.d.ts +9 -0
- package/types/platform/index.d.ts +54 -1
- package/types/platform/type.d.ts +118 -0
- package/types/utils/cos-upload.d.ts +17 -0
- package/types/utils/fs.d.ts +55 -0
- package/types/utils/ignore.d.ts +76 -0
- package/types/utils/index.d.ts +4 -0
package/lib/deploy/framework.js
CHANGED
|
@@ -234,9 +234,10 @@ function resolveHostingOutputDir(config, root, readPkgFile = defaultReadPkgFile)
|
|
|
234
234
|
/**
|
|
235
235
|
* 本地构建单个 hosting 项(仅执行 buildCommand,不安装依赖,不上传)
|
|
236
236
|
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
237
|
+
* 从原 StaticDeployer.runLocalBuild 提炼的纯函数版(该方法已随「声明式部署只部署
|
|
238
|
+
* 不构建」一并移除,构建入口只剩这里):不依赖 Environment,供 CLI 的 tcb app build
|
|
239
|
+
* 与任何需要「只构建不上传」的调用方复用,避免各端各自复制推断/执行逻辑。
|
|
240
|
+
* buildCommand 为空 → 直接返回 null。
|
|
240
241
|
*
|
|
241
242
|
* @param config hosting 项配置(framework/buildCommand/outputDir,root 单独传参)
|
|
242
243
|
* @param root 项目目录(绝对路径)
|
package/lib/env/index.js
CHANGED
package/lib/error-types.js
CHANGED
|
@@ -17,5 +17,12 @@ exports.ERROR_TYPES = {
|
|
|
17
17
|
netError: { code: 'NETWORK_ERROR' },
|
|
18
18
|
apiError: { code: 'API_ERROR' },
|
|
19
19
|
credentialError: { code: 'MISSING_CREDENTIAL' },
|
|
20
|
-
fileError: { code: 'FILE_READ_ERROR' }
|
|
20
|
+
fileError: { code: 'FILE_READ_ERROR' },
|
|
21
|
+
/**
|
|
22
|
+
* 部署前未找到构建产物(目录不存在或为空)。
|
|
23
|
+
*
|
|
24
|
+
* 声明式部署(`tcb deploy`)不执行构建,产物必须由用户先产出;
|
|
25
|
+
* 这个码让 CLI 侧能稳定映射到非零退出码并给出「先构建」的引导。
|
|
26
|
+
*/
|
|
27
|
+
buildOutputNotFound: { code: 'BUILD_OUTPUT_NOT_FOUND' }
|
|
21
28
|
};
|
package/lib/function/index.js
CHANGED
|
@@ -1978,13 +1978,13 @@ class FunctionService {
|
|
|
1978
1978
|
if (!(base64 === null || base64 === void 0 ? void 0 : base64.length)) {
|
|
1979
1979
|
(0, error_1.throwInvalidParam)('文件不能为空');
|
|
1980
1980
|
}
|
|
1981
|
-
|
|
1981
|
+
(0, utils_1.debugLog)(`[${func.name}] 部署方式: ZIP base64 上传`);
|
|
1982
1982
|
return {
|
|
1983
1983
|
ZipFile: base64
|
|
1984
1984
|
};
|
|
1985
1985
|
}
|
|
1986
1986
|
// 默认走 COS 上传(SCF: scf.GetTempCosInfo + COS SDK;CBF: tcb.GetTempCosInfo + PutUrl 直传)
|
|
1987
|
-
|
|
1987
|
+
(0, utils_1.debugLog)(`[${func.name}] 部署方式: COS 上传`);
|
|
1988
1988
|
const { backend } = this.getFunctionConfig();
|
|
1989
1989
|
const region = this.environment.cloudBaseContext.region || constant_1.SCF_TEMP_COS.DEFAULT_REGION;
|
|
1990
1990
|
const result = await this.uploadFunctionZipToCos(options, installDependency);
|
package/lib/hosting/index.js
CHANGED
|
@@ -17,6 +17,7 @@ const error_1 = require("../error");
|
|
|
17
17
|
const utils_1 = require("../utils");
|
|
18
18
|
const parallel_1 = require("../utils/parallel");
|
|
19
19
|
const storage_config_1 = require("../utils/storage-config");
|
|
20
|
+
const ignore_1 = require("../utils/ignore");
|
|
20
21
|
const envDomainCache = new Map();
|
|
21
22
|
const HostingStatusMap = {
|
|
22
23
|
init: '初始化中',
|
|
@@ -135,6 +136,11 @@ class HostingService {
|
|
|
135
136
|
const { localPath, cloudPath, entryFiles, files = [], onProgress, onFileFinish, parallel = 20, ignore = ['.DS_Store'], retryCount, retryInterval, verify = false, safe = false, prune = false } = options;
|
|
136
137
|
const { bucket, region, basePath } = await this.getHostingBucket();
|
|
137
138
|
const storageService = await this.environment.getStorageService();
|
|
139
|
+
// 敏感文件排除在**入口**兜底,不能留给调用方(stbug-cg6I6WXOdb):
|
|
140
|
+
// 下方三条分支(一致发布 / 目录 / 单文件)都会把 ignore 继续透传,
|
|
141
|
+
// 只在某一条分支上补默认清单,就会出现「目录传得上、单文件传得上」的口径分叉。
|
|
142
|
+
// 用户 ignore 只能追加:`undefined` / `[]` 都不表示「不排除 .env / .git / 私钥」。
|
|
143
|
+
const effectiveIgnore = (0, ignore_1.mergeArtifactIgnore)(ignore);
|
|
138
144
|
let uploadFiles = Array.isArray(files) ? files : [];
|
|
139
145
|
// localPath 存在,上传文件夹/文件
|
|
140
146
|
if (localPath) {
|
|
@@ -153,7 +159,7 @@ class HostingService {
|
|
|
153
159
|
onProgress,
|
|
154
160
|
onFileFinish,
|
|
155
161
|
fileId: false,
|
|
156
|
-
ignore,
|
|
162
|
+
ignore: effectiveIgnore,
|
|
157
163
|
parallel,
|
|
158
164
|
retryCount,
|
|
159
165
|
retryInterval,
|
|
@@ -172,7 +178,7 @@ class HostingService {
|
|
|
172
178
|
onProgress,
|
|
173
179
|
onFileFinish,
|
|
174
180
|
fileId: false,
|
|
175
|
-
ignore,
|
|
181
|
+
ignore: effectiveIgnore,
|
|
176
182
|
parallel,
|
|
177
183
|
retryCount,
|
|
178
184
|
retryInterval,
|
|
@@ -189,7 +195,7 @@ class HostingService {
|
|
|
189
195
|
}
|
|
190
196
|
// 文件上传统一通过批量上传接口
|
|
191
197
|
return storageService.uploadFilesCustom({
|
|
192
|
-
ignore,
|
|
198
|
+
ignore: effectiveIgnore,
|
|
193
199
|
parallel,
|
|
194
200
|
onProgress,
|
|
195
201
|
onFileFinish,
|
package/lib/platform/index.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
exports.PlatformService = void 0;
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
4
9
|
const error_1 = require("../error");
|
|
10
|
+
const error_types_1 = require("../error-types");
|
|
5
11
|
const utils_1 = require("../utils");
|
|
12
|
+
const cos_upload_1 = require("../utils/cos-upload");
|
|
6
13
|
/**
|
|
7
14
|
* 平台版服务
|
|
8
15
|
* 提供平台版套餐查询、资源用量统计、环境生命周期管理及 HTTP 访问服务路由管理能力
|
|
@@ -173,5 +180,277 @@ class PlatformService {
|
|
|
173
180
|
async describePlatformHttpServiceRoute(params) {
|
|
174
181
|
return this.cloudService.request('DescribePlatformHTTPServiceRoute', Object.assign({}, params));
|
|
175
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* 上传本地错误页文件并创建 HTTP 访问服务自定义错误页配置
|
|
185
|
+
* 文件上传至平台静态托管桶根目录 error-pages/{PlatformId}/{文件名},不拼接 BasePath。
|
|
186
|
+
* 文件名中的空白和括号替换为 -;同名文件会被覆盖。
|
|
187
|
+
* ResourceType 固定为 STATIC_STORE,ResourceName 固定为 staticstore,RewritePath 自动生成。
|
|
188
|
+
* 支持一次为多个状态码绑定同一个错误页;同一状态码只允许一个配置,创建前会先查重,
|
|
189
|
+
* 命中已有配置时直接报错且不上传文件。
|
|
190
|
+
* 上传失败时不创建规则;创建失败时保留已上传文件,不自动删除或回滚覆盖的对象。
|
|
191
|
+
* @param {CreateHttpServiceErrorPageParams} params 平台版 ID、状态码列表和本地文件路径
|
|
192
|
+
* @returns {Promise<CreateHttpServiceErrorPageRes>} 创建成功个数
|
|
193
|
+
*/
|
|
194
|
+
async createHttpServiceErrorPage(params) {
|
|
195
|
+
var _a;
|
|
196
|
+
const { PlatformId, HttpStatusCode, localPath } = params;
|
|
197
|
+
if (typeof PlatformId !== 'string' || !/^pf-[a-zA-Z0-9-]+$/.test(PlatformId)) {
|
|
198
|
+
(0, error_1.throwInvalidParam)('PlatformId 必须是有效的平台版 ID');
|
|
199
|
+
}
|
|
200
|
+
if (!Array.isArray(HttpStatusCode) ||
|
|
201
|
+
!HttpStatusCode.length ||
|
|
202
|
+
HttpStatusCode.some(code => typeof code !== 'string' || !/^[1-5]\d{2}$/.test(code))) {
|
|
203
|
+
(0, error_1.throwInvalidParam)('HttpStatusCode 必须是非空 HTTP 状态码字符串数组,如 [\'404\', \'403\']');
|
|
204
|
+
}
|
|
205
|
+
const statusCodes = Array.from(new Set(HttpStatusCode));
|
|
206
|
+
// 同一状态码只允许一个配置,先查重,避免重复创建且不白上传文件
|
|
207
|
+
const { Configs } = await this.describeHttpServiceErrorPage({
|
|
208
|
+
PlatformId,
|
|
209
|
+
HttpStatusCode: statusCodes
|
|
210
|
+
});
|
|
211
|
+
const occupiedCodes = [];
|
|
212
|
+
for (const item of Configs !== null && Configs !== void 0 ? Configs : []) {
|
|
213
|
+
for (const code of (_a = item.HttpStatusCode) !== null && _a !== void 0 ? _a : []) {
|
|
214
|
+
if (statusCodes.includes(code) && !occupiedCodes.includes(code)) {
|
|
215
|
+
occupiedCodes.push(code);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (occupiedCodes.length) {
|
|
220
|
+
(0, error_1.throwInvalidParam)(`状态码 ${occupiedCodes.join('、')} 已存在错误页配置,请使用 modifyHttpServiceErrorPage 更新`);
|
|
221
|
+
}
|
|
222
|
+
const rewritePath = await this.uploadErrorPageFile(PlatformId, localPath);
|
|
223
|
+
try {
|
|
224
|
+
return await this.cloudService.request('CreateHTTPServiceErrorPage', {
|
|
225
|
+
PlatformId,
|
|
226
|
+
Config: {
|
|
227
|
+
HttpStatusCode: statusCodes,
|
|
228
|
+
ResourceType: 'STATIC_STORE',
|
|
229
|
+
ResourceName: 'staticstore',
|
|
230
|
+
RewritePath: rewritePath
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
const message = `错误页文件已上传至 ${rewritePath},但创建规则失败,文件已保留:${error instanceof Error ? error.message : String(error)}`;
|
|
236
|
+
throw new error_1.CloudBaseError(message, Object.assign(Object.assign({}, (error instanceof error_1.CloudBaseError
|
|
237
|
+
? { code: error.code, source: error.source || undefined, requestId: error.requestId }
|
|
238
|
+
: { type: error_types_1.ERROR_TYPES.apiError })), { action: 'CreateHTTPServiceErrorPage', original: error instanceof Error ? error : undefined }));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* 更新 HTTP 访问服务自定义错误页配置
|
|
243
|
+
* 与 ModifyHTTPServiceErrorPage 的入参保持一致:按 Id 更新一条配置,HttpStatusCode 为修改后的完整集合。
|
|
244
|
+
* 传入 localPath 时上传新文件并以 error-pages/{PlatformId}/{文件名} 作为 RewritePath;
|
|
245
|
+
* 不传 localPath 时沿用该记录现有文件,仅更新状态码等配置。
|
|
246
|
+
* 一条记录只对应一份错误页文件,换文件会作用于该记录绑定的全部状态码。
|
|
247
|
+
* 上传失败时不更新规则;更新失败时保留已上传文件,不自动删除或回滚覆盖的对象。
|
|
248
|
+
* @param {ModifyHttpServiceErrorPageParams} params 平台版 ID、配置 ID、状态码集合与可选的本地文件路径
|
|
249
|
+
* @returns {Promise<ModifyHttpServiceErrorPageRes>} 更新成功个数
|
|
250
|
+
*/
|
|
251
|
+
async modifyHttpServiceErrorPage(params) {
|
|
252
|
+
var _a;
|
|
253
|
+
const { PlatformId, Id, HttpStatusCode, localPath } = params;
|
|
254
|
+
if (typeof PlatformId !== 'string' || !/^pf-[a-zA-Z0-9-]+$/.test(PlatformId)) {
|
|
255
|
+
(0, error_1.throwInvalidParam)('PlatformId 必须是有效的平台版 ID');
|
|
256
|
+
}
|
|
257
|
+
if (!Number.isInteger(Id) || Id <= 0) {
|
|
258
|
+
(0, error_1.throwInvalidParam)('Id 必须是 describeHttpServiceErrorPage 返回的正整数配置 ID');
|
|
259
|
+
}
|
|
260
|
+
if (!Array.isArray(HttpStatusCode) ||
|
|
261
|
+
!HttpStatusCode.length ||
|
|
262
|
+
HttpStatusCode.some(code => typeof code !== 'string' || !/^[1-5]\d{2}$/.test(code))) {
|
|
263
|
+
(0, error_1.throwInvalidParam)('HttpStatusCode 必须是非空 HTTP 状态码字符串数组,如 [\'404\']');
|
|
264
|
+
}
|
|
265
|
+
const statusCodes = Array.from(new Set(HttpStatusCode));
|
|
266
|
+
// 一次查询同时用于取回现有配置(沿用文件、清理旧对象)和校验状态码占用
|
|
267
|
+
const { Configs } = await this.describeHttpServiceErrorPage({ PlatformId });
|
|
268
|
+
const target = (Configs !== null && Configs !== void 0 ? Configs : []).find(item => item.Id === Id);
|
|
269
|
+
const conflictCodes = [];
|
|
270
|
+
for (const item of Configs !== null && Configs !== void 0 ? Configs : []) {
|
|
271
|
+
// 同一状态码只允许一个配置,跳过自身记录
|
|
272
|
+
if (item.Id === Id)
|
|
273
|
+
continue;
|
|
274
|
+
for (const code of (_a = item.HttpStatusCode) !== null && _a !== void 0 ? _a : []) {
|
|
275
|
+
if (statusCodes.includes(code) && !conflictCodes.includes(code)) {
|
|
276
|
+
conflictCodes.push(code);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (conflictCodes.length) {
|
|
281
|
+
(0, error_1.throwInvalidParam)(`状态码 ${conflictCodes.join('、')} 已存在错误页配置,请先删除后再修改`);
|
|
282
|
+
}
|
|
283
|
+
let uploadedPath;
|
|
284
|
+
let config;
|
|
285
|
+
if (localPath === undefined) {
|
|
286
|
+
if (!(target === null || target === void 0 ? void 0 : target.RewritePath)) {
|
|
287
|
+
(0, error_1.throwNotFound)(`Id 为 ${Id} 的错误页配置不存在或缺少 RewritePath,无法沿用原有文件,请传入 localPath 重新上传`);
|
|
288
|
+
}
|
|
289
|
+
config = {
|
|
290
|
+
HttpStatusCode: statusCodes,
|
|
291
|
+
ResourceType: 'STATIC_STORE',
|
|
292
|
+
ResourceName: target.ResourceName || 'staticstore',
|
|
293
|
+
RewritePath: target.RewritePath
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
uploadedPath = await this.uploadErrorPageFile(PlatformId, localPath);
|
|
298
|
+
config = {
|
|
299
|
+
HttpStatusCode: statusCodes,
|
|
300
|
+
ResourceType: 'STATIC_STORE',
|
|
301
|
+
ResourceName: 'staticstore',
|
|
302
|
+
RewritePath: uploadedPath
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const result = await this.cloudService.request('ModifyHTTPServiceErrorPage', { PlatformId, Id, Config: config });
|
|
307
|
+
// 换了文件:异步清理被替换的旧文件
|
|
308
|
+
if (uploadedPath && (target === null || target === void 0 ? void 0 : target.RewritePath) && target.RewritePath !== uploadedPath) {
|
|
309
|
+
this.cleanupErrorPageFile(PlatformId, target.RewritePath, Id).catch(() => { });
|
|
310
|
+
}
|
|
311
|
+
return result;
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
if (!uploadedPath) {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
const message = `错误页文件已上传至 ${uploadedPath},但更新规则失败,文件已保留:${error instanceof Error ? error.message : String(error)}`;
|
|
318
|
+
throw new error_1.CloudBaseError(message, Object.assign(Object.assign({}, (error instanceof error_1.CloudBaseError
|
|
319
|
+
? { code: error.code, source: error.source || undefined, requestId: error.requestId }
|
|
320
|
+
: { type: error_types_1.ERROR_TYPES.apiError })), { action: 'ModifyHTTPServiceErrorPage', original: error instanceof Error ? error : undefined }));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* 删除 HTTP 访问服务自定义错误页配置
|
|
325
|
+
* 直接按 Id 整条删除;一条记录可能同时绑定多个状态码(共用同一份静态资源与 Id),
|
|
326
|
+
* 删除会一并移除这些状态码的映射。
|
|
327
|
+
* 删除成功后异步清理对应的 COS 文件,清理失败不影响删除结果;
|
|
328
|
+
* 该文件仍被其他配置引用时不会删除。
|
|
329
|
+
* @param {DeleteHttpServiceErrorPageParams} params 平台版 ID 与配置 ID
|
|
330
|
+
* @returns {Promise<DeleteHttpServiceErrorPageRes>} 删除成功个数
|
|
331
|
+
*/
|
|
332
|
+
async deleteHttpServiceErrorPage(params) {
|
|
333
|
+
var _a;
|
|
334
|
+
const { PlatformId, Id } = params;
|
|
335
|
+
if (typeof PlatformId !== 'string' || !/^pf-[a-zA-Z0-9-]+$/.test(PlatformId)) {
|
|
336
|
+
(0, error_1.throwInvalidParam)('PlatformId 必须是有效的平台版 ID');
|
|
337
|
+
}
|
|
338
|
+
if (!Number.isInteger(Id) || Id <= 0) {
|
|
339
|
+
(0, error_1.throwInvalidParam)('Id 必须是 describeHttpServiceErrorPage 返回的正整数配置 ID');
|
|
340
|
+
}
|
|
341
|
+
// 删除前先取回 RewritePath:删除后配置不可查,届时无法定位待清理的文件
|
|
342
|
+
let rewritePath;
|
|
343
|
+
try {
|
|
344
|
+
const { Configs } = await this.describeHttpServiceErrorPage({ PlatformId });
|
|
345
|
+
rewritePath = (_a = Configs === null || Configs === void 0 ? void 0 : Configs.find(item => item.Id === Id)) === null || _a === void 0 ? void 0 : _a.RewritePath;
|
|
346
|
+
}
|
|
347
|
+
catch (_b) {
|
|
348
|
+
// 查询失败不阻断删除,仅跳过文件清理
|
|
349
|
+
rewritePath = undefined;
|
|
350
|
+
}
|
|
351
|
+
const result = await this.cloudService.request('DeleteHTTPServiceErrorPage', { PlatformId, Id });
|
|
352
|
+
if (rewritePath) {
|
|
353
|
+
this.cleanupErrorPageFile(PlatformId, rewritePath, Id).catch(() => { });
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* 查询 HTTP 访问服务自定义错误页配置列表
|
|
359
|
+
* 平台版能力,通过 PlatformId 指定平台。
|
|
360
|
+
* 按 HTTP 状态码数组查询已配置的错误页,如 ['404', '403'];不传状态码时返回该平台全部配置。
|
|
361
|
+
* @param {DescribeHttpServiceErrorPageParams} params 查询参数,包含平台版 ID 及可选的 HTTP 状态码
|
|
362
|
+
* @returns {Promise<DescribeHttpServiceErrorPageRes>} 错误页配置列表及总条数
|
|
363
|
+
*/
|
|
364
|
+
async describeHttpServiceErrorPage(params) {
|
|
365
|
+
return this.cloudService.request('DescribeHTTPServiceErrorPage', Object.assign({}, params));
|
|
366
|
+
}
|
|
367
|
+
// ==================== 错误页配置内部方法 ====================
|
|
368
|
+
/**
|
|
369
|
+
* 解析平台静态托管桶与地域
|
|
370
|
+
* 取自 DescribePlatforms 返回的 hosting 资源 Detail,是所有上传与清理操作的前提
|
|
371
|
+
*/
|
|
372
|
+
async resolveErrorPageHostingBucket(PlatformId) {
|
|
373
|
+
var _a;
|
|
374
|
+
const { PlatformList } = await this.describePlatforms({ PlatformIds: [PlatformId] });
|
|
375
|
+
const platform = PlatformList === null || PlatformList === void 0 ? void 0 : PlatformList.find(item => item.PlatformId === PlatformId);
|
|
376
|
+
if (!platform) {
|
|
377
|
+
(0, error_1.throwNotFound)(`未找到平台 ${PlatformId}`);
|
|
378
|
+
}
|
|
379
|
+
const hosting = (_a = platform.Resources) === null || _a === void 0 ? void 0 : _a.find(resource => resource.ResType === 'hosting');
|
|
380
|
+
if (!hosting || hosting.Status !== 0) {
|
|
381
|
+
(0, error_1.throwApiError)(`平台 ${PlatformId} 的静态托管资源不存在或尚未就绪`);
|
|
382
|
+
}
|
|
383
|
+
let detail;
|
|
384
|
+
try {
|
|
385
|
+
detail = JSON.parse(hosting.Detail);
|
|
386
|
+
}
|
|
387
|
+
catch (_b) {
|
|
388
|
+
(0, error_1.throwApiError)(`平台 ${PlatformId} 的静态托管资源 Detail 不是有效 JSON`);
|
|
389
|
+
}
|
|
390
|
+
if (!detail || typeof detail !== 'object' || Array.isArray(detail)) {
|
|
391
|
+
(0, error_1.throwApiError)(`平台 ${PlatformId} 的静态托管资源 Detail 格式无效`);
|
|
392
|
+
}
|
|
393
|
+
const { Bucket, Region } = detail;
|
|
394
|
+
if (typeof Bucket !== 'string' ||
|
|
395
|
+
!/^[a-z0-9][a-z0-9-]*-\d+$/.test(Bucket) ||
|
|
396
|
+
typeof Region !== 'string' ||
|
|
397
|
+
!/^[a-z][a-z0-9-]+$/.test(Region)) {
|
|
398
|
+
(0, error_1.throwApiError)(`平台 ${PlatformId} 的静态托管资源缺少有效的 Bucket 或 Region`);
|
|
399
|
+
}
|
|
400
|
+
return { Bucket, Region };
|
|
401
|
+
}
|
|
402
|
+
/** 校验本地文件并上传至桶根目录 error-pages/{PlatformId}/{文件名},返回 RewritePath */
|
|
403
|
+
async uploadErrorPageFile(PlatformId, localPath) {
|
|
404
|
+
if (typeof localPath !== 'string' || !localPath.trim()) {
|
|
405
|
+
(0, error_1.throwInvalidParam)('localPath 必须是本地文件路径');
|
|
406
|
+
}
|
|
407
|
+
const filePath = path_1.default.resolve(localPath);
|
|
408
|
+
const fileName = path_1.default.basename(filePath).replace(/[\s()[\]{}]+/g, '-');
|
|
409
|
+
// 文件名会直接拼入 COS Key 与 RewritePath,按白名单限制字符集与长度,避免 URL 保留字符和控制字符进入路径
|
|
410
|
+
if (!/^[\w.\-\u4e00-\u9fa5]{1,128}$/.test(fileName) || fileName === '.' || fileName === '..') {
|
|
411
|
+
(0, error_1.throwInvalidParam)('错误页文件名只能包含字母、数字、点、下划线、连字符和中文字符,且长度不能超过 128');
|
|
412
|
+
}
|
|
413
|
+
let stats;
|
|
414
|
+
try {
|
|
415
|
+
stats = await fs_1.promises.stat(filePath);
|
|
416
|
+
await fs_1.promises.access(filePath, fs_1.constants.R_OK);
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
(0, error_1.throwFileError)(`无法读取错误页文件 ${filePath}:${error instanceof Error ? error.message : String(error)}`);
|
|
420
|
+
}
|
|
421
|
+
if (!stats.isFile()) {
|
|
422
|
+
(0, error_1.throwInvalidParam)('localPath 必须指向普通文件,不能是目录');
|
|
423
|
+
}
|
|
424
|
+
const { Bucket, Region } = await this.resolveErrorPageHostingBucket(PlatformId);
|
|
425
|
+
const key = `error-pages/${PlatformId}/${fileName}`;
|
|
426
|
+
await (0, cos_upload_1.uploadCosFile)(this.environment, { bucket: Bucket, region: Region, key, localPath: filePath });
|
|
427
|
+
return `/${key}`;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* 静默清理错误页文件:仅允许 error-pages/{PlatformId}/ 前缀;
|
|
431
|
+
* 清理前确认没有其他配置继续引用该文件,避免误删共用文件或刚上传的新文件;失败不影响主流程
|
|
432
|
+
*/
|
|
433
|
+
async cleanupErrorPageFile(PlatformId, rewritePath, excludeId) {
|
|
434
|
+
if (!rewritePath || !rewritePath.startsWith(`/error-pages/${PlatformId}/`)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
// 确认没有其他配置继续引用该文件
|
|
439
|
+
const { Configs } = await this.describeHttpServiceErrorPage({ PlatformId });
|
|
440
|
+
const referenced = (Configs !== null && Configs !== void 0 ? Configs : []).some(item => { var _a; return item.Id !== excludeId && ((_a = item.RewritePath) !== null && _a !== void 0 ? _a : '') === rewritePath; });
|
|
441
|
+
if (referenced) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const { Bucket, Region } = await this.resolveErrorPageHostingBucket(PlatformId);
|
|
445
|
+
await (0, cos_upload_1.deleteCosObject)(this.environment, {
|
|
446
|
+
bucket: Bucket,
|
|
447
|
+
region: Region,
|
|
448
|
+
key: rewritePath.replace(/^\/+/, '')
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
catch (_a) {
|
|
452
|
+
// 清理失败静默
|
|
453
|
+
}
|
|
454
|
+
}
|
|
176
455
|
}
|
|
177
456
|
exports.PlatformService = PlatformService;
|
package/lib/storage/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const mime_types_1 = __importDefault(require("mime-types"));
|
|
|
22
22
|
const cos_nodejs_sdk_v5_1 = __importDefault(require("cos-nodejs-sdk-v5"));
|
|
23
23
|
const utils_1 = require("../utils");
|
|
24
24
|
const error_1 = require("../error");
|
|
25
|
+
const fs_2 = require("../utils/fs");
|
|
25
26
|
const parallel_1 = require("../utils/parallel");
|
|
26
27
|
const storage_config_1 = require("../utils/storage-config");
|
|
27
28
|
const runenv_1 = require("../utils/runenv");
|
|
@@ -737,12 +738,27 @@ class StorageService {
|
|
|
737
738
|
cloudFileKey: cloudPath
|
|
738
739
|
};
|
|
739
740
|
})
|
|
740
|
-
|
|
741
|
+
// `filePath` 是 `path.resolve()` 后的本地绝对路径(hosting 单文件分支塞进
|
|
742
|
+
// files[].localPath 的就是这种),而 ignore 是「相对业务根目录」的 glob 语义:
|
|
743
|
+
// micromatch.isMatch('/proj/dist/node_modules/x.js', ['node_modules/**']) 恒为 false。
|
|
744
|
+
// 只转 `/` 分隔只能救 Windows 上的 `**/xxx` 模式,救不了根锚定的
|
|
745
|
+
// `node_modules/**` / 裸 `.DS_Store` ⇒ 必须用 isIgnoredPath 统一匹配基准。
|
|
746
|
+
//
|
|
747
|
+
// 相对基准取 `cloudFileKey`:ignore 的语义根是「本次上传的根」,
|
|
748
|
+
// 而 cloudFileKey 正是文件相对于该根的路径(目录分支 = 相对 localPath,
|
|
749
|
+
// 单文件分支 = 目标文件名)。用它而不是「逐段剥离绝对路径」,
|
|
750
|
+
// 既让根锚定 pattern 生效,又不会把 `static/**` 放大成任意层级匹配。
|
|
751
|
+
//
|
|
752
|
+
// ⚠️ 本函数不追加任何默认清单:`ignore` 传什么就排什么。敏感文件的默认
|
|
753
|
+
// 排除由**静态托管入口**(HostingService.uploadFiles / StaticDeployer)
|
|
754
|
+
// 通过 mergeArtifactIgnore 追加后传进来;通用云存储上传是私有桶语义,
|
|
755
|
+
// 用户可能确实要备份 .env / 配置文件,不能在这里静默丢文件。
|
|
756
|
+
.filter(item => !(0, fs_2.isIgnoredPath)(item.filePath, ignore, item.cloudFileKey));
|
|
741
757
|
const totalFiles = fileList.length;
|
|
742
|
-
|
|
758
|
+
(0, utils_1.progressLog)(`[uploadFiles] 准备上传 ${totalFiles} 个文件`);
|
|
743
759
|
// 模式 1:完全串行模式(最稳定)
|
|
744
760
|
if (process.env.COS_UPLOAD_SERIAL === 'true') {
|
|
745
|
-
|
|
761
|
+
(0, utils_1.debugLog)(`[uploadFiles] 串行安全模式已启用`);
|
|
746
762
|
const serialResult = await this.uploadFilesSerial({
|
|
747
763
|
fileList,
|
|
748
764
|
bucket,
|
|
@@ -766,7 +782,7 @@ class StorageService {
|
|
|
766
782
|
// 模式 2:智能混合模式(默认)
|
|
767
783
|
// - 先并行快速上传
|
|
768
784
|
// - 失败的文件自动走串行重试
|
|
769
|
-
|
|
785
|
+
(0, utils_1.debugLog)(`[uploadFiles] 智能混合模式:先并行快速上传,失败文件自动串行重试`);
|
|
770
786
|
// 第一步:并行上传(分批)
|
|
771
787
|
const parallelFailedFiles = await this.uploadFilesParallel({
|
|
772
788
|
fileList,
|
|
@@ -798,7 +814,7 @@ class StorageService {
|
|
|
798
814
|
serialResult.failedFiles.map(f => `- ${f.cloudFileKey}: ${f.error}`).join('\n'), { code: 'HOSTING_DEPLOY_FAILED' });
|
|
799
815
|
}
|
|
800
816
|
}
|
|
801
|
-
|
|
817
|
+
(0, utils_1.progressLog)(`[uploadFiles] 全部完成!共处理 ${totalFiles} 个文件`);
|
|
802
818
|
return {
|
|
803
819
|
successCount: totalFiles,
|
|
804
820
|
totalFiles,
|
|
@@ -836,7 +852,7 @@ class StorageService {
|
|
|
836
852
|
uploadedCount++;
|
|
837
853
|
// 显示进度(每 100 个文件或最后 5 个)
|
|
838
854
|
if (uploadedCount % 100 === 0 || fileList.length - i <= 5) {
|
|
839
|
-
|
|
855
|
+
(0, utils_1.debugLog)(`[serial-upload] [${uploadedCount}/${fileList.length}] ${file.cloudFileKey}`);
|
|
840
856
|
}
|
|
841
857
|
success = true;
|
|
842
858
|
// 回调成功
|
|
@@ -863,7 +879,7 @@ class StorageService {
|
|
|
863
879
|
}
|
|
864
880
|
}
|
|
865
881
|
}
|
|
866
|
-
|
|
882
|
+
(0, utils_1.progressLog)(`[serial-upload] 完成:成功 ${uploadedCount}/${fileList.length},失败 ${failedFiles.length}`);
|
|
867
883
|
return {
|
|
868
884
|
successCount: uploadedCount,
|
|
869
885
|
failedFiles
|
|
@@ -910,7 +926,7 @@ class StorageService {
|
|
|
910
926
|
for (let batchStart = 0; batchStart < processedFileList.length; batchStart += BATCH_SIZE) {
|
|
911
927
|
const batchEnd = Math.min(batchStart + BATCH_SIZE, processedFileList.length);
|
|
912
928
|
const batchFiles = processedFileList.slice(batchStart, batchEnd);
|
|
913
|
-
|
|
929
|
+
(0, utils_1.progressLog)(`[parallel-upload] 正在上传第 ${Math.floor(batchStart / BATCH_SIZE) + 1} 批次 (文件 ${batchStart + 1}-${batchEnd}/${processedFileList.length})`);
|
|
914
930
|
// 每批次重新获取 COS 实例(刷新 STS 临时凭证)
|
|
915
931
|
const cos = this.getCos(parallel);
|
|
916
932
|
const uploadFiles = util_1.default.promisify(cos.uploadFiles).bind(cos);
|
|
@@ -954,7 +970,7 @@ class StorageService {
|
|
|
954
970
|
interval: retryInterval,
|
|
955
971
|
failedFiles: []
|
|
956
972
|
});
|
|
957
|
-
|
|
973
|
+
(0, utils_1.debugLog)(`[parallel-upload] 第 ${Math.floor(batchStart / BATCH_SIZE) + 1} 批次上传成功`);
|
|
958
974
|
}
|
|
959
975
|
catch (error) {
|
|
960
976
|
console.error(`[parallel-upload] 第 ${Math.floor(batchStart / BATCH_SIZE) + 1} 批次上传失败:`, error.message || error.code || error);
|
|
@@ -979,7 +995,7 @@ class StorageService {
|
|
|
979
995
|
console.warn(`[parallel-upload] 共 ${allFailedFiles.length}/${processedFileList.length} 个文件并行上传失败,等待串行重试...`);
|
|
980
996
|
}
|
|
981
997
|
else {
|
|
982
|
-
|
|
998
|
+
(0, utils_1.debugLog)(`[parallel-upload] 全部 ${processedFileList.length} 个文件并行上传成功`);
|
|
983
999
|
}
|
|
984
1000
|
return allFailedFiles;
|
|
985
1001
|
}
|
|
@@ -1484,10 +1500,17 @@ class StorageService {
|
|
|
1484
1500
|
return files.filter(item => {
|
|
1485
1501
|
// 当前文件全路径
|
|
1486
1502
|
const fullPath = path_1.default.join(currDir, item);
|
|
1487
|
-
//
|
|
1488
|
-
|
|
1489
|
-
//
|
|
1490
|
-
|
|
1503
|
+
// 文件相对于传入目录的路径。
|
|
1504
|
+
// ⚠️ 必须转成 `/` 分隔再交给 micromatch:Windows 下
|
|
1505
|
+
// `path.join` 产出 `sub\dist\.env`,而 ignore 里的 pattern
|
|
1506
|
+
// 一律是 `**/.env` 这种 `/` 写法 ⇒ isMatch 恒为 false,
|
|
1507
|
+
// 整份 ignore 清单失效(静态托管的 `.git` / `.env` / 私钥
|
|
1508
|
+
// 排除全靠它,失效即公开泄露)。
|
|
1509
|
+
const fileRelativePath = (0, fs_2.toPosixPath)(fullPath.replace(path_1.default.join(dir, path_1.default.sep), ''));
|
|
1510
|
+
// 匹配:必须走 matchesIgnorePattern(内部 dot: true),
|
|
1511
|
+
// 直接 isMatch 会让 `**` 无法穿越 .ssh / .gnupg / .aws 等
|
|
1512
|
+
// 点目录,目录上传的私钥排除整体失效(stbug-KlBy1JWhvD)
|
|
1513
|
+
return !(0, fs_2.matchesIgnorePattern)(fileRelativePath, ignore);
|
|
1491
1514
|
});
|
|
1492
1515
|
}
|
|
1493
1516
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uploadCosFile = uploadCosFile;
|
|
7
|
+
exports.deleteCosObject = deleteCosObject;
|
|
8
|
+
const cos_nodejs_sdk_v5_1 = __importDefault(require("cos-nodejs-sdk-v5"));
|
|
9
|
+
const mime_types_1 = require("mime-types");
|
|
10
|
+
const constant_1 = require("../constant");
|
|
11
|
+
const error_1 = require("../error");
|
|
12
|
+
const runenv_1 = require("./runenv");
|
|
13
|
+
/** 构造 COS 客户端,凭证、代理与端点配置与 StorageService.getCos 保持一致 */
|
|
14
|
+
function createCosClient(environment) {
|
|
15
|
+
const { secretId, secretKey, token, proxy } = environment.getAuthConfig();
|
|
16
|
+
const internalEndpoint = environment.cloudBaseContext.isInternalEndpoint();
|
|
17
|
+
const config = {
|
|
18
|
+
FileParallelLimit: 20,
|
|
19
|
+
SecretId: secretId,
|
|
20
|
+
SecretKey: secretKey,
|
|
21
|
+
SecurityToken: token,
|
|
22
|
+
Proxy: process.env.TCB_COS_PROXY || proxy,
|
|
23
|
+
Domain: internalEndpoint ? "{Bucket}.cos-internal.{Region}.tencentcos.cn" /* COS_ENDPOINT.INTERNAL */ : "{Bucket}.cos.{Region}.tencentcos.cn" /* COS_ENDPOINT.PUBLIC */
|
|
24
|
+
};
|
|
25
|
+
if (constant_1.COS_SDK_PROTOCOL) {
|
|
26
|
+
config.Protocol = (constant_1.COS_SDK_PROTOCOL.endsWith(':')
|
|
27
|
+
? constant_1.COS_SDK_PROTOCOL.toLowerCase()
|
|
28
|
+
: `${constant_1.COS_SDK_PROTOCOL.toLowerCase()}:`);
|
|
29
|
+
}
|
|
30
|
+
if (internalEndpoint) {
|
|
31
|
+
config.Protocol = 'http:';
|
|
32
|
+
}
|
|
33
|
+
if (constant_1.COS_SDK_KEEPALIVE) {
|
|
34
|
+
// 与 StorageService.getCos 一致:仅 'true' / 'false' 生效,其他取值保持 SDK 默认
|
|
35
|
+
config.KeepAlive = { true: true, false: false }[constant_1.COS_SDK_KEEPALIVE];
|
|
36
|
+
}
|
|
37
|
+
else if ((0, runenv_1.checkIsInScf)()) {
|
|
38
|
+
config.KeepAlive = false;
|
|
39
|
+
}
|
|
40
|
+
return new cos_nodejs_sdk_v5_1.default(config);
|
|
41
|
+
}
|
|
42
|
+
/** 使用当前 SDK 凭证上传单文件,Key 相对于桶根目录,不加载环境信息或拼接 BasePath。 */
|
|
43
|
+
async function uploadCosFile(environment, options) {
|
|
44
|
+
const cos = createCosClient(environment);
|
|
45
|
+
const mimeType = (0, mime_types_1.lookup)(options.localPath) || 'application/octet-stream';
|
|
46
|
+
const result = await cos.uploadFile({
|
|
47
|
+
Bucket: options.bucket,
|
|
48
|
+
Region: options.region,
|
|
49
|
+
Key: options.key,
|
|
50
|
+
FilePath: options.localPath,
|
|
51
|
+
StorageClass: 'STANDARD',
|
|
52
|
+
ContentType: (0, mime_types_1.contentType)(mimeType) || mimeType
|
|
53
|
+
});
|
|
54
|
+
if (result.statusCode !== 200) {
|
|
55
|
+
(0, error_1.throwApiError)(`COS 文件上传失败,状态码:${result.statusCode}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** 删除桶根目录下的单个对象,Key 不做任何路径变换。 */
|
|
59
|
+
async function deleteCosObject(environment, options) {
|
|
60
|
+
const cos = createCosClient(environment);
|
|
61
|
+
const result = await cos.deleteObject({
|
|
62
|
+
Bucket: options.bucket,
|
|
63
|
+
Region: options.region,
|
|
64
|
+
Key: options.key
|
|
65
|
+
});
|
|
66
|
+
if (result.statusCode !== 200 && result.statusCode !== 204) {
|
|
67
|
+
(0, error_1.throwApiError)(`COS 文件删除失败,状态码:${result.statusCode}`);
|
|
68
|
+
}
|
|
69
|
+
}
|