268 lines
9.0 KiB
TypeScript
268 lines
9.0 KiB
TypeScript
/**
|
|
* 阿里云 OSS 文件上传模块
|
|
* 使用官方 ali-oss SDK
|
|
* 文档: https://help.aliyun.com/zh/oss/user-guide/simple-upload
|
|
*/
|
|
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as crypto from 'crypto';
|
|
import axios from 'axios';
|
|
import logger from '../log/main';
|
|
import { withoutUnsupportedProxy } from '../httpClient';
|
|
|
|
// 动态导入 ali-oss
|
|
let OSS: any = null;
|
|
|
|
async function getOssClient() {
|
|
if (!OSS) {
|
|
try {
|
|
// 尝试动态导入 ali-oss
|
|
OSS = await import('ali-oss').then((m: any) => m.default || m);
|
|
} catch (e) {
|
|
logger.error('[Aliyun:OSS] ali-oss 模块未安装,使用 HTTP API');
|
|
return null;
|
|
}
|
|
}
|
|
return OSS;
|
|
}
|
|
|
|
export interface OssConfig {
|
|
accessKeyId: string;
|
|
accessKeySecret: string;
|
|
bucket: string;
|
|
region: string; // 如 oss-cn-beijing
|
|
endpoint?: string;
|
|
}
|
|
|
|
export interface OssUploadResult {
|
|
success: boolean;
|
|
url?: string;
|
|
error?: string;
|
|
}
|
|
|
|
/**
|
|
* 上传文件到阿里云OSS
|
|
*/
|
|
export async function uploadToOss(
|
|
filePath: string,
|
|
config: OssConfig,
|
|
objectKey?: string
|
|
): Promise<OssUploadResult> {
|
|
try {
|
|
config = {
|
|
accessKeyId: (config.accessKeyId || '').trim(),
|
|
accessKeySecret: (config.accessKeySecret || '').trim(),
|
|
bucket: (config.bucket || '').trim(),
|
|
region: (config.region || '').trim(),
|
|
endpoint: (config.endpoint || '').trim(),
|
|
};
|
|
|
|
if (config.endpoint && !config.endpoint.startsWith('http')) {
|
|
config.endpoint = `https://${config.endpoint}`;
|
|
}
|
|
|
|
logger.info('[Aliyun:OSS] 开始上传文件', { filePath, bucket: config.bucket, region: config.region });
|
|
|
|
// 检查文件是否存在
|
|
if (!fs.existsSync(filePath)) {
|
|
return { success: false, error: `文件不存在: ${filePath}` };
|
|
}
|
|
|
|
// 生成ObjectKey(如果未指定)
|
|
if (!objectKey) {
|
|
const ext = path.extname(filePath);
|
|
const timestamp = Date.now();
|
|
const random = Math.random().toString(36).substring(2, 8);
|
|
objectKey = `audio/${timestamp}_${random}${ext}`;
|
|
}
|
|
|
|
// 尝试使用 ali-oss SDK
|
|
const OssClass = await getOssClient();
|
|
|
|
if (OssClass) {
|
|
const clientConfig: any = {
|
|
accessKeyId: config.accessKeyId,
|
|
accessKeySecret: config.accessKeySecret,
|
|
bucket: config.bucket,
|
|
secure: true,
|
|
};
|
|
|
|
if (config.endpoint) {
|
|
clientConfig.endpoint = config.endpoint;
|
|
} else if (config.region) {
|
|
clientConfig.region = config.region;
|
|
} else {
|
|
return { success: false, error: '缺少 region 配置' };
|
|
}
|
|
|
|
logger.info('[Aliyun:OSS] SDK clientConfig:', {
|
|
bucket: clientConfig.bucket,
|
|
bucketType: typeof clientConfig.bucket,
|
|
region: clientConfig.region,
|
|
endpoint: clientConfig.endpoint,
|
|
});
|
|
|
|
logger.info('[Aliyun:OSS] 使用 SDK 上传', { objectKey, region: config.region, endpoint: config.endpoint });
|
|
|
|
const client = new OssClass(clientConfig);
|
|
|
|
try {
|
|
const result = await client.put(objectKey, filePath);
|
|
|
|
if (result.res.status === 200) {
|
|
// 生成签名 URL(有效期1小时),供 FunASR API 访问
|
|
const signedUrl = client.signatureUrl(objectKey, {
|
|
expires: 3600, // 1小时有效期
|
|
method: 'GET'
|
|
});
|
|
|
|
logger.info('[Aliyun:OSS] SDK上传成功,已生成签名URL', {
|
|
objectKey,
|
|
signedUrl: signedUrl.substring(0, 100) + '...' // 只记录部分URL
|
|
});
|
|
return { success: true, url: signedUrl };
|
|
} else {
|
|
logger.error('[Aliyun:OSS] SDK上传失败', result);
|
|
throw new Error(`上传失败: HTTP ${result.res.status}`);
|
|
}
|
|
} catch (sdkError: any) {
|
|
logger.warn('[Aliyun:OSS] SDK上传失败,降级到 HTTP API', { error: sdkError.message });
|
|
// SDK 失败,降级到 HTTP API 方式
|
|
return await uploadToOssViaHttp(filePath, config, objectKey);
|
|
}
|
|
} else {
|
|
// ali-oss 未安装,使用 HTTP API 方式
|
|
return await uploadToOssViaHttp(filePath, config, objectKey);
|
|
}
|
|
|
|
} catch (error: any) {
|
|
logger.error('[Aliyun:OSS] 上传异常', error);
|
|
|
|
let errorMsg = error.message || '未知错误';
|
|
if (error.code) {
|
|
errorMsg = `${error.code}: ${error.message}`;
|
|
}
|
|
|
|
return { success: false, error: errorMsg };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 使用 HTTP API 方式上传(备用方案)
|
|
*/
|
|
function generateSignedUrl(config: OssConfig, objectKey: string, expires: number): string {
|
|
const expireTime = Math.floor(Date.now() / 1000) + expires;
|
|
const resource = `/${config.bucket}/${objectKey}`;
|
|
const stringToSign = `GET\n\n\n${expireTime}\n${resource}`;
|
|
const hmac = crypto.createHmac('sha1', config.accessKeySecret);
|
|
hmac.update(stringToSign);
|
|
const signature = encodeURIComponent(hmac.digest('base64'));
|
|
return `https://${config.bucket}.${config.region}.aliyuncs.com/${objectKey}?OSSAccessKeyId=${config.accessKeyId}&Expires=${expireTime}&Signature=${signature}`;
|
|
}
|
|
|
|
async function uploadToOssViaHttp(
|
|
filePath: string,
|
|
config: OssConfig,
|
|
objectKey: string
|
|
): Promise<OssUploadResult> {
|
|
try {
|
|
logger.info('[Aliyun:OSS] 使用 HTTP API 上传');
|
|
|
|
const fileContent = fs.readFileSync(filePath);
|
|
const fileSize = fileContent.length;
|
|
|
|
// 确定Content-Type
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const contentTypeMap: Record<string, string> = {
|
|
'.wav': 'audio/wav',
|
|
'.mp3': 'audio/mpeg',
|
|
'.m4a': 'audio/mp4',
|
|
'.aac': 'audio/aac',
|
|
'.ogg': 'audio/ogg',
|
|
'.flac': 'audio/flac',
|
|
};
|
|
const contentType = contentTypeMap[ext] || 'application/octet-stream';
|
|
|
|
// 生成Date
|
|
const date = new Date().toUTCString();
|
|
|
|
// 构建URL
|
|
const host = `${config.bucket}.${config.region}.aliyuncs.com`;
|
|
const resource = `/${config.bucket}/${objectKey}`;
|
|
|
|
// 构建StringToSign (V1签名)
|
|
const stringToSign = [
|
|
'PUT',
|
|
'', // Content-MD5 留空
|
|
contentType,
|
|
date,
|
|
resource
|
|
].join('\n');
|
|
|
|
// 计算HMAC-SHA1签名
|
|
const hmac = crypto.createHmac('sha1', config.accessKeySecret);
|
|
hmac.update(stringToSign);
|
|
const signature = hmac.digest('base64');
|
|
|
|
// 发送上传请求
|
|
const url = `https://${host}/${objectKey}`;
|
|
|
|
const response = await axios.put(url, fileContent, withoutUnsupportedProxy({
|
|
headers: {
|
|
'Content-Type': contentType,
|
|
'Content-Length': fileSize.toString(),
|
|
'Date': date,
|
|
'Authorization': `OSS ${config.accessKeyId}:${signature}`,
|
|
},
|
|
maxBodyLength: Infinity,
|
|
maxContentLength: Infinity,
|
|
timeout: 120000
|
|
}));
|
|
|
|
if (response.status === 200) {
|
|
const unsignedUrl = url;
|
|
const signedUrl = generateSignedUrl(config, objectKey, 3600);
|
|
logger.info('[Aliyun:OSS] HTTP API上传成功', { url: unsignedUrl, signedUrl: signedUrl.substring(0, 100) + '...' });
|
|
return { success: true, url: signedUrl };
|
|
} else {
|
|
logger.error('[Aliyun:OSS] HTTP API上传失败', { status: response.status });
|
|
return { success: false, error: `上传失败: HTTP ${response.status}` };
|
|
}
|
|
|
|
} catch (error: any) {
|
|
logger.error('[Aliyun:OSS] HTTP API上传异常', error);
|
|
|
|
let errorMsg = error.message || '未知错误';
|
|
if (error.response?.data) {
|
|
const data = error.response.data;
|
|
if (typeof data === 'string' && data.includes('<Message>')) {
|
|
const match = data.match(/<Message>(.*?)<\/Message>/);
|
|
if (match) {
|
|
errorMsg = match[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
return { success: false, error: errorMsg };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 从配置中获取OSS配置
|
|
*/
|
|
export function getOssConfigFromProvider(providerData: any): OssConfig | null {
|
|
if (!providerData?.ossAccessKeyId || !providerData?.ossAccessKeySecret ||
|
|
!providerData?.ossBucket || !providerData?.ossRegion) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
accessKeyId: providerData.ossAccessKeyId,
|
|
accessKeySecret: providerData.ossAccessKeySecret,
|
|
bucket: providerData.ossBucket,
|
|
region: providerData.ossRegion,
|
|
endpoint: providerData.ossEndpoint
|
|
};
|
|
}
|