Compare commits
13 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
170affd3a9 | |
|
|
f1545faedf | |
|
|
ab689a6fc2 | |
|
|
6109d8010e | |
|
|
9516284bc9 | |
|
|
56fef25306 | |
|
|
a25c89e6e3 | |
|
|
25ac418fc1 | |
|
|
7a25050213 | |
|
|
c33d55c680 | |
|
|
5279258c64 | |
|
|
20d4540b1e | |
|
|
64a86e9266 |
|
|
@ -15,4 +15,155 @@ export declare class ALiYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<OSS.DeleteResult>;
|
||||
isExistObject(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
initiateMultipartUpload(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
bucket: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
presignMulti(bucket: string, zone: ALiYunZone, key: string, uploadId: string, from: number, to: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[]>;
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
prepareMultipartUpload(bucket: string, zone: ALiYunZone, key: string, partCount: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
parts: {
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
uploadPart(bucket: string, zone: ALiYunZone, key: string, uploadId: string, partNumber: number, file: any, start?: number, end?: number, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
etag: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
completeMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, parts: Array<{
|
||||
number: number;
|
||||
etag: string;
|
||||
}>, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
bucket: string;
|
||||
name: string;
|
||||
etag: string;
|
||||
}>;
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
abortMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<void>;
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
listParts(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
'max-parts'?: number;
|
||||
'part-number-marker'?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
parts: {
|
||||
partNumber: any;
|
||||
lastModified: any;
|
||||
etag: any;
|
||||
size: any;
|
||||
}[];
|
||||
nextPartNumberMarker: number;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
listMultipartUploads(bucket: string, zone: ALiYunZone, options?: {
|
||||
prefix?: string;
|
||||
'max-uploads'?: number;
|
||||
'key-marker'?: string;
|
||||
'upload-id-marker'?: string;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
uploads: {
|
||||
name: any;
|
||||
uploadId: any;
|
||||
initiated: any;
|
||||
}[];
|
||||
nextKeyMarker: any;
|
||||
nextUploadIdMarker: any;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
getSignedUrl(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
method?: 'GET' | 'PUT' | 'DELETE' | 'POST';
|
||||
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>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import OSS from 'ali-oss';
|
||||
import { OakExternalException, } from 'oak-domain/lib/types/Exception';
|
||||
function getEndpoint(RegionID) {
|
||||
return `oss-${RegionID}.aliyuncs.com`;
|
||||
}
|
||||
|
|
@ -180,4 +181,287 @@ export class ALiYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
async initiateMultipartUpload(bucket, zone, key, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.initMultipartUpload(key, options);
|
||||
return {
|
||||
uploadId: result.uploadId,
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(bucket, zone, key, uploadId, from, to, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const presignedUrls = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
uploadId: uploadId,
|
||||
partNumber: String(i),
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
presignedUrls.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return presignedUrls;
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(bucket, zone, key, partCount, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
// 1. 初始化分片上传
|
||||
const initResult = await client.initMultipartUpload(key, {
|
||||
headers: options?.headers,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
partNumber: String(i),
|
||||
uploadId: initResult.uploadId,
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return {
|
||||
uploadId: initResult.uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
async uploadPart(bucket, zone, key, uploadId, partNumber, file, start, end, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.uploadPart(key, uploadId, partNumber, file, start ?? 0, end ?? 0, options);
|
||||
return {
|
||||
etag: result.etag,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
async completeMultipartUpload(bucket, zone, key, uploadId, parts, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.completeMultipartUpload(key, uploadId, parts, options);
|
||||
return {
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
etag: result.etag,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
async abortMultipartUpload(bucket, zone, key, uploadId, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
await client.abortMultipartUpload(key, uploadId, options);
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
async listParts(bucket, zone, key, uploadId, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listParts(key, uploadId, {
|
||||
'max-parts': options?.['max-parts']?.toString(),
|
||||
'part-number-marker': options?.['part-number-marker']?.toString(),
|
||||
});
|
||||
return {
|
||||
parts: result.parts.map((part) => ({
|
||||
partNumber: part.PartNumber,
|
||||
lastModified: part.LastModified,
|
||||
etag: part.ETag,
|
||||
size: part.Size,
|
||||
})),
|
||||
nextPartNumberMarker: result.nextPartNumberMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
async listMultipartUploads(bucket, zone, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listUploads(options || {});
|
||||
return {
|
||||
uploads: result.uploads.map((upload) => ({
|
||||
name: upload.name,
|
||||
uploadId: upload.uploadId,
|
||||
initiated: upload.initiated,
|
||||
})),
|
||||
nextKeyMarker: result.nextKeyMarker,
|
||||
nextUploadIdMarker: result.nextUploadIdMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
async getSignedUrl(bucket, zone, key, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
});
|
||||
try {
|
||||
const url = client.signatureUrl(key, options);
|
||||
return url;
|
||||
}
|
||||
catch (error) {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
export type AssumeRoleOptions = {
|
||||
endpoint: string;
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
roleArn: string;
|
||||
roleSessionName: string;
|
||||
policy?: string;
|
||||
durationSeconds?: number;
|
||||
};
|
||||
export declare function stsAssumeRole(options: AssumeRoleOptions): Promise<{
|
||||
RequestId: string | undefined;
|
||||
AssumedRoleUser: {
|
||||
AssumedRoleId: string | undefined;
|
||||
Arn: string | undefined;
|
||||
};
|
||||
Credentials: {
|
||||
SecurityToken: string | undefined;
|
||||
Expiration: string | undefined;
|
||||
AccessKeySecret: string | undefined;
|
||||
AccessKeyId: string | undefined;
|
||||
};
|
||||
}>;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import STSClient, { AssumeRoleRequest } from '@alicloud/sts20150401';
|
||||
export async function stsAssumeRole(options) {
|
||||
const client = new STSClient({
|
||||
endpoint: options.endpoint,
|
||||
accessKeyId: options.accessKeyId,
|
||||
accessKeySecret: options.accessKeySecret,
|
||||
toMap: () => {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const assumeRoleRequest = new AssumeRoleRequest({
|
||||
roleArn: options.roleArn,
|
||||
roleSessionName: options.roleSessionName,
|
||||
policy: options.policy,
|
||||
durationSeconds: options.durationSeconds,
|
||||
});
|
||||
const res = await client.assumeRole(assumeRoleRequest);
|
||||
return {
|
||||
"RequestId": res.body?.requestId,
|
||||
"AssumedRoleUser": {
|
||||
"AssumedRoleId": res.body?.assumedRoleUser?.assumedRoleId,
|
||||
"Arn": res.body?.assumedRoleUser?.arn
|
||||
},
|
||||
"Credentials": {
|
||||
"SecurityToken": res.body?.credentials?.securityToken,
|
||||
"Expiration": res.body?.credentials?.expiration,
|
||||
"AccessKeySecret": res.body?.credentials?.accessKeySecret,
|
||||
"AccessKeyId": res.body?.credentials?.accessKeyId
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Buffer } from 'buffer';
|
||||
import { QiniuLiveStreamInfo, QiniuZone } from '../../types/Qiniu';
|
||||
export declare class QiniuCloudInstance {
|
||||
private accessKey;
|
||||
|
|
@ -151,6 +152,57 @@ export declare class QiniuCloudInstance {
|
|||
* @returns
|
||||
*/
|
||||
getLiveStreamInfo(hub: string, streamTitle: string, host: string): Promise<QiniuLiveStreamInfo>;
|
||||
/**
|
||||
* 创建块(分片上传第一步)
|
||||
* https://developer.qiniu.com/kodo/1286/mkblk
|
||||
* @param zone 区域
|
||||
* @param blockSize 块大小,单位字节
|
||||
* @param firstChunkData 第一个分片的数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
mkblk(zone: QiniuZone, blockSize: number, firstChunkData: Buffer | Uint8Array, bucket: string): Promise<{
|
||||
ctx: string;
|
||||
checksum: string;
|
||||
crc32: number;
|
||||
offset: number;
|
||||
host: string;
|
||||
expired_at: number;
|
||||
}>;
|
||||
/**
|
||||
* 上传分片数据(后续分片)
|
||||
* https://developer.qiniu.com/kodo/1251/bput
|
||||
* @param zone 区域
|
||||
* @param ctx 前一次上传返回的块上下文
|
||||
* @param offset 当前分片在块中的偏移量
|
||||
* @param chunkData 分片数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
bput(zone: QiniuZone, ctx: string, offset: number, chunkData: Buffer | Uint8Array, bucket: string): Promise<{
|
||||
ctx: string;
|
||||
checksum: string;
|
||||
crc32: number;
|
||||
offset: number;
|
||||
host: string;
|
||||
expired_at: number;
|
||||
}>;
|
||||
/**
|
||||
* 创建文件(合并所有块)
|
||||
* https://developer.qiniu.com/kodo/1287/mkfile
|
||||
* @param zone 区域
|
||||
* @param fileSize 文件总大小,单位字节
|
||||
* @param key 文件名
|
||||
* @param bucket 存储空间名称
|
||||
* @param ctxList 所有块的ctx列表,按顺序排列
|
||||
* @param mimeType 文件MIME类型
|
||||
* @param fileName 原始文件名
|
||||
* @returns
|
||||
*/
|
||||
mkfile(zone: QiniuZone, fileSize: number, key: string, bucket: string, ctxList: string[], mimeType?: string, fileName?: string): Promise<{
|
||||
hash: string;
|
||||
key: string;
|
||||
}>;
|
||||
/**
|
||||
* 管理端访问七牛云服务器
|
||||
* @param path
|
||||
|
|
@ -172,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>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -534,6 +534,149 @@ export class QiniuCloudInstance {
|
|||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建块(分片上传第一步)
|
||||
* https://developer.qiniu.com/kodo/1286/mkblk
|
||||
* @param zone 区域
|
||||
* @param blockSize 块大小,单位字节
|
||||
* @param firstChunkData 第一个分片的数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
async mkblk(zone, blockSize, firstChunkData, bucket) {
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const path = `/mkblk/${blockSize}`;
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
// 将Buffer转换为Uint8Array,然后获取buffer
|
||||
const bodyData = firstChunkData instanceof Buffer
|
||||
? firstChunkData.buffer.slice(firstChunkData.byteOffset, firstChunkData.byteOffset + firstChunkData.byteLength)
|
||||
: firstChunkData.buffer.slice(firstChunkData.byteOffset, firstChunkData.byteOffset + firstChunkData.byteLength);
|
||||
let response;
|
||||
try {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: bodyData,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakNetworkException();
|
||||
}
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片数据(后续分片)
|
||||
* https://developer.qiniu.com/kodo/1251/bput
|
||||
* @param zone 区域
|
||||
* @param ctx 前一次上传返回的块上下文
|
||||
* @param offset 当前分片在块中的偏移量
|
||||
* @param chunkData 分片数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
async bput(zone, ctx, offset, chunkData, bucket) {
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const path = `/bput/${ctx}/${offset}`;
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
// 将Buffer转换为Uint8Array,然后获取buffer
|
||||
const bodyData = chunkData instanceof Buffer
|
||||
? chunkData.buffer.slice(chunkData.byteOffset, chunkData.byteOffset + chunkData.byteLength)
|
||||
: chunkData.buffer.slice(chunkData.byteOffset, chunkData.byteOffset + chunkData.byteLength);
|
||||
let response;
|
||||
try {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: bodyData,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakNetworkException();
|
||||
}
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建文件(合并所有块)
|
||||
* https://developer.qiniu.com/kodo/1287/mkfile
|
||||
* @param zone 区域
|
||||
* @param fileSize 文件总大小,单位字节
|
||||
* @param key 文件名
|
||||
* @param bucket 存储空间名称
|
||||
* @param ctxList 所有块的ctx列表,按顺序排列
|
||||
* @param mimeType 文件MIME类型
|
||||
* @param fileName 原始文件名
|
||||
* @returns
|
||||
*/
|
||||
async mkfile(zone, fileSize, key, bucket, ctxList, mimeType, fileName) {
|
||||
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}`;
|
||||
}
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
const uploadToken = this.generateKodoUploadToken(`${bucket}:${key}`);
|
||||
// ctx列表用逗号分隔作为请求体
|
||||
const body = ctxList.join(',');
|
||||
let response;
|
||||
try {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakNetworkException();
|
||||
}
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 管理端访问七牛云服务器
|
||||
* @param path
|
||||
|
|
@ -665,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', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,4 +28,95 @@ export declare class S3Instance {
|
|||
* 获取文件访问 URL
|
||||
*/
|
||||
getFileUrl(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): string;
|
||||
/**
|
||||
* 创建分片上传
|
||||
*/
|
||||
createMultipartUpload(bucket: string, key: string, endpoint?: string, contentType?: string): Promise<{
|
||||
uploadId: string;
|
||||
bucket: string;
|
||||
key: string;
|
||||
}>;
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
presignMulti(bucket: string, key: string, uploadId: string, from: number, to: number, options?: {
|
||||
endpoint?: string;
|
||||
expiresIn?: number;
|
||||
}): Promise<{
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[]>;
|
||||
/**
|
||||
* 准备分片上传(创建分片上传并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
prepareMultipartUpload(bucket: string, key: string, partCount: number, options?: {
|
||||
endpoint?: string;
|
||||
contentType?: string;
|
||||
expiresIn?: number;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
parts: {
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* 上传分片
|
||||
*/
|
||||
uploadPart(bucket: string, key: string, uploadId: string, partNumber: number, body: Buffer | Uint8Array | string, endpoint?: string): Promise<{
|
||||
partNumber: number;
|
||||
eTag: string;
|
||||
}>;
|
||||
/**
|
||||
* 完成分片上传
|
||||
*/
|
||||
completeMultipartUpload(bucket: string, key: string, uploadId: string, parts: Array<{
|
||||
partNumber: number;
|
||||
eTag: string;
|
||||
}>, endpoint?: string): Promise<{
|
||||
location: string | undefined;
|
||||
bucket: string;
|
||||
key: string;
|
||||
eTag: string | undefined;
|
||||
}>;
|
||||
/**
|
||||
* 中止分片上传
|
||||
*/
|
||||
abortMultipartUpload(bucket: string, key: string, uploadId: string, endpoint?: string): Promise<void>;
|
||||
/**
|
||||
* 列出已上传的分片
|
||||
*/
|
||||
listParts(bucket: string, key: string, uploadId: string, endpoint?: string, maxParts?: number): Promise<{
|
||||
parts: {
|
||||
partNumber: number;
|
||||
eTag: string;
|
||||
size: number;
|
||||
lastModified: Date | undefined;
|
||||
}[];
|
||||
isTruncated: boolean | undefined;
|
||||
nextPartNumberMarker: string | undefined;
|
||||
}>;
|
||||
/**
|
||||
* 获取预签名 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 | undefined, 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, } from '@aws-sdk/client-s3';
|
||||
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', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -118,4 +119,260 @@ export class S3Instance {
|
|||
// AWS S3 默认
|
||||
return `https://${bucket}.s3.${this.defaultRegion}.amazonaws.com/${key}`;
|
||||
}
|
||||
/**
|
||||
* 创建分片上传
|
||||
*/
|
||||
async createMultipartUpload(bucket, key, endpoint, contentType) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
uploadId: response.UploadId,
|
||||
bucket: response.Bucket,
|
||||
key: response.Key,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(bucket, key, uploadId, from, to, options) {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expiresIn = options?.expiresIn || 3600;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadCommand = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: i,
|
||||
});
|
||||
const uploadUrl = await getSignedUrl(client, uploadCommand, {
|
||||
expiresIn: expiresIn,
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
/**
|
||||
* 准备分片上传(创建分片上传并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(bucket, key, partCount, options) {
|
||||
try {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
// 1. 创建分片上传
|
||||
const createCommand = new CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: options?.contentType,
|
||||
});
|
||||
const createResponse = await client.send(createCommand);
|
||||
const uploadId = createResponse.UploadId;
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expiresIn = options?.expiresIn || 3600;
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadCommand = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: i,
|
||||
});
|
||||
const uploadUrl = await getSignedUrl(client, uploadCommand, {
|
||||
expiresIn: expiresIn,
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return {
|
||||
uploadId: uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片
|
||||
*/
|
||||
async uploadPart(bucket, key, uploadId, partNumber, body, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: partNumber,
|
||||
Body: body,
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
partNumber,
|
||||
eTag: response.ETag,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 完成分片上传
|
||||
*/
|
||||
async completeMultipartUpload(bucket, key, uploadId, parts, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new CompleteMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
MultipartUpload: {
|
||||
Parts: parts.map(part => ({
|
||||
PartNumber: part.partNumber,
|
||||
ETag: part.eTag,
|
||||
})),
|
||||
},
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
location: response.Location,
|
||||
bucket: response.Bucket,
|
||||
key: response.Key,
|
||||
eTag: response.ETag,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 中止分片上传
|
||||
*/
|
||||
async abortMultipartUpload(bucket, key, uploadId, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new AbortMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
});
|
||||
await client.send(command);
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列出已上传的分片
|
||||
*/
|
||||
async listParts(bucket, key, uploadId, endpoint, maxParts) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new ListPartsCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
MaxParts: maxParts,
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
parts: response.Parts?.map(part => ({
|
||||
partNumber: part.PartNumber,
|
||||
eTag: part.ETag,
|
||||
size: part.Size,
|
||||
lastModified: part.LastModified,
|
||||
})) || [],
|
||||
isTruncated: response.IsTruncated,
|
||||
nextPartNumberMarker: response.NextPartNumberMarker,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名 URL(通用方法)
|
||||
*/
|
||||
async getSignedUrl(bucket, key, operation = 'get', expiresIn = 3600, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = operation === 'put'
|
||||
? new PutObjectCommand({ Bucket: bucket, Key: key })
|
||||
: new GetObjectCommand({ Bucket: bucket, Key: key });
|
||||
const url = await getSignedUrl(client, command, { expiresIn });
|
||||
return url;
|
||||
}
|
||||
catch (err) {
|
||||
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', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ export class WechatMpInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ export class WechatNativeInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ export class WechatPublicInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ export class WechatWebInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { HuaweiYunInstance } from './service/huawei/Huawei';
|
||||
/**
|
||||
* 华为云SDK主类
|
||||
* 用于管理华为云OBS实例
|
||||
*/
|
||||
declare class HuaweiYunSDK {
|
||||
huaweiMap: Record<string, HuaweiYunInstance>;
|
||||
constructor();
|
||||
/**
|
||||
* 获取或创建华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
* @param accessSecret Access Key Secret
|
||||
* @returns 华为云实例
|
||||
*/
|
||||
getInstance(accessKey: string, accessSecret: string): HuaweiYunInstance;
|
||||
/**
|
||||
* 移除指定的华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
*/
|
||||
removeInstance(accessKey: string): void;
|
||||
/**
|
||||
* 清除所有实例
|
||||
*/
|
||||
clearAllInstances(): void;
|
||||
}
|
||||
declare const SDK: HuaweiYunSDK;
|
||||
export default SDK;
|
||||
export { HuaweiYunInstance };
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.HuaweiYunInstance = void 0;
|
||||
const Huawei_1 = require("./service/huawei/Huawei");
|
||||
Object.defineProperty(exports, "HuaweiYunInstance", { enumerable: true, get: function () { return Huawei_1.HuaweiYunInstance; } });
|
||||
/**
|
||||
* 华为云SDK主类
|
||||
* 用于管理华为云OBS实例
|
||||
*/
|
||||
class HuaweiYunSDK {
|
||||
huaweiMap; // OBS实例映射
|
||||
constructor() {
|
||||
this.huaweiMap = {};
|
||||
}
|
||||
/**
|
||||
* 获取或创建华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
* @param accessSecret Access Key Secret
|
||||
* @returns 华为云实例
|
||||
*/
|
||||
getInstance(accessKey, accessSecret) {
|
||||
if (this.huaweiMap[accessKey]) {
|
||||
return this.huaweiMap[accessKey];
|
||||
}
|
||||
const instance = new Huawei_1.HuaweiYunInstance(accessKey, accessSecret);
|
||||
Object.assign(this.huaweiMap, {
|
||||
[accessKey]: instance,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
/**
|
||||
* 移除指定的华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
*/
|
||||
removeInstance(accessKey) {
|
||||
if (this.huaweiMap[accessKey]) {
|
||||
delete this.huaweiMap[accessKey];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清除所有实例
|
||||
*/
|
||||
clearAllInstances() {
|
||||
this.huaweiMap = {};
|
||||
}
|
||||
}
|
||||
const SDK = new HuaweiYunSDK();
|
||||
exports.default = SDK;
|
||||
|
|
@ -15,4 +15,155 @@ export declare class ALiYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<OSS.DeleteResult>;
|
||||
isExistObject(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
initiateMultipartUpload(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
bucket: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
presignMulti(bucket: string, zone: ALiYunZone, key: string, uploadId: string, from: number, to: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[]>;
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
prepareMultipartUpload(bucket: string, zone: ALiYunZone, key: string, partCount: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
parts: {
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
uploadPart(bucket: string, zone: ALiYunZone, key: string, uploadId: string, partNumber: number, file: any, start?: number, end?: number, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
etag: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
completeMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, parts: Array<{
|
||||
number: number;
|
||||
etag: string;
|
||||
}>, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
bucket: string;
|
||||
name: string;
|
||||
etag: string;
|
||||
}>;
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
abortMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<void>;
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
listParts(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
'max-parts'?: number;
|
||||
'part-number-marker'?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
parts: {
|
||||
partNumber: any;
|
||||
lastModified: any;
|
||||
etag: any;
|
||||
size: any;
|
||||
}[];
|
||||
nextPartNumberMarker: number;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
listMultipartUploads(bucket: string, zone: ALiYunZone, options?: {
|
||||
prefix?: string;
|
||||
'max-uploads'?: number;
|
||||
'key-marker'?: string;
|
||||
'upload-id-marker'?: string;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
uploads: {
|
||||
name: any;
|
||||
uploadId: any;
|
||||
initiated: any;
|
||||
}[];
|
||||
nextKeyMarker: any;
|
||||
nextUploadIdMarker: any;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
getSignedUrl(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
method?: 'GET' | 'PUT' | 'DELETE' | 'POST';
|
||||
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>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.ALiYunInstance = void 0;
|
||||
const tslib_1 = require("tslib");
|
||||
const ali_oss_1 = tslib_1.__importDefault(require("ali-oss"));
|
||||
const Exception_1 = require("oak-domain/lib/types/Exception");
|
||||
function getEndpoint(RegionID) {
|
||||
return `oss-${RegionID}.aliyuncs.com`;
|
||||
}
|
||||
|
|
@ -184,5 +185,288 @@ class ALiYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
async initiateMultipartUpload(bucket, zone, key, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.initMultipartUpload(key, options);
|
||||
return {
|
||||
uploadId: result.uploadId,
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(bucket, zone, key, uploadId, from, to, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const presignedUrls = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
uploadId: uploadId,
|
||||
partNumber: String(i),
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
presignedUrls.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return presignedUrls;
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(bucket, zone, key, partCount, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
// 1. 初始化分片上传
|
||||
const initResult = await client.initMultipartUpload(key, {
|
||||
headers: options?.headers,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
partNumber: String(i),
|
||||
uploadId: initResult.uploadId,
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return {
|
||||
uploadId: initResult.uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
async uploadPart(bucket, zone, key, uploadId, partNumber, file, start, end, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.uploadPart(key, uploadId, partNumber, file, start ?? 0, end ?? 0, options);
|
||||
return {
|
||||
etag: result.etag,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
async completeMultipartUpload(bucket, zone, key, uploadId, parts, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.completeMultipartUpload(key, uploadId, parts, options);
|
||||
return {
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
etag: result.etag,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
async abortMultipartUpload(bucket, zone, key, uploadId, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
await client.abortMultipartUpload(key, uploadId, options);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
async listParts(bucket, zone, key, uploadId, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listParts(key, uploadId, {
|
||||
'max-parts': options?.['max-parts']?.toString(),
|
||||
'part-number-marker': options?.['part-number-marker']?.toString(),
|
||||
});
|
||||
return {
|
||||
parts: result.parts.map((part) => ({
|
||||
partNumber: part.PartNumber,
|
||||
lastModified: part.LastModified,
|
||||
etag: part.ETag,
|
||||
size: part.Size,
|
||||
})),
|
||||
nextPartNumberMarker: result.nextPartNumberMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
async listMultipartUploads(bucket, zone, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listUploads(options || {});
|
||||
return {
|
||||
uploads: result.uploads.map((upload) => ({
|
||||
name: upload.name,
|
||||
uploadId: upload.uploadId,
|
||||
initiated: upload.initiated,
|
||||
})),
|
||||
nextKeyMarker: result.nextKeyMarker,
|
||||
nextUploadIdMarker: result.nextUploadIdMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
async getSignedUrl(bucket, zone, key, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
});
|
||||
try {
|
||||
const url = client.signatureUrl(key, options);
|
||||
return url;
|
||||
}
|
||||
catch (error) {
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
export type AssumeRoleOptions = {
|
||||
endpoint: string;
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
roleArn: string;
|
||||
roleSessionName: string;
|
||||
policy?: string;
|
||||
durationSeconds?: number;
|
||||
};
|
||||
export declare function stsAssumeRole(options: AssumeRoleOptions): Promise<{
|
||||
RequestId: string | undefined;
|
||||
AssumedRoleUser: {
|
||||
AssumedRoleId: string | undefined;
|
||||
Arn: string | undefined;
|
||||
};
|
||||
Credentials: {
|
||||
SecurityToken: string | undefined;
|
||||
Expiration: string | undefined;
|
||||
AccessKeySecret: string | undefined;
|
||||
AccessKeyId: string | undefined;
|
||||
};
|
||||
}>;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.stsAssumeRole = stsAssumeRole;
|
||||
const tslib_1 = require("tslib");
|
||||
const sts20150401_1 = tslib_1.__importStar(require("@alicloud/sts20150401"));
|
||||
async function stsAssumeRole(options) {
|
||||
const client = new sts20150401_1.default({
|
||||
endpoint: options.endpoint,
|
||||
accessKeyId: options.accessKeyId,
|
||||
accessKeySecret: options.accessKeySecret,
|
||||
toMap: () => {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const assumeRoleRequest = new sts20150401_1.AssumeRoleRequest({
|
||||
roleArn: options.roleArn,
|
||||
roleSessionName: options.roleSessionName,
|
||||
policy: options.policy,
|
||||
durationSeconds: options.durationSeconds,
|
||||
});
|
||||
const res = await client.assumeRole(assumeRoleRequest);
|
||||
return {
|
||||
"RequestId": res.body?.requestId,
|
||||
"AssumedRoleUser": {
|
||||
"AssumedRoleId": res.body?.assumedRoleUser?.assumedRoleId,
|
||||
"Arn": res.body?.assumedRoleUser?.arn
|
||||
},
|
||||
"Credentials": {
|
||||
"SecurityToken": res.body?.credentials?.securityToken,
|
||||
"Expiration": res.body?.credentials?.expiration,
|
||||
"AccessKeySecret": res.body?.credentials?.accessKeySecret,
|
||||
"AccessKeyId": res.body?.credentials?.accessKeyId
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Buffer } from 'buffer';
|
||||
import { QiniuLiveStreamInfo, QiniuZone } from '../../types/Qiniu';
|
||||
export declare class QiniuCloudInstance {
|
||||
private accessKey;
|
||||
|
|
@ -151,6 +152,57 @@ export declare class QiniuCloudInstance {
|
|||
* @returns
|
||||
*/
|
||||
getLiveStreamInfo(hub: string, streamTitle: string, host: string): Promise<QiniuLiveStreamInfo>;
|
||||
/**
|
||||
* 创建块(分片上传第一步)
|
||||
* https://developer.qiniu.com/kodo/1286/mkblk
|
||||
* @param zone 区域
|
||||
* @param blockSize 块大小,单位字节
|
||||
* @param firstChunkData 第一个分片的数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
mkblk(zone: QiniuZone, blockSize: number, firstChunkData: Buffer | Uint8Array, bucket: string): Promise<{
|
||||
ctx: string;
|
||||
checksum: string;
|
||||
crc32: number;
|
||||
offset: number;
|
||||
host: string;
|
||||
expired_at: number;
|
||||
}>;
|
||||
/**
|
||||
* 上传分片数据(后续分片)
|
||||
* https://developer.qiniu.com/kodo/1251/bput
|
||||
* @param zone 区域
|
||||
* @param ctx 前一次上传返回的块上下文
|
||||
* @param offset 当前分片在块中的偏移量
|
||||
* @param chunkData 分片数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
bput(zone: QiniuZone, ctx: string, offset: number, chunkData: Buffer | Uint8Array, bucket: string): Promise<{
|
||||
ctx: string;
|
||||
checksum: string;
|
||||
crc32: number;
|
||||
offset: number;
|
||||
host: string;
|
||||
expired_at: number;
|
||||
}>;
|
||||
/**
|
||||
* 创建文件(合并所有块)
|
||||
* https://developer.qiniu.com/kodo/1287/mkfile
|
||||
* @param zone 区域
|
||||
* @param fileSize 文件总大小,单位字节
|
||||
* @param key 文件名
|
||||
* @param bucket 存储空间名称
|
||||
* @param ctxList 所有块的ctx列表,按顺序排列
|
||||
* @param mimeType 文件MIME类型
|
||||
* @param fileName 原始文件名
|
||||
* @returns
|
||||
*/
|
||||
mkfile(zone: QiniuZone, fileSize: number, key: string, bucket: string, ctxList: string[], mimeType?: string, fileName?: string): Promise<{
|
||||
hash: string;
|
||||
key: string;
|
||||
}>;
|
||||
/**
|
||||
* 管理端访问七牛云服务器
|
||||
* @param path
|
||||
|
|
@ -172,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>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -538,6 +538,149 @@ class QiniuCloudInstance {
|
|||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建块(分片上传第一步)
|
||||
* https://developer.qiniu.com/kodo/1286/mkblk
|
||||
* @param zone 区域
|
||||
* @param blockSize 块大小,单位字节
|
||||
* @param firstChunkData 第一个分片的数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
async mkblk(zone, blockSize, firstChunkData, bucket) {
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const path = `/mkblk/${blockSize}`;
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
// 将Buffer转换为Uint8Array,然后获取buffer
|
||||
const bodyData = firstChunkData instanceof buffer_1.Buffer
|
||||
? firstChunkData.buffer.slice(firstChunkData.byteOffset, firstChunkData.byteOffset + firstChunkData.byteLength)
|
||||
: firstChunkData.buffer.slice(firstChunkData.byteOffset, firstChunkData.byteOffset + firstChunkData.byteLength);
|
||||
let response;
|
||||
try {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: bodyData,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new Exception_1.OakNetworkException();
|
||||
}
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片数据(后续分片)
|
||||
* https://developer.qiniu.com/kodo/1251/bput
|
||||
* @param zone 区域
|
||||
* @param ctx 前一次上传返回的块上下文
|
||||
* @param offset 当前分片在块中的偏移量
|
||||
* @param chunkData 分片数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
async bput(zone, ctx, offset, chunkData, bucket) {
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const path = `/bput/${ctx}/${offset}`;
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
// 将Buffer转换为Uint8Array,然后获取buffer
|
||||
const bodyData = chunkData instanceof buffer_1.Buffer
|
||||
? chunkData.buffer.slice(chunkData.byteOffset, chunkData.byteOffset + chunkData.byteLength)
|
||||
: chunkData.buffer.slice(chunkData.byteOffset, chunkData.byteOffset + chunkData.byteLength);
|
||||
let response;
|
||||
try {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: bodyData,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new Exception_1.OakNetworkException();
|
||||
}
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建文件(合并所有块)
|
||||
* https://developer.qiniu.com/kodo/1287/mkfile
|
||||
* @param zone 区域
|
||||
* @param fileSize 文件总大小,单位字节
|
||||
* @param key 文件名
|
||||
* @param bucket 存储空间名称
|
||||
* @param ctxList 所有块的ctx列表,按顺序排列
|
||||
* @param mimeType 文件MIME类型
|
||||
* @param fileName 原始文件名
|
||||
* @returns
|
||||
*/
|
||||
async mkfile(zone, fileSize, key, bucket, ctxList, mimeType, fileName) {
|
||||
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}`;
|
||||
}
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
const uploadToken = this.generateKodoUploadToken(`${bucket}:${key}`);
|
||||
// ctx列表用逗号分隔作为请求体
|
||||
const body = ctxList.join(',');
|
||||
let response;
|
||||
try {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
throw new Exception_1.OakNetworkException();
|
||||
}
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json;
|
||||
}
|
||||
else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 管理端访问七牛云服务器
|
||||
* @param path
|
||||
|
|
@ -669,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;
|
||||
|
|
|
|||
|
|
@ -28,4 +28,95 @@ export declare class S3Instance {
|
|||
* 获取文件访问 URL
|
||||
*/
|
||||
getFileUrl(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): string;
|
||||
/**
|
||||
* 创建分片上传
|
||||
*/
|
||||
createMultipartUpload(bucket: string, key: string, endpoint?: string, contentType?: string): Promise<{
|
||||
uploadId: string;
|
||||
bucket: string;
|
||||
key: string;
|
||||
}>;
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
presignMulti(bucket: string, key: string, uploadId: string, from: number, to: number, options?: {
|
||||
endpoint?: string;
|
||||
expiresIn?: number;
|
||||
}): Promise<{
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[]>;
|
||||
/**
|
||||
* 准备分片上传(创建分片上传并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
prepareMultipartUpload(bucket: string, key: string, partCount: number, options?: {
|
||||
endpoint?: string;
|
||||
contentType?: string;
|
||||
expiresIn?: number;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
parts: {
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* 上传分片
|
||||
*/
|
||||
uploadPart(bucket: string, key: string, uploadId: string, partNumber: number, body: Buffer | Uint8Array | string, endpoint?: string): Promise<{
|
||||
partNumber: number;
|
||||
eTag: string;
|
||||
}>;
|
||||
/**
|
||||
* 完成分片上传
|
||||
*/
|
||||
completeMultipartUpload(bucket: string, key: string, uploadId: string, parts: Array<{
|
||||
partNumber: number;
|
||||
eTag: string;
|
||||
}>, endpoint?: string): Promise<{
|
||||
location: string | undefined;
|
||||
bucket: string;
|
||||
key: string;
|
||||
eTag: string | undefined;
|
||||
}>;
|
||||
/**
|
||||
* 中止分片上传
|
||||
*/
|
||||
abortMultipartUpload(bucket: string, key: string, uploadId: string, endpoint?: string): Promise<void>;
|
||||
/**
|
||||
* 列出已上传的分片
|
||||
*/
|
||||
listParts(bucket: string, key: string, uploadId: string, endpoint?: string, maxParts?: number): Promise<{
|
||||
parts: {
|
||||
partNumber: number;
|
||||
eTag: string;
|
||||
size: number;
|
||||
lastModified: Date | undefined;
|
||||
}[];
|
||||
isTruncated: boolean | undefined;
|
||||
nextPartNumberMarker: string | undefined;
|
||||
}>;
|
||||
/**
|
||||
* 获取预签名 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 | undefined, 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', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -121,5 +122,261 @@ class S3Instance {
|
|||
// AWS S3 默认
|
||||
return `https://${bucket}.s3.${this.defaultRegion}.amazonaws.com/${key}`;
|
||||
}
|
||||
/**
|
||||
* 创建分片上传
|
||||
*/
|
||||
async createMultipartUpload(bucket, key, endpoint, contentType) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new client_s3_1.CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
uploadId: response.UploadId,
|
||||
bucket: response.Bucket,
|
||||
key: response.Key,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(bucket, key, uploadId, from, to, options) {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expiresIn = options?.expiresIn || 3600;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadCommand = new client_s3_1.UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: i,
|
||||
});
|
||||
const uploadUrl = await (0, s3_request_presigner_1.getSignedUrl)(client, uploadCommand, {
|
||||
expiresIn: expiresIn,
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
/**
|
||||
* 准备分片上传(创建分片上传并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(bucket, key, partCount, options) {
|
||||
try {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
// 1. 创建分片上传
|
||||
const createCommand = new client_s3_1.CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: options?.contentType,
|
||||
});
|
||||
const createResponse = await client.send(createCommand);
|
||||
const uploadId = createResponse.UploadId;
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expiresIn = options?.expiresIn || 3600;
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadCommand = new client_s3_1.UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: i,
|
||||
});
|
||||
const uploadUrl = await (0, s3_request_presigner_1.getSignedUrl)(client, uploadCommand, {
|
||||
expiresIn: expiresIn,
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return {
|
||||
uploadId: uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片
|
||||
*/
|
||||
async uploadPart(bucket, key, uploadId, partNumber, body, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new client_s3_1.UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: partNumber,
|
||||
Body: body,
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
partNumber,
|
||||
eTag: response.ETag,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 完成分片上传
|
||||
*/
|
||||
async completeMultipartUpload(bucket, key, uploadId, parts, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new client_s3_1.CompleteMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
MultipartUpload: {
|
||||
Parts: parts.map(part => ({
|
||||
PartNumber: part.partNumber,
|
||||
ETag: part.eTag,
|
||||
})),
|
||||
},
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
location: response.Location,
|
||||
bucket: response.Bucket,
|
||||
key: response.Key,
|
||||
eTag: response.ETag,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 中止分片上传
|
||||
*/
|
||||
async abortMultipartUpload(bucket, key, uploadId, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new client_s3_1.AbortMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
});
|
||||
await client.send(command);
|
||||
}
|
||||
catch (err) {
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列出已上传的分片
|
||||
*/
|
||||
async listParts(bucket, key, uploadId, endpoint, maxParts) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = new client_s3_1.ListPartsCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
MaxParts: maxParts,
|
||||
});
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
parts: response.Parts?.map(part => ({
|
||||
partNumber: part.PartNumber,
|
||||
eTag: part.ETag,
|
||||
size: part.Size,
|
||||
lastModified: part.LastModified,
|
||||
})) || [],
|
||||
isTruncated: response.IsTruncated,
|
||||
nextPartNumberMarker: response.NextPartNumberMarker,
|
||||
};
|
||||
}
|
||||
catch (err) {
|
||||
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名 URL(通用方法)
|
||||
*/
|
||||
async getSignedUrl(bucket, key, operation = 'get', expiresIn = 3600, endpoint) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
const command = operation === 'put'
|
||||
? new client_s3_1.PutObjectCommand({ Bucket: bucket, Key: key })
|
||||
: new client_s3_1.GetObjectCommand({ Bucket: bucket, Key: key });
|
||||
const url = await (0, s3_request_presigner_1.getSignedUrl)(client, command, { expiresIn });
|
||||
return url;
|
||||
}
|
||||
catch (err) {
|
||||
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', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.S3Instance = S3Instance;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ class WechatMpInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ class WechatNativeInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ class WechatPublicInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ class WechatWebInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "oak-external-sdk",
|
||||
"version": "2.3.11",
|
||||
"version": "2.3.14",
|
||||
"description": "",
|
||||
"author": {
|
||||
"name": "XuChang"
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
"dependencies": {
|
||||
"@alicloud/dysmsapi20170525": "^2.0.24",
|
||||
"@alicloud/pop-core": "^1.7.12",
|
||||
"@alicloud/sts20150401": "^1.1.6",
|
||||
"@aws-sdk/client-s3": "^3.910.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.910.0",
|
||||
"ali-oss": "^6.20.0",
|
||||
|
|
@ -36,7 +37,7 @@
|
|||
"cheerio": "^1.0.0-rc.12",
|
||||
"cos-wx-sdk-v5": "^1.7.1",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"oak-domain": "^5.1.28",
|
||||
"oak-domain": "file:../oak-domain",
|
||||
"proj4": "^2.15.0",
|
||||
"tencentcloud-sdk-nodejs": "^4.0.746",
|
||||
"ts-md5": "^1.3.1"
|
||||
|
|
|
|||
|
|
@ -195,4 +195,433 @@ export class ALiYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
async initiateMultipartUpload(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.initMultipartUpload(key, options);
|
||||
return {
|
||||
uploadId: result.uploadId,
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
from: number,
|
||||
to: number,
|
||||
options?: {
|
||||
expires?: number; // URL 过期时间(秒),默认 3600
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const presignedUrls: Array<{ partNumber: number; uploadUrl: string }> =
|
||||
[];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
uploadId: uploadId,
|
||||
partNumber: String(i),
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
} as any);
|
||||
|
||||
presignedUrls.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return presignedUrls;
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
partCount: number,
|
||||
options?: {
|
||||
expires?: number; // URL 过期时间(秒),默认 3600
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 初始化分片上传
|
||||
const initResult = await client.initMultipartUpload(key, {
|
||||
headers: options?.headers,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts: Array<{ partNumber: number; uploadUrl: string }> = [];
|
||||
const expires = options?.expires || 3600;
|
||||
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
partNumber: String(i),
|
||||
uploadId: initResult.uploadId,
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
} as any);
|
||||
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
uploadId: initResult.uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
async uploadPart(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
partNumber: number,
|
||||
file: any,
|
||||
start?: number,
|
||||
end?: number,
|
||||
options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.uploadPart(
|
||||
key,
|
||||
uploadId,
|
||||
partNumber,
|
||||
file,
|
||||
start ?? 0,
|
||||
end ?? 0,
|
||||
options
|
||||
);
|
||||
return {
|
||||
etag: result.etag,
|
||||
name: result.name,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
async completeMultipartUpload(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
parts: Array<{ number: number; etag: string }>,
|
||||
options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.completeMultipartUpload(
|
||||
key,
|
||||
uploadId,
|
||||
parts,
|
||||
options
|
||||
);
|
||||
return {
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
etag: result.etag,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
async abortMultipartUpload(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
|
||||
try {
|
||||
await client.abortMultipartUpload(key, uploadId, options);
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
async listParts(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
options?: {
|
||||
'max-parts'?: number;
|
||||
'part-number-marker'?: number;
|
||||
stsToken?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.listParts(key, uploadId, {
|
||||
'max-parts': options?.['max-parts']?.toString(),
|
||||
'part-number-marker': options?.['part-number-marker']?.toString(),
|
||||
} as any);
|
||||
return {
|
||||
parts: result.parts.map((part: any) => ({
|
||||
partNumber: part.PartNumber,
|
||||
lastModified: part.LastModified,
|
||||
etag: part.ETag,
|
||||
size: part.Size,
|
||||
})),
|
||||
nextPartNumberMarker: result.nextPartNumberMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
async listMultipartUploads(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
options?: {
|
||||
prefix?: string;
|
||||
'max-uploads'?: number;
|
||||
'key-marker'?: string;
|
||||
'upload-id-marker'?: string;
|
||||
stsToken?: string;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.listUploads(options || {});
|
||||
return {
|
||||
uploads: result.uploads.map((upload: any) => ({
|
||||
name: upload.name,
|
||||
uploadId: upload.uploadId,
|
||||
initiated: upload.initiated,
|
||||
})),
|
||||
nextKeyMarker: result.nextKeyMarker,
|
||||
nextUploadIdMarker: result.nextUploadIdMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
async getSignedUrl(
|
||||
bucket: string,
|
||||
zone: ALiYunZone,
|
||||
key: string,
|
||||
options?: {
|
||||
expires?: number;
|
||||
method?: 'GET' | 'PUT' | 'DELETE' | 'POST';
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}
|
||||
) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
});
|
||||
|
||||
try {
|
||||
const url = client.signatureUrl(key, options);
|
||||
return url;
|
||||
} catch (error: any) {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import STSClient, { AssumeRoleRequest } from '@alicloud/sts20150401'
|
||||
|
||||
export type AssumeRoleOptions = {
|
||||
endpoint: string; // 阿里云STS服务地址,一般为 "sts.aliyuncs.com"
|
||||
accessKeyId: string; // 阿里云访问密钥ID
|
||||
accessKeySecret: string; // 阿里云访问密钥Secret
|
||||
roleArn: string; // 角色的资源描述符(ARN)
|
||||
roleSessionName: string; // 角色会话名称
|
||||
policy?: string; // 权限策略
|
||||
durationSeconds?: number; // 临时凭证的有效期,单位为秒,默认3600秒
|
||||
}
|
||||
|
||||
export async function stsAssumeRole(options: AssumeRoleOptions) {
|
||||
const client = new STSClient({
|
||||
endpoint: options.endpoint,
|
||||
accessKeyId: options.accessKeyId,
|
||||
accessKeySecret: options.accessKeySecret,
|
||||
toMap: () => {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const assumeRoleRequest = new AssumeRoleRequest({
|
||||
roleArn: options.roleArn,
|
||||
roleSessionName: options.roleSessionName,
|
||||
policy: options.policy,
|
||||
durationSeconds: options.durationSeconds,
|
||||
});
|
||||
|
||||
const res = await client.assumeRole(assumeRoleRequest);
|
||||
|
||||
return {
|
||||
"RequestId": res.body?.requestId,
|
||||
"AssumedRoleUser": {
|
||||
"AssumedRoleId": res.body?.assumedRoleUser?.assumedRoleId,
|
||||
"Arn": res.body?.assumedRoleUser?.arn
|
||||
},
|
||||
"Credentials": {
|
||||
"SecurityToken": res.body?.credentials?.securityToken,
|
||||
"Expiration": res.body?.credentials?.expiration,
|
||||
"AccessKeySecret": res.body?.credentials?.accessKeySecret,
|
||||
"AccessKeyId": res.body?.credentials?.accessKeyId
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -750,6 +750,208 @@ export class QiniuCloudInstance {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建块(分片上传第一步)
|
||||
* https://developer.qiniu.com/kodo/1286/mkblk
|
||||
* @param zone 区域
|
||||
* @param blockSize 块大小,单位字节
|
||||
* @param firstChunkData 第一个分片的数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
async mkblk(
|
||||
zone: QiniuZone,
|
||||
blockSize: number,
|
||||
firstChunkData: Buffer | Uint8Array,
|
||||
bucket: string
|
||||
) {
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const path = `/mkblk/${blockSize}`;
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
|
||||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
|
||||
// 将Buffer转换为Uint8Array,然后获取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 {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: bodyData,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new OakNetworkException();
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json as {
|
||||
ctx: string; // 块的上下文信息
|
||||
checksum: string; // 块的校验和
|
||||
crc32: number; // 块的crc32值
|
||||
offset: number; // 下一个上传块在切割块中的偏移
|
||||
host: string; // 后续上传接收地址
|
||||
expired_at: number; // ctx过期时间
|
||||
};
|
||||
} else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传分片数据(后续分片)
|
||||
* https://developer.qiniu.com/kodo/1251/bput
|
||||
* @param zone 区域
|
||||
* @param ctx 前一次上传返回的块上下文
|
||||
* @param offset 当前分片在块中的偏移量
|
||||
* @param chunkData 分片数据
|
||||
* @param bucket 存储空间名称
|
||||
* @returns
|
||||
*/
|
||||
async bput(
|
||||
zone: QiniuZone,
|
||||
ctx: string,
|
||||
offset: number,
|
||||
chunkData: Buffer | Uint8Array,
|
||||
bucket: string
|
||||
) {
|
||||
const uploadHost = QINIU_ENDPOINT_LIST[zone].ul;
|
||||
const path = `/bput/${ctx}/${offset}`;
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
|
||||
const uploadToken = this.generateKodoUploadToken(bucket);
|
||||
|
||||
// 将Buffer转换为Uint8Array,然后获取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 {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'application/octet-stream',
|
||||
},
|
||||
body: bodyData,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new OakNetworkException();
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json as {
|
||||
ctx: string; // 块的上下文信息
|
||||
checksum: string; // 块的校验和
|
||||
crc32: number; // 块的crc32值
|
||||
offset: number; // 下一个上传块在切割块中的偏移
|
||||
host: string; // 后续上传接收地址
|
||||
expired_at: number; // ctx过期时间
|
||||
};
|
||||
} else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建文件(合并所有块)
|
||||
* https://developer.qiniu.com/kodo/1287/mkfile
|
||||
* @param zone 区域
|
||||
* @param fileSize 文件总大小,单位字节
|
||||
* @param key 文件名
|
||||
* @param bucket 存储空间名称
|
||||
* @param ctxList 所有块的ctx列表,按顺序排列
|
||||
* @param mimeType 文件MIME类型
|
||||
* @param fileName 原始文件名
|
||||
* @returns
|
||||
*/
|
||||
async mkfile(
|
||||
zone: QiniuZone,
|
||||
fileSize: number,
|
||||
key: string,
|
||||
bucket: string,
|
||||
ctxList: string[],
|
||||
mimeType?: string,
|
||||
fileName?: string
|
||||
) {
|
||||
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}`;
|
||||
}
|
||||
|
||||
const url = `https://${uploadHost}${path}`;
|
||||
const uploadToken = this.generateKodoUploadToken(`${bucket}:${key}`);
|
||||
|
||||
// ctx列表用逗号分隔作为请求体
|
||||
const body = ctxList.join(',');
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await global.fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `UpToken ${uploadToken}`,
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new OakNetworkException();
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
const json = await response.json();
|
||||
return json as {
|
||||
hash: string; // 文件hash值
|
||||
key: string; // 文件名
|
||||
};
|
||||
} else {
|
||||
const json = await response.json();
|
||||
const { error } = json;
|
||||
throw new OakExternalException('qiniu', response.status.toString(), error, {
|
||||
status: response.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端访问七牛云服务器
|
||||
* @param path
|
||||
|
|
@ -920,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',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,9 +3,16 @@ import {
|
|||
PutObjectCommand,
|
||||
DeleteObjectCommand,
|
||||
HeadObjectCommand,
|
||||
CreateMultipartUploadCommand,
|
||||
UploadPartCommand,
|
||||
CompleteMultipartUploadCommand,
|
||||
AbortMultipartUploadCommand,
|
||||
ListPartsCommand,
|
||||
GetObjectCommand,
|
||||
} from '@aws-sdk/client-s3';
|
||||
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
|
||||
import { S3Zone } from '../../types';
|
||||
import { OakExternalException } from 'oak-domain/lib/types';
|
||||
|
||||
export class S3Instance {
|
||||
private accessKey: string;
|
||||
|
|
@ -86,7 +93,14 @@ export class S3Instance {
|
|||
accessKey: this.accessKey,
|
||||
};
|
||||
} catch (err: any) {
|
||||
throw new Error(`生成S3上传URL失败: ${err.message}`);
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +125,14 @@ export class S3Instance {
|
|||
|
||||
await client.send(command);
|
||||
} catch (err: any) {
|
||||
throw new Error(`删除S3文件失败: ${err.message}`);
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,7 +161,14 @@ 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',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,4 +192,413 @@ export class S3Instance {
|
|||
// AWS S3 默认
|
||||
return `https://${bucket}.s3.${this.defaultRegion}.amazonaws.com/${key}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分片上传
|
||||
*/
|
||||
async createMultipartUpload(
|
||||
bucket: string,
|
||||
key: string,
|
||||
endpoint?: string,
|
||||
contentType?: string
|
||||
) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
const command = new CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType,
|
||||
});
|
||||
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
uploadId: response.UploadId!,
|
||||
bucket: response.Bucket!,
|
||||
key: response.Key!,
|
||||
};
|
||||
} catch (err: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(
|
||||
bucket: string,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
from: number,
|
||||
to: number,
|
||||
options?: {
|
||||
endpoint?: string;
|
||||
expiresIn?: number; // URL 过期时间(秒),默认 3600
|
||||
}
|
||||
) {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts: Array<{ partNumber: number; uploadUrl: string }> = [];
|
||||
const expiresIn = options?.expiresIn || 3600;
|
||||
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadCommand = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: i,
|
||||
});
|
||||
|
||||
const uploadUrl = await getSignedUrl(client, uploadCommand, {
|
||||
expiresIn: expiresIn,
|
||||
});
|
||||
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备分片上传(创建分片上传并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(
|
||||
bucket: string,
|
||||
key: string,
|
||||
partCount: number,
|
||||
options?: {
|
||||
endpoint?: string;
|
||||
contentType?: string;
|
||||
expiresIn?: number; // URL 过期时间(秒),默认 3600
|
||||
}
|
||||
) {
|
||||
try {
|
||||
const client = options?.endpoint
|
||||
? this.createClient(options.endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
// 1. 创建分片上传
|
||||
const createCommand = new CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: options?.contentType,
|
||||
});
|
||||
|
||||
const createResponse = await client.send(createCommand);
|
||||
const uploadId = createResponse.UploadId!;
|
||||
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts: Array<{ partNumber: number; uploadUrl: string }> = [];
|
||||
const expiresIn = options?.expiresIn || 3600;
|
||||
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadCommand = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: i,
|
||||
});
|
||||
|
||||
const uploadUrl = await getSignedUrl(client, uploadCommand, {
|
||||
expiresIn: expiresIn,
|
||||
});
|
||||
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
uploadId: uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
} catch (err: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传分片
|
||||
*/
|
||||
async uploadPart(
|
||||
bucket: string,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
partNumber: number,
|
||||
body: Buffer | Uint8Array | string,
|
||||
endpoint?: string
|
||||
) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
const command = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: partNumber,
|
||||
Body: body,
|
||||
});
|
||||
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
partNumber,
|
||||
eTag: response.ETag!,
|
||||
};
|
||||
} catch (err: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 完成分片上传
|
||||
*/
|
||||
async completeMultipartUpload(
|
||||
bucket: string,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
parts: Array<{ partNumber: number; eTag: string }>,
|
||||
endpoint?: string
|
||||
) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
const command = new CompleteMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
MultipartUpload: {
|
||||
Parts: parts.map(part => ({
|
||||
PartNumber: part.partNumber,
|
||||
ETag: part.eTag,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
location: response.Location,
|
||||
bucket: response.Bucket!,
|
||||
key: response.Key!,
|
||||
eTag: response.ETag,
|
||||
};
|
||||
} catch (err: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中止分片上传
|
||||
*/
|
||||
async abortMultipartUpload(
|
||||
bucket: string,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
endpoint?: string
|
||||
) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
const command = new AbortMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
});
|
||||
|
||||
await client.send(command);
|
||||
} catch (err: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出已上传的分片
|
||||
*/
|
||||
async listParts(
|
||||
bucket: string,
|
||||
key: string,
|
||||
uploadId: string,
|
||||
endpoint?: string,
|
||||
maxParts?: number
|
||||
) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
const command = new ListPartsCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
MaxParts: maxParts,
|
||||
});
|
||||
|
||||
const response = await client.send(command);
|
||||
return {
|
||||
parts: response.Parts?.map(part => ({
|
||||
partNumber: part.PartNumber!,
|
||||
eTag: part.ETag!,
|
||||
size: part.Size!,
|
||||
lastModified: part.LastModified,
|
||||
})) || [],
|
||||
isTruncated: response.IsTruncated,
|
||||
nextPartNumberMarker: response.NextPartNumberMarker,
|
||||
};
|
||||
} catch (err: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名 URL(通用方法)
|
||||
*/
|
||||
async getSignedUrl(
|
||||
bucket: string,
|
||||
key: string,
|
||||
operation: 'get' | 'put' = 'get',
|
||||
expiresIn: number = 3600,
|
||||
endpoint?: string
|
||||
) {
|
||||
try {
|
||||
const client = endpoint
|
||||
? this.createClient(endpoint, this.defaultRegion)
|
||||
: this.client;
|
||||
|
||||
const command = operation === 'put'
|
||||
? new PutObjectCommand({ Bucket: bucket, Key: key })
|
||||
: new GetObjectCommand({ Bucket: bucket, Key: key });
|
||||
|
||||
const url = await getSignedUrl(client, command, { expiresIn });
|
||||
return url;
|
||||
} catch (err: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async 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>;
|
||||
}> {
|
||||
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: any) {
|
||||
throw new OakExternalException(
|
||||
's3',
|
||||
error.code,
|
||||
error.message,
|
||||
error,
|
||||
'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',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,7 +98,11 @@ export class WechatMpInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,11 @@ export class WechatNativeInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,11 @@ export class WechatPublicInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,11 @@ export class WechatWebInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue