deotalandAi/packages/utils/src/servers/fileserver.js

419 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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) {
// 判断参数是否为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 (typeof url === 'string' && (url.startsWith('http://') || url.startsWith('https://'))) {
resolve(url);
return;
}
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;
});
}
}