原文转载自「刘悦的技术博客」https://v3u.cn/a_id_175

分片上传并不是什么新概念,尤其是大文件传输的处理中经常会被使用,在之前的一篇文章里:python花式读取大文件(10g/50g/1t)遇到的性能问题(面试向)我们讨论了如何读写超大型文件,本次再来探讨一下如何上传超大型文件,其实原理都是大同小异,原则就是化整为零,将大文件进行分片处理,切割成若干小文件,随后为每个分片创建一个新的临时文件来保存其内容,待全部分片上传完毕后,后端再按顺序读取所有临时文件的内容,将数据写入新文件中,最后将临时文件再删掉。大体流程请见下图:

其实现在市面上有很多前端的三方库都集成了分片上传的功能,比如百度的WebUploader,遗憾的是它已经淡出历史舞台,无人维护了。现在比较推荐主流的库是vue-simple-uploader,不过饿了么公司开源的elementUI市场占有率还是非常高的,但其实大家所不知道的是,这个非常著名的前端UI库也已经许久没人维护了,Vue3.0版本出来这么久了,也没有做适配,由此可见大公司的开源产品还是需要给业务让步。本次我们利用elementUI的自定义上传结合后端的网红框架FastAPI来实现分片上传。

首先前端需要安装需要的库:

npm install element-ui --save
npm install spark-md5 --save
npm install axios --save

随后在入口文件main.js中进行配置:

import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.use(ElementUI) import Axios from 'axios'
Vue.prototype.axios = Axios; import QS from 'qs'
Vue.prototype.qs = QS;

配置好之后,设计方案,前端通过elementUI上传时,通过分片大小的阈值对文件进行切割,并且记录每一片文件的切割顺序(chunk),在这个过程中,通过SparkMD5来计算文件的唯一标识(防止多个文件同时上传的覆盖问题identifier),在每一次分片文件的上传中,会将分片文件实体,切割顺序(chunk)以及唯一标识(identifier)异步发送到后端接口(fastapi),后端将chunk和identifier结合在一起作为临时文件写入服务器磁盘中,当前端将所有的分片文件都发送完毕后,最后请求一次后端另外一个接口,后端将所有文件合并。

根据方案,前端建立chunkupload.js文件:

import SparkMD5 from 'spark-md5'

//错误信息
function getError(action, option, xhr) {
let msg
if (xhr.response) {
msg = `${xhr.response.error || xhr.response}`
} else if (xhr.responseText) {
msg = `${xhr.responseText}`
} else {
msg = `fail to post ${action} ${xhr.status}`
}
const err = new Error(msg)
err.status = xhr.status
err.method = 'post'
err.url = action
return err
}
// 上传成功完成合并之后,获取服务器返回的json
function getBody(xhr) {
const text = xhr.responseText || xhr.response
if (!text) {
return text
}
try {
return JSON.parse(text)
} catch (e) {
return text
}
} // 分片上传的自定义请求,以下请求会覆盖element的默认上传行为
export default function upload(option) {
if (typeof XMLHttpRequest === 'undefined') {
return
}
const spark = new SparkMD5.ArrayBuffer()// md5的ArrayBuffer加密类
const fileReader = new FileReader()// 文件读取类
const action = option.action // 文件上传上传路径
const chunkSize = 1024 * 1024 * 1 // 单个分片大小,这里测试用1m
let md5 = ''// 文件的唯一标识
const optionFile = option.file // 需要分片的文件
let fileChunkedList = [] // 文件分片完成之后的数组
const percentage = [] // 文件上传进度的数组,单项就是一个分片的进度 // 文件开始分片,push到fileChunkedList数组中, 并用第一个分片去计算文件的md5
for (let i = 0; i < optionFile.size; i = i + chunkSize) {
const tmp = optionFile.slice(i, Math.min((i + chunkSize), optionFile.size))
if (i === 0) {
fileReader.readAsArrayBuffer(tmp)
}
fileChunkedList.push(tmp)
} // 在文件读取完毕之后,开始计算文件md5,作为文件唯一标识
fileReader.onload = async (e) => {
spark.append(e.target.result)
md5 = spark.end() + new Date().getTime()
console.log('文件唯一标识--------', md5)
// 将fileChunkedList转成FormData对象,并加入上传时需要的数据
fileChunkedList = fileChunkedList.map((item, index) => {
const formData = new FormData()
if (option.data) {
// 额外加入外面传入的data数据
Object.keys(option.data).forEach(key => {
formData.append(key, option.data[key])
})
// 这些字段看后端需要哪些,就传哪些,也可以自己追加额外参数
formData.append(option.filename, item, option.file.name)// 文件
formData.append('chunkNumber', index + 1)// 当前文件块
formData.append('chunkSize', chunkSize)// 单个分块大小
formData.append('currentChunkSize', item.size)// 当前分块大小
formData.append('totalSize', optionFile.size)// 文件总大小
formData.append('identifier', md5)// 文件标识
formData.append('filename', option.file.name)// 文件名
formData.append('totalChunks', fileChunkedList.length)// 总块数
}
return { formData: formData, index: index }
}) // 更新上传进度条百分比的方法
const updataPercentage = (e) => {
let loaded = 0// 当前已经上传文件的总大小
percentage.forEach(item => {
loaded += item
})
e.percent = loaded / optionFile.size * 100
option.onProgress(e)
} // 创建队列上传任务,limit是上传并发数,默认会用两个并发
function sendRequest(chunks, limit = 2) {
return new Promise((resolve, reject) => {
const len = chunks.length
let counter = 0
let isStop = false
const start = async () => {
if (isStop) {
return
}
const item = chunks.shift()
console.log()
if (item) {
const xhr = new XMLHttpRequest()
const index = item.index
// 分片上传失败回调
xhr.onerror = function error(e) {
isStop = true
reject(e)
}
// 分片上传成功回调
xhr.onload = function onload() {
if (xhr.status < 200 || xhr.status >= 300) {
isStop = true
reject(getError(action, option, xhr))
}
if (counter === len - 1) {
// 最后一个上传完成
resolve()
} else {
counter++
start()
}
}
// 分片上传中回调
if (xhr.upload) {
xhr.upload.onprogress = function progress(e) {
if (e.total > 0) {
e.percent = e.loaded / e.total * 100
}
percentage[index] = e.loaded
console.log(index)
updataPercentage(e)
}
}
xhr.open('post', action, true)
if (option.withCredentials && 'withCredentials' in xhr) {
xhr.withCredentials = true
}
const headers = option.headers || {}
for (const item in headers) {
if (headers.hasOwnProperty(item) && headers[item] !== null) {
xhr.setRequestHeader(item, headers[item])
}
}
// 文件开始上传
xhr.send(item.formData);
}
}
while (limit > 0) {
setTimeout(() => {
start()
}, Math.random() * 1000)
limit -= 1
}
})
} try {
// 调用上传队列方法 等待所有文件上传完成
await sendRequest(fileChunkedList,2)
// 这里的参数根据自己实际情况写
const data = {
identifier: md5,
filename: option.file.name,
totalSize: optionFile.size
}
// 给后端发送文件合并请求
const fileInfo = await this.axios({
method: 'post',
url: 'http://localhost:8000/mergefile/',
data: this.qs.stringify(data)
}, {
headers: {
"Content-Type": "multipart/form-data"
}
}).catch(error => {
console.log("ERRRR:: ", error.response.data); }); console.log(fileInfo); if (fileInfo.data.code === 200) {
const success = getBody(fileInfo.request)
option.onSuccess(success)
return
}
} catch (error) {
option.onError(error)
}
}
}

之后建立upload.vue模板文件,并且引入自定义上传控件:

<template>
<div> <el-upload
:http-request="chunkUpload"
:ref="chunkUpload"
:action="uploadUrl"
:data="uploadData"
:on-error="onError"
:before-remove="beforeRemove"
name="file" > <el-button size="small" type="primary">点击上传</el-button> </el-upload> </div> </template> <script> //js部分
import chunkUpload from './chunkUpload'
export default {
data() {
return {
uploadData: {
//这里面放额外携带的参数
},
//文件上传的路径
uploadUrl: 'http://localhost:8000/uploadfile/', //文件上传的路径
chunkUpload: chunkUpload // 分片上传自定义方法,在头部引入了
}
},
methods: {
onError(err, file, fileList) {
this.$store.getters.chunkUploadXhr.forEach(item => {
item.abort()
})
this.$alert('文件上传失败,请重试', '错误', {
confirmButtonText: '确定'
})
},
beforeRemove(file) {
// 如果正在分片上传,则取消分片上传
if (file.percentage !== 100) {
this.$store.getters.chunkUploadXhr.forEach(item => {
item.abort()
})
}
}
}
} </script> <style> </style>

这里定义的后端上传接口是:http://localhsot:8000/uploadfile/ 合并文件接口是:http://localhsot:8000/mergefile/

此时启动前端的vue.js服务:

npm run dev

页面效果见下图:

前端搞定了,下面我们来编写接口,后端的任务相对简单,利用FastAPI接收分片文件、分片顺序以及唯一标识,并且将文件临时写入到服务器中,当最后一个分片文件完成上传后,第二个接口负责按照分片顺序合并所有文件,合并成功后再删除临时文件,用来节约空间,先安装依赖的三方库

pip3 install python-multipart

当然了,由于是前后端分离项目,别忘了设置一下跨域,编写main.py:

from uploadfile import router
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from model import database
from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [
"*"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
) app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") app.include_router(router) @app.on_event("startup")
async def startup():
await database.connect() @app.on_event("shutdown")
async def shutdown():
await database.disconnect() @app.get("/")
def read_root():
return {"Hello": "World"}

然后编写uploadfile.py:

@router.post("/uploadfile/")
async def uploadfile(file: UploadFile = File(...), chunkNumber: str = Form(...), identifier: str = Form(...)): task = identifier # 获取文件唯一标识符
chunk = chunkNumber # 获取该分片在所有分片中的序号
filename = '%s%s' % (task,chunk) # 构成该分片唯一标识符
contents = await file.read() #异步读取文件
with open('./static/upload/%s' % filename, "wb") as f:
f.write(contents)
print(file.filename)
return {"filename": file.filename} @router.post("/mergefile/")
async def uploadfile(identifier: str = Form(...), filename: str = Form(...)): target_filename = filename # 获取上传文件的文件名
task = identifier # 获取文件的唯一标识符
chunk = 1 # 分片序号
with open('./static/upload/%s' % target_filename, 'wb') as target_file: # 创建新文件
while True:
try:
filename = './static/upload/%s%d' % (task,chunk)
# 按序打开每个分片
source_file = open(filename, 'rb')
# 读取分片内容写入新文件
target_file.write(source_file.read())
source_file.close()
except IOError:
break
chunk += 1
os.remove(filename)
return {"code":200}

值得一提的是这里我们使用UploadFile来定义文件参数,它的优势在于在接收存储文件过程中如果文件过大超过了内存限制就会存储在硬盘中,相当灵活,同时配合await关键字异步读取文件内容,提高了性能和效率。

启动后端服务测试一下效果:

uvicorn main:app --reload

可以看到,当我们上传一张2.9m的图片时,前端会根据设置好的的分片阈值将该图片切割为四份,传递给后端接口uploadfile后,后端在根据参数用接口mergefile将其合并,整个过程一气呵成、行云流水、势如破竹,让人用了之后禁不住心旷神怡、把酒临风。最后奉上项目地址:https://gitee.com/QiHanXiBei/fastapi_blog

原文转载自「刘悦的技术博客」 https://v3u.cn/a_id_175

聚是一团火散作满天星,前端Vue.js+elementUI结合后端FastAPI实现大文件分片上传的更多相关文章

  1. php 文件上传类,功能相当齐全,留作开发中备用吧。

    收藏一个经典好用的php 文件上传类,功能相当齐全,留作开发中备用吧. 好东西,大家都喜欢,才是真的好,哈哈!!! <?php  /**   * 文件上传类   */  class upload ...

  2. DJANGO和UIKIT结合,作一个有进度条的无刷新上传功能

    以前作的上传,在糙了,所以在用户体验上改进一下. 同时,结合DJANGO作定位上传. 这其中分两步进行,第一次上传到TMP目录下, 第二次,将TMP下的文件转移到标准目录下. form.py file ...

  3. DolphinScheduler 功能开发:⼯作流级别任务空跑(后端),测试工作流是否正确执行...

    点击上方 蓝字关注我们 ✎ 编 者 按 在今年由中国科学院软件研究所主办的开源软件所供应链点亮计划-开源之夏活动中,有不少小伙伴提交了关于 DolphinScheduler 的项目,本期是来自成都信息 ...

  4. 《.Net 的冰与火之歌》寄雁传书,你必须知道的C#参数知识大盘点

    引言 参数,也叫参变量,是一个变量.在方法签名中随处可见,实现了不同方法间对于数据的寄雁传书,基本上充斥在代码的各个角落里.在方法签名或者原型中,方法名称后的括号包含方法的参数及其类型的完整列表.参数 ...

  5. fedora环境安装webkit支持作爬虫下载解析JS

    环境: 我使用的fedora19.1-xfce版本,属于redhat系的桌面环境. 1.安装 webkit源码安装webkit失败,这里提供的是yum安装方式. a.查看当前yum库中的webkit资 ...

  6. IOS--工作总结--post上传文件(以流的方式上传)

    1.添加协议 <NSURLConnectionDelegate> 2.创建 @property (nonatomic,retain) NSURLConnection* aSynConnec ...

  7. zookeeper-操作与应用场景-《每日五分钟搞定大数据》

    Zookeeper作为一个分布式协调系统提供了一项基本服务:分布式锁服务,分布式锁是分布式协调技术实现的核心内容.像配置管理.任务分发.组服务.分布式消息队列.分布式通知/协调等,这些应用实际上都是基 ...

  8. vue.js 作一个用户表添加页面----初级

    使用vue.js 制作一个用户表添加页面,实际上是把原来需要使用js写的部分,改写成vue.js的格式 首先,想象一下,先做思考,我们要添加用户表,设涉及到哪些数据,一个是用户id,一个是用户名,一个 ...

  9. 用Python的requests库作接口测试——上传文件

    POST一个多部分编码(Multipart-Encoded)的文件 Requests使得上传多部分编码文件变得很简单: >>> url = 'http://httpbin.org/p ...

随机推荐

  1. 146_ACCESS之HR招聘信息管理_64位

    焦棚子的文章目录 点击下载附件 一.背景: 最近把之前做的一个HR招聘信息管理工具翻新了下,有需要的朋友可以自取,主要想解决的问题是多人在跟进人员招聘的时候信息的不对称,这样下来的就可以及时的看到整个 ...

  2. Fail2ban 配置详解 监禁配置(jail.conf)

    ### # 包含配置 ### [INCLUDES] # after = # 在加载本配置文件之后再加载指定的独立配置文件. before = paths-debian.conf # 在加载本配置文件之 ...

  3. 关于spring整合mybatis

    第一步导入依赖 <dependencies> <dependency> <groupId>org.mybatis</groupId> <artif ...

  4. App上看到就忍不住点的小红点是如何实现的?

    你有没有发现,我们解锁手机后桌面上App右上角总能看到一个小红点,这就是推送角标.推送角标指的是移动设备上App图标右上角的红色圆圈,圆圈内的白色数字表示未读消息数量.角标是一种比较轻的提醒方式,通过 ...

  5. 为什么 SQL 语句使用了索引,但却还是慢查询?

    一.索引与慢查询 聊一聊索引和慢查询,经常遇到的一个问题:一个SQL语句使用了索引,为什么还是会记录到慢查询日志之中? 为了说明,创建一个表t,该表3个字段,一个主键索引,一个普通索引 CREATE ...

  6. 有关安装pycocotools的办法

    1,首先需要安装Visual C++ 2015构建工具,地址https://download.microsoft.com/download/5/f/7/5f7acaeb-8363-451f-9425- ...

  7. [二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇

    目录 [二进制漏洞]PWN学习之格式化字符串漏洞 Linux篇 格式化输出函数 printf函数族功能介绍 printf参数 type(类型) flags(标志) number(宽度) precisi ...

  8. 合宙Air32F103CBT6开发板上手报告

    2022年6月初合宙新上市了 Air32F103 系列 MCU, 市面上 STM32F103 的克隆军队又增加了新的一员. 这次不知道是哪家的贴牌, 分 Air32F103CBT6 和 Air32F1 ...

  9. 【WPF】CAD工程图纸转WPF可直接使用的xaml代码技巧

    前言:随着工业化的进一步发展,制造业.工业自动化等多领域,都可能用到上位监控系统.而WPF在上位监控系统方面,应该算是当下最流行的前端框架之一了.而随着监控体系的不断完善与更新迭代,监控画面会变得越来 ...

  10. React技巧之循环遍历对象

    原文链接:https://bobbyhadz.com/blog/react-loop-through-object 作者:Borislav Hadzhiev 正文从这开始~ 遍历对象的键 在React ...