|
@@ -0,0 +1,521 @@
|
|
|
|
|
+import { GenericCrudService } from '@d8d/shared-crud';
|
|
|
|
|
+import { DataSource } from 'typeorm';
|
|
|
|
|
+import { File } from '../entities/file.entity';
|
|
|
|
|
+import { MinioService } from './minio.service';
|
|
|
|
|
+import { v4 as uuidv4 } from 'uuid';
|
|
|
|
|
+import { logger } from '@d8d/shared-utils';
|
|
|
|
|
+
|
|
|
|
|
+export class FileService extends GenericCrudService<File> {
|
|
|
|
|
+ private readonly minioService: MinioService;
|
|
|
|
|
+
|
|
|
|
|
+ constructor(dataSource: DataSource) {
|
|
|
|
|
+ super(dataSource, File);
|
|
|
|
|
+ this.minioService = new MinioService();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建文件记录并生成预签名上传URL
|
|
|
|
|
+ */
|
|
|
|
|
+ async createFile(data: Partial<File>) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 生成唯一文件存储路径
|
|
|
|
|
+ const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
|
|
|
|
|
+ // 生成MinIO上传策略
|
|
|
|
|
+ const uploadPolicy = await this.minioService.generateUploadPolicy(fileKey);
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 准备文件记录数据
|
|
|
|
|
+ const fileData = {
|
|
|
|
|
+ ...data,
|
|
|
|
|
+ path: fileKey,
|
|
|
|
|
+ uploadTime: new Date(),
|
|
|
|
|
+ createdAt: new Date(),
|
|
|
|
|
+ updatedAt: new Date()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 保存文件记录到数据库
|
|
|
|
|
+ const savedFile = await this.create(fileData as File);
|
|
|
|
|
+
|
|
|
|
|
+ // 返回文件记录和上传策略
|
|
|
|
|
+ return {
|
|
|
|
|
+ file: savedFile,
|
|
|
|
|
+ uploadPolicy
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to create file:', error);
|
|
|
|
|
+ throw new Error('文件创建失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 删除文件记录及对应的MinIO文件
|
|
|
|
|
+ */
|
|
|
|
|
+ async deleteFile(id: number) {
|
|
|
|
|
+ // 获取文件记录
|
|
|
|
|
+ const file = await this.getById(id);
|
|
|
|
|
+ if (!file) {
|
|
|
|
|
+ throw new Error('文件不存在');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 验证文件是否存在于MinIO
|
|
|
|
|
+ const fileExists = await this.minioService.objectExists(this.minioService.bucketName, file.path);
|
|
|
|
|
+ if (!fileExists) {
|
|
|
|
|
+ logger.error(`File not found in MinIO: ${this.minioService.bucketName}/${file.path}`);
|
|
|
|
|
+ // 仍然继续删除数据库记录,但记录警告日志
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 从MinIO删除文件
|
|
|
|
|
+ await this.minioService.deleteObject(this.minioService.bucketName, file.path);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从数据库删除记录
|
|
|
|
|
+ await this.delete(id);
|
|
|
|
|
+
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to delete file:', error);
|
|
|
|
|
+ throw new Error('文件删除失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取文件访问URL
|
|
|
|
|
+ */
|
|
|
|
|
+ async getFileUrl(id: number) {
|
|
|
|
|
+ const file = await this.getById(id);
|
|
|
|
|
+ if (!file) {
|
|
|
|
|
+ throw new Error('文件不存在');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return this.minioService.getPresignedFileUrl(this.minioService.bucketName, file.path);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取文件下载URL(带Content-Disposition头)
|
|
|
|
|
+ */
|
|
|
|
|
+ async getFileDownloadUrl(id: number) {
|
|
|
|
|
+ const file = await this.getById(id);
|
|
|
|
|
+ if (!file) {
|
|
|
|
|
+ throw new Error('文件不存在');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const url = await this.minioService.getPresignedFileDownloadUrl(
|
|
|
|
|
+ this.minioService.bucketName,
|
|
|
|
|
+ file.path,
|
|
|
|
|
+ file.name
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ url,
|
|
|
|
|
+ filename: file.name
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建多部分上传策略
|
|
|
|
|
+ */
|
|
|
|
|
+ async createMultipartUploadPolicy(data: Partial<File>, partCount: number) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 生成唯一文件存储路径
|
|
|
|
|
+ const fileKey = `${data.uploadUserId}/${uuidv4()}-${data.name}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化多部分上传
|
|
|
|
|
+ const uploadId = await this.minioService.createMultipartUpload(
|
|
|
|
|
+ this.minioService.bucketName,
|
|
|
|
|
+ fileKey
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 生成各部分上传URL
|
|
|
|
|
+ const uploadUrls = await this.minioService.generateMultipartUploadUrls(
|
|
|
|
|
+ this.minioService.bucketName,
|
|
|
|
|
+ fileKey,
|
|
|
|
|
+ uploadId,
|
|
|
|
|
+ partCount
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 准备文件记录数据
|
|
|
|
|
+ const fileData = {
|
|
|
|
|
+ ...data,
|
|
|
|
|
+ path: fileKey,
|
|
|
|
|
+ uploadTime: new Date(),
|
|
|
|
|
+ createdAt: new Date(),
|
|
|
|
|
+ updatedAt: new Date()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 保存文件记录到数据库
|
|
|
|
|
+ const savedFile = await this.create(fileData as File);
|
|
|
|
|
+
|
|
|
|
|
+ // 返回文件记录和上传策略
|
|
|
|
|
+ return {
|
|
|
|
|
+ file: savedFile,
|
|
|
|
|
+ uploadId,
|
|
|
|
|
+ uploadUrls,
|
|
|
|
|
+ bucket: this.minioService.bucketName,
|
|
|
|
|
+ key: fileKey
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to create multipart upload policy:', error);
|
|
|
|
|
+ throw new Error('创建多部分上传策略失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 完成分片上传
|
|
|
|
|
+ */
|
|
|
|
|
+ async completeMultipartUpload(data: {
|
|
|
|
|
+ uploadId: string;
|
|
|
|
|
+ bucket: string;
|
|
|
|
|
+ key: string;
|
|
|
|
|
+ parts: Array<{ partNumber: number; etag: string }>;
|
|
|
|
|
+ }) {
|
|
|
|
|
+ logger.db('Starting multipart upload completion:', {
|
|
|
|
|
+ uploadId: data.uploadId,
|
|
|
|
|
+ bucket: data.bucket,
|
|
|
|
|
+ key: data.key,
|
|
|
|
|
+ partsCount: data.parts.length
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 查找文件记录
|
|
|
|
|
+ const file = await this.repository.findOneBy({ path: data.key });
|
|
|
|
|
+ if (!file) {
|
|
|
|
|
+ throw new Error('文件记录不存在');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 完成MinIO分片上传 - 注意格式转换
|
|
|
|
|
+ const result = await this.minioService.completeMultipartUpload(
|
|
|
|
|
+ data.bucket,
|
|
|
|
|
+ data.key,
|
|
|
|
|
+ data.uploadId,
|
|
|
|
|
+ data.parts.map(part => ({ PartNumber: part.partNumber, ETag: part.etag }))
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 更新文件大小等信息
|
|
|
|
|
+ file.size = result.size;
|
|
|
|
|
+ file.updatedAt = new Date();
|
|
|
|
|
+ await this.repository.save(file);
|
|
|
|
|
+
|
|
|
|
|
+ // 生成文件访问URL
|
|
|
|
|
+ const url = this.minioService.getFileUrl(data.bucket, data.key);
|
|
|
|
|
+
|
|
|
|
|
+ logger.db('Multipart upload completed successfully:', {
|
|
|
|
|
+ fileId: file.id,
|
|
|
|
|
+ size: result.size,
|
|
|
|
|
+ key: data.key
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ fileId: file.id,
|
|
|
|
|
+ url,
|
|
|
|
|
+ key: data.key,
|
|
|
|
|
+ size: result.size
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to complete multipart upload:', error);
|
|
|
|
|
+ throw new Error('完成分片上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存文件记录并将文件内容直接上传到MinIO
|
|
|
|
|
+ * @param fileData - 文件基础信息
|
|
|
|
|
+ * @param fileContent - 文件内容(Buffer)
|
|
|
|
|
+ * @param contentType - 文件MIME类型
|
|
|
|
|
+ * @returns 保存的文件记录和文件访问URL
|
|
|
|
|
+ */
|
|
|
|
|
+ async saveFile(
|
|
|
|
|
+ fileData: {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ mimeType: string;
|
|
|
|
|
+ uploadUserId: number;
|
|
|
|
|
+ [key: string]: any;
|
|
|
|
|
+ },
|
|
|
|
|
+ fileContent: Buffer,
|
|
|
|
|
+ contentType?: string
|
|
|
|
|
+ ) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ logger.db('Starting saveFile process:', {
|
|
|
|
|
+ filename: fileData.name,
|
|
|
|
|
+ size: fileData.size,
|
|
|
|
|
+ mimeType: fileData.mimeType,
|
|
|
|
|
+ uploadUserId: fileData.uploadUserId
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 生成唯一文件存储路径
|
|
|
|
|
+ const fileKey = `${fileData.uploadUserId}/${uuidv4()}-${fileData.name}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 确保存储桶存在
|
|
|
|
|
+ await this.minioService.ensureBucketExists();
|
|
|
|
|
+
|
|
|
|
|
+ // 直接上传文件内容到MinIO
|
|
|
|
|
+ const fileUrl = await this.minioService.createObject(
|
|
|
|
|
+ this.minioService.bucketName,
|
|
|
|
|
+ fileKey,
|
|
|
|
|
+ fileContent,
|
|
|
|
|
+ contentType || fileData.mimeType
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 准备文件记录数据
|
|
|
|
|
+ const completeFileData = {
|
|
|
|
|
+ ...fileData,
|
|
|
|
|
+ path: fileKey,
|
|
|
|
|
+ uploadTime: new Date(),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 保存文件记录到数据库
|
|
|
|
|
+ const savedFile = await this.create(completeFileData as any);
|
|
|
|
|
+
|
|
|
|
|
+ logger.db('File saved successfully:', {
|
|
|
|
|
+ fileId: savedFile.id,
|
|
|
|
|
+ filename: savedFile.name,
|
|
|
|
|
+ size: savedFile.size,
|
|
|
|
|
+ url: fileUrl
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ file: savedFile,
|
|
|
|
|
+ url: fileUrl
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to save file:', error);
|
|
|
|
|
+ throw new Error(`文件保存失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存文件记录并将文件内容直接上传到MinIO(支持自定义存储路径)
|
|
|
|
|
+ * @param fileData - 文件基础信息
|
|
|
|
|
+ * @param fileContent - 文件内容(Buffer)
|
|
|
|
|
+ * @param customPath - 自定义存储路径(可选)
|
|
|
|
|
+ * @param contentType - 文件MIME类型
|
|
|
|
|
+ * @returns 保存的文件记录和文件访问URL
|
|
|
|
|
+ */
|
|
|
|
|
+ async saveFileWithCustomPath(
|
|
|
|
|
+ fileData: {
|
|
|
|
|
+ name: string;
|
|
|
|
|
+ size: number;
|
|
|
|
|
+ mimeType: string;
|
|
|
|
|
+ uploadUserId: number;
|
|
|
|
|
+ [key: string]: any;
|
|
|
|
|
+ },
|
|
|
|
|
+ fileContent: Buffer,
|
|
|
|
|
+ customPath?: string,
|
|
|
|
|
+ contentType?: string
|
|
|
|
|
+ ) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ logger.db('Starting saveFileWithCustomPath process:', {
|
|
|
|
|
+ filename: fileData.name,
|
|
|
|
|
+ size: fileData.size,
|
|
|
|
|
+ mimeType: fileData.mimeType,
|
|
|
|
|
+ uploadUserId: fileData.uploadUserId,
|
|
|
|
|
+ customPath: customPath || 'auto-generated'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 使用自定义路径或生成唯一文件存储路径
|
|
|
|
|
+ const fileKey = customPath || `${fileData.uploadUserId}/${uuidv4()}-${fileData.name}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 确保存储桶存在
|
|
|
|
|
+ await this.minioService.ensureBucketExists();
|
|
|
|
|
+
|
|
|
|
|
+ // 直接上传文件内容到MinIO
|
|
|
|
|
+ const fileUrl = await this.minioService.createObject(
|
|
|
|
|
+ this.minioService.bucketName,
|
|
|
|
|
+ fileKey,
|
|
|
|
|
+ fileContent,
|
|
|
|
|
+ contentType || fileData.mimeType
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 准备文件记录数据
|
|
|
|
|
+ const completeFileData = {
|
|
|
|
|
+ ...fileData,
|
|
|
|
|
+ path: fileKey,
|
|
|
|
|
+ uploadTime: new Date(),
|
|
|
|
|
+ // createdAt: new Date(),
|
|
|
|
|
+ // updatedAt: new Date()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 保存文件记录到数据库
|
|
|
|
|
+ const savedFile = await this.create(completeFileData as any);
|
|
|
|
|
+
|
|
|
|
|
+ logger.db('File saved with custom path successfully:', {
|
|
|
|
|
+ fileId: savedFile.id,
|
|
|
|
|
+ filename: savedFile.name,
|
|
|
|
|
+ size: savedFile.size,
|
|
|
|
|
+ path: fileKey,
|
|
|
|
|
+ url: fileUrl
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ file: savedFile,
|
|
|
|
|
+ url: fileUrl
|
|
|
|
|
+ };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to save file with custom path:', error);
|
|
|
|
|
+ throw new Error(`文件保存失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从URL下载文件并保存到MinIO
|
|
|
|
|
+ * @param url - 文件URL
|
|
|
|
|
+ * @param fileData - 文件基础信息(不含name和size,将自动获取)
|
|
|
|
|
+ * @param options - 可选配置
|
|
|
|
|
+ * @returns 保存的文件记录和文件访问URL
|
|
|
|
|
+ */
|
|
|
|
|
+ async downloadAndSaveFromUrl(
|
|
|
|
|
+ url: string,
|
|
|
|
|
+ fileData: {
|
|
|
|
|
+ uploadUserId: number;
|
|
|
|
|
+ mimeType?: string;
|
|
|
|
|
+ customFileName?: string;
|
|
|
|
|
+ customPath?: string;
|
|
|
|
|
+ [key: string]: any;
|
|
|
|
|
+ },
|
|
|
|
|
+ options?: {
|
|
|
|
|
+ timeout?: number;
|
|
|
|
|
+ retries?: number;
|
|
|
|
|
+ }
|
|
|
|
|
+ ) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const axios = require('axios');
|
|
|
|
|
+
|
|
|
|
|
+ logger.db('Starting downloadAndSaveFromUrl process:', {
|
|
|
|
|
+ url,
|
|
|
|
|
+ uploadUserId: fileData.uploadUserId,
|
|
|
|
|
+ customFileName: fileData.customFileName,
|
|
|
|
|
+ customPath: fileData.customPath
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 下载文件
|
|
|
|
|
+ const response = await axios.get(url, {
|
|
|
|
|
+ responseType: 'arraybuffer',
|
|
|
|
|
+ timeout: options?.timeout || 30000,
|
|
|
|
|
+ maxRedirects: 5,
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'User-Agent': 'Mozilla/5.0 (compatible; FileDownloader/1.0)'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const buffer = Buffer.from(response.data);
|
|
|
|
|
+
|
|
|
|
|
+ // 从URL或响应头中获取文件名
|
|
|
|
|
+ let fileName = fileData.customFileName;
|
|
|
|
|
+ if (!fileName) {
|
|
|
|
|
+ // 尝试从Content-Disposition头获取文件名
|
|
|
|
|
+ const contentDisposition = response.headers['content-disposition'];
|
|
|
|
|
+ if (contentDisposition) {
|
|
|
|
|
+ const filenameMatch = contentDisposition.match(/filename[*]?=(?:utf-8'')?(.+)/i);
|
|
|
|
|
+ if (filenameMatch) {
|
|
|
|
|
+ fileName = decodeURIComponent(filenameMatch[1].replace(/['"]/g, ''));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 从URL路径获取文件名
|
|
|
|
|
+ if (!fileName) {
|
|
|
|
|
+ const urlPath = new URL(url).pathname;
|
|
|
|
|
+ fileName = urlPath.split('/').pop() || `file_${Date.now()}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保文件有扩展名
|
|
|
|
|
+ if (!fileName.includes('.') && fileData.mimeType) {
|
|
|
|
|
+ const ext = this.getExtensionFromMimeType(fileData.mimeType);
|
|
|
|
|
+ if (ext) {
|
|
|
|
|
+ fileName += `.${ext}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确定MIME类型
|
|
|
|
|
+ let mimeType = fileData.mimeType || response.headers['content-type'];
|
|
|
|
|
+ if (!mimeType || mimeType === 'application/octet-stream') {
|
|
|
|
|
+ mimeType = this.inferMimeType(fileName);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存文件
|
|
|
|
|
+ const saveResult = await this.saveFileWithCustomPath(
|
|
|
|
|
+ {
|
|
|
|
|
+ ...fileData,
|
|
|
|
|
+ name: fileName,
|
|
|
|
|
+ size: buffer.length,
|
|
|
|
|
+ mimeType,
|
|
|
|
|
+ fileType: this.getFileTypeFromMimeType(mimeType)
|
|
|
|
|
+ },
|
|
|
|
|
+ buffer,
|
|
|
|
|
+ fileData.customPath,
|
|
|
|
|
+ mimeType
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ logger.db('Download and save completed successfully:', {
|
|
|
|
|
+ fileId: saveResult.file.id,
|
|
|
|
|
+ fileName,
|
|
|
|
|
+ size: buffer.length,
|
|
|
|
|
+ url: saveResult.url
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return saveResult;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ logger.error('Failed to download and save file from URL:', {
|
|
|
|
|
+ url,
|
|
|
|
|
+ error: error instanceof Error ? error.message : '未知错误',
|
|
|
|
|
+ stack: error instanceof Error ? error.stack : undefined
|
|
|
|
|
+ });
|
|
|
|
|
+ throw new Error(`从URL下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据MIME类型获取文件扩展名
|
|
|
|
|
+ */
|
|
|
|
|
+ private getExtensionFromMimeType(mimeType: string): string | null {
|
|
|
|
|
+ const mimeMap: Record<string, string> = {
|
|
|
|
|
+ 'image/jpeg': 'jpg',
|
|
|
|
|
+ 'image/png': 'png',
|
|
|
|
|
+ 'image/gif': 'gif',
|
|
|
|
|
+ 'image/webp': 'webp',
|
|
|
|
|
+ 'image/svg+xml': 'svg',
|
|
|
|
|
+ 'application/pdf': 'pdf',
|
|
|
|
|
+ 'text/plain': 'txt',
|
|
|
|
|
+ 'application/json': 'json',
|
|
|
|
|
+ 'application/xml': 'xml',
|
|
|
|
|
+ 'video/mp4': 'mp4',
|
|
|
|
|
+ 'audio/mp3': 'mp3'
|
|
|
|
|
+ };
|
|
|
|
|
+ return mimeMap[mimeType] || null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据文件名推断MIME类型
|
|
|
|
|
+ */
|
|
|
|
|
+ private inferMimeType(fileName: string): string {
|
|
|
|
|
+ const ext = fileName.toLowerCase().split('.').pop();
|
|
|
|
|
+ const extMap: Record<string, string> = {
|
|
|
|
|
+ 'jpg': 'image/jpeg',
|
|
|
|
|
+ 'jpeg': 'image/jpeg',
|
|
|
|
|
+ 'png': 'image/png',
|
|
|
|
|
+ 'gif': 'image/gif',
|
|
|
|
|
+ 'webp': 'image/webp',
|
|
|
|
|
+ 'svg': 'image/svg+xml',
|
|
|
|
|
+ 'pdf': 'application/pdf',
|
|
|
|
|
+ 'txt': 'text/plain',
|
|
|
|
|
+ 'json': 'application/json',
|
|
|
|
|
+ 'xml': 'application/xml',
|
|
|
|
|
+ 'mp4': 'video/mp4',
|
|
|
|
|
+ 'mp3': 'audio/mp3',
|
|
|
|
|
+ 'wav': 'audio/wav'
|
|
|
|
|
+ };
|
|
|
|
|
+ return extMap[ext || ''] || 'application/octet-stream';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 根据MIME类型获取文件类型
|
|
|
|
|
+ */
|
|
|
|
|
+ private getFileTypeFromMimeType(mimeType: string): string {
|
|
|
|
|
+ if (mimeType.startsWith('image/')) return 'image';
|
|
|
|
|
+ if (mimeType.startsWith('video/')) return 'video';
|
|
|
|
|
+ if (mimeType.startsWith('audio/')) return 'audio';
|
|
|
|
|
+ if (mimeType === 'application/pdf') return 'document';
|
|
|
|
|
+ if (mimeType.startsWith('text/')) return 'document';
|
|
|
|
|
+ return 'other';
|
|
|
|
|
+ }
|
|
|
|
|
+}
|