|
|
@@ -356,4 +356,167 @@ export class FileService extends GenericCrudService<File> {
|
|
|
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';
|
|
|
+ }
|
|
|
}
|