|
|
@@ -5,6 +5,7 @@ import { MinioService } from './minio.service';
|
|
|
// import { AppError } from '@/server/utils/errorHandler';
|
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
|
import { logger } from '@/server/utils/logger';
|
|
|
+import axios from 'axios';
|
|
|
|
|
|
export class FileService extends GenericCrudService<File> {
|
|
|
private readonly minioService: MinioService;
|
|
|
@@ -112,6 +113,112 @@ export class FileService extends GenericCrudService<File> {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 从URL下载文件并保存到MinIO,同时创建数据库记录
|
|
|
+ */
|
|
|
+ async downloadAndSaveFromUrl(
|
|
|
+ url: string,
|
|
|
+ fileData: {
|
|
|
+ uploadUserId: number;
|
|
|
+ mimeType?: string;
|
|
|
+ customFileName?: string;
|
|
|
+ customPath?: string;
|
|
|
+ },
|
|
|
+ options: {
|
|
|
+ timeout?: number;
|
|
|
+ retries?: number;
|
|
|
+ } = {}
|
|
|
+ ): Promise<{ file: File; url: string }> {
|
|
|
+ try {
|
|
|
+ const { uploadUserId, mimeType, customFileName, customPath } = fileData;
|
|
|
+ const { timeout = 30000, retries = 0 } = options;
|
|
|
+
|
|
|
+ logger.info('Downloading file from URL:', { url, uploadUserId });
|
|
|
+
|
|
|
+ // 下载文件
|
|
|
+ const response = await axios.get(url, {
|
|
|
+ responseType: 'arraybuffer',
|
|
|
+ timeout,
|
|
|
+ validateStatus: (status) => status === 200
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.data) {
|
|
|
+ throw new Error('下载的文件内容为空');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取文件名和MIME类型
|
|
|
+ const originalFileName = customFileName || this.extractFileNameFromUrl(url);
|
|
|
+ const detectedMimeType = mimeType || response.headers['content-type'] || 'application/octet-stream';
|
|
|
+ const fileSize = Buffer.byteLength(response.data);
|
|
|
+
|
|
|
+ // 生成唯一文件存储路径
|
|
|
+ const fileKey = customPath
|
|
|
+ ? `${customPath}${uploadUserId}/${uuidv4()}-${originalFileName}`
|
|
|
+ : `${uploadUserId}/${uuidv4()}-${originalFileName}`;
|
|
|
+
|
|
|
+ // 上传到MinIO
|
|
|
+ await this.minioService.createObject(
|
|
|
+ this.minioService.bucketName,
|
|
|
+ fileKey,
|
|
|
+ Buffer.from(response.data),
|
|
|
+ detectedMimeType
|
|
|
+ );
|
|
|
+
|
|
|
+ // 创建文件记录
|
|
|
+ const fileRecordData: Partial<File> = {
|
|
|
+ name: originalFileName,
|
|
|
+ path: fileKey,
|
|
|
+ size: fileSize,
|
|
|
+ type: detectedMimeType,
|
|
|
+ uploadUserId,
|
|
|
+ uploadTime: new Date(),
|
|
|
+ createdAt: new Date(),
|
|
|
+ updatedAt: new Date()
|
|
|
+ };
|
|
|
+
|
|
|
+ const savedFile = await this.create(fileRecordData as File);
|
|
|
+
|
|
|
+ // 生成访问URL
|
|
|
+ const fileUrl = this.minioService.getFileUrl(this.minioService.bucketName, fileKey);
|
|
|
+
|
|
|
+ logger.info('File downloaded and saved successfully:', {
|
|
|
+ fileId: savedFile.id,
|
|
|
+ originalUrl: url,
|
|
|
+ fileSize,
|
|
|
+ mimeType: detectedMimeType
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ file: savedFile,
|
|
|
+ url: fileUrl
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ logger.error('Failed to download and save file from URL:', error);
|
|
|
+ throw new Error(`从URL下载文件失败: ${error instanceof Error ? error.message : String(error)}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从URL中提取文件名
|
|
|
+ */
|
|
|
+ private extractFileNameFromUrl(url: string): string {
|
|
|
+ try {
|
|
|
+ const urlObj = new URL(url);
|
|
|
+ const pathname = urlObj.pathname;
|
|
|
+ const fileName = pathname.substring(pathname.lastIndexOf('/') + 1);
|
|
|
+
|
|
|
+ // 如果没有文件名或文件名无效,使用默认名称
|
|
|
+ if (!fileName || fileName === '/' || fileName.includes('?')) {
|
|
|
+ return `downloaded-file-${Date.now()}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 移除查询参数
|
|
|
+ return fileName.split('?')[0];
|
|
|
+ } catch {
|
|
|
+ return `downloaded-file-${Date.now()}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 创建多部分上传策略
|
|
|
*/
|