赞
踩
vue3中大文件上传我使用的是vue-simple-uploader,文件加密使用MD5这个三方插件,比较方便,这个插件很不错,有以下功能亮点:
- 1、支持文件、多文件、文件夹上传
- 2、支持拖拽文件、文件夹上传
- 3、统一对待文件和文件夹,方便操作管理
- 4、可暂停、继续上传
- 5、错误处理
- 6、上传队列管理,支持最大并发上传
- 7、分块上传
- 8、支持进度、预估剩余时间、出错自动重试、重传等操作
在vue3中使用用需要注意安装的版本,否则使用会报错
npm i --save vue-simple-uploader@next spark-md5
安装后版本是:
安装好后在main.js中引入
- // 文件分片上传
- import uploader from 'vue-simple-uploader';
- import 'vue-simple-uploader/dist/style.css';
-
- const app = createApp(App)
-
- app.use(uploader)
- <template>
- <div>
- <uploader
- ref="uploaderRef"
- :options="options"
- :autoStart="false"
- :file-status-text="fileStatusText"
- class="uploader-ui"
- @file-added="onFileAdded"
- @file-success="onFileSuccess"
- @file-progress="onFileProgress"
- @file-error="onFileError"
- >
- <uploader-unsupport></uploader-unsupport>
- <uploader-drop>
- <p>拖拽文件到此处或</p>
- <uploader-btn id="global-uploader-btn" ref="uploadBtn" :attrs="attrs"
- >选择文件<i class="el-icon-upload el-icon--right"></i
- ></uploader-btn>
- </uploader-drop>
- <uploader-list></uploader-list>
- </uploader>
- </div>
- </template>
-
- <script setup>
- import { reactive, ref, onMounted } from "vue";
- import SparkMD5 from "spark-md5";
- import { mergeFile } from "@/apis/upload";
-
- const options = reactive({
- //目标上传 URL,默认POST, import.meta.env.VITE_API_URL = api
- // target ==》http://localhost:8000/api/uploader/chunk
- target: "http://localhost:8000/chunk",
- query: {},
- headers: {
- // 需要携带token信息,当然看各项目情况具体定义
- Authorization: "Bearer " + localStorage.getItem("access_token"),
- },
- //分块大小(单位:字节)
- chunkSize: 1024 * 1024 * 5,
- //上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
- fileParameterName: "file",
- //失败后最多自动重试上传次数
- maxChunkRetries: 3,
- //是否开启服务器分片校验,对应GET类型同名的target URL
- testChunks: true,
- // 剩余时间
- parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
- return parsedTimeRemaining
- .replace(/\syears?/, "年")
- .replace(/\days?/, "天")
- .replace(/\shours?/, "小时")
- .replace(/\sminutes?/, "分钟")
- .replace(/\sseconds?/, "秒");
- },
- // 服务器分片校验函数
- checkChunkUploadedByResponse: function (chunk, response_msg) {
- let objMessage = JSON.parse(response_msg);
- console.log(response_msg, "response_msg");
- if (objMessage.data.isExist) {
- return true;
- }
- return (objMessage.data.uploaded || []).indexOf(chunk.offset + 1) >= 0;
- },
- });
- const attrs = reactive({
- // 设置上传文件类型
- accept: ['.rar','.zip']
- })
- const fileStatusText = reactive({
- success: "上传成功",
- error: "上传失败",
- uploading: "上传中",
- paused: "暂停",
- waiting: "等待上传",
- });
- onMounted(() => {
- console.log(uploaderRef.value, "uploaderRef.value");
- });
- function onFileAdded(file) {
- computeMD5(file);
- }
-
- async function onFileSuccess(rootFile, file, response, chunk) {
- //refProjectId为预留字段,可关联附件所属目标,例如所属档案,所属工程等
- // 判断是否秒传成功,成功取消合并
- console.log("秒传成功", response);
- let objMessage = JSON.parse(response);
- if (objMessage.data.isExist != null && objMessage.data.isExist == true) {
- console.log("秒传成功", response);
- window.$message.success("秒传成功");
- return;
- }
- const res = await mergeFile({
- filename: file.name, //文件名称
- identifier: file.uniqueIdentifier, //文件唯一标识
- totalChunks: chunk.offset + 1, //文件总分片数
- });
- if (res.code === 200) {
- console.log("上传成功", res);
- window.$message.success("上传成功");
- } else {
- console.log("上传失败", res);
- window.$message.error("上传失败");
- }
- }
- function onFileError(rootFile, file, response, chunk) {
- console.log("上传完成后异常信息:" + response);
- }
-
- /**
- * 计算md5,实现断点续传及秒传
- * @param file
- */
- function computeMD5(file) {
- file.pause();
-
- //单个文件的大小限制1G
- let fileSizeLimit = 1 * 1024 * 1024 * 1024;
- console.log("文件大小:" + file.size);
- console.log("限制大小:" + fileSizeLimit);
- if (file.size > fileSizeLimit) {
- file.cancel();
- window.$message.error("文件大小不能超过1G");
- }
-
- let fileReader = new FileReader();
- let time = new Date().getTime();
- let blobSlice =
- File.prototype.slice ||
- File.prototype.mozSlice ||
- File.prototype.webkitSlice;
- let currentChunk = 0;
- const chunkSize = 10 * 1024 * 1000;
- let chunks = Math.ceil(file.size / chunkSize);
- let spark = new SparkMD5.ArrayBuffer();
- //如果文件过大计算非常慢,因此采用只计算第1块文件的md5的方式
- let chunkNumberMD5 = 1;
-
- loadNext();
-
- fileReader.onload = (e) => {
- spark.append(e.target.result);
-
- if (currentChunk < chunkNumberMD5) {
- loadNext();
- } else {
- let md5 = spark.end();
- file.uniqueIdentifier = md5;
- // 计算完毕开始上传
- file.resume();
- console.log(
- `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
- file.size
- } 用时:${new Date().getTime() - time} ms`
- );
- }
- };
-
- fileReader.onerror = function () {
- file.cancel();
- };
-
- function loadNext() {
- let start = currentChunk * chunkSize;
- let end = start + chunkSize >= file.size ? file.size : start + chunkSize;
-
- fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
- currentChunk++;
- console.log("计算第" + currentChunk + "块");
- }
- }
- const uploaderRef = ref();
- function close() {
- uploaderRef.value.cancel();
- }
- function error(msg) {
- console.log(msg, "msg");
- }
- </script>
-
- <style scoped>
- .uploader-ui {
- padding: 15px;
- margin: 40px auto 0;
- font-size: 12px;
- font-family: Microsoft YaHei;
- box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
- }
- .uploader-ui .uploader-btn {
- margin-right: 4px;
- font-size: 12px;
- border-radius: 3px;
- color: #fff;
- background-color: #58bfc1;
- border-color: #58bfc1;
- display: inline-block;
- line-height: 1;
- white-space: nowrap;
- }
- .uploader-ui .uploader-list {
- max-height: 440px;
- overflow: auto;
- overflow-x: hidden;
- overflow-y: auto;
- }
- </style>

后端代码:
使用需要安装FastAPI和aiofiles
安装命令
- pip install fastapi
- pip install aiofiles
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- # @Time : 2023/2/10 16:44
- # @Author : YueJian
- # @File : test2.py
- # @Description :
-
- import os
- import os.path
- import re
- import shutil
- import stat
- from email.utils import formatdate
- from mimetypes import guess_type
- from pathlib import Path
- from urllib.parse import quote
-
- import aiofiles
- import uvicorn
- from fastapi import Body, File, Path as F_Path, UploadFile, Request
- from fastapi import Form, FastAPI
- from starlette.responses import StreamingResponse
-
- app = FastAPI()
- # 上传位置
- UPLOAD_FILE_PATH = "./static/file"
-
-
- @app.get("/file-slice")
- async def check_file(identifier: str, totalChunks: int):
- """
- 判断上传文件是否存在
- :param identifier:
- :param totalChunks:
- :return:
- """
- path = os.path.join(UPLOAD_FILE_PATH, identifier)
- if not os.path.exists(path):
- # 不存在
- return {"code": 200, "data": {"isExist": False}}
- else:
- # 存在时判断分片数是否
- chunks = [int(str(i).split("_")[1]) for i in os.listdir(path)]
- return {"code": 200, "data": {"isExist": False, "uploaded": chunks}}
-
-
- @app.post("/file-slice")
- async def upload_file(
- request: Request,
- identifier: str = Form(..., description="文件唯一标识符"),
- chunkNumber: str = Form(..., description="文件分片序号(初值为1)"),
- file: UploadFile = File(..., description="文件")
- ):
- """文件分片上传"""
- path = Path(UPLOAD_FILE_PATH, identifier)
- if not os.path.exists(path):
- os.makedirs(path)
- file_name = Path(path, f'{identifier}_{chunkNumber}')
- if not os.path.exists(file_name):
- async with aiofiles.open(file_name, 'wb') as f:
- await f.write(await file.read())
- return {"code": 200, "data": {
- 'chunk': f'{identifier}_{chunkNumber}'
- }}
-
-
- @app.put("/file-merge")
- async def merge_file(
- request: Request,
- filename: str = Body(..., description="文件名称含后缀"),
- identifier: str = Body(..., description="文件唯一标识符"),
- totalChunks: int = Body(..., description="总分片数")
- ):
- """合并分片文件"""
- target_file_name = Path(UPLOAD_FILE_PATH, filename)
- path = Path(UPLOAD_FILE_PATH, identifier)
- try:
- async with aiofiles.open(target_file_name, 'wb+') as target_file: # 打开目标文件
- for i in range(len(os.listdir(path))):
- temp_file_name = Path(path, f'{identifier}_{i + 1}')
- async with aiofiles.open(temp_file_name, 'rb') as temp_file: # 按序打开每个分片
- data = await temp_file.read()
- await target_file.write(data) # 分片内容写入目标文件
- except Exception as e:
- print(e)
- return {"code": 400, "msg": "合并失败"}
- shutil.rmtree(path) # 删除临时目录
- return {"code": 200, "data": {"filename": filename, "url": f"/api/file/file-slice/{filename}"}}
-
-
- @app.get("/file-slice/{file_name}")
- async def download_file(request: Request, file_name: str = F_Path(..., description="文件名称(含后缀)")):
- """分片下载文件,支持断点续传"""
- # 检查文件是否存在
- file_path = Path(UPLOAD_FILE_PATH, file_name)
- if not os.path.exists(file_path):
- return {"code": 400, "msg": "文件不存在"}
- # 获取文件的信息
- stat_result = os.stat(file_path)
- content_type, encoding = guess_type(file_path)
- content_type = content_type or 'application/octet-stream'
- # 读取文件的起始位置和终止位置
- range_str = request.headers.get('range', '')
- range_match = re.search(r'bytes=(\d+)-(\d+)', range_str, re.S) or re.search(r'bytes=(\d+)-', range_str, re.S)
- if range_match:
- start_bytes = int(range_match.group(1))
- end_bytes = int(range_match.group(2)) if range_match.lastindex == 2 else stat_result.st_size - 1
- else:
- start_bytes = 0
- end_bytes = stat_result.st_size - 1
- # 这里 content_length 表示剩余待传输的文件字节长度
- content_length = stat_result.st_size - start_bytes if stat.S_ISREG(stat_result.st_mode) else stat_result.st_size
- # 构建文件名称
- name, *suffix = file_name.rsplit('.', 1)
- suffix = f'.{suffix[0]}' if suffix else ''
- filename = quote(f'{name}{suffix}') # 文件名编码,防止中文名报错
- # 打开文件从起始位置开始分片读取文件
- return StreamingResponse(
- file_iterator(file_path, start_bytes, 1024 * 1024 * 5), # 每次读取 5M
- media_type=content_type,
- headers={
- 'content-disposition': f'attachment; filename="{filename}"',
- 'accept-ranges': 'bytes',
- 'connection': 'keep-alive',
- 'content-length': str(content_length),
- 'content-range': f'bytes {start_bytes}-{end_bytes}/{stat_result.st_size}',
- 'last-modified': formatdate(stat_result.st_mtime, usegmt=True),
- },
- status_code=206 if start_bytes > 0 else 200
- )
-
-
- def file_iterator(file_path, offset, chunk_size):
- """
- 文件生成器
- :param file_path: 文件绝对路径
- :param offset: 文件读取的起始位置
- :param chunk_size: 文件读取的块大小
- :return: yield
- """
- with open(file_path, 'rb') as f:
- f.seek(offset, os.SEEK_SET)
- while True:
- data = f.read(chunk_size)
- if data:
- yield data
- else:
- break
-
-
- if __name__ == '__main__':
- uvicorn.run("main:app", host="0.0.0.0", port=8000)

个人博客网站灵动空间,欢迎访问
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。