关于一个打包下载的需求

前些天遇到一个「打包下载」的需求,在调研过程中走了一些弯路,本文记录一下。

比如说某网站有一个文件列表,用户点哪个就可以下载哪个,如果用户想下载多个,无非就是多点几次而已。于是需求来了:当用户想下载多个文件的时候,可以通过一次点击完成打包下载操作。

听起来似乎并不复杂,服务端可以把用户想要下载的文件打包成一个新文件,然后用户点一次就可以下载了,但是这样做有以下几个缺点:

浪费了时间,多了创建新文件的流程。 浪费了空间,同样的文件被多次存储。 用户体验差,下载必须要等到新文件创建好才能开始。

不难得出结论:动态流式下载才是正解,同事提到 tar 可以搞定,于是研究一下:

shell> cat test_0.txt 
xxx
xxx
shell> cat test_1.txt 
yyy
yyy
shell> tar cf test.tar test_0.txt test_1.txt
shell> cat test.tar 
test_0.txt00006440...01014257504126011510 0ustar rootrootxxx
xxx
test_1.txt00006440...01014257504241011507 0ustar rootrootyyy
yyy

如上可见,tar 文件的格式非常简单,多个文件的内容从上到下依次排列,只不过每个文件内容的前面附加了一个头,其中保存了诸如文件名,权限之类的信息。

看上去用 tar 的话确实可以搞定动态流式下载,不过 tar 有个缺点,普通用户搞不清 tar 文件类型是什么东西,相比较而言,他们更乐于接受 zip 文件类型。

不过 zip 文件类型的格式可要比 tar 复杂,我从 wikipedia 找到下图:

zip

zip

对于凡夫俗子的我来说,想要通过手撸 zip 格式来实现动态流式下载绝非易事,就在举棋不定之际,我突然发现 golang 的 zip 标准库已经实现了 Writer 接口,这就意味着,我们只要结合使用 zip.NewWriter 和 http.ResponseWriter 就能实现我们的目的:

package main

import (
	"archive/zip"
	"fmt"
	"io"
	"net/http"
	"os"
)

func main() {
	http.HandleFunc("/test", test)
	http.ListenAndServe(":8080", nil)
}

func test(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/zip")
	w.Header().Set("Content-Disposition", "attachment; filename=test.zip")
	writer := zip.NewWriter(w)
	for i := 0; i < 2; i++ {
		name := fmt.Sprintf("test_%d.txt", i)
		srcFile, err := os.Open(name)
		if err != nil {
			panic(err)
		}
		defer srcFile.Close()
		dstFile, err := writer.Create(name)
		if err != nil {
			panic(err)
		}
		if _, err := io.Copy(dstFile, srcFile); err != nil {
			panic(err)
		}
	}
	writer.Close()
}

如上代码编译运行后,打开浏览器,执行 http://localhost:8080/test 即可看到效果。

文章来源:

Author:老王
link:https://blog.huoding.com/2022/07/01/984