Compare commits
27 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
170affd3a9 | |
|
|
f1545faedf | |
|
|
ab689a6fc2 | |
|
|
6109d8010e | |
|
|
9516284bc9 | |
|
|
56fef25306 | |
|
|
a25c89e6e3 | |
|
|
25ac418fc1 | |
|
|
7a25050213 | |
|
|
c33d55c680 | |
|
|
5279258c64 | |
|
|
20d4540b1e | |
|
|
64a86e9266 | |
|
|
dc1885cb1e | |
|
|
6b8d5cd10c | |
|
|
246b59bba9 | |
|
|
201f174dd1 | |
|
|
84ff7d86c2 | |
|
|
f0abeba024 | |
|
|
759b31bfa2 | |
|
|
e9c9095b45 | |
|
|
304c2f6ac2 | |
|
|
a71d5d339f | |
|
|
dde0a82476 | |
|
|
f8b6b1e8bc | |
|
|
ff7a5c2b63 | |
|
|
a2ba587bea |
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -15,4 +15,155 @@ export declare class ALiYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<OSS.DeleteResult>;
|
||||
isExistObject(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
initiateMultipartUpload(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
bucket: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
presignMulti(bucket: string, zone: ALiYunZone, key: string, uploadId: string, from: number, to: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[]>;
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
prepareMultipartUpload(bucket: string, zone: ALiYunZone, key: string, partCount: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
parts: {
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
uploadPart(bucket: string, zone: ALiYunZone, key: string, uploadId: string, partNumber: number, file: any, start?: number, end?: number, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
etag: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
completeMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, parts: Array<{
|
||||
number: number;
|
||||
etag: string;
|
||||
}>, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
bucket: string;
|
||||
name: string;
|
||||
etag: string;
|
||||
}>;
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
abortMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<void>;
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
listParts(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
'max-parts'?: number;
|
||||
'part-number-marker'?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
parts: {
|
||||
partNumber: any;
|
||||
lastModified: any;
|
||||
etag: any;
|
||||
size: any;
|
||||
}[];
|
||||
nextPartNumberMarker: number;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
listMultipartUploads(bucket: string, zone: ALiYunZone, options?: {
|
||||
prefix?: string;
|
||||
'max-uploads'?: number;
|
||||
'key-marker'?: string;
|
||||
'upload-id-marker'?: string;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
uploads: {
|
||||
name: any;
|
||||
uploadId: any;
|
||||
initiated: any;
|
||||
}[];
|
||||
nextKeyMarker: any;
|
||||
nextUploadIdMarker: any;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
getSignedUrl(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
method?: 'GET' | 'PUT' | 'DELETE' | 'POST';
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<string>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import OSS from 'ali-oss';
|
||||
import { OakExternalException, } from 'oak-domain/lib/types/Exception';
|
||||
function getEndpoint(RegionID) {
|
||||
return `oss-${RegionID}.aliyuncs.com`;
|
||||
}
|
||||
|
|
@ -180,4 +181,287 @@ export class ALiYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
async initiateMultipartUpload(bucket, zone, key, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.initMultipartUpload(key, options);
|
||||
return {
|
||||
uploadId: result.uploadId,
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(bucket, zone, key, uploadId, from, to, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const presignedUrls = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
uploadId: uploadId,
|
||||
partNumber: String(i),
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
presignedUrls.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return presignedUrls;
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(bucket, zone, key, partCount, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
// 1. 初始化分片上传
|
||||
const initResult = await client.initMultipartUpload(key, {
|
||||
headers: options?.headers,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
partNumber: String(i),
|
||||
uploadId: initResult.uploadId,
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return {
|
||||
uploadId: initResult.uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
async uploadPart(bucket, zone, key, uploadId, partNumber, file, start, end, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.uploadPart(key, uploadId, partNumber, file, start ?? 0, end ?? 0, options);
|
||||
return {
|
||||
etag: result.etag,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
async completeMultipartUpload(bucket, zone, key, uploadId, parts, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.completeMultipartUpload(key, uploadId, parts, options);
|
||||
return {
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
etag: result.etag,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
async abortMultipartUpload(bucket, zone, key, uploadId, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
await client.abortMultipartUpload(key, uploadId, options);
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
async listParts(bucket, zone, key, uploadId, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listParts(key, uploadId, {
|
||||
'max-parts': options?.['max-parts']?.toString(),
|
||||
'part-number-marker': options?.['part-number-marker']?.toString(),
|
||||
});
|
||||
return {
|
||||
parts: result.parts.map((part) => ({
|
||||
partNumber: part.PartNumber,
|
||||
lastModified: part.LastModified,
|
||||
etag: part.ETag,
|
||||
size: part.Size,
|
||||
})),
|
||||
nextPartNumberMarker: result.nextPartNumberMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
async listMultipartUploads(bucket, zone, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listUploads(options || {});
|
||||
return {
|
||||
uploads: result.uploads.map((upload) => ({
|
||||
name: upload.name,
|
||||
uploadId: upload.uploadId,
|
||||
initiated: upload.initiated,
|
||||
})),
|
||||
nextKeyMarker: result.nextKeyMarker,
|
||||
nextUploadIdMarker: result.nextUploadIdMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
async getSignedUrl(bucket, zone, key, options) {
|
||||
const client = new OSS({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
});
|
||||
try {
|
||||
const url = client.signatureUrl(key, options);
|
||||
return url;
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('aliyun', error.code, error.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const url = await this.getSignedUrl(bucket, zone, key, {
|
||||
...options,
|
||||
method,
|
||||
});
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ type SendSmsRequest = {
|
|||
outId?: string;
|
||||
};
|
||||
type DescribeSmsTemplateListRequest = {
|
||||
PageIndex: number;
|
||||
PageSize: number;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
};
|
||||
export declare class AliSmsInstance {
|
||||
accessKeyId: string;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
export type AssumeRoleOptions = {
|
||||
endpoint: string;
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
roleArn: string;
|
||||
roleSessionName: string;
|
||||
policy?: string;
|
||||
durationSeconds?: number;
|
||||
};
|
||||
export declare function stsAssumeRole(options: AssumeRoleOptions): Promise<{
|
||||
RequestId: string | undefined;
|
||||
AssumedRoleUser: {
|
||||
AssumedRoleId: string | undefined;
|
||||
Arn: string | undefined;
|
||||
};
|
||||
Credentials: {
|
||||
SecurityToken: string | undefined;
|
||||
Expiration: string | undefined;
|
||||
AccessKeySecret: string | undefined;
|
||||
AccessKeyId: string | undefined;
|
||||
};
|
||||
}>;
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import STSClient, { AssumeRoleRequest } from '@alicloud/sts20150401';
|
||||
export async function stsAssumeRole(options) {
|
||||
const client = new STSClient({
|
||||
endpoint: options.endpoint,
|
||||
accessKeyId: options.accessKeyId,
|
||||
accessKeySecret: options.accessKeySecret,
|
||||
toMap: () => {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const assumeRoleRequest = new AssumeRoleRequest({
|
||||
roleArn: options.roleArn,
|
||||
roleSessionName: options.roleSessionName,
|
||||
policy: options.policy,
|
||||
durationSeconds: options.durationSeconds,
|
||||
});
|
||||
const res = await client.assumeRole(assumeRoleRequest);
|
||||
return {
|
||||
"RequestId": res.body?.requestId,
|
||||
"AssumedRoleUser": {
|
||||
"AssumedRoleId": res.body?.assumedRoleUser?.assumedRoleId,
|
||||
"Arn": res.body?.assumedRoleUser?.arn
|
||||
},
|
||||
"Credentials": {
|
||||
"SecurityToken": res.body?.credentials?.securityToken,
|
||||
"Expiration": res.body?.credentials?.expiration,
|
||||
"AccessKeySecret": res.body?.credentials?.accessKeySecret,
|
||||
"AccessKeyId": res.body?.credentials?.accessKeyId
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -25,4 +25,16 @@ export declare class CTYunInstance {
|
|||
private buildCanonicalRequest;
|
||||
private buildCanonicalQueryString;
|
||||
private calculatePayloadHash;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: CTYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
contentType?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -309,4 +309,72 @@ export class CTYunInstance {
|
|||
hash.update(payload);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const expiresIn = options?.expires || 3600;
|
||||
const service = 's3';
|
||||
// 对 key 进行 URI 编码(保留 /)
|
||||
const encodedKey = key
|
||||
.split('/')
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
const path = `/${encodedKey}`;
|
||||
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
|
||||
const baseUrl = `https://${host}${path}`;
|
||||
// 生成 ISO8601 格式的日期
|
||||
const date = new Date()
|
||||
.toISOString()
|
||||
.replace(/\.\d{3}Z$/, 'Z')
|
||||
.replace(/[:\-]|\.\d{3}/g, '');
|
||||
const dateStamp = date.substring(0, 8);
|
||||
const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`;
|
||||
// 构建查询参数(不含签名)
|
||||
const queryParameters = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': `${this.accessKey}/${credentialScope}`,
|
||||
'X-Amz-Date': date,
|
||||
'X-Amz-Expires': expiresIn.toString(),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
// 构建 Canonical Request
|
||||
// 对于预签名 URL,payload hash 使用 UNSIGNED-PAYLOAD
|
||||
const { canonicalRequest } = this.buildCanonicalRequest(method, path, queryParameters, { host: host }, // 注意:header 名称应为小写
|
||||
'UNSIGNED-PAYLOAD');
|
||||
// 计算 Canonical Request 的 hash
|
||||
const canonicalRequestHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(canonicalRequest)
|
||||
.digest('hex');
|
||||
// 构建 String to Sign
|
||||
const stringToSign = [
|
||||
'AWS4-HMAC-SHA256',
|
||||
date,
|
||||
credentialScope,
|
||||
canonicalRequestHash,
|
||||
].join('\n');
|
||||
// 生成签名密钥并计算签名
|
||||
const signingKey = this.getSignatureKey(dateStamp, zone, service);
|
||||
const signature = crypto
|
||||
.createHmac('sha256', signingKey)
|
||||
.update(stringToSign)
|
||||
.digest('hex');
|
||||
// 将签名添加到查询参数
|
||||
queryParameters['X-Amz-Signature'] = signature;
|
||||
// 构建最终 URL
|
||||
const queryString = this.buildCanonicalQueryString(queryParameters);
|
||||
const url = `${baseUrl}?${queryString}`;
|
||||
// PUT 方法返回需要的 headers
|
||||
if (method === 'PUT' && options?.contentType) {
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': options.contentType,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,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>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}>;
|
||||
}
|
||||
|
|
@ -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', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,4 +17,15 @@ export declare class TencentYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<any>;
|
||||
isExistObject(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: TencentYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import crypto from 'crypto';
|
||||
import { OakExternalException, } from 'oak-domain/lib/types/Exception';
|
||||
function getEndpoint(RegionID) {
|
||||
return `cos-${RegionID}.myqcloud.com`;
|
||||
}
|
||||
|
|
@ -141,4 +142,36 @@ export class TencentYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const client = new this.COS({
|
||||
SecretId: this.accessKey,
|
||||
SecretKey: this.secretKey,
|
||||
});
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.getObjectUrl({
|
||||
Bucket: bucket,
|
||||
Region: zone,
|
||||
Key: key,
|
||||
Method: method,
|
||||
Expires: options?.expires || 3600,
|
||||
Sign: true,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(new OakExternalException('tencent', err.code, err.message, err, 'oak-external-sdk', {}));
|
||||
}
|
||||
else {
|
||||
resolve({ url: data.Url });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
throw new OakExternalException('tencent', error.code, error.message, error, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ export class WechatNativeInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ export class WechatPublicInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ export class WechatWebInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -1 +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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
export {};
|
||||
|
|
@ -4,3 +4,4 @@ export * from './CTYun';
|
|||
export * from './ALiYun';
|
||||
export * from './TencentYun';
|
||||
export * from './AMap';
|
||||
export * from './S3';
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ export * from './CTYun';
|
|||
export * from './ALiYun';
|
||||
export * from './TencentYun';
|
||||
export * from './AMap';
|
||||
export * from './S3';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { HuaweiYunInstance } from './service/huawei/Huawei';
|
||||
/**
|
||||
* 华为云SDK主类
|
||||
* 用于管理华为云OBS实例
|
||||
*/
|
||||
declare class HuaweiYunSDK {
|
||||
huaweiMap: Record<string, HuaweiYunInstance>;
|
||||
constructor();
|
||||
/**
|
||||
* 获取或创建华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
* @param accessSecret Access Key Secret
|
||||
* @returns 华为云实例
|
||||
*/
|
||||
getInstance(accessKey: string, accessSecret: string): HuaweiYunInstance;
|
||||
/**
|
||||
* 移除指定的华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
*/
|
||||
removeInstance(accessKey: string): void;
|
||||
/**
|
||||
* 清除所有实例
|
||||
*/
|
||||
clearAllInstances(): void;
|
||||
}
|
||||
declare const SDK: HuaweiYunSDK;
|
||||
export default SDK;
|
||||
export { HuaweiYunInstance };
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.HuaweiYunInstance = void 0;
|
||||
const Huawei_1 = require("./service/huawei/Huawei");
|
||||
Object.defineProperty(exports, "HuaweiYunInstance", { enumerable: true, get: function () { return Huawei_1.HuaweiYunInstance; } });
|
||||
/**
|
||||
* 华为云SDK主类
|
||||
* 用于管理华为云OBS实例
|
||||
*/
|
||||
class HuaweiYunSDK {
|
||||
huaweiMap; // OBS实例映射
|
||||
constructor() {
|
||||
this.huaweiMap = {};
|
||||
}
|
||||
/**
|
||||
* 获取或创建华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
* @param accessSecret Access Key Secret
|
||||
* @returns 华为云实例
|
||||
*/
|
||||
getInstance(accessKey, accessSecret) {
|
||||
if (this.huaweiMap[accessKey]) {
|
||||
return this.huaweiMap[accessKey];
|
||||
}
|
||||
const instance = new Huawei_1.HuaweiYunInstance(accessKey, accessSecret);
|
||||
Object.assign(this.huaweiMap, {
|
||||
[accessKey]: instance,
|
||||
});
|
||||
return instance;
|
||||
}
|
||||
/**
|
||||
* 移除指定的华为云实例
|
||||
* @param accessKey Access Key ID
|
||||
*/
|
||||
removeInstance(accessKey) {
|
||||
if (this.huaweiMap[accessKey]) {
|
||||
delete this.huaweiMap[accessKey];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清除所有实例
|
||||
*/
|
||||
clearAllInstances() {
|
||||
this.huaweiMap = {};
|
||||
}
|
||||
}
|
||||
const SDK = new HuaweiYunSDK();
|
||||
exports.default = SDK;
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -15,4 +15,155 @@ export declare class ALiYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<OSS.DeleteResult>;
|
||||
isExistObject(srcBucket: string, zone: ALiYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
initiateMultipartUpload(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
bucket: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
presignMulti(bucket: string, zone: ALiYunZone, key: string, uploadId: string, from: number, to: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[]>;
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
prepareMultipartUpload(bucket: string, zone: ALiYunZone, key: string, partCount: number, options?: {
|
||||
expires?: number;
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
uploadId: string;
|
||||
parts: {
|
||||
partNumber: number;
|
||||
uploadUrl: string;
|
||||
}[];
|
||||
}>;
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
uploadPart(bucket: string, zone: ALiYunZone, key: string, uploadId: string, partNumber: number, file: any, start?: number, end?: number, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
accessKeyId?: string;
|
||||
accessKeySecret?: string;
|
||||
}): Promise<{
|
||||
etag: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
completeMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, parts: Array<{
|
||||
number: number;
|
||||
etag: string;
|
||||
}>, options?: {
|
||||
headers?: Record<string, string>;
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
bucket: string;
|
||||
name: string;
|
||||
etag: string;
|
||||
}>;
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
abortMultipartUpload(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
timeout?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<void>;
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
listParts(bucket: string, zone: ALiYunZone, key: string, uploadId: string, options?: {
|
||||
'max-parts'?: number;
|
||||
'part-number-marker'?: number;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
parts: {
|
||||
partNumber: any;
|
||||
lastModified: any;
|
||||
etag: any;
|
||||
size: any;
|
||||
}[];
|
||||
nextPartNumberMarker: number;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
listMultipartUploads(bucket: string, zone: ALiYunZone, options?: {
|
||||
prefix?: string;
|
||||
'max-uploads'?: number;
|
||||
'key-marker'?: string;
|
||||
'upload-id-marker'?: string;
|
||||
stsToken?: string;
|
||||
}): Promise<{
|
||||
uploads: {
|
||||
name: any;
|
||||
uploadId: any;
|
||||
initiated: any;
|
||||
}[];
|
||||
nextKeyMarker: any;
|
||||
nextUploadIdMarker: any;
|
||||
isTruncated: boolean;
|
||||
}>;
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
getSignedUrl(bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
method?: 'GET' | 'PUT' | 'DELETE' | 'POST';
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<string>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: ALiYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
process?: string;
|
||||
response?: Record<string, string>;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.ALiYunInstance = void 0;
|
||||
const tslib_1 = require("tslib");
|
||||
const ali_oss_1 = tslib_1.__importDefault(require("ali-oss"));
|
||||
const Exception_1 = require("oak-domain/lib/types/Exception");
|
||||
function getEndpoint(RegionID) {
|
||||
return `oss-${RegionID}.aliyuncs.com`;
|
||||
}
|
||||
|
|
@ -184,5 +185,288 @@ class ALiYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 初始化分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/initiate-a-multipart-upload-task
|
||||
*/
|
||||
async initiateMultipartUpload(bucket, zone, key, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.initMultipartUpload(key, options);
|
||||
return {
|
||||
uploadId: result.uploadId,
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 为分片上传生成预签名 URL
|
||||
* @param bucket 桶
|
||||
* @param key 对象键
|
||||
* @param uploadId 上传 ID
|
||||
* @param from 起始分片号
|
||||
* @param to 结束分片号
|
||||
* @param options 配置项
|
||||
* @returns 分片预签名 URL 列表
|
||||
*/
|
||||
async presignMulti(bucket, zone, key, uploadId, from, to, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const presignedUrls = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = from; i <= to; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
uploadId: uploadId,
|
||||
partNumber: String(i),
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
presignedUrls.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return presignedUrls;
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 准备分片上传(初始化并生成所有分片的预签名URL)
|
||||
* 用于前端直传场景,返回 uploadId 和每个分片的上传 URL
|
||||
*/
|
||||
async prepareMultipartUpload(bucket, zone, key, partCount, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
// 1. 初始化分片上传
|
||||
const initResult = await client.initMultipartUpload(key, {
|
||||
headers: options?.headers,
|
||||
timeout: options?.timeout,
|
||||
});
|
||||
// 2. 为每个分片生成预签名 URL
|
||||
const parts = [];
|
||||
const expires = options?.expires || 3600;
|
||||
for (let i = 1; i <= partCount; i++) {
|
||||
const uploadUrl = client.signatureUrl(key, {
|
||||
method: 'PUT',
|
||||
expires: expires,
|
||||
subResource: {
|
||||
partNumber: String(i),
|
||||
uploadId: initResult.uploadId,
|
||||
},
|
||||
"Content-Type": "application/octet-stream",
|
||||
});
|
||||
parts.push({
|
||||
partNumber: i,
|
||||
uploadUrl: uploadUrl,
|
||||
});
|
||||
}
|
||||
return {
|
||||
uploadId: initResult.uploadId,
|
||||
parts: parts,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 上传分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/upload-parts
|
||||
*/
|
||||
async uploadPart(bucket, zone, key, uploadId, partNumber, file, start, end, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: options?.accessKeyId || this.accessKey,
|
||||
accessKeySecret: options?.accessKeySecret || this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.uploadPart(key, uploadId, partNumber, file, start ?? 0, end ?? 0, options);
|
||||
return {
|
||||
etag: result.etag,
|
||||
name: result.name,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 完成分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/complete-a-multipart-upload-task
|
||||
*/
|
||||
async completeMultipartUpload(bucket, zone, key, uploadId, parts, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.completeMultipartUpload(key, uploadId, parts, options);
|
||||
return {
|
||||
bucket: result.bucket,
|
||||
name: result.name,
|
||||
etag: result.etag,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 取消分片上传
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/abort-a-multipart-upload-task
|
||||
*/
|
||||
async abortMultipartUpload(bucket, zone, key, uploadId, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
await client.abortMultipartUpload(key, uploadId, options);
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举已上传的分片
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-uploaded-parts
|
||||
*/
|
||||
async listParts(bucket, zone, key, uploadId, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listParts(key, uploadId, {
|
||||
'max-parts': options?.['max-parts']?.toString(),
|
||||
'part-number-marker': options?.['part-number-marker']?.toString(),
|
||||
});
|
||||
return {
|
||||
parts: result.parts.map((part) => ({
|
||||
partNumber: part.PartNumber,
|
||||
lastModified: part.LastModified,
|
||||
etag: part.ETag,
|
||||
size: part.Size,
|
||||
})),
|
||||
nextPartNumberMarker: result.nextPartNumberMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 列举所有执行中的分片上传任务
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/list-initiated-multipart-upload-tasks
|
||||
*/
|
||||
async listMultipartUploads(bucket, zone, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
stsToken: options?.stsToken,
|
||||
});
|
||||
try {
|
||||
const result = await client.listUploads(options || {});
|
||||
return {
|
||||
uploads: result.uploads.map((upload) => ({
|
||||
name: upload.name,
|
||||
uploadId: upload.uploadId,
|
||||
initiated: upload.initiated,
|
||||
})),
|
||||
nextKeyMarker: result.nextKeyMarker,
|
||||
nextUploadIdMarker: result.nextUploadIdMarker,
|
||||
isTruncated: result.isTruncated,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message, {
|
||||
status: error.status,
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取签名URL
|
||||
* https://help.aliyun.com/zh/oss/developer-reference/authorize-access-4
|
||||
*/
|
||||
async getSignedUrl(bucket, zone, key, options) {
|
||||
const client = new ali_oss_1.default({
|
||||
region: `oss-${zone}`,
|
||||
accessKeyId: this.accessKey,
|
||||
accessKeySecret: this.secretKey,
|
||||
bucket: bucket,
|
||||
});
|
||||
try {
|
||||
const url = client.signatureUrl(key, options);
|
||||
return url;
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('aliyun', error.code, error.message);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const url = await this.getSignedUrl(bucket, zone, key, {
|
||||
...options,
|
||||
method,
|
||||
});
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
exports.ALiYunInstance = ALiYunInstance;
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ type SendSmsRequest = {
|
|||
outId?: string;
|
||||
};
|
||||
type DescribeSmsTemplateListRequest = {
|
||||
PageIndex: number;
|
||||
PageSize: number;
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
};
|
||||
export declare class AliSmsInstance {
|
||||
accessKeyId: string;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
export type AssumeRoleOptions = {
|
||||
endpoint: string;
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
roleArn: string;
|
||||
roleSessionName: string;
|
||||
policy?: string;
|
||||
durationSeconds?: number;
|
||||
};
|
||||
export declare function stsAssumeRole(options: AssumeRoleOptions): Promise<{
|
||||
RequestId: string | undefined;
|
||||
AssumedRoleUser: {
|
||||
AssumedRoleId: string | undefined;
|
||||
Arn: string | undefined;
|
||||
};
|
||||
Credentials: {
|
||||
SecurityToken: string | undefined;
|
||||
Expiration: string | undefined;
|
||||
AccessKeySecret: string | undefined;
|
||||
AccessKeyId: string | undefined;
|
||||
};
|
||||
}>;
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.stsAssumeRole = stsAssumeRole;
|
||||
const tslib_1 = require("tslib");
|
||||
const sts20150401_1 = tslib_1.__importStar(require("@alicloud/sts20150401"));
|
||||
async function stsAssumeRole(options) {
|
||||
const client = new sts20150401_1.default({
|
||||
endpoint: options.endpoint,
|
||||
accessKeyId: options.accessKeyId,
|
||||
accessKeySecret: options.accessKeySecret,
|
||||
toMap: () => {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
const assumeRoleRequest = new sts20150401_1.AssumeRoleRequest({
|
||||
roleArn: options.roleArn,
|
||||
roleSessionName: options.roleSessionName,
|
||||
policy: options.policy,
|
||||
durationSeconds: options.durationSeconds,
|
||||
});
|
||||
const res = await client.assumeRole(assumeRoleRequest);
|
||||
return {
|
||||
"RequestId": res.body?.requestId,
|
||||
"AssumedRoleUser": {
|
||||
"AssumedRoleId": res.body?.assumedRoleUser?.assumedRoleId,
|
||||
"Arn": res.body?.assumedRoleUser?.arn
|
||||
},
|
||||
"Credentials": {
|
||||
"SecurityToken": res.body?.credentials?.securityToken,
|
||||
"Expiration": res.body?.credentials?.expiration,
|
||||
"AccessKeySecret": res.body?.credentials?.accessKeySecret,
|
||||
"AccessKeyId": res.body?.credentials?.accessKeyId
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -25,4 +25,16 @@ export declare class CTYunInstance {
|
|||
private buildCanonicalRequest;
|
||||
private buildCanonicalQueryString;
|
||||
private calculatePayloadHash;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: CTYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
contentType?: string;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -313,5 +313,73 @@ class CTYunInstance {
|
|||
hash.update(payload);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const expiresIn = options?.expires || 3600;
|
||||
const service = 's3';
|
||||
// 对 key 进行 URI 编码(保留 /)
|
||||
const encodedKey = key
|
||||
.split('/')
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
const path = `/${encodedKey}`;
|
||||
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
|
||||
const baseUrl = `https://${host}${path}`;
|
||||
// 生成 ISO8601 格式的日期
|
||||
const date = new Date()
|
||||
.toISOString()
|
||||
.replace(/\.\d{3}Z$/, 'Z')
|
||||
.replace(/[:\-]|\.\d{3}/g, '');
|
||||
const dateStamp = date.substring(0, 8);
|
||||
const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`;
|
||||
// 构建查询参数(不含签名)
|
||||
const queryParameters = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': `${this.accessKey}/${credentialScope}`,
|
||||
'X-Amz-Date': date,
|
||||
'X-Amz-Expires': expiresIn.toString(),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
// 构建 Canonical Request
|
||||
// 对于预签名 URL,payload hash 使用 UNSIGNED-PAYLOAD
|
||||
const { canonicalRequest } = this.buildCanonicalRequest(method, path, queryParameters, { host: host }, // 注意:header 名称应为小写
|
||||
'UNSIGNED-PAYLOAD');
|
||||
// 计算 Canonical Request 的 hash
|
||||
const canonicalRequestHash = crypto_1.default
|
||||
.createHash('sha256')
|
||||
.update(canonicalRequest)
|
||||
.digest('hex');
|
||||
// 构建 String to Sign
|
||||
const stringToSign = [
|
||||
'AWS4-HMAC-SHA256',
|
||||
date,
|
||||
credentialScope,
|
||||
canonicalRequestHash,
|
||||
].join('\n');
|
||||
// 生成签名密钥并计算签名
|
||||
const signingKey = this.getSignatureKey(dateStamp, zone, service);
|
||||
const signature = crypto_1.default
|
||||
.createHmac('sha256', signingKey)
|
||||
.update(stringToSign)
|
||||
.digest('hex');
|
||||
// 将签名添加到查询参数
|
||||
queryParameters['X-Amz-Signature'] = signature;
|
||||
// 构建最终 URL
|
||||
const queryString = this.buildCanonicalQueryString(queryParameters);
|
||||
const url = `${baseUrl}?${queryString}`;
|
||||
// PUT 方法返回需要的 headers
|
||||
if (method === 'PUT' && options?.contentType) {
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': options.contentType,
|
||||
},
|
||||
};
|
||||
}
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
exports.CTYunInstance = CTYunInstance;
|
||||
|
|
|
|||
|
|
@ -1,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>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}>;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -17,4 +17,15 @@ export declare class TencentYunInstance {
|
|||
private getSignInfo;
|
||||
removeFile(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<any>;
|
||||
isExistObject(srcBucket: string, zone: TencentYunZone, srcKey: string): Promise<boolean>;
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
presignObjectUrl(method: 'GET' | 'PUT' | 'POST' | 'DELETE', bucket: string, zone: TencentYunZone, key: string, options?: {
|
||||
expires?: number;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.TencentYunInstance = void 0;
|
||||
const tslib_1 = require("tslib");
|
||||
const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
||||
const Exception_1 = require("oak-domain/lib/types/Exception");
|
||||
function getEndpoint(RegionID) {
|
||||
return `cos-${RegionID}.myqcloud.com`;
|
||||
}
|
||||
|
|
@ -145,5 +146,37 @@ class TencentYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
async presignObjectUrl(method, bucket, zone, key, options) {
|
||||
const client = new this.COS({
|
||||
SecretId: this.accessKey,
|
||||
SecretKey: this.secretKey,
|
||||
});
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.getObjectUrl({
|
||||
Bucket: bucket,
|
||||
Region: zone,
|
||||
Key: key,
|
||||
Method: method,
|
||||
Expires: options?.expires || 3600,
|
||||
Sign: true,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(new Exception_1.OakExternalException('tencent', err.code, err.message, err, 'oak-external-sdk', {}));
|
||||
}
|
||||
else {
|
||||
resolve({ url: data.Url });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
throw new Exception_1.OakExternalException('tencent', error.code, error.message, error, 'oak-external-sdk', {});
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.TencentYunInstance = TencentYunInstance;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ class WechatNativeInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ class WechatPublicInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,11 @@ class WechatWebInstance {
|
|||
this.accessToken = accessToken;
|
||||
}
|
||||
else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
async getAccessToken() {
|
||||
|
|
|
|||
|
|
@ -1 +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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
|
|
@ -4,3 +4,4 @@ export * from './CTYun';
|
|||
export * from './ALiYun';
|
||||
export * from './TencentYun';
|
||||
export * from './AMap';
|
||||
export * from './S3';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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';
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import STSClient, { AssumeRoleRequest } from '@alicloud/sts20150401'
|
||||
|
||||
export type AssumeRoleOptions = {
|
||||
endpoint: string; // 阿里云STS服务地址,一般为 "sts.aliyuncs.com"
|
||||
accessKeyId: string; // 阿里云访问密钥ID
|
||||
accessKeySecret: string; // 阿里云访问密钥Secret
|
||||
roleArn: string; // 角色的资源描述符(ARN)
|
||||
roleSessionName: string; // 角色会话名称
|
||||
policy?: string; // 权限策略
|
||||
durationSeconds?: number; // 临时凭证的有效期,单位为秒,默认3600秒
|
||||
}
|
||||
|
||||
export async function stsAssumeRole(options: AssumeRoleOptions) {
|
||||
const client = new STSClient({
|
||||
endpoint: options.endpoint,
|
||||
accessKeyId: options.accessKeyId,
|
||||
accessKeySecret: options.accessKeySecret,
|
||||
toMap: () => {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
||||
const assumeRoleRequest = new AssumeRoleRequest({
|
||||
roleArn: options.roleArn,
|
||||
roleSessionName: options.roleSessionName,
|
||||
policy: options.policy,
|
||||
durationSeconds: options.durationSeconds,
|
||||
});
|
||||
|
||||
const res = await client.assumeRole(assumeRoleRequest);
|
||||
|
||||
return {
|
||||
"RequestId": res.body?.requestId,
|
||||
"AssumedRoleUser": {
|
||||
"AssumedRoleId": res.body?.assumedRoleUser?.assumedRoleId,
|
||||
"Arn": res.body?.assumedRoleUser?.arn
|
||||
},
|
||||
"Credentials": {
|
||||
"SecurityToken": res.body?.credentials?.securityToken,
|
||||
"Expiration": res.body?.credentials?.expiration,
|
||||
"AccessKeySecret": res.body?.credentials?.accessKeySecret,
|
||||
"AccessKeyId": res.body?.credentials?.accessKeyId
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -297,9 +297,9 @@ export class CTYunInstance {
|
|||
|
||||
const authorizationHeader = [
|
||||
'AWS4-HMAC-SHA256 Credential=' +
|
||||
this.accessKey +
|
||||
'/' +
|
||||
credentialScope,
|
||||
this.accessKey +
|
||||
'/' +
|
||||
credentialScope,
|
||||
'SignedHeaders=' + signedHeaders,
|
||||
'Signature=' + signature,
|
||||
].join(', ');
|
||||
|
|
@ -379,4 +379,104 @@ export class CTYunInstance {
|
|||
hash.update(payload);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成天翼云对象的预签名访问URL(兼容 AWS S3 Signature V4)
|
||||
*/
|
||||
async presignObjectUrl(
|
||||
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
||||
bucket: string,
|
||||
zone: CTYunZone,
|
||||
key: string,
|
||||
options?: {
|
||||
expires?: number;
|
||||
contentType?: string; // PUT 上传时可指定 Content-Type
|
||||
}
|
||||
): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}> {
|
||||
const expiresIn = options?.expires || 3600;
|
||||
const service = 's3';
|
||||
|
||||
// 对 key 进行 URI 编码(保留 /)
|
||||
const encodedKey = key
|
||||
.split('/')
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
const path = `/${encodedKey}`;
|
||||
|
||||
const host = `${bucket}.${CTYun_ENDPOINT_LIST[zone].ul}`;
|
||||
const baseUrl = `https://${host}${path}`;
|
||||
|
||||
// 生成 ISO8601 格式的日期
|
||||
const date = new Date()
|
||||
.toISOString()
|
||||
.replace(/\.\d{3}Z$/, 'Z')
|
||||
.replace(/[:\-]|\.\d{3}/g, '');
|
||||
const dateStamp = date.substring(0, 8);
|
||||
|
||||
const credentialScope = `${dateStamp}/${zone}/${service}/aws4_request`;
|
||||
|
||||
// 构建查询参数(不含签名)
|
||||
const queryParameters: Record<string, string> = {
|
||||
'X-Amz-Algorithm': 'AWS4-HMAC-SHA256',
|
||||
'X-Amz-Credential': `${this.accessKey}/${credentialScope}`,
|
||||
'X-Amz-Date': date,
|
||||
'X-Amz-Expires': expiresIn.toString(),
|
||||
'X-Amz-SignedHeaders': 'host',
|
||||
};
|
||||
|
||||
// 构建 Canonical Request
|
||||
// 对于预签名 URL,payload hash 使用 UNSIGNED-PAYLOAD
|
||||
const { canonicalRequest } = this.buildCanonicalRequest(
|
||||
method,
|
||||
path,
|
||||
queryParameters,
|
||||
{ host: host }, // 注意:header 名称应为小写
|
||||
'UNSIGNED-PAYLOAD'
|
||||
);
|
||||
|
||||
// 计算 Canonical Request 的 hash
|
||||
const canonicalRequestHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(canonicalRequest)
|
||||
.digest('hex');
|
||||
|
||||
// 构建 String to Sign
|
||||
const stringToSign = [
|
||||
'AWS4-HMAC-SHA256',
|
||||
date,
|
||||
credentialScope,
|
||||
canonicalRequestHash,
|
||||
].join('\n');
|
||||
|
||||
// 生成签名密钥并计算签名
|
||||
const signingKey = this.getSignatureKey(dateStamp, zone, service);
|
||||
const signature = crypto
|
||||
.createHmac('sha256', signingKey)
|
||||
.update(stringToSign)
|
||||
.digest('hex');
|
||||
|
||||
// 将签名添加到查询参数
|
||||
queryParameters['X-Amz-Signature'] = signature;
|
||||
|
||||
// 构建最终 URL
|
||||
const queryString = this.buildCanonicalQueryString(queryParameters);
|
||||
const url = `${baseUrl}?${queryString}`;
|
||||
|
||||
// PUT 方法返回需要的 headers
|
||||
if (method === 'PUT' && options?.contentType) {
|
||||
return {
|
||||
url,
|
||||
headers: {
|
||||
'Content-Type': options.contentType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return { url };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -173,4 +173,67 @@ export class TencentYunInstance {
|
|||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取预签名对象URL(统一接口)
|
||||
* 生成腾讯云对象的预签名访问URL
|
||||
*/
|
||||
async presignObjectUrl(
|
||||
method: 'GET' | 'PUT' | 'POST' | 'DELETE',
|
||||
bucket: string,
|
||||
zone: TencentYunZone,
|
||||
key: string,
|
||||
options?: {
|
||||
expires?: number;
|
||||
}
|
||||
): Promise<{
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
formdata?: Record<string, any>;
|
||||
}> {
|
||||
const client = new this.COS({
|
||||
SecretId: this.accessKey,
|
||||
SecretKey: this.secretKey,
|
||||
});
|
||||
|
||||
try {
|
||||
return new Promise((resolve, reject) => {
|
||||
client.getObjectUrl(
|
||||
{
|
||||
Bucket: bucket,
|
||||
Region: zone,
|
||||
Key: key,
|
||||
Method: method,
|
||||
Expires: options?.expires || 3600,
|
||||
Sign: true,
|
||||
},
|
||||
(err: any, data: { Url: string }) => {
|
||||
if (err) {
|
||||
reject(
|
||||
new OakExternalException(
|
||||
'tencent',
|
||||
err.code,
|
||||
err.message,
|
||||
err,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
)
|
||||
);
|
||||
} else {
|
||||
resolve({ url: data.Url });
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new OakExternalException(
|
||||
'tencent',
|
||||
error.code,
|
||||
error.message,
|
||||
error,
|
||||
'oak-external-sdk',
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -98,7 +98,11 @@ export class WechatMpInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,11 @@ export class WechatNativeInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,11 @@ export class WechatPublicInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,11 @@ export class WechatWebInstance {
|
|||
if (accessToken) {
|
||||
this.accessToken = accessToken;
|
||||
} else {
|
||||
this.refreshAccessToken();
|
||||
this.refreshAccessToken().then(() => {
|
||||
console.log(`WechatMp初始化获取accessToken成功,appId: [${this.appId}]`);
|
||||
}).catch(() => {
|
||||
console.log(`WechatMp初始化获取accessToken失败,appId: [${this.appId}]`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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; //是否开启鉴黄
|
||||
}
|
||||
|
|
@ -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; // 允许自定义区域
|
||||
|
|
@ -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';
|
||||
Loading…
Reference in New Issue