2016-09-29 73 views
4

有一些关于发布使用于去http.Request文件的教程,但几乎无一例外地,他们开始像这样:如何将文件上传为流?

file, err := os.Open(path) 
if err != nil { 
    return nil, err 
} 
fileContents, err := ioutil.ReadAll(file) 

这就是说,你读整个文件到内存中,然后将其转换成Buffer并传递到请求中,这样的事情:

func send(client *http.Client, file *os.File, endpoint string) { 
    body := &bytes.Buffer{} 
    io.Copy(body, file) 
    req, _ := http.NewRequest("POST", endpoint, body) 
    resp, _ := client.Do(req) 
} 

如果你想发布一个巨大的文件,并避免将其读入内存,而是蒸文件起来块......你会怎么做?

+2

'* os.File'实现了所需的'io.Reader'的一个例子。所以你基本上可以做'req,_:= http.NewRequest(“POST”,endpoint,file)'。试试看!它不会“大块”,但你避免把它全部放在记忆中。 – ANisus

+1

身体如果一个http.Request是一个简单的io.Reader(有点简化)。只需让你的流和io.Reader。如何使用文件做到这一点取决于您想要实现速率限制,缓冲,重试,分块/范围等的细节。 – Volker

回答

3

如果您需要设置Content-Length,可以手动完成。下面的片段是上传文件和额外的参数作为流(基于Buffer-less Multipart POST in Golang的代码)

//NOTE: for simplicity, error check is omitted 
func uploadLargeFile(uri, filePath string, chunkSize int, params map[string]string) { 
    //open file and retrieve info 
    file, _ := os.Open(filePath) 
    fi, _ := file.Stat() 
    defer file.Close()  

    //buffer for storing multipart data 
    byteBuf := &bytes.Buffer{} 

    //part: parameters 
    mpWriter := multipart.NewWriter(byteBuf) 
    for key, value := range params { 
     _ = mpWriter.WriteField(key, value) 
    } 

    //part: file 
    mpWriter.CreateFormFile("file", fi.Name()) 
    contentType := mpWriter.FormDataContentType() 

    nmulti := byteBuf.Len() 
    multi := make([]byte, nmulti) 
    _, _ = byteBuf.Read(multi)  

    //part: latest boundary 
    //when multipart closed, latest boundary is added 
    mpWriter.Close() 
    nboundary := byteBuf.Len() 
    lastBoundary := make([]byte, nboundary) 
    _, _ = byteBuf.Read(lastBoundary) 

    //calculate content length 
    totalSize := int64(nmulti) + fi.Size() + int64(nboundary) 
    log.Printf("Content length = %v byte(s)\n", totalSize) 

    //use pipe to pass request 
    rd, wr := io.Pipe() 
    defer rd.Close() 

    go func() { 
     defer wr.Close() 

     //write multipart 
     _, _ = wr.Write(multi) 

     //write file 
     buf := make([]byte, chunkSize) 
     for { 
      n, err := file.Read(buf) 
      if err != nil { 
       break 
      } 
      _, _ = wr.Write(buf[:n]) 
     }   
     //write boundary 
     _, _ = wr.Write(lastBoundary)   
    }() 

    //construct request with rd 
    req, _ := http.NewRequest("POST", uri, rd) 
    req.Header.Set("Content-Type", contentType) 
    req.ContentLength = totalSize 

    //process request 
    client := &http.Client{} 
    resp, err := client.Do(req) 
    if err != nil { 
     log.Fatal(err) 
    } else { 
     log.Println(resp.StatusCode) 
     log.Println(resp.Header) 

     body := &bytes.Buffer{} 
     _, _ = body.ReadFrom(resp.Body) 
     resp.Body.Close() 
     log.Println(body) 
    } 
} 
+0

你有没有用多个文件做这个例子?所以所有的文件都在同一个请求中,而不是多个请求? –

+1

@RasmusHansen我写了一个小型的上传器,它可以处理同一个请求的多个文件。代码是[可在这里](https://github.com/ipsusila/gogo/tree/master/http)。 – putu

0

原来你实际上可以将*File(或任何类似流的)对象直接传递给NewRequest

但是请注意告诫,即NewRequest(如下图所示:https://golang.org/src/net/http/request.go?s=21674:21746#L695)实际上不会设置ContentLength除非流是明确之一:

  • * bytes.Buffer
  • *字节.Reader
  • * strings.Reader

由于*File是不是其中之一,该热曲除非您手动设置est,否则est将被发送而没有内容长度,这可能会导致某些服务器丢弃传入请求的正文,从服务器正确发送时导致服务器上出现“正文” 。

+1

大多数服务器都将正确接受没有Content-Length的消息,并且他们绝不应拒绝主体静静地(如果一个API声明需要'Content-Length',则由客户端来完成)。客户端将使用“Transfer-Encoding:chunked”,这只是旧HTTP/1.0服务器或明确禁止分块传输的服务器的问题。 – JimB