import { randomId } from '../utils/random-id.ts'; import { UploadProgress } from './upload-progress.ts'; export type ConvertOpts = { appKey?: string; version?: string; username?: string; directory?: string; isPublic?: boolean; filename?: string; /** * 是否不检查应用文件, 默认 true,默认不检测 */ noCheckAppFiles?: boolean; }; // createEventSource: (baseUrl: string, searchParams: URLSearchParams) => { // return new EventSource(baseUrl + '/api/s1/events?' + searchParams.toString()); // }, export type UploadOpts = { uploadProgress: UploadProgress; /** * 创建 EventSource 兼容 nodejs * @param baseUrl 基础 URL * @param searchParams 查询参数 * @returns EventSource */ createEventSource: (baseUrl: string, searchParams: URLSearchParams) => EventSource; baseUrl?: string; token: string; FormDataFn: any; }; export const uploadFileChunked = async (file: File, opts: ConvertOpts, opts2: UploadOpts) => { const { directory, appKey, version, username, isPublic, noCheckAppFiles = true } = opts; const { uploadProgress, createEventSource, baseUrl = '', token, FormDataFn } = opts2 || {}; return new Promise(async (resolve, reject) => { const taskId = randomId(); const filename = opts.filename || file.name; uploadProgress?.start(`${filename} 上传中...`); const searchParams = new URLSearchParams(); searchParams.set('taskId', taskId); if (isPublic) { searchParams.set('public', 'true'); } if (noCheckAppFiles) { searchParams.set('noCheckAppFiles', '1'); } const eventSource = createEventSource(baseUrl + '/api/s1/events', searchParams); let isError = false; // 监听服务器推送的进度更新 eventSource.onmessage = function (event) { console.log('Progress update:', event.data); const parseIfJson = (data: string) => { try { return JSON.parse(data); } catch (e) { return data; } }; const receivedData = parseIfJson(event.data); if (typeof receivedData === 'string') return; const progress = Number(receivedData.progress); const progressFixed = progress.toFixed(2); uploadProgress?.set(progress, { ...receivedData, progressFixed, filename, taskId }); }; eventSource.onerror = function (event) { console.log('eventSource.onerror', event); isError = true; reject(event); }; const chunkSize = 1 * 1024 * 1024; // 1MB const totalChunks = Math.ceil(file.size / chunkSize); for (let currentChunk = 0; currentChunk < totalChunks; currentChunk++) { const start = currentChunk * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const formData = new FormDataFn(); formData.append('file', chunk, filename); formData.append('chunkIndex', currentChunk.toString()); formData.append('totalChunks', totalChunks.toString()); const isLast = currentChunk === totalChunks - 1; if (directory) { formData.append('directory', directory); } if (appKey && version) { formData.append('appKey', appKey); formData.append('version', version); } if (username) { formData.append('username', username); } try { const res = await fetch(baseUrl + '/api/s1/resources/upload/chunk?taskId=' + taskId, { method: 'POST', body: formData, headers: { 'task-id': taskId, Authorization: `Bearer ${token}`, }, }).then((response) => response.json()); if (res?.code !== 200) { console.log('uploadChunk error', res); uploadProgress?.error(res?.message || '上传失败'); isError = true; eventSource.close(); uploadProgress?.done(); reject(new Error(res?.message || '上传失败')); return; } if (isLast) { fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId); eventSource.close(); uploadProgress?.done(); resolve(res); } // console.log(`Chunk ${currentChunk + 1}/${totalChunks} uploaded`, res); } catch (error) { console.log('Error uploading chunk', error); fetch(baseUrl + '/api/s1/events/close?taskId=' + taskId); reject(error); return; } } // 循环结束 if (!uploadProgress?.end) { uploadProgress?.done(); } }); };