全网最简单的大文件上传与下载代码实现(React+Go)
前言
前段时间我需要实现大文件上传的需求,在网上查找了很多资料,并且也发现已经有很多优秀的博客讲了大文件上传下载这个功能。
我的项目是个比较简单的项目,并没有采用特别复杂的实现方式,所以我这篇文章的目的主要是讲如何最简单地实现大文件上传与下载这个功能,不会讲太多原理之类的东西。
大文件上传
在实际场景中,上传大文件主要会遇到的问题有:
- 体积大/网络不好时,上传时间会非常久
- 前端/后端某处设置了最大请求时长/最大读写时长等,造成文件上传超时
- Nginx/后端某处对请求大小进行了限制,造成文件因体积过大而上传失败
- 上传失败后,需要重新开始上传
实现思路
业界最普遍的方案就是切片上传,简单地说就是把文件切割成若干个小文件,再将小文件们传输到后端,最后按照顺序把小文件们重新拼成这个大文件。
所以具体的实现逻辑如下:
把大文件进行切片,对切片的文件内容进行加密生成一个标识串,用于标识唯一的切片
服务端在临时目录里保存各段文件
浏览器端所有分片上传完成,发送给服务端一个合并文件的请求
服务端根据分片顺序进行文件合并
删除分片文件
也有其他合并文件的方式,本文不做讨论,详情可以参考如何做大文件上传。
具体实现
前端部分
前端需要做的部分是:
- 把大文件进行切片,对切片的文件内容进行加密生成一个标识串
- 上传所有切片,最后发送合并文件的请求
在这里我使用了一个开源库react-chunk-upload
,它提供了加密文件函数和获取文件的相应切片内容的函数(如图),这就不用我自己写啦(偷懒小技巧)。
那么前端部分完整的代码如下:
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadText, setUploadText] = useState("");
const CHUNK_SIZE = 3 * 1024 * 1024; // 设置切片大小为 3Mb
const chunkMD5List = [];
const chunkNum = Math.ceil(file.size / CHUNK_SIZE);
for (let i = 0; i < chunkNum; i++) {
const start = i * CHUNK_SIZE; // 切片的开始位置
const end = Math.min(file.size, start + CHUNK_SIZE); // 切片的结束位置
const chunkBlob = blobSlice.call(file, start, end); // 获取相应位置的切片文件
const chunkFile = new File([chunkBlob], "file", {
lastModified: file.lastModified,
});
const md5 = await hashFile(chunkFile, CHUNK_SIZE); // 获取切片标识符
chunkMD5List.push(md5);
await beforeUploadCheckApi(md5) // 上传前检查这个切片是否已存在的接口
.then(async (res) => {
if (res.code === SUCCESS_CODE) {
if (!res.data.exist_status) { // 如果不存在才上传
await uploadChunkCSVApi(chunkFile, md5).then((res) => { // 上传切片的接口
if (res.code === SUCCESS_CODE) {
const progress = Math.floor(((i + 1) / chunkNum) * 10000) / 100; // 计算上传进度,这里为了更好的用户体验,我特意预留了3%给最后的合并文件步骤
setUploadProgress(progress < 3 ? 0 : progress - 3);
}
});
} else {
const progress = Math.floor(((i + 1) / chunkNum) * 10000) / 100;
setUploadProgress(progress < 3 ? 0 : progress - 3);
}
}
})
.catch(() => {
setUploadText("上传失败");
});
}
mergeChunkApi(f.name, JSON.stringify(chunkMD5List)) // 合并切片的接口
.then((res) => {
if (res.code === SUCCESS_CODE) {
setUploadText(`上传 ${file.name} 成功`);
setUploadProgress(100); // 合并文件需要一些时间,所以合并完再让进度条到100
}
})
.catch(() => {
setUploadText(`合并保存文件失败`);
});
后端部分
后端需要提供三个接口,分别是:
- 判断切片文件是否已经上传过
- 上传切片文件
- 合并切片文件
前两个接口的逻辑都很简单,第一个接口是判断文件目录是否存在,第二个接口是把文件放到指定目录。
第三个接口的合并逻辑也不难,就是按照顺序读取切片文件然后写入,代码如下:
// 创建一个空文件
filePath := ".....省略"
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println("打开文件失败: %v", err)
}
chunkMD5Array := []string{}
```
前端需要传给后端一个切片名称的有序数组,此处省略具体处理过程
```
for _, chunkMD5 := range chunkMD5Array {
chunkPath := fmt.Sprintf("/temp/%v", chunkMD5)
chunk, err := os.Open(chunkPath)
if err != nil {
fmt.Println("打开文件的切片 %v 内容失败: %v", chunkMD5, err)
}
content, err := ioutil.ReadAll(chunk)
if err != nil {
fmt.Println("读取文件的切片 %v 内容失败: %v", chunkMD5, err)
}
_, err = f.Write(content)
if err != nil {
fmt.Println("写入文件的切片 %v 内容失败: %v", chunkMD5, err)
}
chunk.Close()
}
// 写入完毕,关闭文件
f.Close()
// 合并后删除切片文件
for _, chunkMD5 := range chunkMD5Array {
chunkPath := fmt.Sprintf("/temp/%v", chunkMD5)
err := os.RemoveAll(chunkPath)
if err != nil {
fmt.Println("删除切片文件%v失败:%v", chunkMD5, err)
}
}
大文件上传就这么简单地搞定了,并且这个实现方法虽然不是断点续传,但是也会大大提高文件的上传速度。
大文件下载
大文件下载的方案则需要区分两种情况:
①window.open
方法
②分片下载
其余的下载方式,例如a标签下载、表单下载等,都适用于较小文件,这里不讨论。
window.open
方法
使用window.open
方法有一个前提条件:后端接口返回的是文件流。那么用window.open
去开启一个新窗口打开这个链接,浏览器就会去处理下载的过程。前端的示例代码如下:
window.open('http://xxxxxxxxxx', '_blank')
需要注意的地方是后端接口需要指定请求的Content-Disposition
属性。
在常规的HTTP应答中,
Content-Disposition
响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地——来源 MDN(https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition)
优点
- 浏览器自己处理下载过程,不需要额外实现进度条等逻辑。
- 代码简单。
缺点
- 会受到浏览器的兼容性以及浏览器安全策略等因素的影响。
- 有时候
window.open
不会下载文件,而会预览文件,行为不符合预期。 - 会新打开一个页面,有些开发者不喜欢这个行为。
分片下载
实现思路
分片下载的逻辑类似于上文所提到的切片上传,具体的实现逻辑如下:
- 获取文件的大小
- 计算文件的分片数(即需要发送多少次下载分片的请求)
- 下载所有分片
- 按照顺序合并所有分片
- 保存合并好的文件
前端部分
前端代码按照实现思路来讲,可以实现为四个函数:
- 获取要下载的文件大小
- 下载文件指定位置的分片
blob
- 合并所有分片
blob
- 保存
blob
为文件
在这里,我把这个流程封装为了一个开源库react-chunks-to-file
,提供后端的接口地址即可完成下载操作。
示例代码:
// 进度
const [percent, setPercent] = useState<number>();
// 状态
const [status, setStatus] = useState<number>();
return(
<ChunksDownload
reqSetting={{
getSizeAPI: `${APP_DOMAIN}/csv/size?`, // 获取文件大小的接口url
getSizeParams: {
token: getToken(),
id: csvId,
},
chunkDownloadAPI: `${APP_DOMAIN}/csv/download_chunk?`, // 下载分片文件的接口url
chunkDownloadParams: {
token: getToken(),
id: csvId,
},
}}
fileName={csv.csv_name}
mime={"text/csv"} // 文件类型
size={3} // 分片大小
concurrency={5} // 并发数
setStatus={setStatus}
setPercent={setPercent}
style={{ display: "inline" }}
>
<Button
type="link"
onClick={() => downloadCSV(csv.csv_name)}
>
下载
</Button>
</ChunksDownload>
);
缺点
- 由于使用了
blob
,不同浏览器对可以下载的文件大小有限制,比如Chrome里是2GB - 使用这个开源库,后端接口的定义需要符合要求,详情请看
react-chunks-to-file
介绍
优点
- 使用简单
- 可以自己定义控制下载进度条等其他交互UI,不会新打开窗口
- 实现了并发下载
参考资源
全网最简单的大文件上传与下载代码实现(React+Go)的更多相关文章
- Nginx集群之WCF大文件上传及下载(支持6G传输)
目录 1 大概思路... 1 2 Nginx集群之WCF大文件上传及下载... 1 3 BasicHttpBinding相关配置解析... 2 4 编写 ...
- java实现大文件上传和下载
[文件上传和下载]是很多系统必备功能, 比如PM\OA\ERP等:系统中常见的开发模式有B/S和C/S,而前者主要是通过浏览器来访问web服务器,一般采用七层协议中的[应用层http]进行数据传输,后 ...
- PHP实现大文件上传和下载
一提到大文件上传,首先想到的是啥??? 没错,就是修改php.ini文件里的上传限制,那就是upload_max_filesize.修改成合适参数我们就可以进行愉快的上传文件了.当然啦,这是一般情况下 ...
- ASP.NET实现大文件上传和下载
总结一下大文件分片上传和断点续传的问题.因为文件过大(比如1G以上),必须要考虑上传过程网络中断的情况.http的网络请求中本身就已经具备了分片上传功能,当传输的文件比较大时,http协议自动会将文件 ...
- JSP实现大文件上传和下载
javaweb上传文件 上传文件的jsp中的部分 上传文件同样可以使用form表单向后端发请求,也可以使用 ajax向后端发请求 1.通过form表单向后端发送请求 <form id=" ...
- WEB实现大文件上传和下载
我们平时经常做的是上传文件,上传文件夹与上传文件类似,但也有一些不同之处,这次做了上传文件夹就记录下以备后用. 这次项目的需求: 支持大文件的上传和续传,要求续传支持所有浏览器,包括ie6,ie7,i ...
- xshell简单配置(文件上传和下载)
1.安装lrzsz 1.1直接安装#yum install lrzsz 1.2sudo命令安装#sudo yum install lrzsz -y检查是否安装成功.#rpm -qa |grep lrz ...
- ASP.NET 大文件上传的简单处理
在 ASP.NET 开发的过程中,文件上传往往使用自带的 FileUpload 控件,可是用过的人都知道,这个控件的局限性十分大,最大的问题就在于上传大文件时让开发者尤为的头疼,而且,上传时无法方便的 ...
- ASP.NET 中对大文件上传的简单处理
在 ASP.NET 开发的过程中,文件上传往往使用自带的 FileUpload 控件,可是用过的人都知道,这个控件的局限性十分大,最大的问题就在于上传大文件时让开发者尤为的头疼,而且,上传时无法方便的 ...
随机推荐
- CVPR2022 | 可精简域适应
前言 在本文中,作者引入了一个简单的框架,即Slimmable Domain Adaptation,以通过权重共享模型库改进跨域泛化,从中可以对不同容量的模型进行采样,以适应不同的精度效率权衡.此外, ...
- ms10_002 IE浏览器漏洞
一.环境说明 kali linux 靶机:xp 二.ms10_002漏洞利用 msf5 exploit(windows/smb/ms08_067_netapi) > search ms10_00 ...
- UiPath文本操作Set Text的介绍和使用
一.Set Text的介绍 向输入框/文本框写入文本的一种操作 二.Set Text在UiPath中的使用 1.打开设计器,在设计库中新建一个Sequence,为序列命名及设置Sequence存放的路 ...
- 3. Caller 服务调用 - dapr
前言 上一篇我们讲了使用HttpClient的方式调用,那么如果我们现在需要更换为通过dapr实现服务调用,我们需要做哪些事情呢? Caller.Dapr 入门 如果我们的项目原本使用的是Caller ...
- Contest
Contest 题目 链接 题目描述 \(n\) 支队伍一共参加了三场比赛. 一支队伍 \(x\) 认为自己比另一支队伍 \(y\) 强当且仅当 \(x\) 在至少一场比赛中比 \(y\) 的排名高. ...
- logback-spring 集成 ELK、kafka的配置
pom.xml <dependency> <groupId>com.github.danielwegener</groupId> <artifactId> ...
- 关于 STrAduts
\(\mathbb{No \ hay \ cosa \ mas \ feliz \ en \ el \ mundo \ que \ ver \ tu \ sonrisa \ mi \ [数据删除]}\ ...
- SpringBoot数据库管理 - 用Liquibase对数据库管理和迁移?
Liquibase是一个用于用于跟踪.管理和应用数据库变化的开源工具,通过日志文件(changelog)的形式记录数据库的变更(changeset),然后执行日志文件中的修改,将数据库更新或回滚(ro ...
- 简单易用的任务队列-beanstalkd
概述 beanstalkd 是一个简单快速的分布式工作队列系统,协议基于 ASCII 编码运行在 TCP 上.其最初设计的目的是通过后台异步执行耗时任务的方式降低高容量 Web 应用的页面延时.其具有 ...
- CF1701A Grass Field 题解
根据题意,给定一个 \(2\times2\) 的仅包含 \(0\) 和 \(1\) 的二维数组.定义一个操作,每次可以选择一行和一列将其变成 \(0\),求最小操作次数. 思路:根据枚举可得共有 \( ...