Files
BYW-VoiceHub/server/utils/s3Client.ts
T

125 lines
4.6 KiB
TypeScript

import { createHash, createHmac } from 'crypto'
/** S3 请求超时(毫秒) */
const S3_TIMEOUT = 120_000
/** AWS Signature V4 签名工具 */
export function sha256(data: string): string {
return createHash('sha256').update(data).digest('hex')
}
export function hmacSha256(key: Buffer | string, data: string): Buffer {
return createHmac('sha256', key).update(data).digest()
}
export function getSignatureKey(key: string, dateStamp: string, region: string, service: string): Buffer {
const kDate = hmacSha256(`AWS4${key}`, dateStamp)
const kRegion = hmacSha256(kDate, region)
const kService = hmacSha256(kRegion, service)
return hmacSha256(kService, 'aws4_request')
}
/** 上传文件到 S3 兼容存储 */
export async function uploadToS3(
endpoint: string,
bucket: string,
region: string,
accessKey: string,
secretKey: string,
key: string,
body: string | Buffer,
contentType: string = 'application/json'
): Promise<void> {
const url = new URL(endpoint)
const host = url.host
const service = 's3'
const now = new Date()
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
const dateStamp = amzDate.slice(0, 8)
const payloadHash = sha256(typeof body === 'string' ? body : body.toString())
const contentLength = Buffer.byteLength(typeof body === 'string' ? body : body).toString()
// 构建规范请求
const canonicalUri = `/${bucket}/${key}`
const canonicalQuerystring = ''
const canonicalHeaders = `content-type:${contentType}\nhost:${host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n`
const signedHeaders = 'content-type;host;x-amz-content-sha256;x-amz-date'
const canonicalRequest = `PUT\n${canonicalUri}\n${canonicalQuerystring}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`
// 构建待签名字符串
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`
const stringToSign = `AWS4-HMAC-SHA256\n${amzDate}\n${credentialScope}\n${sha256(canonicalRequest)}`
// 计算签名
const signingKey = getSignatureKey(secretKey, dateStamp, region, service)
const signature = hmacSha256(signingKey, stringToSign).toString('hex')
// 构建 Authorization 头
const authorization = `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
// 发送请求
const response = await fetch(`${endpoint.replace(/\/$/, '')}/${bucket}/${key}`, {
method: 'PUT',
headers: {
'Content-Type': contentType,
'Content-Length': contentLength,
'Host': host,
'x-amz-content-sha256': payloadHash,
'x-amz-date': amzDate,
'Authorization': authorization
},
body: typeof body === 'string' ? body : new Uint8Array(body),
signal: AbortSignal.timeout(S3_TIMEOUT)
})
if (!response.ok) {
const errorBody = await response.text().catch(() => '')
throw new Error(`S3 上传失败: ${response.status} ${response.statusText} - ${errorBody}`)
}
}
/** 从 S3 兼容存储删除文件 */
export async function deleteFromS3(
endpoint: string,
bucket: string,
region: string,
accessKey: string,
secretKey: string,
key: string
): Promise<void> {
const url = new URL(endpoint)
const host = url.host
const service = 's3'
const now = new Date()
const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, '')
const dateStamp = amzDate.slice(0, 8)
const payloadHash = sha256('')
const canonicalUri = `/${bucket}/${key}`
const canonicalHeaders = `host:${host}\nx-amz-content-sha256:${payloadHash}\nx-amz-date:${amzDate}\n`
const signedHeaders = 'host;x-amz-content-sha256;x-amz-date'
const canonicalRequest = `DELETE\n${canonicalUri}\n\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`
const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`
const stringToSign = `AWS4-HMAC-SHA256\n${amzDate}\n${credentialScope}\n${sha256(canonicalRequest)}`
const signingKey = getSignatureKey(secretKey, dateStamp, region, service)
const signature = hmacSha256(signingKey, stringToSign).toString('hex')
const authorization = `AWS4-HMAC-SHA256 Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`
const response = await fetch(`${endpoint.replace(/\/$/, '')}/${bucket}/${key}`, {
method: 'DELETE',
headers: {
'Host': host,
'x-amz-content-sha256': payloadHash,
'x-amz-date': amzDate,
'Authorization': authorization
},
signal: AbortSignal.timeout(S3_TIMEOUT)
})
if (!response.ok && response.status !== 404) {
const errorBody = await response.text().catch(() => '')
throw new Error(`S3 删除失败: ${response.status} ${response.statusText} - ${errorBody}`)
}
}