15

Golang实现http server提供压缩文件下载功能

 4 years ago
source link: https://studygolang.com/articles/32501
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近遇到了一个下载静态html报表的需求,需要以提供压缩包的形式完成下载功能,实现的过程中发现相关文档非常杂,故总结一下自己的实现。

开发环境:

系统环境:MacOS + Chrome

框架:beego

压缩功能:tar + gzip

目标压缩文件:自带数据和全部包的静态html文件

首先先提一下http server文件下载的实现,其实就是在后端返回前端的数据包中,将数据头设置为下载文件的格式,这样前端收到返回的响应时,会直接触发下载功能(就像时平时我们在chrome中点击下载那样)

数据头设置格式如下:

func (c *Controller)Download() {
    //...文件信息的产生逻辑
    //
    //rw为responseWriter
    rw := c.Ctx.ResponseWriter
    //规定下载后的文件名
    rw.Header().Set("Content-Disposition", "attachment; filename="+"(文件名字)")
    rw.Header().Set("Content-Description", "File Transfer")
    //标明传输文件类型
    //如果是其他类型,请参照:https://www.runoob.com/http/http-content-type.html
    rw.Header().Set("Content-Type", "application/octet-stream")
    rw.Header().Set("Content-Transfer-Encoding", "binary")
    rw.Header().Set("Expires", "0")
    rw.Header().Set("Cache-Control", "must-revalidate")
    rw.Header().Set("Pragma", "public")
    rw.WriteHeader(http.StatusOK)
    //文件的传输是用byte slice类型,本例子中:b是一个bytes.Buffer,则需调用b.Bytes()
    http.ServeContent(rw, c.Ctx.Request, "(文件名字)", time.Now(), bytes.NewReader(b.Bytes()))
}

这样,beego后端就会将在头部标记为下载文件的数据包发送给前端,前端收到后会自动启动下载功能。

然而这只是最后一步的情况,如何将我们的文件先进行压缩再发送给前端提供下载呢?

如果需要下载的不只一个文件,需要用tar打包,再用gzip进行压缩,实现如下:

//最内层用bytes.Buffer来进行文件的存储
    var b bytes.Buffer
    //嵌套tar包的writter和gzip包的writer
    gw := gzip.NewWriter(&b)
    tw := tar.NewWriter(gw)


    dataFile := //...文件的产生逻辑,dataFile为File类型
    info, _ := dataFile.Stat()
    header, _ := tar.FileInfoHeader(info, "")
    //下载后当前文件的路径设置
    header.Name = "report" + "/" + header.Name
    err := tw.WriteHeader(header)
    if err != nil {
        utils.LogErrorln(err.Error())
        return
    }
    _, err = io.Copy(tw, dataFile)
    if err != nil {
        utils.LogErrorln(err.Error())
    }
    //...可以继续添加文件
    //tar writer 和 gzip writer的关闭顺序一定不能反
    tw.Close()
    gw.Close()

最后和中间步骤完成了,我们只剩File文件的产生逻辑了,由于是静态html文件,我们需要把所有html引用的依赖包全部完整的写入到生成的文件中的<script>和<style>标签下。此外,在本例子中,报告部分还需要一些静态的json数据来填充表格和图像,这部分数据是以map存储在内存中的。当然可以先保存成文件再进行上面一步的打包压缩,但是这样会产生并发的问题,因此我们需要先将所有的依赖包文件和数据写入一个byte.Buffer中,最后将这个byte.Buffer转回File格式。

Golang中并没有写好的byte.Buffer转文件的函数可以用,于是我们需要自己实现。

实现如下:

type myFileInfo struct {
    name string
    data []byte
}

func (mif myFileInfo) Name() string       { return mif.name }
func (mif myFileInfo) Size() int64        { return int64(len(mif.data)) }
func (mif myFileInfo) Mode() os.FileMode  { return 0444 }        // Read for all
func (mif myFileInfo) ModTime() time.Time { return time.Time{} } // Return whatever you want
func (mif myFileInfo) IsDir() bool        { return false }
func (mif myFileInfo) Sys() interface{}   { return nil }

type MyFile struct {
    *bytes.Reader
    mif myFileInfo
}

func (mf *MyFile) Close() error { return nil } // Noop, nothing to do

func (mf *MyFile) Readdir(count int) ([]os.FileInfo, error) {
    return nil, nil // We are not a directory but a single file
}

func (mf *MyFile) Stat() (os.FileInfo, error) {
    return mf.mif, nil
}

依赖包和数据的写入逻辑:

func testWrite(data map[string]interface{}, taskId string) http.File {
    //最后生成的html,打开html模版
    tempfileP, _ := os.Open("views/traffic/generatePage.html")
    info, _ := tempfileP.Stat()
    html := make([]byte, info.Size())
    _, err := tempfileP.Read(html)
    // 将data数据写入html
    var b bytes.Buffer
    // 创建Json编码器
    encoder := json.NewEncoder(&b)

    err = encoder.Encode(data)
    if err != nil {
        utils.LogErrorln(err.Error())
    }
    
    // 将json数据添加到html模版中
    // 方式为在html模版中插入一个特殊的替换字段,本例中为{Data_Json_Source}
    html = bytes.Replace(html, []byte("{Data_Json_Source}"), b.Bytes(), 1)

    // 将静态文件添加进html
    // 如果是.css,则前后增加<style></style>标签
    // 如果是.js,则前后增加<script><script>标签
    allStaticFiles := make([][]byte, 0)
    // jquery 需要最先进行添加
    tempfilename := "static/report/jquery.min.js"

    tempfileP, _ = os.Open(tempfilename)
    info, _ = os.Stat(tempfilename)
    curFileByte := make([]byte, info.Size())
    _, err = tempfileP.Read(curFileByte)

    allStaticFiles = append(allStaticFiles, []byte("<script>"))
    allStaticFiles = append(allStaticFiles, curFileByte)
    allStaticFiles = append(allStaticFiles, []byte("</script>"))
    //剩下的所有静态文件
    staticFiles, _ := ioutil.ReadDir("static/report/")
    for _, tempfile := range staticFiles {
        if tempfile.Name() == "jquery.min.js" {
            continue
        }
        tempfilename := "static/report/" + tempfile.Name()

        tempfileP, _ := os.Open(tempfilename)
        info, _ := os.Stat(tempfilename)
        curFileByte := make([]byte, info.Size())
        _, err := tempfileP.Read(curFileByte)
        if err != nil {
            utils.LogErrorln(err.Error())
        }
        if isJs, _ := regexp.MatchString(`\.js$`, tempfilename); isJs {
            allStaticFiles = append(allStaticFiles, []byte("<script>"))
            allStaticFiles = append(allStaticFiles, curFileByte)
            allStaticFiles = append(allStaticFiles, []byte("</script>"))
        } else if isCss, _ := regexp.MatchString(`\.css$`, tempfilename); isCss {
            allStaticFiles = append(allStaticFiles, []byte("<style>"))
            allStaticFiles = append(allStaticFiles, curFileByte)
            allStaticFiles = append(allStaticFiles, []byte("</style>"))
        }
        tempfileP.Close()
    }
    
    // 转成http.File格式进行返回
    mf := &MyFile{
        Reader: bytes.NewReader(html),
        mif: myFileInfo{
            name: "report.html",
            data: html,
        },
    }
    var f http.File = mf
    return f
}

OK! 目前为止,后端的文件生成->打包->压缩都已经做好啦,我们把他们串起来:

func (c *Controller)Download() {
    var b bytes.Buffer
    gw := gzip.NewWriter(&b)

    tw := tar.NewWriter(gw)

    // 生成动态report,并添加进压缩包
    // 调用上文中的testWrite方法
    dataFile := testWrite(responseByRules, strTaskId)
    info, _ := dataFile.Stat()
    header, _ := tar.FileInfoHeader(info, "")
    header.Name = "report_" + strTaskId + "/" + header.Name
    err := tw.WriteHeader(header)
    if err != nil {
        utils.LogErrorln(err.Error())
        return
    }
    _, err = io.Copy(tw, dataFile)
    if err != nil {
        utils.LogErrorln(err.Error())
    }

    tw.Close()
    gw.Close()
    rw := c.Ctx.ResponseWriter
    rw.Header().Set("Content-Disposition", "attachment; filename="+"report_"+strTaskId+".tar.gz")
    rw.Header().Set("Content-Description", "File Transfer")
    rw.Header().Set("Content-Type", "application/octet-stream")
    rw.Header().Set("Content-Transfer-Encoding", "binary")
    rw.Header().Set("Expires", "0")
    rw.Header().Set("Cache-Control", "must-revalidate")
    rw.Header().Set("Pragma", "public")
    rw.WriteHeader(http.StatusOK)
    http.ServeContent(rw, c.Ctx.Request, "report_"+strTaskId+".tar.gz", time.Now(), bytes.NewReader(b.Bytes()))
}

后端部分已经全部实现了,前端部分如何接收呢,本例中我做了一个按钮嵌套 <a> 标签来进行请求:

<a href="/traffic/download_indicator?task_id={{$.taskId}}&task_type={{$.taskType}}&status={{$.status}}&agent_addr={{$.agentAddr}}&glaucus_addr={{$.glaucusAddr}}">
     <button style="font-family: 'SimHei';font-size: 14px;font-weight: bold;color: #0d6aad;text-decoration: underline;margin-left: 40px;" type="button" class="btn btn-link">下载报表</button>
</a>

这样,当前端页面中点击下载报表按钮之后,会自动启动下载,下载我们后端传回的report.tar.gz文件。

有疑问加站长微信联系(非本文作者)

eUjI7rn.png!mobile

Recommend

  • 85
    • 微信 mp.weixin.qq.com 7 years ago
    • Cache

    文件下载的一些安全小细节

    文件下载的一些安全小细节 Original...

  • 46
    • down.51cto.com 7 years ago
    • Cache

    ShuttleCsp11_3003的dll文件下载

    ShuttleCsp11_3003的dll文件下载

  • 94
    • blog.51cto.com 7 years ago
    • Cache

    文件下载-奔跑吧爽爽的博客

    在创建一个文件夹,名称download,先拷入要下载的一些文件编写下载的页面,提供一些下载的超链接,点击超链接,弹出下载的窗口编写下载的Servlet类,完成下载protectedvoiddoGet(HttpServletRequestrequest,HttpServletResponseresponse)throwsServletException,IO...

  • 88

    电商产品的后台设计较为复杂,考虑的因素有很多。本文通过对具体设计原型的拆解分析,从商城首页、商品详情+购物车管理、支付结果、地址管理、订单管理、红包管理、后台等7个方面,全面介绍电商后台的设计重点,望对你有所帮助。 电商产品功能设计较难的部分是后台...

  • 95
    • www.10tiao.com 6 years ago
    • Cache

    PHP实现文件下载断点续传

    如果我们的网站提供文件下载的服务,那么通常我们都希望下载可以断点续传(Resumable Download),也就是说用户可以暂停下载,并在未来的某个时间从暂停处继续下载,而不必重新下载整个文件。 通常情况下,Web服...

  • 43

    之前处理文件下载功能一直使用的是window.open(URL)的方式。这样当然也是可以实现文件下载功能的,而且使用起来还很方便。 但是有一次测试发现这个方法在Safari浏览器中并不能正常下载,于是我又开始寻找兼容性更好的下载功能实现方案。今天给大家推荐的是

  • 80
    • 微信 mp.weixin.qq.com 6 years ago
    • Cache

    前端JS实现字符串/图片/excel文件下载

  • 13

    Node.js后端文件上传、文件接收保存及文件下载实现 2020年12月27日 | 最近更新于 下午10:19直接给下Node.js后端两种文件上传方式、后端服务接收保存文件,以及后端文件下载的具体代码实现。​ 一、Node.js后端文件上传

  • 6

    【笔记】利用模拟点击a标签实现文件下载 2022-07-21 1...

  • 4

    我们有很多上传图片的场景,都要用到裁剪和压缩的功能! 我们在日常开发过程中,有很多需要上传图片的场景。那用户在上传时,通常会遇到两个问题:不知道前端显示的尺寸比例是多少,导致最终上传图片会变形;

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK