Compare commits

...

17 Commits
2.3.10 ... dev

73 changed files with 4603 additions and 56 deletions

10
es/S3SDK.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import { S3Instance } from './service/s3/S3';
import { S3Zone } from './types';
declare class S3SDK {
s3Map: Record<string, S3Instance>;
constructor();
getInstance(accessKey: string, accessSecret: string, endpoint?: string, region?: S3Zone): S3Instance;
}
declare const SDK: S3SDK;
export default SDK;
export { S3Instance };

20
es/S3SDK.js Normal file
View File

@ -0,0 +1,20 @@
import { S3Instance } from './service/s3/S3';
class S3SDK {
s3Map;
constructor() {
this.s3Map = {};
}
getInstance(accessKey, accessSecret, endpoint, region = 'us-east-1') {
// 使用 accessKey + endpoint 作为唯一标识
const key = endpoint ? `${accessKey}:${endpoint}` : accessKey;
if (this.s3Map[key]) {
return this.s3Map[key];
}
const instance = new S3Instance(accessKey, accessSecret, endpoint, region);
this.s3Map[key] = instance;
return instance;
}
}
const SDK = new S3SDK();
export default SDK;
export { S3Instance };

3
es/index.d.ts vendored
View File

@ -7,6 +7,7 @@ import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK';
import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK';
import MapwordSDK, { MapWorldInstance } from './MapWorldSDK';
import LocalSDK, { LocalInstance } from './LocalSDK';
import S3SDK, { S3Instance } from './S3SDK';
export * from './service/amap/Amap';
export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, MapwordSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, WechatNativeInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, MapWorldInstance, LocalSDK, LocalInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, };
export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, MapwordSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, WechatNativeInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, MapWorldInstance, LocalSDK, LocalInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, S3SDK, S3Instance, };
export * from './types';

View File

@ -7,6 +7,7 @@ import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK';
import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK';
import MapwordSDK, { MapWorldInstance } from './MapWorldSDK';
import LocalSDK, { LocalInstance } from './LocalSDK';
import S3SDK, { S3Instance } from './S3SDK';
export * from './service/amap/Amap';
export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, MapwordSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, WechatNativeInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, MapWorldInstance, LocalSDK, LocalInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, };
export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, MapwordSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, WechatNativeInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, MapWorldInstance, LocalSDK, LocalInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, S3SDK, S3Instance, };
export * from './types';

View File

@ -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>;
}>;
}

View File

@ -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 };
}
}

22
es/service/ali/sts.d.ts vendored Normal file
View File

@ -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;
};
}>;

31
es/service/ali/sts.js Normal file
View File

@ -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
},
};
}

View File

@ -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>;
}>;
}

View File

@ -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
// 对于预签名 URLpayload 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 };
}
}

View File

@ -1,3 +1,4 @@
import { Buffer } from 'buffer';
import { QiniuLiveStreamInfo, QiniuZone } from '../../types/Qiniu';
export declare class QiniuCloudInstance {
private accessKey;
@ -48,7 +49,7 @@ export declare class QiniuCloudInstance {
* @returns
*/
getKodoFileList(bucket: string, zone: QiniuZone, marker?: string, limit?: number, prefix?: string, delimiter?: string, mockData?: any): Promise<{
marker?: string | undefined;
marker?: string;
items: Array<{
key: string;
hash: string;
@ -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>;
}>;
}

View File

@ -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', {});
}
}
}

122
es/service/s3/S3.d.ts vendored Normal file
View File

@ -0,0 +1,122 @@
import { S3Zone } from '../../types';
export declare class S3Instance {
private accessKey;
private secretKey;
private client;
private defaultEndpoint?;
private defaultRegion;
constructor(accessKey: string, secretKey: string, endpoint?: string, region?: S3Zone);
private createClient;
/**
* ( URL)
*/
getUploadInfo(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): Promise<{
key: string;
uploadUrl: string;
bucket: string;
accessKey: string;
}>;
/**
*
*/
removeFile(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): Promise<void>;
/**
*
*/
isExistObject(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): Promise<boolean>;
/**
* 访 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>;
}>;
}

378
es/service/s3/S3.js Normal file
View File

@ -0,0 +1,378 @@
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;
client;
defaultEndpoint;
defaultRegion;
constructor(accessKey, secretKey, endpoint, region = 'us-east-1') {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.defaultEndpoint = endpoint;
this.defaultRegion = region;
// 创建默认客户端
this.client = this.createClient(endpoint, region);
}
createClient(endpoint, region = this.defaultRegion) {
const config = {
region,
credentials: {
accessKeyId: this.accessKey,
secretAccessKey: this.secretKey,
},
};
// 如果指定了 endpoint (Minio 场景)
if (endpoint) {
let url = endpoint;
if (!/^https?:\/\//.test(url)) {
url = `http://${url}`; // 自动补协议
}
if (!url.endsWith('/')) {
url += '/'; // 自动补尾斜杠
}
config.endpoint = url;
config.tls = url.startsWith('https');
config.forcePathStyle = true; // Minio 通常需要路径风格
}
return new S3Client(config);
}
/**
* 获取上传信息(预签名 URL)
*/
async getUploadInfo(bucket, key, endpoint, pathStyle = false) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
});
// 生成预签名 URL有效期 1 小时
const uploadUrl = await getSignedUrl(client, command, {
expiresIn: 3600,
});
return {
key,
uploadUrl,
bucket,
accessKey: this.accessKey,
};
}
catch (err) {
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
}
}
/**
* 删除文件
*/
async removeFile(bucket, key, endpoint, pathStyle = false) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new DeleteObjectCommand({
Bucket: bucket,
Key: key,
});
await client.send(command);
}
catch (err) {
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
}
}
/**
* 检查对象是否存在
*/
async isExistObject(bucket, key, endpoint, pathStyle = false) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new HeadObjectCommand({
Bucket: bucket,
Key: key,
});
await client.send(command);
return true;
}
catch (err) {
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
return false;
}
throw new OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
}
}
/**
* 获取文件访问 URL
*/
getFileUrl(bucket, key, endpoint, pathStyle = false) {
if (endpoint) {
// Minio 或自定义 endpoint
if (pathStyle) {
return `${endpoint}/${bucket}/${key}`;
}
return `${endpoint.replace(/https?:\/\//, `$&${bucket}.`)}/${key}`;
}
// 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', {});
}
}
}

View File

@ -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>;
}>;
}

View File

@ -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', {});
}
}
}

View File

@ -121,10 +121,10 @@ export declare class WechatMpInstance {
type: number;
content: string;
example: string;
keywordEnumValueList?: {
keywordEnumValueList?: Array<{
keywordCode: string;
enumValueList: Array<string>;
}[] | undefined;
}>;
}[]>;
private isJson;
getURLScheme(options: {
@ -159,7 +159,7 @@ export declare class WechatMpInstance {
delevery_mode: 1 | 2;
logistics_type: 1 | 2 | 3 | 4;
finish_shipping: boolean;
goods_desc?: string | undefined;
goods_desc?: string;
finish_shipping_count: 0 | 1 | 2;
shipping_list: Array<{
tracking_no?: string;

View File

@ -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() {
@ -187,7 +191,7 @@ export class WechatMpInstance {
const { type, media, filetype, filename } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
});
const getLength = () => {
@ -332,7 +336,7 @@ export class WechatMpInstance {
body: JSON.stringify({
jump_wxa: jump_wxa,
is_expire: true,
expire_type: expireType,
expire_type: expireType, //默认是零,到期失效的 scheme 码失效类型失效时间类型0失效间隔天数类型1
expire_time: expiresAt,
expire_interval: expireInterval,
}),

View File

@ -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() {

View File

@ -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() {
@ -511,8 +515,8 @@ export class WechatPublicInstance {
const { type, media, description, filetype, filename, fileLength } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype,
filename: filename,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
knownLength: fileLength,
});
if (type === 'video') {
@ -535,8 +539,8 @@ export class WechatPublicInstance {
const { media, filetype, filename, fileLength } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype,
filename: filename,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
knownLength: fileLength,
});
const headers = formData.getHeaders();
@ -556,8 +560,8 @@ export class WechatPublicInstance {
const { type, media, filetype, filename, fileLength } = options;
const formData = new FormData();
formData.append('media', media, {
contentType: filetype,
filename: filename,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
knownLength: fileLength,
});
const headers = formData.getHeaders();

View File

@ -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() {

1
es/types/S3.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export type S3Zone = 'us-east-1' | 'us-east-2' | 'us-west-1' | 'us-west-2' | 'eu-west-1' | 'eu-west-2' | 'eu-west-3' | 'eu-central-1' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-northeast-1' | 'ap-northeast-2' | 'ap-south-1' | string;

1
es/types/S3.js Normal file
View File

@ -0,0 +1 @@
export {};

1
es/types/index.d.ts vendored
View File

@ -4,3 +4,4 @@ export * from './CTYun';
export * from './ALiYun';
export * from './TencentYun';
export * from './AMap';
export * from './S3';

View File

@ -4,3 +4,4 @@ export * from './CTYun';
export * from './ALiYun';
export * from './TencentYun';
export * from './AMap';
export * from './S3';

View File

@ -1,5 +1,5 @@
declare const formData: {
new (form?: HTMLFormElement | undefined, submitter?: HTMLElement | null | undefined): FormData;
new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
prototype: FormData;
};
export default formData;

View File

@ -1,5 +1,5 @@
declare const formData: {
new (form?: HTMLFormElement | undefined, submitter?: HTMLElement | null | undefined): FormData;
new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
prototype: FormData;
};
export default formData;

28
lib/HuaweiYunSDK.d.ts vendored Normal file
View File

@ -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 };

48
lib/HuaweiYunSDK.js Normal file
View File

@ -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;

10
lib/S3SDK.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import { S3Instance } from './service/s3/S3';
import { S3Zone } from './types';
declare class S3SDK {
s3Map: Record<string, S3Instance>;
constructor();
getInstance(accessKey: string, accessSecret: string, endpoint?: string, region?: S3Zone): S3Instance;
}
declare const SDK: S3SDK;
export default SDK;
export { S3Instance };

23
lib/S3SDK.js Normal file
View File

@ -0,0 +1,23 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.S3Instance = void 0;
const S3_1 = require("./service/s3/S3");
Object.defineProperty(exports, "S3Instance", { enumerable: true, get: function () { return S3_1.S3Instance; } });
class S3SDK {
s3Map;
constructor() {
this.s3Map = {};
}
getInstance(accessKey, accessSecret, endpoint, region = 'us-east-1') {
// 使用 accessKey + endpoint 作为唯一标识
const key = endpoint ? `${accessKey}:${endpoint}` : accessKey;
if (this.s3Map[key]) {
return this.s3Map[key];
}
const instance = new S3_1.S3Instance(accessKey, accessSecret, endpoint, region);
this.s3Map[key] = instance;
return instance;
}
}
const SDK = new S3SDK();
exports.default = SDK;

3
lib/index.d.ts vendored
View File

@ -7,6 +7,7 @@ import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK';
import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK';
import MapwordSDK, { MapWorldInstance } from './MapWorldSDK';
import LocalSDK, { LocalInstance } from './LocalSDK';
import S3SDK, { S3Instance } from './S3SDK';
export * from './service/amap/Amap';
export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, MapwordSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, WechatNativeInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, MapWorldInstance, LocalSDK, LocalInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, };
export { AmapSDK, QiniuSDK, WechatSDK, CTYunSDK, ALiYunSDK, TencentYunSDK, MapwordSDK, CTYunInstance, WechatMpInstance, WechatPublicInstance, WechatWebInstance, WechatNativeInstance, QiniuCloudInstance, ALiYunInstance, TencentYunInstance, MapWorldInstance, LocalSDK, LocalInstance, SmsSdk, TencentSmsInstance, AliSmsInstance, CTYunSmsInstance, S3SDK, S3Instance, };
export * from './types';

View File

@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CTYunSmsInstance = exports.AliSmsInstance = exports.TencentSmsInstance = exports.SmsSdk = exports.LocalInstance = exports.LocalSDK = exports.MapWorldInstance = exports.TencentYunInstance = exports.ALiYunInstance = exports.QiniuCloudInstance = exports.WechatNativeInstance = exports.WechatWebInstance = exports.WechatPublicInstance = exports.WechatMpInstance = exports.CTYunInstance = exports.MapwordSDK = exports.TencentYunSDK = exports.ALiYunSDK = exports.CTYunSDK = exports.WechatSDK = exports.QiniuSDK = exports.AmapSDK = void 0;
exports.S3Instance = exports.S3SDK = exports.CTYunSmsInstance = exports.AliSmsInstance = exports.TencentSmsInstance = exports.SmsSdk = exports.LocalInstance = exports.LocalSDK = exports.MapWorldInstance = exports.TencentYunInstance = exports.ALiYunInstance = exports.QiniuCloudInstance = exports.WechatNativeInstance = exports.WechatWebInstance = exports.WechatPublicInstance = exports.WechatMpInstance = exports.CTYunInstance = exports.MapwordSDK = exports.TencentYunSDK = exports.ALiYunSDK = exports.CTYunSDK = exports.WechatSDK = exports.QiniuSDK = exports.AmapSDK = void 0;
const tslib_1 = require("tslib");
const WechatSDK_1 = tslib_1.__importStar(require("./WechatSDK"));
exports.WechatSDK = WechatSDK_1.default;
@ -33,5 +33,8 @@ Object.defineProperty(exports, "MapWorldInstance", { enumerable: true, get: func
const LocalSDK_1 = tslib_1.__importStar(require("./LocalSDK"));
exports.LocalSDK = LocalSDK_1.default;
Object.defineProperty(exports, "LocalInstance", { enumerable: true, get: function () { return LocalSDK_1.LocalInstance; } });
const S3SDK_1 = tslib_1.__importStar(require("./S3SDK"));
exports.S3SDK = S3SDK_1.default;
Object.defineProperty(exports, "S3Instance", { enumerable: true, get: function () { return S3SDK_1.S3Instance; } });
tslib_1.__exportStar(require("./service/amap/Amap"), exports);
tslib_1.__exportStar(require("./types"), exports);

View File

@ -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>;
}>;
}

View File

@ -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;

22
lib/service/ali/sts.d.ts vendored Normal file
View File

@ -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;
};
}>;

35
lib/service/ali/sts.js Normal file
View File

@ -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
},
};
}

View File

@ -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>;
}>;
}

View File

@ -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
// 对于预签名 URLpayload 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;

View File

@ -1,3 +1,4 @@
import { Buffer } from 'buffer';
import { QiniuLiveStreamInfo, QiniuZone } from '../../types/Qiniu';
export declare class QiniuCloudInstance {
private accessKey;
@ -48,7 +49,7 @@ export declare class QiniuCloudInstance {
* @returns
*/
getKodoFileList(bucket: string, zone: QiniuZone, marker?: string, limit?: number, prefix?: string, delimiter?: string, mockData?: any): Promise<{
marker?: string | undefined;
marker?: string;
items: Array<{
key: string;
hash: string;
@ -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>;
}>;
}

View File

@ -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;

122
lib/service/s3/S3.d.ts vendored Normal file
View File

@ -0,0 +1,122 @@
import { S3Zone } from '../../types';
export declare class S3Instance {
private accessKey;
private secretKey;
private client;
private defaultEndpoint?;
private defaultRegion;
constructor(accessKey: string, secretKey: string, endpoint?: string, region?: S3Zone);
private createClient;
/**
* ( URL)
*/
getUploadInfo(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): Promise<{
key: string;
uploadUrl: string;
bucket: string;
accessKey: string;
}>;
/**
*
*/
removeFile(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): Promise<void>;
/**
*
*/
isExistObject(bucket: string, key: string, endpoint?: string, pathStyle?: boolean): Promise<boolean>;
/**
* 访 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>;
}>;
}

382
lib/service/s3/S3.js Normal file
View File

@ -0,0 +1,382 @@
"use strict";
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;
client;
defaultEndpoint;
defaultRegion;
constructor(accessKey, secretKey, endpoint, region = 'us-east-1') {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.defaultEndpoint = endpoint;
this.defaultRegion = region;
// 创建默认客户端
this.client = this.createClient(endpoint, region);
}
createClient(endpoint, region = this.defaultRegion) {
const config = {
region,
credentials: {
accessKeyId: this.accessKey,
secretAccessKey: this.secretKey,
},
};
// 如果指定了 endpoint (Minio 场景)
if (endpoint) {
let url = endpoint;
if (!/^https?:\/\//.test(url)) {
url = `http://${url}`; // 自动补协议
}
if (!url.endsWith('/')) {
url += '/'; // 自动补尾斜杠
}
config.endpoint = url;
config.tls = url.startsWith('https');
config.forcePathStyle = true; // Minio 通常需要路径风格
}
return new client_s3_1.S3Client(config);
}
/**
* 获取上传信息(预签名 URL)
*/
async getUploadInfo(bucket, key, endpoint, pathStyle = false) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new client_s3_1.PutObjectCommand({
Bucket: bucket,
Key: key,
});
// 生成预签名 URL有效期 1 小时
const uploadUrl = await (0, s3_request_presigner_1.getSignedUrl)(client, command, {
expiresIn: 3600,
});
return {
key,
uploadUrl,
bucket,
accessKey: this.accessKey,
};
}
catch (err) {
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
}
}
/**
* 删除文件
*/
async removeFile(bucket, key, endpoint, pathStyle = false) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new client_s3_1.DeleteObjectCommand({
Bucket: bucket,
Key: key,
});
await client.send(command);
}
catch (err) {
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
}
}
/**
* 检查对象是否存在
*/
async isExistObject(bucket, key, endpoint, pathStyle = false) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new client_s3_1.HeadObjectCommand({
Bucket: bucket,
Key: key,
});
await client.send(command);
return true;
}
catch (err) {
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
return false;
}
throw new types_1.OakExternalException('s3', err.code, err.message, err, 'oak-external-sdk', {});
}
}
/**
* 获取文件访问 URL
*/
getFileUrl(bucket, key, endpoint, pathStyle = false) {
if (endpoint) {
// Minio 或自定义 endpoint
if (pathStyle) {
return `${endpoint}/${bucket}/${key}`;
}
return `${endpoint.replace(/https?:\/\//, `$&${bucket}.`)}/${key}`;
}
// 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;

View File

@ -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>;
}>;
}

View File

@ -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;

View File

@ -121,10 +121,10 @@ export declare class WechatMpInstance {
type: number;
content: string;
example: string;
keywordEnumValueList?: {
keywordEnumValueList?: Array<{
keywordCode: string;
enumValueList: Array<string>;
}[] | undefined;
}>;
}[]>;
private isJson;
getURLScheme(options: {
@ -159,7 +159,7 @@ export declare class WechatMpInstance {
delevery_mode: 1 | 2;
logistics_type: 1 | 2 | 3 | 4;
finish_shipping: boolean;
goods_desc?: string | undefined;
goods_desc?: string;
finish_shipping_count: 0 | 1 | 2;
shipping_list: Array<{
tracking_no?: string;

View File

@ -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() {
@ -191,7 +195,7 @@ class WechatMpInstance {
const { type, media, filetype, filename } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
});
const getLength = () => {
@ -336,7 +340,7 @@ class WechatMpInstance {
body: JSON.stringify({
jump_wxa: jump_wxa,
is_expire: true,
expire_type: expireType,
expire_type: expireType, //默认是零,到期失效的 scheme 码失效类型失效时间类型0失效间隔天数类型1
expire_time: expiresAt,
expire_interval: expireInterval,
}),

View File

@ -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() {

View File

@ -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() {
@ -515,8 +519,8 @@ class WechatPublicInstance {
const { type, media, description, filetype, filename, fileLength } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
filename: filename,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
knownLength: fileLength,
});
if (type === 'video') {
@ -539,8 +543,8 @@ class WechatPublicInstance {
const { media, filetype, filename, fileLength } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
filename: filename,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
knownLength: fileLength,
});
const headers = formData.getHeaders();
@ -560,8 +564,8 @@ class WechatPublicInstance {
const { type, media, filetype, filename, fileLength } = options;
const formData = new form_data_1.default();
formData.append('media', media, {
contentType: filetype,
filename: filename,
contentType: filetype, // 微信识别需要
filename: filename, // 微信识别需要
knownLength: fileLength,
});
const headers = formData.getHeaders();

View File

@ -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
lib/types/S3.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export type S3Zone = 'us-east-1' | 'us-east-2' | 'us-west-1' | 'us-west-2' | 'eu-west-1' | 'eu-west-2' | 'eu-west-3' | 'eu-central-1' | 'ap-southeast-1' | 'ap-southeast-2' | 'ap-northeast-1' | 'ap-northeast-2' | 'ap-south-1' | string;

2
lib/types/S3.js Normal file
View File

@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

View File

@ -4,3 +4,4 @@ export * from './CTYun';
export * from './ALiYun';
export * from './TencentYun';
export * from './AMap';
export * from './S3';

View File

@ -7,3 +7,4 @@ tslib_1.__exportStar(require("./CTYun"), exports);
tslib_1.__exportStar(require("./ALiYun"), exports);
tslib_1.__exportStar(require("./TencentYun"), exports);
tslib_1.__exportStar(require("./AMap"), exports);
tslib_1.__exportStar(require("./S3"), exports);

View File

@ -1,8 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.load = void 0;
exports.load = load;
const assert_1 = require("oak-domain/lib/utils/assert");
function load(content) {
(0, assert_1.assert)(false, 'cheerio load not implemented');
}
exports.load = load;

View File

@ -1,8 +1,7 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.load = void 0;
exports.load = load;
const assert_1 = require("oak-domain/lib/utils/assert");
function load(content) {
(0, assert_1.assert)(false, 'cheerio load not implemented');
}
exports.load = load;

View File

@ -1,5 +1,5 @@
declare const formData: {
new (form?: HTMLFormElement | undefined, submitter?: HTMLElement | null | undefined): FormData;
new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
prototype: FormData;
};
export default formData;

View File

@ -1,5 +1,5 @@
declare const formData: {
new (form?: HTMLFormElement | undefined, submitter?: HTMLElement | null | undefined): FormData;
new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
prototype: FormData;
};
export default formData;

View File

@ -1,6 +1,6 @@
{
"name": "oak-external-sdk",
"version": "2.3.10",
"version": "2.3.14",
"description": "",
"author": {
"name": "XuChang"
@ -29,12 +29,15 @@
"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",
"aws-sdk": "^2.1499.0",
"cheerio": "^1.0.0-rc.12",
"cos-wx-sdk-v5": "^1.7.1",
"isomorphic-fetch": "^3.0.0",
"oak-domain": "^5.1.27",
"oak-domain": "file:../oak-domain",
"proj4": "^2.15.0",
"tencentcloud-sdk-nodejs": "^4.0.746",
"ts-md5": "^1.3.1"

34
src/S3SDK.ts Normal file
View File

@ -0,0 +1,34 @@
import { S3Instance } from './service/s3/S3';
import { S3Zone } from './types';
class S3SDK {
s3Map: Record<string, S3Instance>;
constructor() {
this.s3Map = {};
}
getInstance(
accessKey: string,
accessSecret: string,
endpoint?: string,
region: S3Zone = 'us-east-1'
) {
// 使用 accessKey + endpoint 作为唯一标识
const key = endpoint ? `${accessKey}:${endpoint}` : accessKey;
if (this.s3Map[key]) {
return this.s3Map[key];
}
const instance = new S3Instance(accessKey, accessSecret, endpoint, region);
this.s3Map[key] = instance;
return instance;
}
}
const SDK = new S3SDK();
export default SDK;
export { S3Instance };

View File

@ -11,6 +11,7 @@ import ALiYunSDK, { ALiYunInstance } from './ALiYunSDK';
import TencentYunSDK, { TencentYunInstance } from './TencentYunSDK';
import MapwordSDK, { MapWorldInstance } from './MapWorldSDK';
import LocalSDK, { LocalInstance } from './LocalSDK';
import S3SDK, { S3Instance } from './S3SDK';
export * from './service/amap/Amap';
export {
@ -37,6 +38,9 @@ export {
TencentSmsInstance,
AliSmsInstance,
CTYunSmsInstance,
S3SDK,
S3Instance,
};
export * from './types';

View File

@ -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 };
}
}

45
src/service/ali/sts.ts Normal file
View File

@ -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
},
}
}

View File

@ -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
// 对于预签名 URLpayload 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 };
}
}

View File

@ -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',
{}
);
}
}
}

604
src/service/s3/S3.ts Normal file
View File

@ -0,0 +1,604 @@
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 { S3Zone } from '../../types';
import { OakExternalException } from 'oak-domain/lib/types';
export class S3Instance {
private accessKey: string;
private secretKey: string;
private client: S3Client;
private defaultEndpoint?: string;
private defaultRegion: string;
constructor(
accessKey: string,
secretKey: string,
endpoint?: string,
region: S3Zone = 'us-east-1'
) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.defaultEndpoint = endpoint;
this.defaultRegion = region;
// 创建默认客户端
this.client = this.createClient(endpoint, region);
}
private createClient(endpoint?: string, region: string = this.defaultRegion) {
const config: any = {
region,
credentials: {
accessKeyId: this.accessKey,
secretAccessKey: this.secretKey,
},
};
// 如果指定了 endpoint (Minio 场景)
if (endpoint) {
let url = endpoint;
if (!/^https?:\/\//.test(url)) {
url = `http://${url}`; // 自动补协议
}
if (!url.endsWith('/')) {
url += '/'; // 自动补尾斜杠
}
config.endpoint = url;
config.tls = url.startsWith('https');
config.forcePathStyle = true; // Minio 通常需要路径风格
}
return new S3Client(config)
}
/**
* ( URL)
*/
async getUploadInfo(
bucket: string,
key: string,
endpoint?: string,
pathStyle: boolean = false
) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
});
// 生成预签名 URL有效期 1 小时
const uploadUrl = await getSignedUrl(client, command, {
expiresIn: 3600,
});
return {
key,
uploadUrl,
bucket,
accessKey: this.accessKey,
};
} catch (err: any) {
throw new OakExternalException(
's3',
err.code,
err.message,
err,
'oak-external-sdk',
{}
);
}
}
/**
*
*/
async removeFile(
bucket: string,
key: string,
endpoint?: string,
pathStyle: boolean = false
) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new DeleteObjectCommand({
Bucket: bucket,
Key: key,
});
await client.send(command);
} catch (err: any) {
throw new OakExternalException(
's3',
err.code,
err.message,
err,
'oak-external-sdk',
{}
);
}
}
/**
*
*/
async isExistObject(
bucket: string,
key: string,
endpoint?: string,
pathStyle: boolean = false
) {
try {
const client = endpoint
? this.createClient(endpoint, this.defaultRegion)
: this.client;
const command = new HeadObjectCommand({
Bucket: bucket,
Key: key,
});
await client.send(command);
return true;
} catch (err: any) {
if (err.name === 'NotFound' || err.$metadata?.httpStatusCode === 404) {
return false;
}
throw new OakExternalException(
's3',
err.code,
err.message,
err,
'oak-external-sdk',
{}
);
}
}
/**
* 访 URL
*/
getFileUrl(
bucket: string,
key: string,
endpoint?: string,
pathStyle: boolean = false
) {
if (endpoint) {
// Minio 或自定义 endpoint
if (pathStyle) {
return `${endpoint}/${bucket}/${key}`;
}
return `${endpoint.replace(/https?:\/\//, `$&${bucket}.`)}/${key}`;
}
// 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',
{}
);
}
}
}

View File

@ -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',
{}
);
}
}
}

View File

@ -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}]`);
});
}
}

View File

@ -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}]`);
});
}
}

View File

@ -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}]`);
});
}
}

View File

@ -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}]`);
});
}
}

16
src/types/S3.ts Normal file
View File

@ -0,0 +1,16 @@
// types/S3.ts
export type S3Zone =
| 'us-east-1'
| 'us-east-2'
| 'us-west-1'
| 'us-west-2'
| 'eu-west-1'
| 'eu-west-2'
| 'eu-west-3'
| 'eu-central-1'
| 'ap-southeast-1'
| 'ap-southeast-2'
| 'ap-northeast-1'
| 'ap-northeast-2'
| 'ap-south-1'
| string; // 允许自定义区域

View File

@ -3,4 +3,5 @@ export * from './Qiniu';
export * from './CTYun';
export * from './ALiYun';
export * from './TencentYun';
export * from './AMap';
export * from './AMap';
export * from './S3';