import { request as requestUtils } from '../utils/request.js' import * as clientApi from '../api/frontend/index.js' let urlRule = 'https://api.deotaland.aiIMGURL' export class FileServer { //文件上传缓存映射 - 静态属性,所有实例共享 static fileCacheMap = new Map(); constructor() { } //文件拼接 concatUrl(url) { return urlRule.replace('IMGURL',url) } //生成唯一的缓存键 generateUniqueCacheKey(input) { let content = ''; if (typeof input === 'string') { if (input.startsWith('data:')) { // 对于base64字符串,使用文件类型、原始长度和内容哈希 const mimeMatch = input.match(/data:([^;]+)/); const mimeType = mimeMatch ? mimeMatch[1] : 'unknown'; const contentLength = input.length; // 取base64内容的前100个字符进行唯一标识 const contentPreview = input.substring(0, 100); content = `base64_${mimeType}_${contentLength}_${contentPreview}`; } else if (input.startsWith('http://') || input.startsWith('https://')) { // 对于URL,使用完整URL content = input; } else { // 对于本地路径 content = input; } } else if (input instanceof File) { // 对于File对象,使用文件名、大小、类型和最后修改时间 content = `file_${input.name}_${input.size}_${input.type}_${input.lastModified}`; } // 使用同步哈希算法生成16位缓存键 return this.generateSimpleHash(content).substring(0, 16); } //简单的字符串哈希函数(同步) generateSimpleHash(content) { // 使用MurmurHash或简单的多项式哈希 let hash = 0; const prime = 31; for (let i = 0; i < content.length; i++) { const char = content.charCodeAt(i); hash = (prime * hash + char) | 0; // 使用位运算确保32位整数 } // 转换为16进制字符串 return (hash >>> 0).toString(16).padStart(8, '0'); } //文件压缩 /** * 压缩文件 - 支持多种格式 * @param {File|string} fileInput - 文件对象、本地路径、线上URL或base64字符串 * @param {number} quality - 压缩质量 (0-1),默认0.5 * @param {number} maxWidth - 最大宽度,默认原图宽度 * @param {number} maxHeight - 最大高度,默认原图高度 * @returns {Promise} 压缩后的base64字符串 */ async compressFile(fileInput, quality = 0.5, maxWidth = null, maxHeight = null) { try { let base64String; // 根据输入类型获取base64字符串 if (fileInput instanceof File) { // 直接是File对象 base64String = await this.fileToBase64(URL.createObjectURL(fileInput)); } else if (typeof fileInput === 'string') { if (fileInput.startsWith('data:')) { // 已经是base64格式 base64String = fileInput; } else if (fileInput.startsWith('http://') || fileInput.startsWith('https://')) { // 线上URL base64String = await this.fileToBase64(fileInput); } else { // 本地路径,尝试转换为base64 try { base64String = await this.fileToBase64(fileInput); } catch (error) { throw new Error(`无法处理本地路径: ${fileInput}`); } } } else { throw new Error('不支持的文件输入类型'); } // 压缩图片 return await this.compressImageFromBase64(base64String, quality, maxWidth, maxHeight); } catch (error) { console.error('文件压缩失败:', error); throw error; } } /** * 从base64字符串压缩图片 * @param {string} base64String - base64图片字符串 * @param {number} quality - 压缩质量 * @param {number} maxWidth - 最大宽度 * @param {number} maxHeight - 最大高度 * @returns {Promise} 压缩后的base64字符串 */ compressImageFromBase64(base64String, quality = 0.5, maxWidth = null, maxHeight = null) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => { try { // 计算新的尺寸 let { width, height } = img; if (maxWidth && width > maxWidth) { height = (height * maxWidth) / width; width = maxWidth; } if (maxHeight && height > maxHeight) { width = (width * maxHeight) / height; height = maxHeight; } const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); canvas.width = width; canvas.height = height; // 使用高质量的图像缩放 ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; ctx.drawImage(img, 0, 0, width, height); const compressed = canvas.toDataURL('image/jpeg', quality); resolve(compressed); } catch (error) { reject(error); } }; img.onerror = () => { reject(new Error('图片加载失败')); }; img.src = base64String; }); } /** * 批量压缩文件 * @param {Array} fileInputs - 文件输入数组 * @param {Object} options - 压缩选项 * @returns {Promise} 压缩后的base64数组 */ async compressFilesBatch(fileInputs, options = {}) { const { quality = 0.5, maxWidth = null, maxHeight = null } = options; const results = []; for (const fileInput of fileInputs) { try { const compressed = await this.compressFile(fileInput, quality, maxWidth, maxHeight); results.push({ success: true, original: fileInput, compressed: compressed }); } catch (error) { results.push({ success: false, original: fileInput, error: error.message }); } } return results; } /** * 从URL中提取有效的文件名 * @param {*} url 文件URL * @returns 有效的文件名 */ extractFileName(url) { // 如果是base64格式,根据MIME类型生成文件名 if (url.startsWith('data:')) { const mimeType = url.match(/data:([^;]+)/)?.[1]; const extension = this.getExtensionFromMimeType(mimeType); return `uploaded_file_${Date.now()}.${extension}`; } // 如果是普通URL,提取文件名并清理 try { const urlObj = new URL(url); const pathname = urlObj.pathname; const fileName = pathname.split('/').pop() || 'uploaded_file'; // 移除查询参数和哈希 const cleanFileName = fileName.split('?')[0].split('#')[0]; // 如果没有扩展名,尝试从URL路径推断 if (!cleanFileName.includes('.')) { const extension = this.inferExtensionFromPath(pathname); return cleanFileName + (extension ? `.${extension}` : ''); } return cleanFileName || `uploaded_file_${Date.now()}`; } catch (error) { // URL解析失败,使用默认文件名 return `uploaded_file_${Date.now()}`; } } /** * 从MIME类型获取文件扩展名 */ getExtensionFromMimeType(mimeType) { const mimeMap = { 'image/jpeg': 'jpg', 'image/jpg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/bmp': 'bmp', 'image/webp': 'webp', 'image/svg+xml': 'svg', 'application/pdf': 'pdf', 'text/plain': 'txt', 'application/json': 'json' }; return mimeMap[mimeType] || 'bin'; } /** * 从URL路径推断文件扩展名 */ inferExtensionFromPath(pathname) { // 常见的图片路径模式 if (pathname.includes('/image/') || pathname.includes('/img/')) return 'jpg'; if (pathname.includes('/photo/')) return 'jpg'; if (pathname.includes('/picture/')) return 'png'; return null; } //轮询获取并发时的线上文件映射 async pollFileCacheMap(cacheKey) { return new Promise((resolve, reject) => { let pollCount = 0; const maxPollCount = 20; const interval = setInterval(() => { pollCount++; if (!(FileServer.fileCacheMap.get(cacheKey))) { resolve('loading'); } else if (FileServer.fileCacheMap.get(cacheKey) != 'loading') { clearInterval(interval); resolve(FileServer.fileCacheMap.get(cacheKey)); } else if (pollCount >= maxPollCount) { clearInterval(interval); resolve('loading'); } }, 1000); // 每1秒检查一次 }); } //上传文件 async uploadFile(url) { // 如果是网络路径直接返回 if (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) { return Promise.resolve(url); } // 判断参数是否为File对象,如果是则先转换为base64 if (url instanceof File) { try { url = await this.fileToBase64FromFile(url); } catch (error) { console.error('File对象转base64失败:', error); throw error; } } const cacheKey = this.generateUniqueCacheKey(url);//生成唯一的缓存key return new Promise(async (resolve, reject) => { if(FileServer.fileCacheMap.has(cacheKey)&&FileServer.fileCacheMap.get(cacheKey)!='loading'){ // resolve(this.concatUrl(FileServer.fileCacheMap.get(cacheKey))); resolve(FileServer.fileCacheMap.get(cacheKey)); return; } let loadUrl = null; if(FileServer.fileCacheMap.get(cacheKey)=='loading'){ loadUrl = await this.pollFileCacheMap(cacheKey); } if(loadUrl!='loading'&&loadUrl!=null){ // resolve(this.concatUrl(loadUrl)); resolve(loadUrl); return } FileServer.fileCacheMap.set(cacheKey,'loading'); let file = await this.fileToBlob(url);//将文件或者base64文件转为blob对象 // 检查文件大小,如果超过10MB则进行压缩 const maxSizeInBytes = 10 * 1024 * 1024; // 10MB if (file.size > maxSizeInBytes) { try { console.log(`文件大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB,超过10MB限制,开始压缩...`); // 将Blob转换为File对象以便压缩 const fileName = this.extractFileName(url); const fileObject = new File([file], fileName, { type: file.type }); const compressedFile = await this.compressFile(fileObject, 0.7); // 使用0.7质量压缩 if (compressedFile && compressedFile.length < file.size) { // 将压缩后的base64转换回Blob const response = await fetch(compressedFile); const compressedBlob = await response.blob(); file = compressedBlob; console.log(`文件压缩成功,压缩后大小为 ${(file.size / 1024 / 1024).toFixed(2)}MB`); } } catch (error) { console.warn('文件压缩失败,使用原文件上传:', error.message); } } // const formData = new FormData(); // 从URL中提取文件名,如果没有则使用默认文件名 const fileName = this.extractFileName(url); // formData.append('file', file, fileName); try { // const response = await requestUtils.upload(clientApi.default.UPLOAD.url, formData); let params = { filename:fileName, file_type:file.type.split('/')[0], prefix:'images' } const response = await requestUtils.common(clientApi.default.UPLOADS3, params); if(response.code==0){ let data = response.data; let {url,fields,file_key,file_url } = data; const formData = new FormData(); for (const key in fields) { formData.append(key, fields[key]); } formData.append('file', file, fileName); // 上传到S3 const uploadResponse = await fetch(url, { method: 'POST', body: formData, mode: 'cors' // 明确指定CORS模式 }); // 注意:S3可能返回204或303状态码表示成功 if (uploadResponse.status === 204 || uploadResponse.status === 303 || uploadResponse.ok) { if(file_url){ // 截取后八位作为缓存 key FileServer.fileCacheMap.set(cacheKey, file_url); // resolve(urlRule.replace('IMGURL',file_url)); resolve(file_url); } } else { reject(errorMsg); } } } catch (error) { //删除对应键值 FileServer.fileCacheMap.delete(cacheKey); reject(error); console.error('上传文件失败:', error); throw error; } }) } //文件文件或者base64文件转为blob对象 fileToBlob(fileOrBase64) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', fileOrBase64); xhr.responseType = 'blob'; xhr.onload = () => { if (xhr.status === 200) { resolve(xhr.response); } else { reject(new Error('文件转换失败')); } }; xhr.onerror = reject; xhr.send(); }); } //本地文件或者oss文件转为base64格式 async fileToBase64(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = 'blob'; xhr.onload = () => { if (xhr.status === 200) { const reader = new FileReader(); reader.readAsDataURL(xhr.response); reader.onloadend = () => { resolve(reader.result); }; } else { reject(new Error('文件转换失败')); } }; xhr.onerror = reject; xhr.send(); }); } // File对象转为base64格式 async fileToBase64FromFile(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onloadend = () => { resolve(reader.result); }; reader.onerror = reject; }); } }