我想要提供一个动态创建的 .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://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())
,想知道这样是否更好?也许更有经验的用户可以澄清这一点。
这里有一个稍微简单的方法,使用 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)
}
}
评分最高的答案应该有效,但在大文件上内存使用率会很高。它将整个文件读入内存。虽然它避免写入磁盘,但在处理大文件时可能会出现其他性能问题。
好消息是 golang zip 库能够进行流式传输,而无需将整个文件加载到内存中!以下是具体操作方法。
使用库的两个较低代码选项(披露我是作者):
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()
}