import { Provide, Config } from "@midwayjs/core"; import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { createReadStream, statSync } from "fs"; import { randomUUID } from "crypto"; import { extname } from "path"; import { UploadFileInfo } from "@midwayjs/upload"; // RustFS 配置结构(见 config.default.ts 的 rustfs 段) interface RustFsConfig { endpoint: string; port: number; useSSL: boolean; accessKey: string; secretKey: string; region: string; bucket: string; bucketProd: string; bucketTest: string; forcePathStyle: boolean; presignExpires: number; keyPrefix: string; } export interface UploadResult { key: string; url: string; name: string; size: number; } @Provide() export class UploadService { @Config("rustfs") rustfsConfig!: RustFsConfig; private client: S3Client | null = null; private getClient(): S3Client { if (!this.client) { const protocol = this.rustfsConfig.useSSL ? "https" : "http"; this.client = new S3Client({ endpoint: `${protocol}://${this.rustfsConfig.endpoint}:${this.rustfsConfig.port}`, region: this.rustfsConfig.region, credentials: { accessKeyId: this.rustfsConfig.accessKey, secretAccessKey: this.rustfsConfig.secretKey }, // RustFS 仅支持 path-style 访问 forcePathStyle: this.rustfsConfig.forcePathStyle }); } return this.client; } private get bucket(): string { return this.rustfsConfig.bucket; } /** * 上传单个文件到 RustFS,返回对象 key 与预签名访问 URL */ async uploadFile(file: UploadFileInfo): Promise { if (!file || !file.data) { throw new Error("上传文件为空"); } const ext = extname(file.filename || "").toLowerCase(); const now = new Date(); const yyyymm = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}`; const key = `${this.rustfsConfig.keyPrefix}/${yyyymm}/${randomUUID()}${ext}`; await this.getClient().send( new PutObjectCommand({ Bucket: this.bucket, Key: key, Body: createReadStream(file.data), ContentType: file.mimeType || "application/octet-stream" }) ); let size = 0; try { size = statSync(file.data).size; } catch { size = 0; } return { key, url: await this.getPresignedUrl(key), name: file.filename, size }; } /** * 按对象 key 生成预签名访问 URL(默认 1 小时有效) */ async getPresignedUrl(key: string): Promise { if (!key) return ""; return getSignedUrl( this.getClient(), new GetObjectCommand({ Bucket: this.bucket, Key: key }), { expiresIn: this.rustfsConfig.presignExpires } ); } /** * 批量把对象 key 转成预签名 URL,返回 key → url 映射(key 非法时返回空串) */ async getPresignedUrlMap(keys: string[]): Promise> { const map: Record = {}; for (const key of keys) { try { map[key] = await this.getPresignedUrl(key); } catch { map[key] = ""; } } return map; } }