feat: chunk uploads

This commit is contained in:
Rogee
2025-01-15 00:26:10 +08:00
parent 591771ce77
commit 9bfdf0e0ea
16 changed files with 451 additions and 40 deletions

View File

@@ -0,0 +1,140 @@
<template>
<div>
<FileUpload :customUpload="true" @uploader="handleUpload" multiple :maxFileSize="50000000">
<template #empty>
<p>拖拽文件到此处上传</p>
</template>
</FileUpload>
<ProgressBar v-if="progress > 0" :value="progress" />
<small v-if="progress > 0">{{ status }}</small>
</div>
</template>
<script setup>
import SparkMD5 from 'spark-md5';
import { ref } from 'vue';
const CHUNK_SIZE = 2 * 1024 * 1024; // 2MB chunks
const progress = ref(0);
const status = ref('');
const createChunks = (file) => {
const chunks = [];
let start = 0;
while (start < file.size) {
chunks.push(file.slice(start, start + CHUNK_SIZE));
start += CHUNK_SIZE;
}
return chunks;
};
const calculateFileMD5 = async (file) => {
status.value = '计算文件MD5...';
progress.value = 0;
const chunkSize = 2097152; // 2MB
const chunks = Math.ceil(file.size / chunkSize);
const spark = new SparkMD5.ArrayBuffer();
const fileReader = new FileReader();
let currentChunk = 0;
const readNextChunk = () => {
const start = currentChunk * chunkSize;
const end = Math.min(file.size, start + chunkSize);
fileReader.readAsArrayBuffer(file.slice(start, end));
};
return new Promise((resolve, reject) => {
fileReader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
progress.value = Math.round((currentChunk / chunks) * 100);
if (currentChunk < chunks) {
readNextChunk();
} else {
const md5 = spark.end();
progress.value = 0;
resolve(md5);
}
};
fileReader.onerror = () => {
reject('MD5计算失败');
};
readNextChunk();
});
};
const MAX_CONCURRENT_UPLOADS = 3;
const uploadChunks = async (chunks, file, fileMD5) => {
const pending = [...Array(chunks.length).keys()];
const uploading = new Set();
const completed = new Set();
while (pending.length > 0 || uploading.size > 0) {
while (uploading.size < MAX_CONCURRENT_UPLOADS && pending.length > 0) {
const index = pending.shift();
uploading.add(index);
const formData = new FormData();
formData.append('file', chunks[index]);
formData.append('fileName', file.name);
formData.append('chunkNumber', index);
formData.append('totalChunks', chunks.length);
formData.append('fileMD5', fileMD5);
uploadChunk(formData, index)
.then(() => {
uploading.delete(index);
completed.add(index);
progress.value = Math.round((completed.size / chunks.length) * 100);
})
.catch((error) => {
uploading.delete(index);
pending.push(index);
console.error(`Chunk ${index} failed:`, error);
});
}
await new Promise(resolve => setTimeout(resolve, 100));
}
};
const uploadChunk = async (formData, index) => {
const response = await fetch('/api/v1/medias/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
const handleUpload = async (event) => {
const files = event.files;
for (const file of files) {
try {
const fileMD5 = await calculateFileMD5(file);
status.value = '开始上传文件...';
const chunks = createChunks(file);
await uploadChunks(chunks, file, fileMD5);
status.value = '上传完成';
setTimeout(() => {
progress.value = 0;
status.value = '';
}, 2000);
} catch (error) {
console.error('Upload failed:', error);
status.value = '上传失败: ' + error;
}
}
};
</script>