bridgecrate
Version:
一个提供数据传输和桥接功能的JavaScript工具包
199 lines (171 loc) • 5.47 kB
JavaScript
class FileUploader {
constructor(options = {}) {
this.chunkSize = options.chunkSize || 1024 * 1024; // 默认1MB
this.concurrent = options.concurrent || 3; // 并发数
this.uploadFunction = options.uploadFunction || this.defaultUploadFunction;
this.onProgress = options.onProgress || (() => {});
this.onError = options.onError || (() => {});
this.onComplete = options.onComplete || (() => {});
}
// 生成文件切片
async generateChunks(file) {
const chunks = [];
const totalChunks = Math.ceil(file.size / this.chunkSize);
for (let i = 0; i < totalChunks; i++) {
const start = i * this.chunkSize;
const end = Math.min(start + this.chunkSize, file.size);
const chunk = file.slice(start, end);
// 异步生成hash
const hash = await this.generateChunkHash(chunk, i, file.name, file.size);
chunks.push({
index: i,
chunk: chunk,
start: start,
end: end,
size: end - start,
hash: hash
});
}
return {
chunks,
totalChunks,
fileInfo: {
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
}
};
}
// 生成chunk哈希
async generateChunkHash(chunk, index, fileName, fileSize) {
try {
// 使用 Web Crypto API 生成真正的 hash
const buffer = await chunk.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// 组合文件信息和chunk hash,确保唯一性
return `${fileName}_${fileSize}_${index}_${hashHex}`;
} catch (error) {
console.warn('Failed to generate hash, falling back to simple hash:', error);
// 降级方案:如果hash生成失败,使用简单规则
return `${fileName}_${fileSize}_${index}_${Date.now()}`;
}
}
// 默认上传函数
async defaultUploadFunction(chunkData) {
const formData = new FormData();
formData.append('chunk', chunkData.chunk);
formData.append('index', chunkData.index);
formData.append('hash', chunkData.hash);
formData.append('fileName', chunkData.fileName);
formData.append('totalChunks', chunkData.totalChunks);
const response = await fetch('/api/upload/chunk', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
return await response.json();
}
// 上传文件
async upload(file) {
try {
const { chunks, totalChunks, fileInfo } = await this.generateChunks(file);
const uploadPromises = [];
const results = [];
let completedChunks = 0;
// 控制并发上传
const semaphore = new Semaphore(this.concurrent);
for (const chunkInfo of chunks) {
const uploadPromise = semaphore.acquire().then(async (release) => {
try {
const chunkData = {
...chunkInfo,
fileName: fileInfo.name,
totalChunks: totalChunks,
fileInfo: fileInfo
};
const result = await this.uploadFunction(chunkData);
completedChunks++;
const progress = (completedChunks / totalChunks) * 100;
this.onProgress({
progress,
completedChunks,
totalChunks,
currentChunk: chunkInfo.index
});
return result;
} catch (error) {
this.onError({
error,
chunk: chunkInfo,
fileName: fileInfo.name
});
throw error;
} finally {
release();
}
});
uploadPromises.push(uploadPromise);
}
const uploadResults = await Promise.all(uploadPromises);
this.onComplete({
fileName: fileInfo.name,
fileSize: fileInfo.size,
totalChunks,
results: uploadResults
});
return {
success: true,
fileInfo,
results: uploadResults
};
} catch (error) {
throw new Error(`File upload failed: ${error.message}`);
}
}
// 合并chunks的辅助方法
async mergeChunks(fileName, totalChunks, mergeFunction) {
if (!mergeFunction) {
throw new Error('Merge function is required');
}
return await mergeFunction({
fileName,
totalChunks
});
}
}
// 信号量类用于控制并发
class Semaphore {
constructor(max) {
this.max = max;
this.current = 0;
this.queue = [];
}
acquire() {
return new Promise((resolve) => {
if (this.current < this.max) {
this.current++;
resolve(this.createRelease());
} else {
this.queue.push(() => {
this.current++;
resolve(this.createRelease());
});
}
});
}
createRelease() {
return () => {
this.current--;
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
}
};
}
}
export default FileUploader;