创建 + 提供(通过 HTTP).ZIP 文件,而不写入磁盘?

问题描述 投票:0回答:3

我想要提供一个动态创建的 .ZIP 文件,而不必将其写入磁盘(I/O 会降低性能)并通过 HTTP 将其提供给客户端。

这是我第一次尝试的方法:

func ZipServe(W http.ResponseWriter, R *http.Request) {

buf := new(bytes.Buffer)
writer := zip.NewWriter(buf)

// for the sake of this demonstration, this is the data I will zip
data := ioutil.ReadFile("randomfile.jpg")

f, err := writer.Create("randomfile.jpg")
if err != nil { 
    fmt.Println(err)
}

_, err = f.Write(data)
if err != nil {
    fmt.Println(err)
}

io.Copy(W, buf)

err := writer.Close()
if err != nil {
    fmt.Println(err)
}

}

这不好,因为 .ZIP 在下载后最终会损坏。我想这个问题与 io.Copy 有关;我应该使用不同的方法吗?

http go zip
3个回答
15
投票

我发现这个很有趣,只是为了测试而想出了这个:

http://play.golang.org/p/JKAde2jbR3

package main

import (
    "archive/zip"
    "bytes"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

func zipHandler(w http.ResponseWriter, r *http.Request) {
    filename := "randomfile.jpg"
    buf := new(bytes.Buffer)
    writer := zip.NewWriter(buf)
    data, err := ioutil.ReadFile(filename)
    if err != nil {
        log.Fatal(err)
    }
    f, err := writer.Create(filename)
    if err != nil {
        log.Fatal(err)
    }
    _, err = f.Write([]byte(data))
    if err != nil {
        log.Fatal(err)
    }
    err = writer.Close()
    if err != nil {
        log.Fatal(err)
    }
    w.Header().Set("Content-Type", "application/zip")
    w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", filename))
    //io.Copy(w, buf)
    w.Write(buf.Bytes())
}

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

我只是添加一些标题,例如

Content-Type
Content-Disposition

我也没有使用

io.Copy(w, buf)
,而是直接写
w.Write(buf.Bytes())
,想知道这样是否更好?也许更有经验的用户可以澄清这一点。


4
投票

这里有一个稍微简单的方法,使用 io.Copy。对于较大的文件大小,它的性能可能不如使用缓冲区,但它对我有用:

func handleZip(w http.ResponseWriter, r *http.Request) {
    f, err := os.Open("main.go")
    if err != nil {
        log.Fatal(err)
    }
    defer func() {
        if err := f.Close(); err != nil {
            log.Fatal(err)
        }
    }()

    // write straight to the http.ResponseWriter
    zw := zip.NewWriter(w)
    cf, err := zw.Create(f.Name())
    if err != nil {
        log.Fatal(err)
    }

    w.Header().Set("Content-Type", "application/zip")
    w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.zip\"", f.Name()))


    // copy the file contents to the zip Writer
    _, err = io.Copy(cf, f)
    if err != nil {
        log.Fatal(err)
    }

    // close the zip Writer to flush the contents to the ResponseWriter
    err = zw.Close()
    if err != nil {
        log.Fatal(err)
    }
}

2
投票

评分最高的答案应该有效,但在大文件上内存使用率会很高。它将整个文件读入内存。虽然它避免写入磁盘,但在处理大文件时可能会出现其他性能问题。

好消息是 golang zip 库能够进行流式传输,而无需将整个文件加载到内存中!以下是具体操作方法。

使用库的两个较低代码选项(披露我是作者):

  1. 您可以为此使用专用的微服务,而无需自己编写(披露我是作者):ZipStreamer
  2. 如果您想将其合并到您自己的服务中,您可以使用与库相同的项目。使用
    ZipStream.NewStream
    创建流,并将其传递给构造函数 http.ResponseWriter writer。有关设置所有必要的 HTTP 标头的示例,请参阅
    server.go/streamEntries
    。它会照顾一切以保持内存和 CPU 较低。

如果您想从头开始编写此内容,这里有一个示例。关键是使用类似

io.Copy
的东西,它内置了 32kb 缓冲区和缓冲读取器;这样您就不会将整个文件加载到内存中,并且它应该扩展到任何大小的文件。

func zipHandler(w http.ResponseWriter, r *http.Request) error {
    // You need to write the headers and status code before any bytes
    w.Header().Set("Content-Type", "application/zip")
    // the filename which will be suggested in the save file dialog
    w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", "attachment.zip"))
    w.WriteHeader(http.StatusOK)

    zipWriter := zip.NewWriter(w)

    for _, entry := range <<yourListOfFiles>> {
        header := &zip.FileHeader{
            Name:     entry.pathInNewZip,
            Method:   zip.Store, // deflate also works, but at a cost
            Modified: time.Now(),
        }
        entryWriter, err := zipWriter.CreateHeader(header)
        if err != nil {
            return err
        }

        fileReader := bufio.NewReader(entry.pathToFile)

        _, err = io.Copy(entryWriter, fileReader)
        if err != nil {
            return err
        }

        zipWriter.Flush()
        flushingWriter, ok := w.(http.Flusher)
        if ok {
            flushingWriter.Flush()
        }
    }

    return zipWriter.Close()
}
© www.soinside.com 2019 - 2024. All rights reserved.