419 lines
14 KiB
JavaScript
419 lines
14 KiB
JavaScript
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<string>} 压缩后的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<string>} 压缩后的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<Array>} 压缩后的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;
|
||
});
|
||
}
|
||
} |