@cloudbase/manager-node 4.11.0-alpha.3 → 4.11.0-alpha.5
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/lib/function/index.js +13 -8
- package/lib/storage/index.js +424 -10
- package/package.json +1 -1
- package/types/function/index.d.ts +4 -4
- package/types/storage/index.d.ts +151 -3
package/lib/function/index.js
CHANGED
|
@@ -488,22 +488,25 @@ class FunctionService {
|
|
|
488
488
|
* @param {string} qualifier 需要删除的版本号,不填默认删除函数下全部版本。
|
|
489
489
|
* @returns {Promise<IResponseInfo>}
|
|
490
490
|
*/
|
|
491
|
-
async deleteFunction(
|
|
491
|
+
async deleteFunction(name) {
|
|
492
492
|
var _a;
|
|
493
|
+
const funcName = typeof name === 'string'
|
|
494
|
+
? name
|
|
495
|
+
: name === null || name === void 0 ? void 0 : name.name;
|
|
493
496
|
const { namespace } = this.getFunctionConfig();
|
|
494
497
|
// 检测是否绑定了 API 网关
|
|
495
498
|
const accessService = this.environment.getAccessService();
|
|
496
499
|
const res = await accessService.getAccessList({
|
|
497
|
-
name
|
|
500
|
+
name: funcName
|
|
498
501
|
});
|
|
499
502
|
// 删除绑定的 API 网关
|
|
500
503
|
if (((_a = res === null || res === void 0 ? void 0 : res.APISet) === null || _a === void 0 ? void 0 : _a.length) > 0) {
|
|
501
504
|
await accessService.deleteAccess({
|
|
502
|
-
name
|
|
505
|
+
name: funcName
|
|
503
506
|
});
|
|
504
507
|
}
|
|
505
|
-
await this.scfService.request('DeleteFunction', {
|
|
506
|
-
FunctionName:
|
|
508
|
+
return await this.scfService.request('DeleteFunction', {
|
|
509
|
+
FunctionName: funcName,
|
|
507
510
|
Namespace: namespace
|
|
508
511
|
});
|
|
509
512
|
}
|
|
@@ -741,10 +744,13 @@ class FunctionService {
|
|
|
741
744
|
// 注意:dnsCache 需要 'TRUE'/'FALSE' 字符串,在特殊处理阶段处理
|
|
742
745
|
'intranetconfig': 'IntranetConfig',
|
|
743
746
|
};
|
|
747
|
+
const { TopicId, LogsetId } = this.getClsServiceConfig();
|
|
744
748
|
// 构建参数
|
|
745
749
|
const params = {
|
|
746
750
|
Namespace: namespace,
|
|
747
|
-
FunctionName: func.name
|
|
751
|
+
FunctionName: func.name,
|
|
752
|
+
ClsTopicId: TopicId,
|
|
753
|
+
ClsLogsetId: LogsetId
|
|
748
754
|
};
|
|
749
755
|
// 白名单字段自动转换(大小写不敏感,统一输出 PascalCase)
|
|
750
756
|
for (const key of Object.keys(func)) {
|
|
@@ -1047,13 +1053,12 @@ class FunctionService {
|
|
|
1047
1053
|
async deleteFunctionTrigger(name, triggerName) {
|
|
1048
1054
|
const { namespace } = this.getFunctionConfig();
|
|
1049
1055
|
try {
|
|
1050
|
-
await this.scfService.request('DeleteTrigger', {
|
|
1056
|
+
return await this.scfService.request('DeleteTrigger', {
|
|
1051
1057
|
FunctionName: name,
|
|
1052
1058
|
Namespace: namespace,
|
|
1053
1059
|
TriggerName: triggerName,
|
|
1054
1060
|
Type: 'timer'
|
|
1055
1061
|
});
|
|
1056
|
-
(0, utils_1.successLog)(`[${name}] 删除云函数触发器 ${triggerName} 成功!`);
|
|
1057
1062
|
}
|
|
1058
1063
|
catch (e) {
|
|
1059
1064
|
throw new error_1.CloudBaseError(`[${name}] 删除触发器失败:${e.message}`);
|
package/lib/storage/index.js
CHANGED
|
@@ -617,21 +617,17 @@ class StorageService {
|
|
|
617
617
|
Error
|
|
618
618
|
};
|
|
619
619
|
}
|
|
620
|
-
|
|
621
|
-
* 获取文件存储权限
|
|
622
|
-
* READONLY:所有用户可读,仅创建者和管理员可写
|
|
623
|
-
* PRIVATE:仅创建者及管理员可读写
|
|
624
|
-
* ADMINWRITE:所有用户可读,仅管理员可写
|
|
625
|
-
* ADMINONLY:仅管理员可读写
|
|
626
|
-
* CUSTOM:自定义安全规则
|
|
627
|
-
* @returns {Promise<{ acl: AclType, rule?: IStorageAclRule }>}
|
|
628
|
-
*/
|
|
629
|
-
async getStorageAcl() {
|
|
620
|
+
async getStorageAcl(options) {
|
|
630
621
|
const { bucket, env } = this.getStorageConfig();
|
|
631
622
|
const res = await this.tcbService.request('DescribeStorageSafeRule', {
|
|
632
623
|
EnvId: env,
|
|
633
624
|
Bucket: bucket
|
|
634
625
|
});
|
|
626
|
+
// 默认行为:返回简单的 AclType(向后兼容)
|
|
627
|
+
if (!(options === null || options === void 0 ? void 0 : options.withRule)) {
|
|
628
|
+
return res.AclTag;
|
|
629
|
+
}
|
|
630
|
+
// 带规则模式:返回对象
|
|
635
631
|
const result = { acl: res.AclTag };
|
|
636
632
|
if (res.AclTag === 'CUSTOM' && res.Rule) {
|
|
637
633
|
try {
|
|
@@ -730,6 +726,56 @@ class StorageService {
|
|
|
730
726
|
}
|
|
731
727
|
return fileList;
|
|
732
728
|
}
|
|
729
|
+
/**
|
|
730
|
+
* 列出指定目录下的文件和子目录(不递归,只列当前层)
|
|
731
|
+
* 使用 COS Delimiter 参数实现
|
|
732
|
+
* @param {string} dirPath 目录路径,空字符串表示根目录
|
|
733
|
+
* @returns {Promise<{ files: IListFileInfo[], directories: string[] }>}
|
|
734
|
+
*/
|
|
735
|
+
async listCurrentDirectory(dirPath = '') {
|
|
736
|
+
const { bucket, region } = this.getStorageConfig();
|
|
737
|
+
const cos = this.getCos();
|
|
738
|
+
const getBucket = util_1.default.promisify(cos.getBucket).bind(cos);
|
|
739
|
+
// 确保目录路径以 / 结尾(除了根目录)
|
|
740
|
+
let prefix = dirPath;
|
|
741
|
+
if (prefix && !prefix.endsWith('/')) {
|
|
742
|
+
prefix = prefix + '/';
|
|
743
|
+
}
|
|
744
|
+
const files = [];
|
|
745
|
+
const directories = [];
|
|
746
|
+
let marker = '';
|
|
747
|
+
let isTruncated = true;
|
|
748
|
+
while (isTruncated) {
|
|
749
|
+
const res = await getBucket({
|
|
750
|
+
Bucket: bucket,
|
|
751
|
+
Region: region,
|
|
752
|
+
Prefix: prefix,
|
|
753
|
+
Delimiter: '/', // 关键:使用分隔符,不递归子目录
|
|
754
|
+
MaxKeys: 1000,
|
|
755
|
+
Marker: marker
|
|
756
|
+
});
|
|
757
|
+
// 文件列表(不包含子目录)
|
|
758
|
+
if (res.Contents) {
|
|
759
|
+
// 过滤掉目录本身(Key === prefix 的情况)
|
|
760
|
+
const fileItems = res.Contents.filter((item) => item.Key !== prefix);
|
|
761
|
+
files.push(...fileItems);
|
|
762
|
+
}
|
|
763
|
+
// 子目录列表(CommonPrefixes)
|
|
764
|
+
if (res.CommonPrefixes) {
|
|
765
|
+
const dirs = res.CommonPrefixes.map((item) => item.Prefix);
|
|
766
|
+
directories.push(...dirs);
|
|
767
|
+
}
|
|
768
|
+
isTruncated = res.IsTruncated === 'true' || res.IsTruncated === true;
|
|
769
|
+
if (isTruncated) {
|
|
770
|
+
// 防止无限循环:如果 IsTruncated 为 true 但 NextMarker 为空,则跳出循环
|
|
771
|
+
if (!res.NextMarker) {
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
marker = res.NextMarker;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return { files, directories };
|
|
778
|
+
}
|
|
733
779
|
/**
|
|
734
780
|
* 遍历本地文件夹
|
|
735
781
|
* 忽略不包含 dir 路径,即如果 ignore 匹配 dir,dir 也不会被忽略
|
|
@@ -862,6 +908,356 @@ class StorageService {
|
|
|
862
908
|
});
|
|
863
909
|
return res;
|
|
864
910
|
}
|
|
911
|
+
// ===== 复制/移动/搜索方法 =====
|
|
912
|
+
/**
|
|
913
|
+
* 复制单个文件
|
|
914
|
+
* @param options.sourcePath 源文件路径(云端路径,如 images/a.jpg)
|
|
915
|
+
* @param options.destPath 目标文件路径(云端路径,如 backup/a.jpg)
|
|
916
|
+
* @param options.force 是否强制覆盖
|
|
917
|
+
* @param options.skipExisting 是否跳过已存在的文件
|
|
918
|
+
* @returns 复制结果
|
|
919
|
+
*/
|
|
920
|
+
async copyFile(options) {
|
|
921
|
+
const { sourcePath, destPath, force = false, skipExisting = false } = options;
|
|
922
|
+
const { bucket, region } = this.getStorageConfig();
|
|
923
|
+
// 1. 检查源文件是否存在
|
|
924
|
+
let sourceFileInfo;
|
|
925
|
+
try {
|
|
926
|
+
sourceFileInfo = await this.getFileInfo(sourcePath);
|
|
927
|
+
}
|
|
928
|
+
catch (e) {
|
|
929
|
+
if (e.statusCode === 404 || e.code === 'NoSuchKey') {
|
|
930
|
+
throw new error_1.CloudBaseError(`源文件不存在: ${sourcePath}`, {
|
|
931
|
+
code: 'SOURCE_NOT_FOUND'
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
throw e;
|
|
935
|
+
}
|
|
936
|
+
// 2. 检查目标文件是否存在
|
|
937
|
+
let destExists = false;
|
|
938
|
+
let destFileInfo = null;
|
|
939
|
+
try {
|
|
940
|
+
destFileInfo = await this.getFileInfo(destPath);
|
|
941
|
+
destExists = true;
|
|
942
|
+
}
|
|
943
|
+
catch (e) {
|
|
944
|
+
if (e.statusCode !== 404 && e.code !== 'NoSuchKey') {
|
|
945
|
+
throw e;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (destExists) {
|
|
949
|
+
if (skipExisting) {
|
|
950
|
+
return {
|
|
951
|
+
success: true,
|
|
952
|
+
sourcePath,
|
|
953
|
+
destPath,
|
|
954
|
+
status: 'skipped',
|
|
955
|
+
// COS SDK 返回的 Size 单位已经是 bytes
|
|
956
|
+
size: (destFileInfo === null || destFileInfo === void 0 ? void 0 : destFileInfo.Size) ? parseInt(destFileInfo.Size, 10) : undefined
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
if (!force) {
|
|
960
|
+
throw new error_1.CloudBaseError(`目标文件已存在: ${destPath}`, {
|
|
961
|
+
code: 'DEST_EXISTS'
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
// 3. 执行复制(使用 COS SDK putObjectCopy)
|
|
966
|
+
const cos = this.getCos();
|
|
967
|
+
const putObjectCopy = util_1.default.promisify(cos.putObjectCopy).bind(cos);
|
|
968
|
+
// CopySource 格式: {bucket}.cos.{region}.myqcloud.com/{key}
|
|
969
|
+
// 注意: key 需要 URL 编码
|
|
970
|
+
// cspell:ignore myqcloud
|
|
971
|
+
const copySource = `${bucket}.cos.${region}.myqcloud.com/${encodeURIComponent(sourcePath)}`;
|
|
972
|
+
const res = await putObjectCopy({
|
|
973
|
+
Bucket: bucket,
|
|
974
|
+
Region: region,
|
|
975
|
+
Key: destPath,
|
|
976
|
+
CopySource: copySource
|
|
977
|
+
});
|
|
978
|
+
if (res.statusCode !== 200) {
|
|
979
|
+
throw new error_1.CloudBaseError(`复制文件失败:${JSON.stringify(res)}`);
|
|
980
|
+
}
|
|
981
|
+
// 4. 返回复制结果
|
|
982
|
+
return {
|
|
983
|
+
success: true,
|
|
984
|
+
sourcePath,
|
|
985
|
+
destPath,
|
|
986
|
+
status: 'copied',
|
|
987
|
+
// COS SDK 返回的 Size 单位已经是 bytes
|
|
988
|
+
size: (sourceFileInfo === null || sourceFileInfo === void 0 ? void 0 : sourceFileInfo.Size) ? parseInt(sourceFileInfo.Size, 10) : undefined
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* 复制目录
|
|
993
|
+
* 遍历源目录下的所有文件,并行复制到目标目录
|
|
994
|
+
* @param options.sourcePath 源目录路径
|
|
995
|
+
* @param options.destPath 目标目录路径
|
|
996
|
+
* @param options.include 包含模式(glob 格式)
|
|
997
|
+
* @param options.exclude 排除模式(glob 格式)
|
|
998
|
+
* @param options.force 是否强制覆盖
|
|
999
|
+
* @param options.skipExisting 是否跳过已存在的文件
|
|
1000
|
+
* @param options.parallel 并发数,默认 20
|
|
1001
|
+
* @param options.onProgress 进度回调
|
|
1002
|
+
* @returns 复制结果数组
|
|
1003
|
+
*/
|
|
1004
|
+
async copyDirectory(options) {
|
|
1005
|
+
const { sourcePath, destPath, include, exclude, force = false, skipExisting = false, parallel = 20, onProgress } = options;
|
|
1006
|
+
// 1. 列出源目录所有文件
|
|
1007
|
+
const sourceKey = this.getCloudKey(sourcePath);
|
|
1008
|
+
const files = await this.walkCloudDir(sourceKey);
|
|
1009
|
+
if (!files || !files.length) {
|
|
1010
|
+
// 进一步检查目录是否存在(通过 listCurrentDirectory)
|
|
1011
|
+
// 如果目录存在但为空,listCurrentDirectory 会返回空的 files 和 directories
|
|
1012
|
+
// 如果目录不存在,COS 仍然返回成功但结果为空
|
|
1013
|
+
// 这里通过检查源路径的父目录来判断
|
|
1014
|
+
const parentPath = sourcePath.includes('/')
|
|
1015
|
+
? sourcePath.substring(0, sourcePath.lastIndexOf('/'))
|
|
1016
|
+
: '';
|
|
1017
|
+
const { directories } = await this.listCurrentDirectory(parentPath);
|
|
1018
|
+
const dirExists = directories.some(d => {
|
|
1019
|
+
const normalizedDir = d.endsWith('/') ? d.slice(0, -1) : d;
|
|
1020
|
+
return normalizedDir === sourceKey.slice(0, -1) || normalizedDir === sourcePath;
|
|
1021
|
+
});
|
|
1022
|
+
if (dirExists) {
|
|
1023
|
+
throw new error_1.CloudBaseError(`源目录为空: ${sourcePath}`, {
|
|
1024
|
+
code: 'SOURCE_EMPTY'
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
throw new error_1.CloudBaseError(`源目录不存在: ${sourcePath}`, {
|
|
1029
|
+
code: 'SOURCE_NOT_FOUND'
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// 2. 过滤文件(使用 micromatch)
|
|
1034
|
+
let filteredFiles = files.filter(f => !f.Key.endsWith('/')); // 排除目录
|
|
1035
|
+
if (include) {
|
|
1036
|
+
const patterns = Array.isArray(include) ? include : [include];
|
|
1037
|
+
filteredFiles = filteredFiles.filter(f => {
|
|
1038
|
+
const relativePath = f.Key.replace(sourceKey, '');
|
|
1039
|
+
return micromatch_1.default.isMatch(relativePath, patterns);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (exclude) {
|
|
1043
|
+
const patterns = Array.isArray(exclude) ? exclude : [exclude];
|
|
1044
|
+
filteredFiles = filteredFiles.filter(f => {
|
|
1045
|
+
const relativePath = f.Key.replace(sourceKey, '');
|
|
1046
|
+
return !micromatch_1.default.isMatch(relativePath, patterns);
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
if (!filteredFiles.length) {
|
|
1050
|
+
return [];
|
|
1051
|
+
}
|
|
1052
|
+
// 3. 构建复制任务
|
|
1053
|
+
let copiedCount = 0;
|
|
1054
|
+
let skippedCount = 0;
|
|
1055
|
+
const destKey = destPath.endsWith('/') ? destPath : destPath + '/';
|
|
1056
|
+
const tasks = filteredFiles.map(file => async () => {
|
|
1057
|
+
const relativePath = file.Key.replace(sourceKey, '');
|
|
1058
|
+
const targetPath = destKey + relativePath;
|
|
1059
|
+
try {
|
|
1060
|
+
const result = await this.copyFile({
|
|
1061
|
+
sourcePath: file.Key,
|
|
1062
|
+
destPath: targetPath,
|
|
1063
|
+
force,
|
|
1064
|
+
skipExisting
|
|
1065
|
+
});
|
|
1066
|
+
if (result.status === 'copied')
|
|
1067
|
+
copiedCount++;
|
|
1068
|
+
if (result.status === 'skipped')
|
|
1069
|
+
skippedCount++;
|
|
1070
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
|
|
1071
|
+
copied: copiedCount,
|
|
1072
|
+
skipped: skippedCount,
|
|
1073
|
+
total: filteredFiles.length,
|
|
1074
|
+
currentFile: file.Key
|
|
1075
|
+
});
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1078
|
+
catch (e) {
|
|
1079
|
+
return {
|
|
1080
|
+
success: false,
|
|
1081
|
+
sourcePath: file.Key,
|
|
1082
|
+
destPath: targetPath,
|
|
1083
|
+
status: 'failed',
|
|
1084
|
+
error: e.message
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
// 4. 使用 AsyncTaskParallelController 并发执行
|
|
1089
|
+
const controller = new parallel_1.AsyncTaskParallelController(parallel, 50);
|
|
1090
|
+
controller.loadTasks(tasks);
|
|
1091
|
+
return controller.run();
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* 移动文件(复制后删除源文件)
|
|
1095
|
+
* 注意:移动操作是「复制 + 删除」的两步操作,如果删除失败,文件会同时存在于源和目标位置
|
|
1096
|
+
* @param options 与 copyFile 相同的选项
|
|
1097
|
+
* @returns 移动结果,如果删除源文件失败会在 error 字段说明
|
|
1098
|
+
*/
|
|
1099
|
+
async moveFile(options) {
|
|
1100
|
+
const result = await this.copyFile(options);
|
|
1101
|
+
if (result.status === 'copied') {
|
|
1102
|
+
// 复制成功后使用 COS SDK 直接删除源文件
|
|
1103
|
+
const { bucket, region } = this.getStorageConfig();
|
|
1104
|
+
const cos = this.getCos();
|
|
1105
|
+
const deleteObject = util_1.default.promisify(cos.deleteObject).bind(cos);
|
|
1106
|
+
// 注意:文件路径不需要用 getCloudKey(),getCloudKey() 会在末尾加 /,用于目录
|
|
1107
|
+
const sourceKey = options.sourcePath;
|
|
1108
|
+
try {
|
|
1109
|
+
await deleteObject({
|
|
1110
|
+
Bucket: bucket,
|
|
1111
|
+
Region: region,
|
|
1112
|
+
Key: sourceKey
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
catch (e) {
|
|
1116
|
+
// 删除失败时,文件已复制成功,返回结果但附带错误信息
|
|
1117
|
+
return Object.assign(Object.assign({}, result), { error: `文件已复制但删除源文件失败: ${e.message}` });
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return result;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* 移动目录(复制后删除源目录)
|
|
1124
|
+
* 注意:移动操作是「复制 + 删除」的两步操作,如果部分删除失败,文件会同时存在于源和目标位置
|
|
1125
|
+
* @param options 与 copyDirectory 相同的选项
|
|
1126
|
+
* @returns 移动结果数组,删除失败的文件会在对应结果的 error 字段说明
|
|
1127
|
+
*/
|
|
1128
|
+
async moveDirectory(options) {
|
|
1129
|
+
const results = await this.copyDirectory(options);
|
|
1130
|
+
// 只删除复制成功的文件
|
|
1131
|
+
const copiedFiles = results
|
|
1132
|
+
.filter(r => r.status === 'copied')
|
|
1133
|
+
.map(r => r.sourcePath);
|
|
1134
|
+
if (copiedFiles.length > 0) {
|
|
1135
|
+
// 使用 COS SDK deleteMultipleObject 批量删除源文件
|
|
1136
|
+
const { bucket, region } = this.getStorageConfig();
|
|
1137
|
+
const cos = this.getCos();
|
|
1138
|
+
const deleteMultipleObject = util_1.default.promisify(cos.deleteMultipleObject).bind(cos);
|
|
1139
|
+
// COS deleteMultipleObject 单次最多删除 1000 个对象,这里按 500 分片
|
|
1140
|
+
const files = [...copiedFiles];
|
|
1141
|
+
const total = Math.ceil(files.length / 500);
|
|
1142
|
+
const sliceGroup = [];
|
|
1143
|
+
for (let i = 0; i < total; i++) {
|
|
1144
|
+
sliceGroup.push(files.splice(0, 500));
|
|
1145
|
+
}
|
|
1146
|
+
// 收集删除失败的文件
|
|
1147
|
+
const deleteFailedFiles = new Map();
|
|
1148
|
+
const tasks = sliceGroup.map(async (group) => {
|
|
1149
|
+
try {
|
|
1150
|
+
const res = await deleteMultipleObject({
|
|
1151
|
+
Bucket: bucket,
|
|
1152
|
+
Region: region,
|
|
1153
|
+
Objects: group.map(file => ({ Key: file }))
|
|
1154
|
+
});
|
|
1155
|
+
// 检查部分删除失败的情况
|
|
1156
|
+
if (res.Error && res.Error.length > 0) {
|
|
1157
|
+
for (const err of res.Error) {
|
|
1158
|
+
deleteFailedFiles.set(err.Key, err.Message || 'Unknown error');
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch (e) {
|
|
1163
|
+
// 整批删除失败,记录所有文件
|
|
1164
|
+
for (const file of group) {
|
|
1165
|
+
deleteFailedFiles.set(file, e.message);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
1169
|
+
await Promise.all(tasks);
|
|
1170
|
+
// 将删除失败信息附加到对应的结果中
|
|
1171
|
+
if (deleteFailedFiles.size > 0) {
|
|
1172
|
+
for (const result of results) {
|
|
1173
|
+
if (result.status === 'copied' && deleteFailedFiles.has(result.sourcePath)) {
|
|
1174
|
+
result.error = `文件已复制但删除源文件失败: ${deleteFailedFiles.get(result.sourcePath)}`;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return results;
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* 搜索文件和目录(只搜索当前目录,不递归)
|
|
1183
|
+
* @param options.pattern 搜索模式(支持 glob 或正则表达式)
|
|
1184
|
+
* @param options.path 搜索目录路径(不指定则搜索根目录)
|
|
1185
|
+
* @param options.isRegex 是否为正则表达式模式
|
|
1186
|
+
* @param options.fileType 文件类型过滤
|
|
1187
|
+
* @param options.limit 限制返回数量,默认 100
|
|
1188
|
+
* @param options.includeDetails 是否包含详细信息(大小、修改时间)
|
|
1189
|
+
* @returns 搜索结果数组(包含文件和目录)
|
|
1190
|
+
*/
|
|
1191
|
+
async searchFiles(options) {
|
|
1192
|
+
const { pattern, path: searchPath = '', isRegex = false, fileType, limit = 100, includeDetails = true } = options;
|
|
1193
|
+
// 构建匹配器
|
|
1194
|
+
let matcher;
|
|
1195
|
+
if (isRegex) {
|
|
1196
|
+
// 正则表达式模式
|
|
1197
|
+
// 安全检查:限制正则表达式长度和复杂度,防止 ReDoS 攻击
|
|
1198
|
+
const MAX_REGEX_LENGTH = 200;
|
|
1199
|
+
if (pattern.length > MAX_REGEX_LENGTH) {
|
|
1200
|
+
throw new error_1.CloudBaseError(`正则表达式过长,最大允许 ${MAX_REGEX_LENGTH} 字符`, {
|
|
1201
|
+
code: 'PATTERN_TOO_LONG'
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
// 检测潜在的危险模式(嵌套量词)
|
|
1205
|
+
const dangerousPatterns = /(\+|\*|\{[0-9,]+\})\s*(\+|\*|\?|\{[0-9,]+\})/;
|
|
1206
|
+
if (dangerousPatterns.test(pattern)) {
|
|
1207
|
+
throw new error_1.CloudBaseError('正则表达式包含潜在的危险模式(嵌套量词),可能导致性能问题', {
|
|
1208
|
+
code: 'PATTERN_DANGEROUS'
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
const regex = new RegExp(pattern);
|
|
1213
|
+
matcher = (name) => regex.test(name);
|
|
1214
|
+
}
|
|
1215
|
+
catch (e) {
|
|
1216
|
+
throw new error_1.CloudBaseError(`正则表达式语法错误: ${e.message}`, {
|
|
1217
|
+
code: 'PATTERN_SYNTAX_ERROR'
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
else {
|
|
1222
|
+
// Glob 模式 - 使用 micromatch
|
|
1223
|
+
matcher = (name) => micromatch_1.default.isMatch(name, pattern);
|
|
1224
|
+
}
|
|
1225
|
+
const results = [];
|
|
1226
|
+
// 只列出当前目录(使用 Delimiter,不递归)
|
|
1227
|
+
const { files, directories } = await this.listCurrentDirectory(searchPath);
|
|
1228
|
+
// 处理目录
|
|
1229
|
+
for (const dir of directories) {
|
|
1230
|
+
// 获取目录名(去掉末尾的 / 和前缀)
|
|
1231
|
+
const dirPath = dir.endsWith('/') ? dir.slice(0, -1) : dir;
|
|
1232
|
+
const dirName = dirPath.split('/').pop() || dirPath;
|
|
1233
|
+
if (matcher(dirName)) {
|
|
1234
|
+
results.push({
|
|
1235
|
+
key: dir,
|
|
1236
|
+
type: 'directory'
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
// 处理文件
|
|
1241
|
+
let matchedFiles = files.filter(f => {
|
|
1242
|
+
const basename = f.Key.split('/').pop() || f.Key;
|
|
1243
|
+
return matcher(basename);
|
|
1244
|
+
});
|
|
1245
|
+
// 文件类型过滤
|
|
1246
|
+
if (fileType) {
|
|
1247
|
+
const ext = fileType.startsWith('.') ? fileType : `.${fileType}`;
|
|
1248
|
+
matchedFiles = matchedFiles.filter(f => f.Key.toLowerCase().endsWith(ext.toLowerCase()));
|
|
1249
|
+
}
|
|
1250
|
+
for (const f of matchedFiles) {
|
|
1251
|
+
results.push({
|
|
1252
|
+
key: f.Key,
|
|
1253
|
+
type: 'file',
|
|
1254
|
+
size: includeDetails ? parseInt(f.Size, 10) : undefined,
|
|
1255
|
+
lastModified: includeDetails ? new Date(f.LastModified).toISOString() : undefined
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
// 限制总数量
|
|
1259
|
+
return results.slice(0, limit);
|
|
1260
|
+
}
|
|
865
1261
|
/**
|
|
866
1262
|
* 获取 COS 配置
|
|
867
1263
|
*/
|
|
@@ -1074,9 +1470,27 @@ __decorate([
|
|
|
1074
1470
|
__decorate([
|
|
1075
1471
|
(0, utils_1.preLazy)()
|
|
1076
1472
|
], StorageService.prototype, "walkCloudDirCustom", null);
|
|
1473
|
+
__decorate([
|
|
1474
|
+
(0, utils_1.preLazy)()
|
|
1475
|
+
], StorageService.prototype, "listCurrentDirectory", null);
|
|
1077
1476
|
__decorate([
|
|
1078
1477
|
(0, utils_1.preLazy)()
|
|
1079
1478
|
], StorageService.prototype, "putBucketWebsite", null);
|
|
1080
1479
|
__decorate([
|
|
1081
1480
|
(0, utils_1.preLazy)()
|
|
1082
1481
|
], StorageService.prototype, "getBucket", null);
|
|
1482
|
+
__decorate([
|
|
1483
|
+
(0, utils_1.preLazy)()
|
|
1484
|
+
], StorageService.prototype, "copyFile", null);
|
|
1485
|
+
__decorate([
|
|
1486
|
+
(0, utils_1.preLazy)()
|
|
1487
|
+
], StorageService.prototype, "copyDirectory", null);
|
|
1488
|
+
__decorate([
|
|
1489
|
+
(0, utils_1.preLazy)()
|
|
1490
|
+
], StorageService.prototype, "moveFile", null);
|
|
1491
|
+
__decorate([
|
|
1492
|
+
(0, utils_1.preLazy)()
|
|
1493
|
+
], StorageService.prototype, "moveDirectory", null);
|
|
1494
|
+
__decorate([
|
|
1495
|
+
(0, utils_1.preLazy)()
|
|
1496
|
+
], StorageService.prototype, "searchFiles", null);
|
package/package.json
CHANGED
|
@@ -277,9 +277,9 @@ export declare class FunctionService {
|
|
|
277
277
|
* @param {string} qualifier 需要删除的版本号,不填默认删除函数下全部版本。
|
|
278
278
|
* @returns {Promise<IResponseInfo>}
|
|
279
279
|
*/
|
|
280
|
-
deleteFunction(
|
|
281
|
-
name:
|
|
282
|
-
}): Promise<
|
|
280
|
+
deleteFunction(name: string | {
|
|
281
|
+
name: string;
|
|
282
|
+
}): Promise<IResponseInfo>;
|
|
283
283
|
/**
|
|
284
284
|
* 批量删除云函数
|
|
285
285
|
* @param {Object} options
|
|
@@ -404,7 +404,7 @@ export declare class FunctionService {
|
|
|
404
404
|
* @param {string} triggerName 云函数触发器名称
|
|
405
405
|
* @returns {Promise<IResponseInfo>}
|
|
406
406
|
*/
|
|
407
|
-
deleteFunctionTrigger(name: string, triggerName: string): Promise<
|
|
407
|
+
deleteFunctionTrigger(name: string, triggerName: string): Promise<IResponseInfo>;
|
|
408
408
|
batchDeleteTriggers(options: IFunctionBatchOptions): Promise<void>;
|
|
409
409
|
/**
|
|
410
410
|
* 下载云函数代码
|
package/types/storage/index.d.ts
CHANGED
|
@@ -66,6 +66,82 @@ export interface IStorageAclRule {
|
|
|
66
66
|
/** 写权限规则,true 表示所有用户可写,字符串表示自定义规则表达式 */
|
|
67
67
|
write: boolean | string;
|
|
68
68
|
}
|
|
69
|
+
/** 复制文件选项 */
|
|
70
|
+
export interface ICopyOptions {
|
|
71
|
+
/** 源路径(云端路径,如 images/a.jpg) */
|
|
72
|
+
sourcePath: string;
|
|
73
|
+
/** 目标路径(云端路径,如 backup/a.jpg) */
|
|
74
|
+
destPath: string;
|
|
75
|
+
/** 是否强制覆盖已存在的文件 */
|
|
76
|
+
force?: boolean;
|
|
77
|
+
/** 是否跳过已存在的文件 */
|
|
78
|
+
skipExisting?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** 复制目录选项 */
|
|
81
|
+
export interface ICopyDirectoryOptions extends ICopyOptions {
|
|
82
|
+
/** 包含模式(glob 格式) */
|
|
83
|
+
include?: string | string[];
|
|
84
|
+
/** 排除模式(glob 格式) */
|
|
85
|
+
exclude?: string | string[];
|
|
86
|
+
/** 并发数,默认 20 */
|
|
87
|
+
parallel?: number;
|
|
88
|
+
/** 进度回调 */
|
|
89
|
+
onProgress?: (info: ICopyProgress) => void;
|
|
90
|
+
}
|
|
91
|
+
/** 复制进度信息 */
|
|
92
|
+
export interface ICopyProgress {
|
|
93
|
+
/** 已复制数量 */
|
|
94
|
+
copied: number;
|
|
95
|
+
/** 已跳过数量 */
|
|
96
|
+
skipped: number;
|
|
97
|
+
/** 总数量 */
|
|
98
|
+
total: number;
|
|
99
|
+
/** 当前正在处理的文件 */
|
|
100
|
+
currentFile: string;
|
|
101
|
+
}
|
|
102
|
+
/** 复制结果状态 */
|
|
103
|
+
export type CopyStatus = 'copied' | 'skipped' | 'failed';
|
|
104
|
+
/** 复制结果 */
|
|
105
|
+
export interface ICopyResult {
|
|
106
|
+
/** 是否成功 */
|
|
107
|
+
success: boolean;
|
|
108
|
+
/** 源路径 */
|
|
109
|
+
sourcePath: string;
|
|
110
|
+
/** 目标路径 */
|
|
111
|
+
destPath: string;
|
|
112
|
+
/** 文件大小 (bytes) */
|
|
113
|
+
size?: number;
|
|
114
|
+
/** 复制状态 */
|
|
115
|
+
status: CopyStatus;
|
|
116
|
+
/** 失败时的错误信息 */
|
|
117
|
+
error?: string;
|
|
118
|
+
}
|
|
119
|
+
/** 搜索文件选项 */
|
|
120
|
+
export interface ISearchOptions {
|
|
121
|
+
/** 搜索模式(glob 或正则表达式) */
|
|
122
|
+
pattern: string;
|
|
123
|
+
/** 搜索目录路径(不指定则搜索根目录) */
|
|
124
|
+
path?: string;
|
|
125
|
+
/** 是否为正则表达式 */
|
|
126
|
+
isRegex?: boolean;
|
|
127
|
+
/** 文件类型过滤(如 jpg, png) */
|
|
128
|
+
fileType?: string;
|
|
129
|
+
/** 限制数量,默认 100 */
|
|
130
|
+
limit?: number;
|
|
131
|
+
/** 是否包含详细信息 */
|
|
132
|
+
includeDetails?: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** 搜索结果 */
|
|
135
|
+
export interface ISearchResult {
|
|
136
|
+
/** 文件路径(云端路径) */
|
|
137
|
+
key: string;
|
|
138
|
+
/** 类型:file 或 directory */
|
|
139
|
+
type: 'file' | 'directory';
|
|
140
|
+
/** 文件大小 (bytes),仅文件有效 */
|
|
141
|
+
size?: number;
|
|
142
|
+
/** 最后修改时间 (ISO 格式),仅文件有效 */
|
|
143
|
+
lastModified?: string;
|
|
144
|
+
}
|
|
69
145
|
type OnProgress = (progressData: IProgressData) => void;
|
|
70
146
|
type OnFileFinish = (error: Error, res: any, fileData: any) => void;
|
|
71
147
|
export declare class StorageService {
|
|
@@ -227,9 +303,23 @@ export declare class StorageService {
|
|
|
227
303
|
* ADMINWRITE:所有用户可读,仅管理员可写
|
|
228
304
|
* ADMINONLY:仅管理员可读写
|
|
229
305
|
* CUSTOM:自定义安全规则
|
|
230
|
-
*
|
|
231
|
-
|
|
232
|
-
|
|
306
|
+
*
|
|
307
|
+
* @param options.withRule 是否返回自定义安全规则,默认 false 以保持向后兼容
|
|
308
|
+
* @returns 不传参数时返回 AclType(向后兼容),传入 { withRule: true } 时返回 { acl: AclType, rule?: IStorageAclRule }
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* // 向后兼容的简单用法(返回 AclType)
|
|
312
|
+
* const acl = await storage.getStorageAcl()
|
|
313
|
+
* if (acl === 'READONLY') { ... }
|
|
314
|
+
*
|
|
315
|
+
* // 获取自定义安全规则(返回对象)
|
|
316
|
+
* const result = await storage.getStorageAcl({ withRule: true })
|
|
317
|
+
* console.log(result.acl, result.rule)
|
|
318
|
+
*/
|
|
319
|
+
getStorageAcl(): Promise<AclType>;
|
|
320
|
+
getStorageAcl(options: {
|
|
321
|
+
withRule: true;
|
|
322
|
+
}): Promise<{
|
|
233
323
|
acl: AclType;
|
|
234
324
|
rule?: IStorageAclRule;
|
|
235
325
|
}>;
|
|
@@ -270,6 +360,16 @@ export declare class StorageService {
|
|
|
270
360
|
* @returns {Promise<IListFileInfo[]>}
|
|
271
361
|
*/
|
|
272
362
|
walkCloudDirCustom(options: IWalkCloudDirOptions): Promise<IListFileInfo[]>;
|
|
363
|
+
/**
|
|
364
|
+
* 列出指定目录下的文件和子目录(不递归,只列当前层)
|
|
365
|
+
* 使用 COS Delimiter 参数实现
|
|
366
|
+
* @param {string} dirPath 目录路径,空字符串表示根目录
|
|
367
|
+
* @returns {Promise<{ files: IListFileInfo[], directories: string[] }>}
|
|
368
|
+
*/
|
|
369
|
+
listCurrentDirectory(dirPath?: string): Promise<{
|
|
370
|
+
files: IListFileInfo[];
|
|
371
|
+
directories: string[];
|
|
372
|
+
}>;
|
|
273
373
|
/**
|
|
274
374
|
* 遍历本地文件夹
|
|
275
375
|
* 忽略不包含 dir 路径,即如果 ignore 匹配 dir,dir 也不会被忽略
|
|
@@ -300,6 +400,54 @@ export declare class StorageService {
|
|
|
300
400
|
* @memberof StorageService
|
|
301
401
|
*/
|
|
302
402
|
getBucket(options: IGetBucketOpions): Promise<any>;
|
|
403
|
+
/**
|
|
404
|
+
* 复制单个文件
|
|
405
|
+
* @param options.sourcePath 源文件路径(云端路径,如 images/a.jpg)
|
|
406
|
+
* @param options.destPath 目标文件路径(云端路径,如 backup/a.jpg)
|
|
407
|
+
* @param options.force 是否强制覆盖
|
|
408
|
+
* @param options.skipExisting 是否跳过已存在的文件
|
|
409
|
+
* @returns 复制结果
|
|
410
|
+
*/
|
|
411
|
+
copyFile(options: ICopyOptions): Promise<ICopyResult>;
|
|
412
|
+
/**
|
|
413
|
+
* 复制目录
|
|
414
|
+
* 遍历源目录下的所有文件,并行复制到目标目录
|
|
415
|
+
* @param options.sourcePath 源目录路径
|
|
416
|
+
* @param options.destPath 目标目录路径
|
|
417
|
+
* @param options.include 包含模式(glob 格式)
|
|
418
|
+
* @param options.exclude 排除模式(glob 格式)
|
|
419
|
+
* @param options.force 是否强制覆盖
|
|
420
|
+
* @param options.skipExisting 是否跳过已存在的文件
|
|
421
|
+
* @param options.parallel 并发数,默认 20
|
|
422
|
+
* @param options.onProgress 进度回调
|
|
423
|
+
* @returns 复制结果数组
|
|
424
|
+
*/
|
|
425
|
+
copyDirectory(options: ICopyDirectoryOptions): Promise<ICopyResult[]>;
|
|
426
|
+
/**
|
|
427
|
+
* 移动文件(复制后删除源文件)
|
|
428
|
+
* 注意:移动操作是「复制 + 删除」的两步操作,如果删除失败,文件会同时存在于源和目标位置
|
|
429
|
+
* @param options 与 copyFile 相同的选项
|
|
430
|
+
* @returns 移动结果,如果删除源文件失败会在 error 字段说明
|
|
431
|
+
*/
|
|
432
|
+
moveFile(options: ICopyOptions): Promise<ICopyResult>;
|
|
433
|
+
/**
|
|
434
|
+
* 移动目录(复制后删除源目录)
|
|
435
|
+
* 注意:移动操作是「复制 + 删除」的两步操作,如果部分删除失败,文件会同时存在于源和目标位置
|
|
436
|
+
* @param options 与 copyDirectory 相同的选项
|
|
437
|
+
* @returns 移动结果数组,删除失败的文件会在对应结果的 error 字段说明
|
|
438
|
+
*/
|
|
439
|
+
moveDirectory(options: ICopyDirectoryOptions): Promise<ICopyResult[]>;
|
|
440
|
+
/**
|
|
441
|
+
* 搜索文件和目录(只搜索当前目录,不递归)
|
|
442
|
+
* @param options.pattern 搜索模式(支持 glob 或正则表达式)
|
|
443
|
+
* @param options.path 搜索目录路径(不指定则搜索根目录)
|
|
444
|
+
* @param options.isRegex 是否为正则表达式模式
|
|
445
|
+
* @param options.fileType 文件类型过滤
|
|
446
|
+
* @param options.limit 限制返回数量,默认 100
|
|
447
|
+
* @param options.includeDetails 是否包含详细信息(大小、修改时间)
|
|
448
|
+
* @returns 搜索结果数组(包含文件和目录)
|
|
449
|
+
*/
|
|
450
|
+
searchFiles(options: ISearchOptions): Promise<ISearchResult[]>;
|
|
303
451
|
/**
|
|
304
452
|
* 获取 COS 配置
|
|
305
453
|
*/
|