feat: 各个COS的SDK中添加presignObjectUrl
This commit is contained in:
parent
c33d55c680
commit
7a25050213
|
|
@ -133,4 +133,16 @@ export declare class ALiYunInstance {
|
|||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<string>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -410,4 +410,14 @@ export class ALiYunInstance {
|
|||
throw new OakExternalException('aliyun', error.code, error.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const url = await this.getSignedUrl(bucket, zone, key, {
|
||||
...options,
|
||||
method,
|
||||
});
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,4 +25,16 @@ export declare class CTYunInstance {
|
|||
private buildCanonicalRequest;
|
||||
private buildCanonicalQueryString;
|
||||
private calculatePayloadHash;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: CTYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
contentType?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -309,4 +309,72 @@ export class CTYunInstance {
|
|||
hash.update(payload);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const expiresIn = options?.expires || 3600;
|
||||
const service = 's3';
|
||||
// 对 key 进行 URI 编码(保留 /)
|
||||
const encodedKey = key
|
||||
.split('/')
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
const path = `/${encodedKey}`;
|
||||
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
|
||||
const baseUrl = `https://${host}${path}`;
|
||||
// 生成 ISO8601 格式的日期
|
||||
const date = new Date()
|
||||
.toISOString()
|
||||
.replace(/\.\d{3}Z$/, 'Z')
|
||||
.replace(/[:\-]|\.\d{3}/g, '');
|
||||
const dateStamp = date.substring(0, 8);
|
||||
const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`;
|
||||
// 构建查询参数(不含签名)
|
||||
const queryParameters = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': `${this.accessKey}/${credentialScope}`,
|
||||
'X-Amz-Date': date,
|
||||
'X-Amz-Expires': expiresIn.toString(),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
// 构建 Canonical Request
|
||||
// 对于预签名 URL,payload hash 使用 UNSIGNED-PAYLOAD
|
||||
const { canonicalRequest } = this.buildCanonicalRequest(method, path, queryParameters, { host: host }, // 注意:header 名称应为小写
|
||||
'UNSIGNED-PAYLOAD');
|
||||
// 计算 Canonical Request 的 hash
|
||||
const canonicalRequestHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(canonicalRequest)
|
||||
.digest('hex');
|
||||
// 构建 String to Sign
|
||||
const stringToSign = [
|
||||
'AWS4-HMAC-SHA256',
|
||||
date,
|
||||
credentialScope,
|
||||
canonicalRequestHash,
|
||||
].join('\n');
|
||||
// 生成签名密钥并计算签名
|
||||
const signingKey = this.getSignatureKey(dateStamp, zone, service);
|
||||
const signature = crypto
|
||||
.createHmac('sha256', signingKey)
|
||||
.update(stringToSign)
|
||||
.digest('hex');
|
||||
// 将签名添加到查询参数
|
||||
queryParameters['X-Amz-Signature'] = signature;
|
||||
// 构建最终 URL
|
||||
const queryString = this.buildCanonicalQueryString(queryParameters);
|
||||
const url = `${baseUrl}?${queryString}`;
|
||||
// PUT 方法返回需要的 headers
|
||||
if (method === 'PUT' && options?.contentType) {
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': options.contentType,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,4 +224,19 @@ export declare class QiniuCloudInstance {
|
|||
private base64ToUrlSafe;
|
||||
private hmacSha1;
|
||||
private urlSafeBase64Encode;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成七牛云对象的访问/下载/上传URL
|
||||
*
|
||||
* 注意:七牛云下载需要使用存储桶绑定的域名,不能使用内网域名
|
||||
* 上传使用表单方式,返回 formdata 中包含上传凭证
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: QiniuZone, key: string, options?: {
|
||||
expires?: number;
|
||||
domain?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -808,4 +808,57 @@ export class QiniuCloudInstance {
|
|||
const encoded = Buffer.from(jsonFlags).toString('base64');
|
||||
return this.base64ToUrlSafe(encoded);
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成七牛云对象的访问/下载/上传URL
|
||||
*
|
||||
* 注意:七牛云下载需要使用存储桶绑定的域名,不能使用内网域名
|
||||
* 上传使用表单方式,返回 formdata 中包含上传凭证
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const deadline = Math.floor(Date.now() / 1000) + (options?.expires || 3600);
|
||||
if (method === 'GET') {
|
||||
// 下载场景:需要提供存储桶绑定的域名
|
||||
if (!options?.domain) {
|
||||
throw new OakExternalException('qiniu', 'MISSING_DOMAIN', '七牛云下载需要提供存储桶绑定的域名(domain 参数)', {}, 'oak-external-sdk', {});
|
||||
}
|
||||
const baseUrl = `https://${options.domain}/${encodeURIComponent(key)}`;
|
||||
// 生成下载凭证
|
||||
// SignStr = downloadUrl?e=<deadline>
|
||||
const signStr = `${baseUrl}?e=${deadline}`;
|
||||
const encodedSign = this.hmacSha1(signStr, this.secretKey);
|
||||
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
|
||||
const downloadToken = `${this.accessKey}:${encodedSignSafe}`;
|
||||
const url = `${baseUrl}?e=${deadline}&token=${downloadToken}`;
|
||||
return { url };
|
||||
}
|
||||
else if (method === 'PUT' || method === 'POST') {
|
||||
// 上传场景:七牛使用表单上传方式
|
||||
const scope = key ? `${bucket}:${key}` : bucket;
|
||||
// 构造上传策略
|
||||
const putPolicy = {
|
||||
scope,
|
||||
deadline,
|
||||
};
|
||||
// 生成上传凭证
|
||||
const encodedPolicy = this.urlSafeBase64Encode(JSON.stringify(putPolicy));
|
||||
const encodedSign = this.hmacSha1(encodedPolicy, this.secretKey);
|
||||
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
|
||||
const uploadToken = `${this.accessKey}:${encodedSignSafe}:${encodedPolicy}`;
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const url = `https://${uploadHost}`;
|
||||
return {
|
||||
url,
|
||||
formdata: {
|
||||
key,
|
||||
token: uploadToken,
|
||||
// file 字段需要调用方自行添加
|
||||
},
|
||||
};
|
||||
}
|
||||
else {
|
||||
// DELETE 等操作需要使用管理凭证,不支持预签名URL方式
|
||||
throw new OakExternalException('qiniu', 'UNSUPPORTED_METHOD', `七牛云不支持 ${method} 方法的预签名URL,请使用 removeKodoFile 等管理接口`, {}, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,4 +91,15 @@ export declare class S3Instance {
|
|||
* 获取预签名 URL(通用方法)
|
||||
*/
|
||||
getSignedUrl(bucket: string, key: string, operation?: 'get' | 'put', expiresIn?: number, endpoint?: string): Promise<string>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: S3Zone, key: string, options?: {
|
||||
expires?: number;
|
||||
endpoint?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { S3Client, PutObjectCommand, DeleteObjectCommand, HeadObjectCommand, CreateMultipartUploadCommand, UploadPartCommand, CompleteMultipartUploadCommand, AbortMultipartUploadCommand, ListPartsCommand, GetObjectCommand, } from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { OakExternalException } from 'oak-domain/lib/types';
|
||||
export class S3Instance {
|
||||
accessKey;
|
||||
secretKey;
|
||||
|
|
@ -61,7 +62,7 @@ export class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`生成S3上传URL失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -79,7 +80,7 @@ export class S3Instance {
|
|||
await client.send(command);
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`删除S3文件失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -101,7 +102,7 @@ export class S3Instance {
|
|||
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -139,7 +140,7 @@ export class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`创建分片上传失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -183,7 +184,7 @@ export class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`准备分片上传失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -208,7 +209,7 @@ export class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`上传分片失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -239,7 +240,7 @@ export class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`完成分片上传失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -258,7 +259,7 @@ export class S3Instance {
|
|||
await client.send(command);
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`中止分片上传失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -288,7 +289,7 @@ export class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`列出分片失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -306,7 +307,38 @@ export class S3Instance {
|
|||
return url;
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`生成预签名URL失败: ${err.message}`);
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
try {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
let req;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
req = new GetObjectCommand({ Bucket: bucket, Key: key });
|
||||
break;
|
||||
case 'PUT':
|
||||
req = new PutObjectCommand({ Bucket: bucket, Key: key });
|
||||
break;
|
||||
case 'DELETE':
|
||||
req = new DeleteObjectCommand({ Bucket: bucket, Key: key });
|
||||
break;
|
||||
case 'POST':
|
||||
throw new Error('S3 不支持 POST 方法的预签名 URL');
|
||||
}
|
||||
const url = await getSignedUrl(client, req, {
|
||||
expiresIn: options?.expires || 3600,
|
||||
});
|
||||
return { url };
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('s3', error.code, error.message, error, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,15 @@ export declare class TencentYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<any>;
|
||||
isExistObject(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: TencentYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import crypto from 'crypto';
|
||||
import { OakExternalException, } from 'oak-domain/lib/types/Exception';
|
||||
function getEndpoint(RegionID) {
|
||||
return `cos-${RegionID}.myqcloud.com`;
|
||||
}
|
||||
|
|
@ -141,4 +142,36 @@ export class TencentYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const client = new this.COS({
|
||||
SecretId: this.accessKey,
|
||||
SecretKey: this.secretKey,
|
||||
});
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.getObjectUrl({
|
||||
Bucket: bucket,
|
||||
Region: zone,
|
||||
Key: key,
|
||||
Method: method,
|
||||
Expires: options?.expires || 3600,
|
||||
Sign: true,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(new OakExternalException('tencent', err.code, err.message, err, 'oak-external-sdk', {}));
|
||||
}
|
||||
else {
|
||||
resolve({ url: data.Url });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('tencent', error.code, error.message, error, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,4 +133,16 @@ export declare class ALiYunInstance {
|
|||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<string>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,5 +414,15 @@ class ALiYunInstance {
|
|||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const url = await this.getSignedUrl(bucket, zone, key, {
|
||||
...options,
|
||||
method,
|
||||
});
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
exports.ALiYunInstance = ALiYunInstance;
|
||||
|
|
|
|||
|
|
@ -25,4 +25,16 @@ export declare class CTYunInstance {
|
|||
private buildCanonicalRequest;
|
||||
private buildCanonicalQueryString;
|
||||
private calculatePayloadHash;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: CTYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
contentType?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,5 +313,73 @@ class CTYunInstance {
|
|||
hash.update(payload);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const expiresIn = options?.expires || 3600;
|
||||
const service = 's3';
|
||||
// 对 key 进行 URI 编码(保留 /)
|
||||
const encodedKey = key
|
||||
.split('/')
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
const path = `/${encodedKey}`;
|
||||
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
|
||||
const baseUrl = `https://${host}${path}`;
|
||||
// 生成 ISO8601 格式的日期
|
||||
const date = new Date()
|
||||
.toISOString()
|
||||
.replace(/\.\d{3}Z$/, 'Z')
|
||||
.replace(/[:\-]|\.\d{3}/g, '');
|
||||
const dateStamp = date.substring(0, 8);
|
||||
const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`;
|
||||
// 构建查询参数(不含签名)
|
||||
const queryParameters = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': `${this.accessKey}/${credentialScope}`,
|
||||
'X-Amz-Date': date,
|
||||
'X-Amz-Expires': expiresIn.toString(),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
// 构建 Canonical Request
|
||||
// 对于预签名 URL,payload hash 使用 UNSIGNED-PAYLOAD
|
||||
const { canonicalRequest } = this.buildCanonicalRequest(method, path, queryParameters, { host: host }, // 注意:header 名称应为小写
|
||||
'UNSIGNED-PAYLOAD');
|
||||
// 计算 Canonical Request 的 hash
|
||||
const canonicalRequestHash = crypto_1.default
|
||||
.createHash('sha256')
|
||||
.update(canonicalRequest)
|
||||
.digest('hex');
|
||||
// 构建 String to Sign
|
||||
const stringToSign = [
|
||||
'AWS4-HMAC-SHA256',
|
||||
date,
|
||||
credentialScope,
|
||||
canonicalRequestHash,
|
||||
].join('\n');
|
||||
// 生成签名密钥并计算签名
|
||||
const signingKey = this.getSignatureKey(dateStamp, zone, service);
|
||||
const signature = crypto_1.default
|
||||
.createHmac('sha256', signingKey)
|
||||
.update(stringToSign)
|
||||
.digest('hex');
|
||||
// 将签名添加到查询参数
|
||||
queryParameters['X-Amz-Signature'] = signature;
|
||||
// 构建最终 URL
|
||||
const queryString = this.buildCanonicalQueryString(queryParameters);
|
||||
const url = `${baseUrl}?${queryString}`;
|
||||
// PUT 方法返回需要的 headers
|
||||
if (method === 'PUT' && options?.contentType) {
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': options.contentType,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
exports.CTYunInstance = CTYunInstance;
|
||||
|
|
|
|||
|
|
@ -224,4 +224,19 @@ export declare class QiniuCloudInstance {
|
|||
private base64ToUrlSafe;
|
||||
private hmacSha1;
|
||||
private urlSafeBase64Encode;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成七牛云对象的访问/下载/上传URL
|
||||
*
|
||||
* 注意:七牛云下载需要使用存储桶绑定的域名,不能使用内网域名
|
||||
* 上传使用表单方式,返回 formdata 中包含上传凭证
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: QiniuZone, key: string, options?: {
|
||||
expires?: number;
|
||||
domain?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -812,5 +812,58 @@ class QiniuCloudInstance {
|
|||
const encoded = buffer_1.Buffer.from(jsonFlags).toString('base64');
|
||||
return this.base64ToUrlSafe(encoded);
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成七牛云对象的访问/下载/上传URL
|
||||
*
|
||||
* 注意:七牛云下载需要使用存储桶绑定的域名,不能使用内网域名
|
||||
* 上传使用表单方式,返回 formdata 中包含上传凭证
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const deadline = Math.floor(Date.now() / 1000) + (options?.expires || 3600);
|
||||
if (method === 'GET') {
|
||||
// 下载场景:需要提供存储桶绑定的域名
|
||||
if (!options?.domain) {
|
||||
throw new Exception_1.OakExternalException('qiniu', 'MISSING_DOMAIN', '七牛云下载需要提供存储桶绑定的域名(domain 参数)', {}, 'oak-external-sdk', {});
|
||||
}
|
||||
const baseUrl = `https://${options.domain}/${encodeURIComponent(key)}`;
|
||||
// 生成下载凭证
|
||||
// SignStr = downloadUrl?e=<deadline>
|
||||
const signStr = `${baseUrl}?e=${deadline}`;
|
||||
const encodedSign = this.hmacSha1(signStr, this.secretKey);
|
||||
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
|
||||
const downloadToken = `${this.accessKey}:${encodedSignSafe}`;
|
||||
const url = `${baseUrl}?e=${deadline}&token=${downloadToken}`;
|
||||
return { url };
|
||||
}
|
||||
else if (method === 'PUT' || method === 'POST') {
|
||||
// 上传场景:七牛使用表单上传方式
|
||||
const scope = key ? `${bucket}:${key}` : bucket;
|
||||
// 构造上传策略
|
||||
const putPolicy = {
|
||||
scope,
|
||||
deadline,
|
||||
};
|
||||
// 生成上传凭证
|
||||
const encodedPolicy = this.urlSafeBase64Encode(JSON.stringify(putPolicy));
|
||||
const encodedSign = this.hmacSha1(encodedPolicy, this.secretKey);
|
||||
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
|
||||
const uploadToken = `${this.accessKey}:${encodedSignSafe}:${encodedPolicy}`;
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const url = `https://${uploadHost}`;
|
||||
return {
|
||||
url,
|
||||
formdata: {
|
||||
key,
|
||||
token: uploadToken,
|
||||
// file 字段需要调用方自行添加
|
||||
},
|
||||
};
|
||||
}
|
||||
else {
|
||||
// DELETE 等操作需要使用管理凭证,不支持预签名URL方式
|
||||
throw new Exception_1.OakExternalException('qiniu', 'UNSUPPORTED_METHOD', `七牛云不支持 ${method} 方法的预签名URL,请使用 removeKodoFile 等管理接口`, {}, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.QiniuCloudInstance = QiniuCloudInstance;
|
||||
|
|
|
|||
|
|
@ -91,4 +91,15 @@ export declare class S3Instance {
|
|||
* 获取预签名 URL(通用方法)
|
||||
*/
|
||||
getSignedUrl(bucket: string, key: string, operation?: 'get' | 'put', expiresIn?: number, endpoint?: string): Promise<string>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: S3Zone, key: string, options?: {
|
||||
expires?: number;
|
||||
endpoint?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.S3Instance = void 0;
|
||||
const client_s3_1 = require("@aws-sdk/client-s3");
|
||||
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
|
||||
const types_1 = require("oak-domain/lib/types");
|
||||
class S3Instance {
|
||||
accessKey;
|
||||
secretKey;
|
||||
|
|
@ -64,7 +65,7 @@ class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`生成S3上传URL失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -82,7 +83,7 @@ class S3Instance {
|
|||
await client.send(command);
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`删除S3文件失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -104,7 +105,7 @@ class S3Instance {
|
|||
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -142,7 +143,7 @@ class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`创建分片上传失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -186,7 +187,7 @@ class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`准备分片上传失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -211,7 +212,7 @@ class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`上传分片失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -242,7 +243,7 @@ class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`完成分片上传失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -261,7 +262,7 @@ class S3Instance {
|
|||
await client.send(command);
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`中止分片上传失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -291,7 +292,7 @@ class S3Instance {
|
|||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`列出分片失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -309,7 +310,38 @@ class S3Instance {
|
|||
return url;
|
||||
}
|
||||
catch (err) {
|
||||
throw new Error(`生成预签名URL失败: ${err.message}`);
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
try {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
let req;
|
||||
switch (method) {
|
||||
case 'GET':
|
||||
req = new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key });
|
||||
break;
|
||||
case 'PUT':
|
||||
req = new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key });
|
||||
break;
|
||||
case 'DELETE':
|
||||
req = new client_s3_1.DeleteObjectCommand({ Bucket: bucket, Key: key });
|
||||
break;
|
||||
case 'POST':
|
||||
throw new Error('S3 不支持 POST 方法的预签名 URL');
|
||||
}
|
||||
const url = await (0, s3_request_presigner_1.getSignedUrl)(client, req, {
|
||||
expiresIn: options?.expires || 3600,
|
||||
});
|
||||
return { url };
|
||||
}
|
||||
catch (error) {
|
||||
throw new types_1.OakExternalException('s3', error.code, error.message, error, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,4 +17,15 @@ export declare class TencentYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<any>;
|
||||
isExistObject(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: TencentYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.TencentYunInstance = void 0;
|
||||
const tslib_1 = require("tslib");
|
||||
const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
||||
const Exception_1 = require("oak-domain/lib/types/Exception");
|
||||
function getEndpoint(RegionID) {
|
||||
return `cos-${RegionID}.myqcloud.com`;
|
||||
}
|
||||
|
|
@ -145,5 +146,37 @@ class TencentYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const client = new this.COS({
|
||||
SecretId: this.accessKey,
|
||||
SecretKey: this.secretKey,
|
||||
});
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.getObjectUrl({
|
||||
Bucket: bucket,
|
||||
Region: zone,
|
||||
Key: key,
|
||||
Method: method,
|
||||
Expires: options?.expires || 3600,
|
||||
Sign: true,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(new Exception_1.OakExternalException('tencent', err.code, err.message, err, 'oak-external-sdk', {}));
|
||||
}
|
||||
else {
|
||||
resolve({ url: data.Url });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('tencent', error.code, error.message, error, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.TencentYunInstance = TencentYunInstance;
|
||||
|
|
|
|||
|
|
@ -538,4 +538,30 @@ export class ALiYunInstance {
|
|||
throw new OakExternalException('aliyun', error.code, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async presignObjectUrl(
|
||||
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
options?: {
|
||||
expires?: number;
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}
|
||||
): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}> {
|
||||
const url = await this.getSignedUrl(bucket, zone, key, {
|
||||
...options,
|
||||
method,
|
||||
});
|
||||
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
|
@ -297,9 +297,9 @@ export class CTYunInstance {
|
|||
|
||||
const authorizationHeader = [
|
||||
'AWS4-HMAC-SHA256 Credential=' +
|
||||
this.accessKey +
|
||||
'/' +
|
||||
credentialScope,
|
||||
this.accessKey +
|
||||
'/' +
|
||||
credentialScope,
|
||||
'SignedHeaders=' + signedHeaders,
|
||||
'Signature=' + signature,
|
||||
].join(', ');
|
||||
|
|
@ -379,4 +379,104 @@ export class CTYunInstance {
|
|||
hash.update(payload);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
async presignObjectUrl(
|
||||
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
||||
bucket: string,
|
||||
zone: CTYunZone,
|
||||
key: string,
|
||||
options?: {
|
||||
expires?: number;
|
||||
contentType?: string; // PUT 上传时可指定 Content-Type
|
||||
}
|
||||
): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}> {
|
||||
const expiresIn = options?.expires || 3600;
|
||||
const service = 's3';
|
||||
|
||||
// 对 key 进行 URI 编码(保留 /)
|
||||
const encodedKey = key
|
||||
.split('/')
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
const path = `/${encodedKey}`;
|
||||
|
||||
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
|
||||
const baseUrl = `https://${host}${path}`;
|
||||
|
||||
// 生成 ISO8601 格式的日期
|
||||
const date = new Date()
|
||||
.toISOString()
|
||||
.replace(/\.\d{3}Z$/, 'Z')
|
||||
.replace(/[:\-]|\.\d{3}/g, '');
|
||||
const dateStamp = date.substring(0, 8);
|
||||
|
||||
const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`;
|
||||
|
||||
// 构建查询参数(不含签名)
|
||||
const queryParameters: Record<string, string> = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': `${this.accessKey}/${credentialScope}`,
|
||||
'X-Amz-Date': date,
|
||||
'X-Amz-Expires': expiresIn.toString(),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
|
||||
// 构建 Canonical Request
|
||||
// 对于预签名 URL,payload hash 使用 UNSIGNED-PAYLOAD
|
||||
const { canonicalRequest } = this.buildCanonicalRequest(
|
||||
method,
|
||||
path,
|
||||
queryParameters,
|
||||
{ host: host }, // 注意:header 名称应为小写
|
||||
'UNSIGNED-PAYLOAD'
|
||||
);
|
||||
|
||||
// 计算 Canonical Request 的 hash
|
||||
const canonicalRequestHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(canonicalRequest)
|
||||
.digest('hex');
|
||||
|
||||
// 构建 String to Sign
|
||||
const stringToSign = [
|
||||
'AWS4-HMAC-SHA256',
|
||||
date,
|
||||
credentialScope,
|
||||
canonicalRequestHash,
|
||||
].join('\n');
|
||||
|
||||
// 生成签名密钥并计算签名
|
||||
const signingKey = this.getSignatureKey(dateStamp, zone, service);
|
||||
const signature = crypto
|
||||
.createHmac('sha256', signingKey)
|
||||
.update(stringToSign)
|
||||
.digest('hex');
|
||||
|
||||
// 将签名添加到查询参数
|
||||
queryParameters['X-Amz-Signature'] = signature;
|
||||
|
||||
// 构建最终 URL
|
||||
const queryString = this.buildCanonicalQueryString(queryParameters);
|
||||
const url = `${baseUrl}?${queryString}`;
|
||||
|
||||
// PUT 方法返回需要的 headers
|
||||
if (method === 'PUT' && options?.contentType) {
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': options.contentType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -699,7 +699,7 @@ export class QiniuCloudInstance {
|
|||
status: response.status,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -772,15 +772,15 @@ export class QiniuCloudInstance {
|
|||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
|
||||
// 将Buffer转换为Uint8Array,然后获取buffer
|
||||
const bodyData = firstChunkData instanceof Buffer
|
||||
const bodyData = firstChunkData instanceof Buffer
|
||||
? firstChunkData.buffer.slice(
|
||||
firstChunkData.byteOffset,
|
||||
firstChunkData.byteOffset + firstChunkData.byteLength
|
||||
)
|
||||
)
|
||||
: (firstChunkData as Uint8Array).buffer.slice(
|
||||
(firstChunkData as Uint8Array).byteOffset,
|
||||
(firstChunkData as Uint8Array).byteOffset + (firstChunkData as Uint8Array).byteLength
|
||||
);
|
||||
);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
|
|
@ -839,15 +839,15 @@ export class QiniuCloudInstance {
|
|||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
|
||||
// 将Buffer转换为Uint8Array,然后获取buffer
|
||||
const bodyData = chunkData instanceof Buffer
|
||||
const bodyData = chunkData instanceof Buffer
|
||||
? chunkData.buffer.slice(
|
||||
chunkData.byteOffset,
|
||||
chunkData.byteOffset + chunkData.byteLength
|
||||
)
|
||||
)
|
||||
: (chunkData as Uint8Array).buffer.slice(
|
||||
(chunkData as Uint8Array).byteOffset,
|
||||
(chunkData as Uint8Array).byteOffset + (chunkData as Uint8Array).byteLength
|
||||
);
|
||||
);
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
|
|
@ -906,12 +906,12 @@ export class QiniuCloudInstance {
|
|||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const encodedKey = this.urlSafeBase64Encode(key);
|
||||
let path = `/mkfile/${fileSize}/key/${encodedKey}`;
|
||||
|
||||
|
||||
if (mimeType) {
|
||||
const encodedMimeType = this.urlSafeBase64Encode(mimeType);
|
||||
path += `/mimeType/${encodedMimeType}`;
|
||||
}
|
||||
|
||||
|
||||
if (fileName) {
|
||||
const encodedFileName = this.urlSafeBase64Encode(fileName);
|
||||
path += `/fname/${encodedFileName}`;
|
||||
|
|
@ -1122,4 +1122,92 @@ export class QiniuCloudInstance {
|
|||
const encoded = Buffer.from(jsonFlags).toString('base64');
|
||||
return this.base64ToUrlSafe(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成七牛云对象的访问/下载/上传URL
|
||||
*
|
||||
* 注意:七牛云下载需要使用存储桶绑定的域名,不能使用内网域名
|
||||
* 上传使用表单方式,返回 formdata 中包含上传凭证
|
||||
*/
|
||||
async presignObjectUrl(
|
||||
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
||||
bucket: string,
|
||||
zone: QiniuZone,
|
||||
key: string,
|
||||
options?: {
|
||||
expires?: number;
|
||||
domain?: string; // 下载域名(GET 时必须提供)
|
||||
}
|
||||
): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}> {
|
||||
const deadline = Math.floor(Date.now() / 1000) + (options?.expires || 3600);
|
||||
|
||||
if (method === 'GET') {
|
||||
// 下载场景:需要提供存储桶绑定的域名
|
||||
if (!options?.domain) {
|
||||
throw new OakExternalException(
|
||||
'qiniu',
|
||||
'MISSING_DOMAIN',
|
||||
'七牛云下载需要提供存储桶绑定的域名(domain 参数)',
|
||||
{},
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
const baseUrl = `https://${options.domain}/${encodeURIComponent(key)}`;
|
||||
|
||||
// 生成下载凭证
|
||||
// SignStr = downloadUrl?e=<deadline>
|
||||
const signStr = `${baseUrl}?e=${deadline}`;
|
||||
const encodedSign = this.hmacSha1(signStr, this.secretKey);
|
||||
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
|
||||
const downloadToken = `${this.accessKey}:${encodedSignSafe}`;
|
||||
|
||||
const url = `${baseUrl}?e=${deadline}&token=${downloadToken}`;
|
||||
|
||||
return { url };
|
||||
} else if (method === 'PUT' || method === 'POST') {
|
||||
// 上传场景:七牛使用表单上传方式
|
||||
const scope = key ? `${bucket}:${key}` : bucket;
|
||||
|
||||
// 构造上传策略
|
||||
const putPolicy = {
|
||||
scope,
|
||||
deadline,
|
||||
};
|
||||
|
||||
// 生成上传凭证
|
||||
const encodedPolicy = this.urlSafeBase64Encode(JSON.stringify(putPolicy));
|
||||
const encodedSign = this.hmacSha1(encodedPolicy, this.secretKey);
|
||||
const encodedSignSafe = this.base64ToUrlSafe(encodedSign);
|
||||
const uploadToken = `${this.accessKey}:${encodedSignSafe}:${encodedPolicy}`;
|
||||
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const url = `https://${uploadHost}`;
|
||||
|
||||
return {
|
||||
url,
|
||||
formdata: {
|
||||
key,
|
||||
token: uploadToken,
|
||||
// file 字段需要调用方自行添加
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// DELETE 等操作需要使用管理凭证,不支持预签名URL方式
|
||||
throw new OakExternalException(
|
||||
'qiniu',
|
||||
'UNSUPPORTED_METHOD',
|
||||
`七牛云不支持 ${method} 方法的预签名URL,请使用 removeKodoFile 等管理接口`,
|
||||
{},
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -173,4 +173,67 @@ export class TencentYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
async presignObjectUrl(
|
||||
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
||||
bucket: string,
|
||||
zone: TencentYunZone,
|
||||
key: string,
|
||||
options?: {
|
||||
expires?: number;
|
||||
}
|
||||
): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}> {
|
||||
const client = new this.COS({
|
||||
SecretId: this.accessKey,
|
||||
SecretKey: this.secretKey,
|
||||
});
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.getObjectUrl(
|
||||
{
|
||||
Bucket: bucket,
|
||||
Region: zone,
|
||||
Key: key,
|
||||
Method: method,
|
||||
Expires: options?.expires || 3600,
|
||||
Sign: true,
|
||||
},
|
||||
(err: any, data: { Url: string }) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new OakExternalException(
|
||||
'tencent',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
resolve({ url: data.Url });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException(
|
||||
'tencent',
|
||||
error.code,
|
||||
error.message,
|
||||
error,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue