parent
455d614f4a
commit
174a714286
9 changed files with 950 additions and 151 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
|||||||
|
Not found: /minio@7.1.3/dist/minio.min.js |
||||||
@ -1,26 +1,627 @@ |
|||||||
// MinIO client stub - install 'minio' package for full functionality
|
// MinIO client implementation using AWS S3 SDK (MinIO is S3-compatible)
|
||||||
let minioClient = null |
let s3Client = null |
||||||
|
|
||||||
|
/** |
||||||
|
* 初始化 MinIO 客户端(使用 AWS S3 SDK) |
||||||
|
* @param {object} config - MinIO 配置 |
||||||
|
* @param {string} config.endPoint - 服务器地址 |
||||||
|
* @param {number} config.port - 端口号 |
||||||
|
* @param {string} config.accessKey - 访问密钥 |
||||||
|
* @param {string} config.secretKey - 秘密密钥 |
||||||
|
* @param {boolean} config.useSSL - 是否使用 HTTPS |
||||||
|
*/ |
||||||
export function initMinioClient(config) { |
export function initMinioClient(config) { |
||||||
console.warn('MinIO client not available. Install minio package for full functionality.') |
// 检查是否已加载 AWS SDK
|
||||||
minioClient = { |
if (typeof AWS === 'undefined') { |
||||||
endPoint: config.endPoint, |
console.error('AWS SDK not loaded! Please include aws-sdk.min.js in your HTML') |
||||||
port: config.port |
return |
||||||
} |
} |
||||||
|
|
||||||
|
s3Client = new AWS.S3({ |
||||||
|
endpoint: `${config.useSSL ? 'https' : 'http'}://${config.endPoint}:${config.port}`, |
||||||
|
accessKeyId: config.accessKey, |
||||||
|
secretAccessKey: config.secretKey, |
||||||
|
s3ForcePathStyle: true, // 必须强制路径风格以兼容 MinIO
|
||||||
|
signatureVersion: 'v4', // 使用 SigV4 签名
|
||||||
|
region: 'us-east-1' // MinIO 默认区域
|
||||||
|
}) |
||||||
|
|
||||||
|
console.log('✅ MinIO client initialized with AWS S3 SDK:', { |
||||||
|
endPoint: config.endPoint, |
||||||
|
port: config.port, |
||||||
|
useSSL: config.useSSL, |
||||||
|
region: 'us-east-1' |
||||||
|
}) |
||||||
} |
} |
||||||
|
|
||||||
export function getMinioClient() { |
export function getMinioClient() { |
||||||
return minioClient |
return s3Client |
||||||
} |
} |
||||||
|
|
||||||
export function uploadFile(bucket, objectName, filePath, metaData = {}) { |
/** |
||||||
return Promise.reject(new Error('MinIO client not initialized')) |
* 上传文件到 MinIO |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称(文件路径) |
||||||
|
* @param {File} file - 文件对象 |
||||||
|
* @param {object} metaData - 元数据(可选) |
||||||
|
* @param {function} onProgress - 进度回调函数(可选) |
||||||
|
* @returns {Promise} - 上传结果 |
||||||
|
*/ |
||||||
|
export async function uploadFile(bucket, objectName, file, metaData = {}, onProgress) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
console.log('=== MinIO Upload ===') |
||||||
|
console.log('Bucket:', bucket) |
||||||
|
console.log('Object:', objectName) |
||||||
|
console.log('File:', file.name, file.size, 'bytes') |
||||||
|
|
||||||
|
// 准备上传参数
|
||||||
|
const params = { |
||||||
|
Bucket: bucket, |
||||||
|
Key: objectName, |
||||||
|
Body: file, |
||||||
|
ContentType: file.type || 'application/octet-stream', |
||||||
|
...metaData |
||||||
|
} |
||||||
|
|
||||||
|
// 使用 AWS SDK 的 upload 方法
|
||||||
|
const upload = s3Client.upload(params) |
||||||
|
|
||||||
|
// 进度监控
|
||||||
|
upload.on('httpUploadProgress', (progress) => { |
||||||
|
if (progress && onProgress) { |
||||||
|
const percent = Math.round((progress.loaded / progress.total) * 100) |
||||||
|
onProgress(percent) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 执行上传
|
||||||
|
upload.promise() |
||||||
|
.then((data) => { |
||||||
|
console.log('✅ Upload successful:', data) |
||||||
|
resolve({ |
||||||
|
bucket, |
||||||
|
objectName: data.Key, |
||||||
|
etag: data.ETag, |
||||||
|
url: `${s3Client.endpoint.host}/${bucket}/${data.Key}` |
||||||
|
}) |
||||||
|
}) |
||||||
|
.catch((err) => { |
||||||
|
console.error('❌ Upload failed:', err) |
||||||
|
reject(new Error('Upload failed: ' + err.message)) |
||||||
|
}) |
||||||
|
}) |
||||||
} |
} |
||||||
|
|
||||||
export function downloadFile(bucket, objectName, filePath) { |
/** |
||||||
return Promise.reject(new Error('MinIO client not initialized')) |
* 下载文件(获取预签名 URL) |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称 |
||||||
|
* @param {number} expiry - 过期时间(秒),默认 24 小时 |
||||||
|
* @returns {Promise} - 预签名 URL |
||||||
|
*/ |
||||||
|
export async function downloadFile(bucket, objectName, expiry = 24 * 60 * 60) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
const params = { |
||||||
|
Bucket: bucket, |
||||||
|
Key: objectName, |
||||||
|
Expires: expiry |
||||||
|
} |
||||||
|
|
||||||
|
s3Client.getSignedUrl('getObject', params, (err, url) => { |
||||||
|
if (err) { |
||||||
|
reject(new Error('Failed to get presigned URL: ' + err.message)) |
||||||
|
} else { |
||||||
|
resolve(url) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取预签名上传 URL |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称 |
||||||
|
* @param {number} expiry - 过期时间(秒),默认 24 小时 |
||||||
|
* @returns {Promise} - 预签名上传 URL |
||||||
|
*/ |
||||||
|
export async function getPresignedUrl(bucket, objectName, expiry = 24 * 60 * 60) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
const params = { |
||||||
|
Bucket: bucket, |
||||||
|
Key: objectName, |
||||||
|
Expires: expiry |
||||||
|
} |
||||||
|
|
||||||
|
s3Client.getSignedUrl('putObject', params, (err, url) => { |
||||||
|
if (err) { |
||||||
|
reject(new Error('Failed to get presigned URL: ' + err.message)) |
||||||
|
} else { |
||||||
|
resolve(url) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 检查存储桶是否存在 |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @returns {Promise} - 是否存在 |
||||||
|
*/ |
||||||
|
export async function bucketExists(bucket) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
s3Client.headBucket({ Bucket: bucket }, (err) => { |
||||||
|
if (err) { |
||||||
|
if (err.code === 'NotFound') { |
||||||
|
resolve(false) |
||||||
|
} else { |
||||||
|
reject(new Error('Failed to check bucket: ' + err.message)) |
||||||
|
} |
||||||
|
} else { |
||||||
|
resolve(true) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
} |
} |
||||||
|
|
||||||
export function getPresignedUrl(bucket, objectName, expiry = 24 * 60 * 60) { |
/** |
||||||
return Promise.reject(new Error('MinIO client not initialized')) |
* 获取存储桶列表 |
||||||
|
* @returns {Promise} - 存储桶列表 |
||||||
|
*/ |
||||||
|
export async function listBuckets() { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
s3Client.listBuckets((err, data) => { |
||||||
|
if (err) { |
||||||
|
reject(new Error('Failed to list buckets: ' + err.message)) |
||||||
|
} else { |
||||||
|
resolve(data.Buckets) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 删除对象 |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称 |
||||||
|
* @returns {Promise} - 删除结果 |
||||||
|
*/ |
||||||
|
export async function deleteObject(bucket, objectName) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
const params = { |
||||||
|
Bucket: bucket, |
||||||
|
Key: objectName |
||||||
|
} |
||||||
|
|
||||||
|
s3Client.deleteObject(params, (err, data) => { |
||||||
|
if (err) { |
||||||
|
reject(new Error('Failed to delete object: ' + err.message)) |
||||||
|
} else { |
||||||
|
resolve(data) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取对象信息 |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称 |
||||||
|
* @returns {Promise} - 对象元数据 |
||||||
|
*/ |
||||||
|
export async function getObjectMetadata(bucket, objectName) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
const params = { |
||||||
|
Bucket: bucket, |
||||||
|
Key: objectName |
||||||
|
} |
||||||
|
|
||||||
|
s3Client.headObject(params, (err, data) => { |
||||||
|
if (err) { |
||||||
|
reject(new Error('Failed to get object metadata: ' + err.message)) |
||||||
|
} else { |
||||||
|
resolve(data) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 解析服务器返回的 MinIO 路径(兼容 C++ 端 ParseMinioFilePath 逻辑) |
||||||
|
* @param {string} path - 格式:bucket/object_path(如 remote-file-test/file/meeting/xxx.pdf) |
||||||
|
* @returns {object} { bucket, object } |
||||||
|
*/ |
||||||
|
export function parseMinioFilePath(path) { |
||||||
|
const result = { bucket: '', object: '' } |
||||||
|
if (!path || typeof path !== 'string') return result |
||||||
|
|
||||||
|
const pathList = path.split('/') |
||||||
|
if (pathList.length >= 2) { |
||||||
|
result.bucket = pathList[0] |
||||||
|
// 拼接剩余部分为 object 路径(移除末尾多余的 /)
|
||||||
|
result.object = pathList.slice(1).join('/').replace(/\/$/, '') |
||||||
|
} |
||||||
|
return result |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取完整的文件访问 URL |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称 |
||||||
|
* @returns {string} - 完整 URL |
||||||
|
*/ |
||||||
|
export function getFullFileUrl(bucket, objectName) { |
||||||
|
if (!s3Client) { |
||||||
|
console.warn('MinIO client not initialized') |
||||||
|
return '' |
||||||
|
} |
||||||
|
|
||||||
|
const endpoint = s3Client.endpoint.host |
||||||
|
const useSSL = s3Client.endpoint.protocol === 'https:' |
||||||
|
const protocol = useSSL ? 'https' : 'http' |
||||||
|
return `${protocol}://${endpoint}/${bucket}/${objectName}` |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 列出存储桶中的对象 |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} prefix - 对象前缀(可选) |
||||||
|
* @returns {Promise} - 对象列表 |
||||||
|
*/ |
||||||
|
export async function listObjects(bucket, prefix = '') { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
const params = { |
||||||
|
Bucket: bucket, |
||||||
|
Prefix: prefix |
||||||
|
} |
||||||
|
|
||||||
|
s3Client.listObjectsV2(params, (err, data) => { |
||||||
|
if (err) { |
||||||
|
reject(new Error('Failed to list objects: ' + err.message)) |
||||||
|
} else { |
||||||
|
resolve(data.Contents) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 生成 PDF 缩略图 |
||||||
|
* @param {File} file - PDF 文件对象 |
||||||
|
* @param {number} width - 缩略图宽度(默认 200) |
||||||
|
* @param {number} height - 缩略图高度(默认 280) |
||||||
|
* @returns {Promise} - 缩略图 Blob 对象 |
||||||
|
*/ |
||||||
|
export async function generatePdfThumbnail(file, width = 200, height = 280) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
// 检查是否是 PDF 文件
|
||||||
|
if (!file.name.toLowerCase().endsWith('.pdf')) { |
||||||
|
return reject(new Error('File is not a PDF')) |
||||||
|
} |
||||||
|
|
||||||
|
// 检查 PDF.js 是否加载
|
||||||
|
if (typeof pdfjsLib === 'undefined') { |
||||||
|
return reject(new Error('PDF.js is not loaded')) |
||||||
|
} |
||||||
|
|
||||||
|
// 设置 PDF.js worker
|
||||||
|
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js' |
||||||
|
|
||||||
|
const fileReader = new FileReader() |
||||||
|
|
||||||
|
fileReader.onload = async function() { |
||||||
|
try { |
||||||
|
// 加载 PDF
|
||||||
|
const typedArray = new Uint8Array(this.result) |
||||||
|
const pdf = await pdfjsLib.getDocument(typedArray).promise |
||||||
|
|
||||||
|
// 获取第一页
|
||||||
|
const page = await pdf.getPage(1) |
||||||
|
|
||||||
|
// 创建 canvas
|
||||||
|
const canvas = document.createElement('canvas') |
||||||
|
const ctx = canvas.getContext('2d') |
||||||
|
|
||||||
|
// 获取页面尺寸
|
||||||
|
const viewport = page.getViewport({ scale: 1 }) |
||||||
|
|
||||||
|
// 计算缩放比例以适应指定尺寸
|
||||||
|
const scale = Math.min(width / viewport.width, height / viewport.height) |
||||||
|
const scaledViewport = page.getViewport({ scale }) |
||||||
|
|
||||||
|
// 设置 canvas 尺寸
|
||||||
|
canvas.width = scaledViewport.width |
||||||
|
canvas.height = scaledViewport.height |
||||||
|
|
||||||
|
// 渲染页面到 canvas
|
||||||
|
await page.render({ |
||||||
|
canvasContext: ctx, |
||||||
|
viewport: scaledViewport |
||||||
|
}).promise |
||||||
|
|
||||||
|
// 转换为 Blob
|
||||||
|
canvas.toBlob((blob) => { |
||||||
|
if (blob) { |
||||||
|
resolve(blob) |
||||||
|
} else { |
||||||
|
reject(new Error('Failed to create thumbnail')) |
||||||
|
} |
||||||
|
}, 'image/png', 1.0) |
||||||
|
} catch (error) { |
||||||
|
reject(new Error('Failed to generate thumbnail: ' + error.message)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fileReader.onerror = function() { |
||||||
|
reject(new Error('Failed to read file')) |
||||||
|
} |
||||||
|
|
||||||
|
fileReader.readAsArrayBuffer(file) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 生成视频缩略图(截取第一帧) |
||||||
|
* @param {File} file - 视频文件对象 |
||||||
|
* @param {number} width - 缩略图宽度(默认 200) |
||||||
|
* @param {number} height - 缩略图高度(默认 150) |
||||||
|
* @returns {Promise} - 缩略图 Blob 对象 |
||||||
|
*/ |
||||||
|
export async function generateVideoThumbnail(file, width = 200, height = 150) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
// 检查是否是视频文件
|
||||||
|
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv'] |
||||||
|
const fileName = file.name.toLowerCase() |
||||||
|
const isVideo = videoExtensions.some(ext => fileName.endsWith(ext)) |
||||||
|
|
||||||
|
if (!isVideo) { |
||||||
|
return reject(new Error('File is not a video')) |
||||||
|
} |
||||||
|
|
||||||
|
// 创建视频元素
|
||||||
|
const video = document.createElement('video') |
||||||
|
video.crossOrigin = 'anonymous' |
||||||
|
|
||||||
|
// 创建 canvas
|
||||||
|
const canvas = document.createElement('canvas') |
||||||
|
const ctx = canvas.getContext('2d') |
||||||
|
|
||||||
|
// 设置视频源
|
||||||
|
const url = URL.createObjectURL(file) |
||||||
|
video.src = url |
||||||
|
|
||||||
|
// 设置视频属性
|
||||||
|
video.autoplay = false |
||||||
|
video.muted = true |
||||||
|
video.playsInline = true |
||||||
|
|
||||||
|
// 监听视频加载完成
|
||||||
|
video.addEventListener('loadedmetadata', () => { |
||||||
|
try { |
||||||
|
// 设置当前时间为第一帧(0.5秒处,避免黑屏)
|
||||||
|
video.currentTime = 0.5 |
||||||
|
|
||||||
|
// 监听视频帧加载
|
||||||
|
video.addEventListener('seeked', () => { |
||||||
|
try { |
||||||
|
// 获取视频原始尺寸
|
||||||
|
const videoWidth = video.videoWidth |
||||||
|
const videoHeight = video.videoHeight |
||||||
|
|
||||||
|
// 计算缩放比例以适应指定尺寸
|
||||||
|
const scale = Math.min(width / videoWidth, height / videoHeight) |
||||||
|
const newWidth = Math.round(videoWidth * scale) |
||||||
|
const newHeight = Math.round(videoHeight * scale) |
||||||
|
|
||||||
|
// 设置 canvas 尺寸
|
||||||
|
canvas.width = newWidth |
||||||
|
canvas.height = newHeight |
||||||
|
|
||||||
|
// 绘制视频帧到 canvas
|
||||||
|
ctx.drawImage(video, 0, 0, newWidth, newHeight) |
||||||
|
|
||||||
|
// 转换为 Blob
|
||||||
|
canvas.toBlob((blob) => { |
||||||
|
// 清理资源
|
||||||
|
URL.revokeObjectURL(url) |
||||||
|
video.remove() |
||||||
|
canvas.remove() |
||||||
|
|
||||||
|
if (blob) { |
||||||
|
resolve(blob) |
||||||
|
} else { |
||||||
|
reject(new Error('Failed to create video thumbnail')) |
||||||
|
} |
||||||
|
}, 'image/png', 1.0) |
||||||
|
} catch (error) { |
||||||
|
URL.revokeObjectURL(url) |
||||||
|
video.remove() |
||||||
|
canvas.remove() |
||||||
|
reject(new Error('Failed to capture video frame: ' + error.message)) |
||||||
|
} |
||||||
|
}, { once: true }) |
||||||
|
|
||||||
|
// 如果 seeked 事件没有触发,使用 timeout 作为备选
|
||||||
|
setTimeout(() => { |
||||||
|
try { |
||||||
|
const videoWidth = video.videoWidth |
||||||
|
const videoHeight = video.videoHeight |
||||||
|
const scale = Math.min(width / videoWidth, height / videoHeight) |
||||||
|
const newWidth = Math.round(videoWidth * scale) |
||||||
|
const newHeight = Math.round(videoHeight * scale) |
||||||
|
|
||||||
|
canvas.width = newWidth |
||||||
|
canvas.height = newHeight |
||||||
|
ctx.drawImage(video, 0, 0, newWidth, newHeight) |
||||||
|
|
||||||
|
canvas.toBlob((blob) => { |
||||||
|
URL.revokeObjectURL(url) |
||||||
|
video.remove() |
||||||
|
canvas.remove() |
||||||
|
if (blob) { |
||||||
|
resolve(blob) |
||||||
|
} |
||||||
|
}, 'image/png', 1.0) |
||||||
|
} catch (e) { |
||||||
|
console.warn('Timeout fallback for video thumbnail:', e) |
||||||
|
} |
||||||
|
}, 3000) |
||||||
|
} catch (error) { |
||||||
|
URL.revokeObjectURL(url) |
||||||
|
video.remove() |
||||||
|
canvas.remove() |
||||||
|
reject(new Error('Failed to load video metadata: ' + error.message)) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// 错误处理
|
||||||
|
video.addEventListener('error', () => { |
||||||
|
URL.revokeObjectURL(url) |
||||||
|
video.remove() |
||||||
|
canvas.remove() |
||||||
|
reject(new Error('Failed to load video: ' + video.error?.message || 'Unknown error')) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取文件类型 |
||||||
|
* @param {string} fileName - 文件名 |
||||||
|
* @returns {string} - 文件类型:'pdf' | 'video' | 'other' |
||||||
|
*/ |
||||||
|
function getFileType(fileName) { |
||||||
|
const lowerName = fileName.toLowerCase() |
||||||
|
if (lowerName.endsWith('.pdf')) { |
||||||
|
return 'pdf' |
||||||
|
} |
||||||
|
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.wmv', '.flv', '.mkv'] |
||||||
|
if (videoExtensions.some(ext => lowerName.endsWith(ext))) { |
||||||
|
return 'video' |
||||||
|
} |
||||||
|
return 'other' |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 上传文件并生成缩略图(支持 PDF 和视频) |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称 |
||||||
|
* @param {File} file - 文件对象 |
||||||
|
* @param {object} metaData - 元数据 |
||||||
|
* @param {function} onProgress - 进度回调 |
||||||
|
* @returns {Promise} - 上传结果,包含缩略图路径 |
||||||
|
*/ |
||||||
|
export async function uploadFileWithThumbnail(bucket, objectName, file, metaData = {}, onProgress) { |
||||||
|
const fileType = getFileType(file.name) |
||||||
|
|
||||||
|
// 对于非 PDF/视频文件,直接上传,不执行缩略图逻辑
|
||||||
|
if (fileType === 'other') { |
||||||
|
console.log('Non-PDF/Video file detected, skipping thumbnail generation') |
||||||
|
const uploadResult = await uploadFile(bucket, objectName, file, metaData, onProgress) |
||||||
|
return { |
||||||
|
...uploadResult, |
||||||
|
thumbnailPath: '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 先上传原始文件
|
||||||
|
console.log(`${fileType.toUpperCase()} file detected, will generate thumbnail after upload`) |
||||||
|
const uploadResult = await uploadFile(bucket, objectName, file, metaData, onProgress) |
||||||
|
|
||||||
|
// 生成并上传缩略图
|
||||||
|
let thumbnailPath = '' |
||||||
|
try { |
||||||
|
console.log(`Generating ${fileType} thumbnail...`) |
||||||
|
|
||||||
|
let thumbnailBlob |
||||||
|
if (fileType === 'pdf') { |
||||||
|
thumbnailBlob = await generatePdfThumbnail(file, 200, 280) |
||||||
|
} else { |
||||||
|
thumbnailBlob = await generateVideoThumbnail(file, 200, 150) |
||||||
|
} |
||||||
|
|
||||||
|
// 生成缩略图文件名(替换扩展名)
|
||||||
|
const thumbnailName = objectName.replace(/\.[^/.]+$/, '.png') |
||||||
|
|
||||||
|
// 上传缩略图
|
||||||
|
const thumbnailResult = await uploadFile(bucket, thumbnailName, new File([thumbnailBlob], thumbnailName, { type: 'image/png' })) |
||||||
|
thumbnailPath = thumbnailResult.objectName |
||||||
|
|
||||||
|
console.log(`${fileType.toUpperCase()} thumbnail uploaded:`, thumbnailPath) |
||||||
|
} catch (error) { |
||||||
|
console.warn(`Failed to generate ${fileType} thumbnail:`, error.message) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
...uploadResult, |
||||||
|
thumbnailPath |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 简单上传文件(不分块,适用于小文件) |
||||||
|
* @param {string} bucket - 存储桶名称 |
||||||
|
* @param {string} objectName - 对象名称 |
||||||
|
* @param {File} file - 文件对象 |
||||||
|
* @param {object} metaData - 元数据 |
||||||
|
* @returns {Promise} - 上传结果 |
||||||
|
*/ |
||||||
|
export async function simpleUpload(bucket, objectName, file, metaData = {}) { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
if (!s3Client) { |
||||||
|
return reject(new Error('MinIO client not initialized')) |
||||||
|
} |
||||||
|
|
||||||
|
const params = { |
||||||
|
Bucket: bucket, |
||||||
|
Key: objectName, |
||||||
|
Body: file, |
||||||
|
ContentType: file.type || 'application/octet-stream', |
||||||
|
...metaData |
||||||
|
} |
||||||
|
|
||||||
|
// 使用 putObject 而不是 upload,避免分块
|
||||||
|
s3Client.putObject(params, (err, data) => { |
||||||
|
if (err) { |
||||||
|
reject(new Error('Upload failed: ' + err.message)) |
||||||
|
} else { |
||||||
|
resolve({ |
||||||
|
bucket, |
||||||
|
objectName, |
||||||
|
etag: data.ETag, |
||||||
|
url: `${s3Client.endpoint.host}/${bucket}/${objectName}` |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
} |
} |
||||||
|
|||||||
Loading…
Reference in new issue