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
|
||||
let minioClient = null |
||||
// MinIO client implementation using AWS S3 SDK (MinIO is S3-compatible)
|
||||
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) { |
||||
console.warn('MinIO client not available. Install minio package for full functionality.') |
||||
minioClient = { |
||||
endPoint: config.endPoint, |
||||
port: config.port |
||||
// 检查是否已加载 AWS SDK
|
||||
if (typeof AWS === 'undefined') { |
||||
console.error('AWS SDK not loaded! Please include aws-sdk.min.js in your HTML') |
||||
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() { |
||||
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