import { request } from '@/plugins/http-fetch'; import { encode } from 'js-base64'; /** * 下载文件 * @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png * @param range 断点续传标识,填入字符串 `bytes=${startByte}-${endByte}` * @returns object */ export async function downloadFile(filePath: string, range?: string) { return request({ url: `/file/download/${encode(filePath)}`, method: 'get', headers: range ? { range } : {}, responseType: 'blob', }); } /** * 下载文件切片 * @param filePath 文件路径带/,如:/upload/default/2023/06/xx.png * @param chunkSize 数据块大小MB,默认1MB * @returns bolb */ export async function downloadFileChunk( filePath: string, chunkSize: number = 1 ): Promise { chunkSize = chunkSize * 1024 * 1024; let start = 0; // 文件块的起始字节 let end = chunkSize - 1; // 文件块的结束字节 let totalSize = 0; // 文件总大小 let downloadedSize = 0; // 已下载的文件大小 let filePart: Blob[] = []; // 文件数据块内容 // 发送带有 Range 请求头的 HTTP 请求 async function sendRequest() { const range = `bytes=${start}-${end}`; const res = await downloadFile(filePath, range); if (res.code === 200 && res.status === 206) { // 总大小 const contentRange = res.headers.get('content-range') || '0/0'; totalSize = parseInt(contentRange.split('/')[1]); // 已下载大小 const contentLength = res.headers.get('content-length') || '0'; const chunkSize = parseInt(contentLength); // 下一段数据块区间 start += chunkSize; end = Math.min(start + chunkSize - 1, totalSize - 1); // 记录下载结果 filePart.push(res.data); downloadedSize += chunkSize; // 小于总大小继续下载后续数据 if (downloadedSize < totalSize) { await sendRequest(); } } else { return res; } } await sendRequest(); return new Blob(filePart, { type: 'application/octet-stream' }); } /** * 上传文件 * @param data 表单数据对象 * @returns object */ export function uploadFile(data: FormData) { return request({ url: '/file/upload', method: 'post', data, dataType: 'form-data', }); } /** * 上传切片文件 * @param file 文件对象 * @param chunkSize 数据块大小MB,默认1MB * @param subPath 归属子路径, 默认default * @returns */ export async function uploadFileChunk( fileData: File, chunkSize: number = 1, subPath: string = 'default' ) { const { name, size } = fileData; const chunkSizeInBytes = chunkSize * 1024 * 1024; // 文件标识使用唯一编码 MD5(文件名+文件大小) const fileIdentifier = `${name}-${size}`; // 文件切分为多少份进行上传 const chunksCount = Math.ceil(size / chunkSizeInBytes); // 切块的数据数据用于上传 const fileChunks: { index: number; chunk: Blob }[] = []; for (let i = 0; i < chunksCount; i++) { const start = i * chunkSizeInBytes; const end = Math.min(start + chunkSizeInBytes, size); fileChunks.push({ index: i, chunk: fileData.slice(start, end), }); } // 检查是否已上传部分数据块 const resCheck = await chunkCheck(fileIdentifier, name); if (resCheck.code !== 200) { return resCheck; } let uploadedSize = 0; let uploadProgress = 0; for (const { index, chunk } of fileChunks) { const chunksIndex = `${index}`; // 跳过已上传块 if (resCheck.data.includes(chunksIndex)) { continue; } // 上传数据块 const formData = new FormData(); formData.append('file', chunk, name); formData.append('index', chunksIndex); formData.append('identifier', fileIdentifier); const resUpload = await chunkUpload(formData); if (resUpload.code === 200) { uploadedSize += chunk.size; uploadProgress = (uploadedSize / size) * 100; console.log(`上传进度:${uploadProgress}%`); } else { // 上传失败处理 break; } } // 上传数据完整后合并数据块 if (uploadedSize === size) { return await chunkMerge(fileIdentifier, name, subPath); } return { code: 500, msg: '上传出错,请重试' }; } /** * 切片文件检查 * @param identifier 文件标识 * @param fileName 原文件名称 * @returns object */ export function chunkCheck(identifier: string, fileName: string) { return request({ url: '/file/chunkCheck', method: 'post', data: { identifier, fileName }, }); } /** * 切片文件合并 * @param identifier 文件标识 * @param fileName 原文件名称 * @param subPath 文件归属 * @returns object */ export function chunkMerge( identifier: string, fileName: string, subPath: string = 'default' ) { return request({ url: '/file/chunkMerge', method: 'post', data: { identifier, fileName, subPath }, }); } /** * 切片文件上传 * @param data 表单数据对象 * @returns object */ export function chunkUpload(data: FormData) { return request({ url: '/file/chunkUpload', method: 'post', data, dataType: 'form-data', }); }