Compare commits

...

27 Commits
2.3.7 ... dev

Author SHA1 Message Date
Pan Qiancheng 170affd3a9 2.3.14-dev 2026-01-21 17:03:57 +08:00
Xu Chang f1545faedf 2.3.13-dev 2026-01-21 11:00:40 +08:00
Xu Chang ab689a6fc2 2.3.13-pub 2026-01-21 10:59:04 +08:00
Pan Qiancheng 6109d8010e Merge branch 'dev' of https://gitea.51mars.com/Oak-Team/oak-external-sdk into dev 2026-01-14 15:53:33 +08:00
Pan Qiancheng 9516284bc9 feat: 初始化分片等方法 2026-01-14 15:53:30 +08:00
Xu Chang 56fef25306 2.3.13-dev 2026-01-09 15:47:22 +08:00
Xu Chang a25c89e6e3 2.3.12-pub 2026-01-09 15:46:19 +08:00
Pan Qiancheng 25ac418fc1 fix: S3 可选zone 2025-12-29 16:36:34 +08:00
Pan Qiancheng 7a25050213 feat: 各个COS的SDK中添加presignObjectUrl 2025-12-29 16:32:19 +08:00
Pan Qiancheng c33d55c680 fix: 修复了分片预签名时,ali必须提供Content-Type参数 2025-12-26 23:26:04 +08:00
Pan Qiancheng 5279258c64 feat: 新增分片上传,预签名等方法 2025-12-26 13:13:04 +08:00
Pan Qiancheng 20d4540b1e fix: 修复微信相关SDK初始化时异步调用接口导致的崩溃 2025-12-17 18:52:58 +08:00
Xu Chang 64a86e9266 2.3.12-dev 2025-10-16 09:14:34 +08:00
Xu Chang dc1885cb1e 2.3.11-pub 2025-10-16 09:13:42 +08:00
Pan Qiancheng 6b8d5cd10c build 2025-10-15 16:29:41 +08:00
Pan Qiancheng 246b59bba9 feat: 支持了S3协议的文件上传SDK 2025-10-15 16:29:34 +08:00
Xu Chang 201f174dd1 2.3.11 2025-08-04 15:16:46 +08:00
Xu Chang 84ff7d86c2 2.3.10 2025-08-04 15:14:43 +08:00
wkj f0abeba024 fix 阿里云获取短信模板列表 pageSize和pageIndex 参数错误 2025-08-04 14:17:30 +08:00
Xu Chang 759b31bfa2 2.3.10-dev 2025-08-02 21:24:37 +08:00
Xu Chang e9c9095b45 2.3.9-pub 2025-08-02 21:23:21 +08:00
lxy 304c2f6ac2 调整创建直播流方法名称,新增查询直播流信息方法 2025-07-14 16:45:22 +08:00
lxy a71d5d339f 七牛云直播相关接口修正 2025-07-11 16:51:38 +08:00
Xu Chang dde0a82476 2.3.9-dev 2025-06-16 12:13:49 +08:00
Xu Chang f8b6b1e8bc 2.3.8-pub 2025-06-16 12:12:54 +08:00
lxy ff7a5c2b63 七牛云直播相关接口修改 2025-06-06 18:34:51 +08:00
Xu Chang a2ba587bea 2.3.8-dev 2025-05-29 10:55:50 +08:00
81 changed files with 5741 additions and 405 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 };
}
}

View File

@ -8,8 +8,8 @@ type SendSmsRequest = {
outId?: string;
};
type DescribeSmsTemplateListRequest = {
PageIndex: number;
PageSize: number;
pageIndex: number;
pageSize: number;
};
export declare class AliSmsInstance {
accessKeyId: string;

View File

@ -42,11 +42,11 @@ export class AliSmsInstance {
}
}
async syncTemplate(params) {
const { PageIndex, PageSize } = params;
const { pageIndex, pageSize } = params;
try {
let querySmsTemplateListRequest = new $Dysmsapi20170525.QuerySmsTemplateListRequest({
PageIndex,
PageSize,
pageIndex,
pageSize,
});
const result = await this.client.querySmsTemplateListWithOptions(querySmsTemplateListRequest, new $Util.RuntimeOptions({}));
const { statusCode, body } = result;

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,4 +1,5 @@
import { QiniuZone } from '../../types/Qiniu';
import { Buffer } from 'buffer';
import { QiniuLiveStreamInfo, QiniuZone } from '../../types/Qiniu';
export declare class QiniuCloudInstance {
private accessKey;
private secretKey;
@ -17,26 +18,6 @@ export declare class QiniuCloudInstance {
uploadHost: string;
bucket: string;
};
/**
* token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getLiveToken(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, host: string, rawQuery?: string, contentType?: string, bodyStr?: string): string;
getLiveStream(hub: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', streamTitle: string, host: string, publishDomain: string, playDomain: string, publishKey: string, playKey: string, expireAt: number): Promise<{
streamTitle: string;
hub: string;
rtmpPushUrl: string;
rtmpPlayUrl: string;
pcPushUrl: string;
streamKey: string;
expireAt: number;
}>;
/**
* https://developer.qiniu.com/kodo/1308/stat
* GET方法nodejs-sdk里看是POST方法
@ -68,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;
@ -82,26 +63,146 @@ export declare class QiniuCloudInstance {
moveKodoFile(srcBucket: string, zone: QiniuZone, srcKey: string, destBucket: string, destKey: string, force?: boolean, mockData?: any): Promise<void>;
copyKodoFile(srcBucket: string, zone: QiniuZone, srcKey: string, destBucket: string, destKey: string, force?: boolean, mockData?: any): Promise<void>;
/**
*
* @param publishDomain
* @param playDomain
* @param hub
* @param publishKey
* @param playKey
* @param streamTitle
* @param expireAt
* token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getStreamObj(publishDomain: string, playDomain: string, hub: string, publishKey: string, playKey: string, streamTitle: string, expireAt: number): {
getLiveToken(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, host: string, rawQuery?: string, contentType?: string, bodyStr?: string): string;
/**
*
* @param hub
* @param streamTitle
* @param host
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param expireAt
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
createLiveStream(hub: string, streamTitle: string, host: string, publishDomain: string, playDomainType: 'rtmp' | 'hls' | 'flv', playDomain: string, expireAt: number, publishSecurity: 'none' | 'static' | 'expiry' | 'expiry_sk', publishKey: string, playKey: string): Promise<{
streamTitle: string;
hub: string;
rtmpPushUrl: string;
rtmpPlayUrl: string;
playUrl: string;
pcPushUrl: string;
streamKey: string;
expireAt: number;
}>;
/**
*
* @param hub
* @param streamTitle
* @param expireAt
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
getStreamObj(hub: string, streamTitle: string, expireAt: number, publishDomain: string, playDomainType: 'rtmp' | 'hls' | 'flv', playDomain: string, publishSecurity: 'none' | 'static' | 'expiry' | 'expiry_sk', publishKey: string, playKey: string): {
streamTitle: string;
hub: string;
rtmpPushUrl: string;
playUrl: string;
pcPushUrl: string;
streamKey: string;
expireAt: number;
};
getPlayBackUrl(hub: string, playBackDomain: string, streamTitle: string, start: number, end: number, method: 'GET' | 'POST' | 'PUT' | 'DELETE', host: string, rawQuery?: string): Promise<string>;
/**
*
* @param hub
* @param streamTitle
* @param host
* @param disabledTill
* @param disablePeriodSecond
*/
disabledStream(hub: string, streamTitle: string, host: string, disabledTill: number, //禁播结束时间 -1永久禁播0解除禁播
disablePeriodSecond?: number): Promise<void>;
/**
*
* @param hub
* @param host
* @param liveOnly
* @param prefix
* @param limit
* @param marker
* @returns
*/
getLiveStreamList(hub: string, host: string, liveOnly?: boolean, prefix?: string, limit?: number, //取值范围0~5000
marker?: string): Promise<{
streamTitles: string[];
marker: string;
}>;
/**
*
* @param hub
* @param streamTitle
* @param host
* @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
@ -123,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

@ -135,63 +135,6 @@ export class QiniuCloudInstance {
throw err;
}
}
/**
* 计算直播需要的token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getLiveToken(method, path, host, rawQuery, contentType, bodyStr) {
// 1. 添加 Path
let data = `${method} ${path}`;
if (rawQuery) {
data += `?${rawQuery}`;
}
data += `\nHost: ${host}`;
if (contentType) {
data += `\nContent-Type: ${contentType}`;
}
data += '\n\n';
if (bodyStr &&
contentType &&
contentType !== 'application/octet-stream') {
data += bodyStr;
}
const sign = this.hmacSha1(data, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const toke = 'Qiniu ' + this.accessKey + ':' + encodedSign;
return toke;
}
async getLiveStream(hub, method, streamTitle, host, publishDomain, playDomain, publishKey, playKey, expireAt) {
// 七牛创建直播流接口路径
const path = `/v2/hubs/${hub}/streams`;
// 如果用户没给streamTitle那么随机生成一个
let key = streamTitle;
if (!key) {
key = `class${new Date().getTime()}`;
}
const bodyStr = JSON.stringify({
key,
});
const contentType = 'application/json';
const token = this.getLiveToken(method, path, host);
const url = `https://pili.qiniuapi.com/v2/hubs/${hub}/streams`;
await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
mode: 'no-cors',
});
const obj = this.getStreamObj(publishDomain, playDomain, hub, publishKey, playKey, streamTitle, expireAt);
return obj;
}
/**
* https://developer.qiniu.com/kodo/1308/stat
* 文档里写的是GET方法从nodejs-sdk里看是POST方法
@ -272,35 +215,162 @@ export class QiniuCloudInstance {
}, undefined, 'POST', undefined, mockData);
}
/**
* 计算直播流地址相关信息
* @param publishDomain
* @param playDomain
* @param hub
* @param publishKey
* @param playKey
* @param streamTitle
* @param expireAt
* 计算直播需要的token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getStreamObj(publishDomain, playDomain, hub, publishKey, playKey, streamTitle, expireAt) {
const signStr = `/${hub}/${streamTitle}?expire=${expireAt}`;
const sourcePath = `/${hub}/${streamTitle}`;
const token = this.base64ToUrlSafe(this.hmacSha1(signStr, publishKey));
const rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
// 生成播放地址
const t = expireAt.toString(16).toLowerCase();
const playSign = Md5.hashStr(playKey + sourcePath + t)
.toString()
.toLowerCase();
const rtmpPlayUrl = `https://${playDomain}${sourcePath}.m3u8?sign=${playSign}&t=${t}`;
// obs推流需要的地址和串流密钥
getLiveToken(method, path, host, rawQuery, contentType, bodyStr) {
// 1. 添加 Path
let data = `${method} ${path}`;
if (rawQuery) {
data += `?${rawQuery}`;
}
data += `\nHost: ${host}`;
if (contentType) {
data += `\nContent-Type: ${contentType}`;
}
data += '\n\n';
if (bodyStr &&
contentType &&
contentType !== 'application/octet-stream') {
data += bodyStr;
}
const sign = this.hmacSha1(data, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const toke = 'Qiniu ' + this.accessKey + ':' + encodedSign;
return toke;
}
/**
* 创建直播流
* @param hub
* @param streamTitle
* @param host
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param expireAt
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
async createLiveStream(hub, streamTitle, host, publishDomain, playDomainType, playDomain, expireAt, publishSecurity, publishKey, playKey) {
// 七牛创建直播流接口路径
const path = `/v2/hubs/${hub}/streams`;
// 如果用户没给streamTitle那么随机生成一个
let key = streamTitle;
if (!key) {
key = `class${new Date().getTime()}`;
}
const bodyStr = JSON.stringify({
key,
});
const contentType = 'application/json';
const token = this.getLiveToken('POST', path, host, undefined, contentType, bodyStr);
const url = `https://pili.qiniuapi.com/v2/hubs/${hub}/streams`;
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
// mode: 'no-cors',
});
}
catch (err) {
throw new OakNetworkException();
}
if (response.status !== 200) {
if (response.status === 614) {
throw new OakExternalException('qiniu', response.status.toString(), '直播流名已存在', {
status: response.status,
});
}
else {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
const obj = this.getStreamObj(hub, streamTitle, expireAt, publishDomain, playDomainType, playDomain, publishSecurity, publishKey, playKey);
return obj;
}
/**
* 计算直播流地址相关信息
* @param hub
* @param streamTitle
* @param expireAt
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
getStreamObj(hub, streamTitle, expireAt, publishDomain, playDomainType, playDomain, publishSecurity, publishKey, playKey) {
// 根据推流鉴权方式生成推流地址 rtmp推流
const pcPushUrl = `rtmp://${publishDomain}/${hub}/`;
const streamKey = `${streamTitle}?expire=${expireAt}&token=${token}`;
let rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`, streamKey = `${streamTitle}`;
if (publishSecurity === 'none') {
//无校验鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`;
streamKey = `${streamTitle}`;
}
else if (publishSecurity === 'static') {
//静态鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?key=<PublishKey>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}?key=${publishKey}`;
streamKey = `${streamTitle}?key=${publishKey}`;
}
else if (publishSecurity === 'expiry') {
//限时鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?expire=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?expire=${expireAt}`;
const token = this.base64ToUrlSafe(this.hmacSha1(signStr, publishKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?expire=${expireAt}&token=${token}`;
}
else if (publishSecurity === 'expiry_sk') {
//限时鉴权sk rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?e=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?e=${expireAt}`;
const token = this.accessKey + ':' + this.base64ToUrlSafe(this.hmacSha1(signStr, this.secretKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?e=${expireAt}&token=${token}`;
}
//根据拉流域名类型生成播放地址
let playUrl = '', playPath = '';
if (playDomainType === 'rtmp') {
playPath = `/${hub}/${streamTitle}`;
playUrl = `rtmp://${playDomain}${playPath}`;
}
else if (playDomainType === 'hls') {
playPath = `/${hub}/${streamTitle}.m3u8`;
playUrl = `http://${playDomain}${playPath}`;
}
else if (playDomainType === 'flv') {
playPath = `/${hub}/${streamTitle}.flv`;
playUrl = `http://${playDomain}${playPath}`;
}
if (playKey && playKey !== '') {
//开启时间戳防盗链
const t = expireAt.toString(16).toLowerCase();
const playSign = Md5.hashStr(playKey + playPath + t).toString().toLowerCase();
playUrl += `?sign=${playSign}&t=${t}`;
}
return {
streamTitle,
hub,
rtmpPushUrl,
rtmpPlayUrl,
playUrl,
pcPushUrl,
streamKey,
expireAt,
@ -328,6 +398,285 @@ export class QiniuCloudInstance {
});
return `https://${playBackDomain}/${streamTitle}.m3u8`;
}
/**
* 禁用直播流
* @param hub
* @param streamTitle
* @param host
* @param disabledTill
* @param disablePeriodSecond
*/
async disabledStream(hub, streamTitle, host, disabledTill, //禁播结束时间 -1永久禁播0解除禁播
disablePeriodSecond) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}/disabled`;
const bodyStr = JSON.stringify({
disabledTill,
disablePeriodSecond,
});
const contentType = 'application/json';
const token = this.getLiveToken('POST', path, host, undefined, contentType, bodyStr);
const url = `https://pili.qiniuapi.com${path}`;
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
});
}
catch (err) {
throw new OakNetworkException();
}
if (response.status !== 200) {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 查询直播流列表
* @param hub
* @param host
* @param liveOnly
* @param prefix
* @param limit
* @param marker
* @returns
*/
async getLiveStreamList(hub, host, liveOnly, prefix, limit, //取值范围0~5000
marker) {
const path = `/v2/hubs/${hub}/streams`;
const url = new URL(`https://${host}${path}`);
if (liveOnly) {
url.searchParams.set('liveOnly', 'true');
}
if (prefix && prefix !== '') {
url.searchParams.set('prefix', prefix);
}
if (limit) {
url.searchParams.set('limit', limit.toString());
}
if (marker && marker !== '') {
url.searchParams.set('prefix', marker);
}
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken('GET', path, host, undefined, contentType);
let response;
try {
response = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
}
catch (err) {
throw new OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
const streamTitles = json.items?.map((ele) => ele.key);
return {
streamTitles,
marker: json.marker,
};
}
else {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 查询直播流信息
* @param hub
* @param streamTitle
* @param host
* @returns
*/
async getLiveStreamInfo(hub, streamTitle, host) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}`;
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken('GET', path, host, undefined, contentType);
const url = `https://pili.qiniuapi.com${path}`;
let response;
try {
response = await global.fetch(url, {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
}
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/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
@ -459,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() {

11
es/types/Qiniu.d.ts vendored
View File

@ -1 +1,12 @@
export type QiniuZone = 'z0' | 'cn-east-2' | 'z1' | 'z2' | 'na0' | 'as0';
export type QiniuLiveStreamInfo = {
createdAt: number;
updatedAt: number;
expireAt: number;
disabledTill: number;
converts: string[];
watermark: boolean;
publishSecurity: string;
publishKey: string;
nropEnable: boolean;
};

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;

View File

@ -8,8 +8,8 @@ type SendSmsRequest = {
outId?: string;
};
type DescribeSmsTemplateListRequest = {
PageIndex: number;
PageSize: number;
pageIndex: number;
pageSize: number;
};
export declare class AliSmsInstance {
accessKeyId: string;

View File

@ -46,11 +46,11 @@ class AliSmsInstance {
}
}
async syncTemplate(params) {
const { PageIndex, PageSize } = params;
const { pageIndex, pageSize } = params;
try {
let querySmsTemplateListRequest = new $Dysmsapi20170525.QuerySmsTemplateListRequest({
PageIndex,
PageSize,
pageIndex,
pageSize,
});
const result = await this.client.querySmsTemplateListWithOptions(querySmsTemplateListRequest, new $Util.RuntimeOptions({}));
const { statusCode, body } = result;

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,4 +1,5 @@
import { QiniuZone } from '../../types/Qiniu';
import { Buffer } from 'buffer';
import { QiniuLiveStreamInfo, QiniuZone } from '../../types/Qiniu';
export declare class QiniuCloudInstance {
private accessKey;
private secretKey;
@ -17,26 +18,6 @@ export declare class QiniuCloudInstance {
uploadHost: string;
bucket: string;
};
/**
* token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getLiveToken(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, host: string, rawQuery?: string, contentType?: string, bodyStr?: string): string;
getLiveStream(hub: string, method: 'GET' | 'POST' | 'PUT' | 'DELETE', streamTitle: string, host: string, publishDomain: string, playDomain: string, publishKey: string, playKey: string, expireAt: number): Promise<{
streamTitle: string;
hub: string;
rtmpPushUrl: string;
rtmpPlayUrl: string;
pcPushUrl: string;
streamKey: string;
expireAt: number;
}>;
/**
* https://developer.qiniu.com/kodo/1308/stat
* GET方法nodejs-sdk里看是POST方法
@ -68,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;
@ -82,26 +63,146 @@ export declare class QiniuCloudInstance {
moveKodoFile(srcBucket: string, zone: QiniuZone, srcKey: string, destBucket: string, destKey: string, force?: boolean, mockData?: any): Promise<void>;
copyKodoFile(srcBucket: string, zone: QiniuZone, srcKey: string, destBucket: string, destKey: string, force?: boolean, mockData?: any): Promise<void>;
/**
*
* @param publishDomain
* @param playDomain
* @param hub
* @param publishKey
* @param playKey
* @param streamTitle
* @param expireAt
* token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getStreamObj(publishDomain: string, playDomain: string, hub: string, publishKey: string, playKey: string, streamTitle: string, expireAt: number): {
getLiveToken(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, host: string, rawQuery?: string, contentType?: string, bodyStr?: string): string;
/**
*
* @param hub
* @param streamTitle
* @param host
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param expireAt
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
createLiveStream(hub: string, streamTitle: string, host: string, publishDomain: string, playDomainType: 'rtmp' | 'hls' | 'flv', playDomain: string, expireAt: number, publishSecurity: 'none' | 'static' | 'expiry' | 'expiry_sk', publishKey: string, playKey: string): Promise<{
streamTitle: string;
hub: string;
rtmpPushUrl: string;
rtmpPlayUrl: string;
playUrl: string;
pcPushUrl: string;
streamKey: string;
expireAt: number;
}>;
/**
*
* @param hub
* @param streamTitle
* @param expireAt
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
getStreamObj(hub: string, streamTitle: string, expireAt: number, publishDomain: string, playDomainType: 'rtmp' | 'hls' | 'flv', playDomain: string, publishSecurity: 'none' | 'static' | 'expiry' | 'expiry_sk', publishKey: string, playKey: string): {
streamTitle: string;
hub: string;
rtmpPushUrl: string;
playUrl: string;
pcPushUrl: string;
streamKey: string;
expireAt: number;
};
getPlayBackUrl(hub: string, playBackDomain: string, streamTitle: string, start: number, end: number, method: 'GET' | 'POST' | 'PUT' | 'DELETE', host: string, rawQuery?: string): Promise<string>;
/**
*
* @param hub
* @param streamTitle
* @param host
* @param disabledTill
* @param disablePeriodSecond
*/
disabledStream(hub: string, streamTitle: string, host: string, disabledTill: number, //禁播结束时间 -1永久禁播0解除禁播
disablePeriodSecond?: number): Promise<void>;
/**
*
* @param hub
* @param host
* @param liveOnly
* @param prefix
* @param limit
* @param marker
* @returns
*/
getLiveStreamList(hub: string, host: string, liveOnly?: boolean, prefix?: string, limit?: number, //取值范围0~5000
marker?: string): Promise<{
streamTitles: string[];
marker: string;
}>;
/**
*
* @param hub
* @param streamTitle
* @param host
* @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
@ -123,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

@ -139,63 +139,6 @@ class QiniuCloudInstance {
throw err;
}
}
/**
* 计算直播需要的token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getLiveToken(method, path, host, rawQuery, contentType, bodyStr) {
// 1. 添加 Path
let data = `${method} ${path}`;
if (rawQuery) {
data += `?${rawQuery}`;
}
data += `\nHost: ${host}`;
if (contentType) {
data += `\nContent-Type: ${contentType}`;
}
data += '\n\n';
if (bodyStr &&
contentType &&
contentType !== 'application/octet-stream') {
data += bodyStr;
}
const sign = this.hmacSha1(data, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const toke = 'Qiniu ' + this.accessKey + ':' + encodedSign;
return toke;
}
async getLiveStream(hub, method, streamTitle, host, publishDomain, playDomain, publishKey, playKey, expireAt) {
// 七牛创建直播流接口路径
const path = `/v2/hubs/${hub}/streams`;
// 如果用户没给streamTitle那么随机生成一个
let key = streamTitle;
if (!key) {
key = `class${new Date().getTime()}`;
}
const bodyStr = JSON.stringify({
key,
});
const contentType = 'application/json';
const token = this.getLiveToken(method, path, host);
const url = `https://pili.qiniuapi.com/v2/hubs/${hub}/streams`;
await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
mode: 'no-cors',
});
const obj = this.getStreamObj(publishDomain, playDomain, hub, publishKey, playKey, streamTitle, expireAt);
return obj;
}
/**
* https://developer.qiniu.com/kodo/1308/stat
* 文档里写的是GET方法从nodejs-sdk里看是POST方法
@ -276,35 +219,162 @@ class QiniuCloudInstance {
}, undefined, 'POST', undefined, mockData);
}
/**
* 计算直播流地址相关信息
* @param publishDomain
* @param playDomain
* @param hub
* @param publishKey
* @param playKey
* @param streamTitle
* @param expireAt
* 计算直播需要的token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getStreamObj(publishDomain, playDomain, hub, publishKey, playKey, streamTitle, expireAt) {
const signStr = `/${hub}/${streamTitle}?expire=${expireAt}`;
const sourcePath = `/${hub}/${streamTitle}`;
const token = this.base64ToUrlSafe(this.hmacSha1(signStr, publishKey));
const rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
// 生成播放地址
const t = expireAt.toString(16).toLowerCase();
const playSign = ts_md5_1.Md5.hashStr(playKey + sourcePath + t)
.toString()
.toLowerCase();
const rtmpPlayUrl = `https://${playDomain}${sourcePath}.m3u8?sign=${playSign}&t=${t}`;
// obs推流需要的地址和串流密钥
getLiveToken(method, path, host, rawQuery, contentType, bodyStr) {
// 1. 添加 Path
let data = `${method} ${path}`;
if (rawQuery) {
data += `?${rawQuery}`;
}
data += `\nHost: ${host}`;
if (contentType) {
data += `\nContent-Type: ${contentType}`;
}
data += '\n\n';
if (bodyStr &&
contentType &&
contentType !== 'application/octet-stream') {
data += bodyStr;
}
const sign = this.hmacSha1(data, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const toke = 'Qiniu ' + this.accessKey + ':' + encodedSign;
return toke;
}
/**
* 创建直播流
* @param hub
* @param streamTitle
* @param host
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param expireAt
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
async createLiveStream(hub, streamTitle, host, publishDomain, playDomainType, playDomain, expireAt, publishSecurity, publishKey, playKey) {
// 七牛创建直播流接口路径
const path = `/v2/hubs/${hub}/streams`;
// 如果用户没给streamTitle那么随机生成一个
let key = streamTitle;
if (!key) {
key = `class${new Date().getTime()}`;
}
const bodyStr = JSON.stringify({
key,
});
const contentType = 'application/json';
const token = this.getLiveToken('POST', path, host, undefined, contentType, bodyStr);
const url = `https://pili.qiniuapi.com/v2/hubs/${hub}/streams`;
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
// mode: 'no-cors',
});
}
catch (err) {
throw new Exception_1.OakNetworkException();
}
if (response.status !== 200) {
if (response.status === 614) {
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), '直播流名已存在', {
status: response.status,
});
}
else {
const json = await response.json();
const { error } = json;
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
const obj = this.getStreamObj(hub, streamTitle, expireAt, publishDomain, playDomainType, playDomain, publishSecurity, publishKey, playKey);
return obj;
}
/**
* 计算直播流地址相关信息
* @param hub
* @param streamTitle
* @param expireAt
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
getStreamObj(hub, streamTitle, expireAt, publishDomain, playDomainType, playDomain, publishSecurity, publishKey, playKey) {
// 根据推流鉴权方式生成推流地址 rtmp推流
const pcPushUrl = `rtmp://${publishDomain}/${hub}/`;
const streamKey = `${streamTitle}?expire=${expireAt}&token=${token}`;
let rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`, streamKey = `${streamTitle}`;
if (publishSecurity === 'none') {
//无校验鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`;
streamKey = `${streamTitle}`;
}
else if (publishSecurity === 'static') {
//静态鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?key=<PublishKey>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}?key=${publishKey}`;
streamKey = `${streamTitle}?key=${publishKey}`;
}
else if (publishSecurity === 'expiry') {
//限时鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?expire=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?expire=${expireAt}`;
const token = this.base64ToUrlSafe(this.hmacSha1(signStr, publishKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?expire=${expireAt}&token=${token}`;
}
else if (publishSecurity === 'expiry_sk') {
//限时鉴权sk rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?e=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?e=${expireAt}`;
const token = this.accessKey + ':' + this.base64ToUrlSafe(this.hmacSha1(signStr, this.secretKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?e=${expireAt}&token=${token}`;
}
//根据拉流域名类型生成播放地址
let playUrl = '', playPath = '';
if (playDomainType === 'rtmp') {
playPath = `/${hub}/${streamTitle}`;
playUrl = `rtmp://${playDomain}${playPath}`;
}
else if (playDomainType === 'hls') {
playPath = `/${hub}/${streamTitle}.m3u8`;
playUrl = `http://${playDomain}${playPath}`;
}
else if (playDomainType === 'flv') {
playPath = `/${hub}/${streamTitle}.flv`;
playUrl = `http://${playDomain}${playPath}`;
}
if (playKey && playKey !== '') {
//开启时间戳防盗链
const t = expireAt.toString(16).toLowerCase();
const playSign = ts_md5_1.Md5.hashStr(playKey + playPath + t).toString().toLowerCase();
playUrl += `?sign=${playSign}&t=${t}`;
}
return {
streamTitle,
hub,
rtmpPushUrl,
rtmpPlayUrl,
playUrl,
pcPushUrl,
streamKey,
expireAt,
@ -332,6 +402,285 @@ class QiniuCloudInstance {
});
return `https://${playBackDomain}/${streamTitle}.m3u8`;
}
/**
* 禁用直播流
* @param hub
* @param streamTitle
* @param host
* @param disabledTill
* @param disablePeriodSecond
*/
async disabledStream(hub, streamTitle, host, disabledTill, //禁播结束时间 -1永久禁播0解除禁播
disablePeriodSecond) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}/disabled`;
const bodyStr = JSON.stringify({
disabledTill,
disablePeriodSecond,
});
const contentType = 'application/json';
const token = this.getLiveToken('POST', path, host, undefined, contentType, bodyStr);
const url = `https://pili.qiniuapi.com${path}`;
let response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
});
}
catch (err) {
throw new Exception_1.OakNetworkException();
}
if (response.status !== 200) {
const json = await response.json();
const { error } = json;
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 查询直播流列表
* @param hub
* @param host
* @param liveOnly
* @param prefix
* @param limit
* @param marker
* @returns
*/
async getLiveStreamList(hub, host, liveOnly, prefix, limit, //取值范围0~5000
marker) {
const path = `/v2/hubs/${hub}/streams`;
const url = new url_1.url(`https://${host}${path}`);
if (liveOnly) {
url.searchParams.set('liveOnly', 'true');
}
if (prefix && prefix !== '') {
url.searchParams.set('prefix', prefix);
}
if (limit) {
url.searchParams.set('limit', limit.toString());
}
if (marker && marker !== '') {
url.searchParams.set('prefix', marker);
}
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken('GET', path, host, undefined, contentType);
let response;
try {
response = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
}
catch (err) {
throw new Exception_1.OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
const streamTitles = json.items?.map((ele) => ele.key);
return {
streamTitles,
marker: json.marker,
};
}
else {
const json = await response.json();
const { error } = json;
throw new Exception_1.OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
* 查询直播流信息
* @param hub
* @param streamTitle
* @param host
* @returns
*/
async getLiveStreamInfo(hub, streamTitle, host) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}`;
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken('GET', path, host, undefined, contentType);
const url = `https://pili.qiniuapi.com${path}`;
let response;
try {
response = await global.fetch(url, {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
}
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/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
@ -463,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() {

11
lib/types/Qiniu.d.ts vendored
View File

@ -1 +1,12 @@
export type QiniuZone = 'z0' | 'cn-east-2' | 'z1' | 'z2' | 'na0' | 'as0';
export type QiniuLiveStreamInfo = {
createdAt: number;
updatedAt: number;
expireAt: number;
disabledTill: number;
converts: string[];
watermark: boolean;
publishSecurity: string;
publishKey: string;
nropEnable: boolean;
};

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.7",
"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.25",
"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 };
}
}

View File

@ -20,8 +20,8 @@ type SendSmsResponse = {
};
type DescribeSmsTemplateListRequest = {
PageIndex: number;
PageSize: number;
pageIndex: number;
pageSize: number;
};
@ -80,13 +80,13 @@ export class AliSmsInstance {
}
}
async syncTemplate(params: DescribeSmsTemplateListRequest) {
const { PageIndex, PageSize } = params;
const { pageIndex, pageSize } = params;
try {
let querySmsTemplateListRequest =
new $Dysmsapi20170525.QuerySmsTemplateListRequest({
PageIndex,
PageSize,
pageIndex,
pageSize,
});
const result = await this.client.querySmsTemplateListWithOptions(
querySmsTemplateListRequest,

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

@ -8,7 +8,7 @@ import {
OakNetworkException,
} from 'oak-domain/lib/types/Exception';
import { url as URL, urlObject as UrlObject } from 'oak-domain/lib/utils/url';
import { QiniuZone } from '../../types/Qiniu';
import { QiniuLiveStreamInfo, QiniuZone } from '../../types/Qiniu';
/**
* qiniu endpoint list
@ -148,93 +148,6 @@ export class QiniuCloudInstance {
}
}
/**
* token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getLiveToken(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
path: string,
host: string,
rawQuery?: string,
contentType?: string,
bodyStr?: string
) {
// 1. 添加 Path
let data = `${method} ${path}`;
if (rawQuery) {
data += `?${rawQuery}`;
}
data += `\nHost: ${host}`;
if (contentType) {
data += `\nContent-Type: ${contentType}`;
}
data += '\n\n';
if (
bodyStr &&
contentType &&
contentType !== 'application/octet-stream'
) {
data += bodyStr;
}
const sign = this.hmacSha1(data, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const toke = 'Qiniu ' + this.accessKey + ':' + encodedSign;
return toke;
}
async getLiveStream(
hub: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
streamTitle: string,
host: string,
publishDomain: string,
playDomain: string,
publishKey: string,
playKey: string,
expireAt: number
) {
// 七牛创建直播流接口路径
const path = `/v2/hubs/${hub}/streams`;
// 如果用户没给streamTitle那么随机生成一个
let key: string = streamTitle;
if (!key) {
key = `class${new Date().getTime()}`;
}
const bodyStr = JSON.stringify({
key,
});
const contentType = 'application/json';
const token = this.getLiveToken(method, path, host);
const url = `https://pili.qiniuapi.com/v2/hubs/${hub}/streams`;
await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
mode: 'no-cors',
});
const obj = this.getStreamObj(
publishDomain,
playDomain,
hub,
publishKey,
playKey,
streamTitle,
expireAt
);
return obj;
}
/**
* https://developer.qiniu.com/kodo/1308/stat
* GET方法nodejs-sdk里看是POST方法
@ -422,43 +335,201 @@ export class QiniuCloudInstance {
}
/**
*
* @param publishDomain
* @param playDomain
* @param hub
* @param publishKey
* @param playKey
* @param streamTitle
* @param expireAt
* token
* @param method
* @param path
* @param host
* @param rawQuery
* @param contentType
* @param bodyStr
* @returns
*/
getStreamObj(
publishDomain: string,
playDomain: string,
getLiveToken(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
path: string,
host: string,
rawQuery?: string,
contentType?: string,
bodyStr?: string
) {
// 1. 添加 Path
let data = `${method} ${path}`;
if (rawQuery) {
data += `?${rawQuery}`;
}
data += `\nHost: ${host}`;
if (contentType) {
data += `\nContent-Type: ${contentType}`;
}
data += '\n\n';
if (
bodyStr &&
contentType &&
contentType !== 'application/octet-stream'
) {
data += bodyStr;
}
const sign = this.hmacSha1(data, this.secretKey);
const encodedSign = this.base64ToUrlSafe(sign);
const toke = 'Qiniu ' + this.accessKey + ':' + encodedSign;
return toke;
}
/**
*
* @param hub
* @param streamTitle
* @param host
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param expireAt
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
async createLiveStream(
hub: string,
streamTitle: string,
host: string,
publishDomain: string,
playDomainType: 'rtmp' | 'hls' | 'flv',
playDomain: string,
expireAt: number,
publishSecurity: 'none' | 'static' | 'expiry' | 'expiry_sk',
publishKey: string,
playKey: string,
streamTitle: string,
expireAt: number
) {
const signStr = `/${hub}/${streamTitle}?expire=${expireAt}`;
const sourcePath = `/${hub}/${streamTitle}`;
const token = this.base64ToUrlSafe(this.hmacSha1(signStr, publishKey));
const rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
// 生成播放地址
const t = expireAt.toString(16).toLowerCase();
const playSign = Md5.hashStr(playKey + sourcePath + t)
.toString()
.toLowerCase();
const rtmpPlayUrl = `https://${playDomain}${sourcePath}.m3u8?sign=${playSign}&t=${t}`;
// obs推流需要的地址和串流密钥
// 七牛创建直播流接口路径
const path = `/v2/hubs/${hub}/streams`;
// 如果用户没给streamTitle那么随机生成一个
let key: string = streamTitle;
if (!key) {
key = `class${new Date().getTime()}`;
}
const bodyStr = JSON.stringify({
key,
});
const contentType = 'application/json';
const token = this.getLiveToken('POST', path, host, undefined, contentType, bodyStr);
const url = `https://pili.qiniuapi.com/v2/hubs/${hub}/streams`;
let response: Response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
// mode: 'no-cors',
});
}
catch (err) {
throw new OakNetworkException();
}
if (response.status !== 200) {
if (response.status === 614) {
throw new OakExternalException('qiniu', response.status.toString(), '直播流名已存在', {
status: response.status,
});
} else {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
const obj = this.getStreamObj(
hub,
streamTitle,
expireAt,
publishDomain,
playDomainType,
playDomain,
publishSecurity,
publishKey,
playKey,
);
return obj;
}
/**
*
* @param hub
* @param streamTitle
* @param expireAt
* @param publishDomain
* @param playDomainType
* @param playDomain
* @param publishSecurity
* @param publishKey
* @param playKey
* @returns
*/
getStreamObj(
hub: string,
streamTitle: string,
expireAt: number,
publishDomain: string,
playDomainType: 'rtmp' | 'hls' | 'flv',
playDomain: string,
publishSecurity: 'none' | 'static' | 'expiry' | 'expiry_sk',
publishKey: string,
playKey: string,
) {
// 根据推流鉴权方式生成推流地址 rtmp推流
const pcPushUrl = `rtmp://${publishDomain}/${hub}/`;
const streamKey = `${streamTitle}?expire=${expireAt}&token=${token}`;
let rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`,
streamKey = `${streamTitle}`;
if (publishSecurity === 'none') {
//无校验鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}`;
streamKey = `${streamTitle}`;
} else if (publishSecurity === 'static') {
//静态鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?key=<PublishKey>
rtmpPushUrl = `rtmp://${publishDomain}/${hub}/${streamTitle}?key=${publishKey}`;
streamKey = `${streamTitle}?key=${publishKey}`;
} else if (publishSecurity === 'expiry') {
//限时鉴权 rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?expire=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?expire=${expireAt}`;
const token = this.base64ToUrlSafe(this.hmacSha1(signStr, publishKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?expire=${expireAt}&token=${token}`;
} else if (publishSecurity === 'expiry_sk') {
//限时鉴权sk rtmp://<RTMPPublishDomain>/<Hub>/<streamTitle>?e=<ExpireAt>&token=<Token>
const signStr = `/${hub}/${streamTitle}?e=${expireAt}`;
const token = this.accessKey + ':' + this.base64ToUrlSafe(this.hmacSha1(signStr, this.secretKey));
rtmpPushUrl = `rtmp://${publishDomain}${signStr}&token=${token}`;
streamKey = `${streamTitle}?e=${expireAt}&token=${token}`;
}
//根据拉流域名类型生成播放地址
let playUrl = '', playPath = '';
if (playDomainType === 'rtmp') {
playPath = `/${hub}/${streamTitle}`;
playUrl = `rtmp://${playDomain}${playPath}`
} else if (playDomainType === 'hls') {
playPath = `/${hub}/${streamTitle}.m3u8`;
playUrl = `http://${playDomain}${playPath}`;
} else if (playDomainType === 'flv') {
playPath = `/${hub}/${streamTitle}.flv`;
playUrl = `http://${playDomain}${playPath}`;
}
if (playKey && playKey !== '') {
//开启时间戳防盗链
const t = expireAt.toString(16).toLowerCase();
const playSign = Md5.hashStr(playKey + playPath + t).toString().toLowerCase();
playUrl += `?sign=${playSign}&t=${t}`;
}
return {
streamTitle,
hub,
rtmpPushUrl,
rtmpPlayUrl,
playUrl,
pcPushUrl,
streamKey,
expireAt,
@ -505,6 +576,382 @@ export class QiniuCloudInstance {
return `https://${playBackDomain}/${streamTitle}.m3u8`;
}
/**
*
* @param hub
* @param streamTitle
* @param host
* @param disabledTill
* @param disablePeriodSecond
*/
async disabledStream(
hub: string,
streamTitle: string,
host: string,
disabledTill: number, //禁播结束时间 -1永久禁播0解除禁播
disablePeriodSecond?: number, //禁播时长当disabledTill为0时生效
) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}/disabled`;
const bodyStr = JSON.stringify({
disabledTill,
disablePeriodSecond,
});
const contentType = 'application/json';
const token = this.getLiveToken(
'POST',
path,
host,
undefined,
contentType,
bodyStr
);
const url = `https://pili.qiniuapi.com${path}`;
let response: Response;
try {
response = await global.fetch(url, {
method: 'POST',
headers: {
Authorization: token,
'Content-Type': contentType,
},
body: bodyStr,
});
} catch (err) {
throw new OakNetworkException();
}
if (response.status !== 200) {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
*
* @param hub
* @param host
* @param liveOnly
* @param prefix
* @param limit
* @param marker
* @returns
*/
async getLiveStreamList(
hub: string,
host: string,
liveOnly?: boolean,
prefix?: string,
limit?: number, //取值范围0~5000
marker?: string,
) {
const path = `/v2/hubs/${hub}/streams`;
const url = new URL(`https://${host}${path}`);
if (liveOnly) {
url.searchParams.set('liveOnly', 'true');
}
if (prefix && prefix !== '') {
url.searchParams.set('prefix', prefix);
}
if (limit) {
url.searchParams.set('limit', limit.toString());
}
if (marker && marker !== '') {
url.searchParams.set('prefix', marker);
}
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken(
'GET',
path,
host,
undefined,
contentType
);
let response: Response;
try {
response = await fetch(url.toString(), {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
} catch (err) {
throw new OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
const streamTitles = json.items?.map((ele: { key: string }) => ele.key);
return {
streamTitles,
marker: json.marker,
} as {
streamTitles: string[];
marker: string;
};
} else {
const json = await response.json();
const { error } = json;
throw new OakExternalException('qiniu', response.status.toString(), error, {
status: response.status,
});
}
}
/**
*
* @param hub
* @param streamTitle
* @param host
* @returns
*/
async getLiveStreamInfo(
hub: string,
streamTitle: string,
host: string,
) {
const encodedStreamTitle = this.urlSafeBase64Encode(streamTitle);
const path = `/v2/hubs/${hub}/streams/${encodedStreamTitle}`;
const contentType = 'application/x-www-form-urlencoded';
const token = this.getLiveToken(
'GET',
path,
host,
undefined,
contentType,
);
const url = `https://pili.qiniuapi.com${path}`;
let response: Response;
try {
response = await global.fetch(url, {
method: 'GET',
headers: {
Authorization: token,
'Content-Type': contentType,
},
});
} catch (err) {
throw new OakNetworkException();
}
if (response.status === 200) {
const json = await response.json();
return json as QiniuLiveStreamInfo;
} 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/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
@ -675,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}]`);
});
}
}

View File

@ -1 +1,13 @@
export type QiniuZone = 'z0' | 'cn-east-2' | 'z1' | 'z2' | 'na0' | 'as0';
export type QiniuZone = 'z0' | 'cn-east-2' | 'z1' | 'z2' | 'na0' | 'as0';
export type QiniuLiveStreamInfo = {
createdAt: number;
updatedAt: number;
expireAt: number; //过期时间一个流持续N天设置的直播空间的存储过期时间不推流回被自动清除
disabledTill: number; //-1表示永久禁播
converts: string[]; //转码配置
watermark: boolean; //是否开启水印
publishSecurity: string; //推流鉴权类型
publishKey: string; //推流密钥
nropEnable: boolean; //是否开启鉴黄
}

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