【问题标题】:Golang io.copy twice on the request bodyGolang io.copy 在请求正文上两次
【发布时间】:2014-10-29 13:07:43
【问题描述】:

我正在构建一个 blob 存储系统,我选择 Go 作为编程语言。 我创建了一个流来执行从客户端到 Blob 服务器的多部分文件上传。

流工作正常,但我想从请求正文中创建一个 sha1 哈希。我需要 io.Copy 身体两次。 sha1 被创建,但之后多部分流 0 个字节。

  1. 用于创建哈希
  2. 用于将正文作为多部分流式传输

知道我该怎么做吗?

客户端上传

func (c *Client) Upload(h *UploadHandle) (*PutResult, error) {
body, bodySize, err := h.Read()
if err != nil {
    return nil, err
}

// Creating a sha1 hash from the bytes of body
dropRef, err := drop.Sha1FromReader(body)
if err != nil {
    return nil, err
}

bodyReader, bodyWriter := io.Pipe()
writer := multipart.NewWriter(bodyWriter)

errChan := make(chan error, 1)
go func() {
    defer bodyWriter.Close()
    part, err := writer.CreateFormFile(dropRef, dropRef)
    if err != nil {
        errChan <- err
        return
    }
    if _, err := io.Copy(part, body); err != nil {
        errChan <- err
        return
    }
    if err = writer.Close(); err != nil {
        errChan <- err
    }
}()

req, err := http.NewRequest("POST", c.Server+"/drops/upload", bodyReader)
req.Header.Add("Content-Type", writer.FormDataContentType())
resp, err := c.Do(req)
if err != nil {
    return nil, err
}
  .....
 }

sha1 函数

func Sha1FromReader(src io.Reader) (string, error) {
hash := sha1.New()
_, err := io.Copy(hash, src)
if err != nil {
    return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil

}

上传句柄

func (h *UploadHandle) Read() (io.Reader, int64, error) {
var b bytes.Buffer

hw := &Hasher{&b, sha1.New()}
n, err := io.Copy(hw, h.Contents)

if err != nil {
    return nil, 0, err
}

return &b, n, nil

}

【问题讨论】:

  • 您真的不需要那么多代码,而且您肯定不需要将 blob 的整个连续副本存储在 RAM 中。我在 RAM 匮乏的设备上对多 GB 的 blob 做了类似的事情。
  • @Dustin 在这种情况下你会怎么做?
  • 你对写入缓冲区的内容做了什么?在不将其写入缓冲区的情况下执行此操作。考虑this playground link——它在操场上不起作用,因为您无法创建文件,但可以正确处理任意长的输入,并且内存使用量最少。我为 CAS 做了类似的事情,它也验证了带外哈希,然后在成功关闭文件后重命名文件。
  • 作为替代方案,您可以使用这个库https://github.com/hyperboloide/sprocess 来处理文件上传流

标签: io go multipart


【解决方案1】:

你可以使用Request.GetBody:

package main

import (
   "io"
   "net/http"
   "os"
   "strings"
)

func main() {
   read := strings.NewReader("north east south west")
   req, e := http.NewRequest("GET", "https://stackoverflow.com", read)
   if e != nil {
      panic(e)
   }
   // one
   io.Copy(os.Stdout, req.Body)
   // two
   body, e := req.GetBody()
   if e != nil {
      panic(e)
   }
   io.Copy(os.Stdout, body)
}

https://golang.org/pkg/net/http#Request.GetBody

【讨论】:

    【解决方案2】:

    我们可以将流转换为字符串并根据需要多次创建它。 例如

    readerStream := your stream from source
    
    buf := new(bytes.Buffer)
    buf.ReadFrom(readerStream)
    rawBody := buf.String()
    
    newReader1 := strings.NewReader(rawBody)
    newReader2 := strings.NewReader(rawBody)
    

    但如果能避免就好了。

    我不确定这是最好的方法。但它对我有用。

    【讨论】:

      【解决方案3】:

      您不能直接这样做,但您可以编写一个包装器,对 io.Copy 进行散列处理

      // this works for either a reader or writer, 
      //  but if you use both in the same time the hash will be wrong.
      type Hasher struct {
          io.Writer
          io.Reader
          hash.Hash
          Size uint64
      }
      
      func (h *Hasher) Write(p []byte) (n int, err error) {
          n, err = h.Writer.Write(p)
          h.Hash.Write(p)
          h.Size += uint64(n)
          return
      }
      
      func (h *Hasher) Read(p []byte) (n int, err error) {
          n, err = h.Reader.Read(p)
          h.Hash.Write(p[:n]) //on error n is gonna be 0 so this is still safe.
          return
      }
      
      func (h *Hasher) Sum() string {
          return hex.EncodeToString(h.Hash.Sum(nil))
      }
      
      func (h *UploadHandle) Read() (io.Reader, string, int64, error) {
          var b bytes.Buffer
      
          hashedReader := &Hasher{Reader: h.Contents, Hash: sha1.New()}
          n, err := io.Copy(&b, hashedReader)
      
          if err != nil {
              return nil, "", 0, err
          }
      
          return &b, hashedReader.Sum(), n, nil
      }
      

      // 基于@Dustin 的评论更新版本,因为我完全忘记了io.TeeReader 的存在。

      func (h *UploadHandle) Read() (io.Reader, string, int64, error) {
          var b bytes.Buffer
      
          hash := sha1.New()
          n, err := io.Copy(&b, io.TeeReader(h.Contents, hash))
      
          if err != nil {
              return nil, "", 0, err
          }
      
          return &b, hex.EncodeToString(hash.Sum(nil)), n, nil
      }
      

      【讨论】:

      • @fabrizioM 哈希写入保证不会返回错误。全市 6 小时停电很有趣
      • 知道如何实现吗?我添加了上传句柄读取函数
      • @AnthonyDeMeulemeester 我更新了代码并添加了一个示例。我想指出您的代码容易受到 DoS 攻击,因为您将文件保存在内存中(在 bytes.Buffer 中)并且有人可以继续上传大文件。
      • @OneOfOne 非常感谢,我怎样才能防止 DoS?之后刷新缓冲区?
      • 我发布了一个playground link,它的机制更小更便宜。 (一半的代码是一个测试 io.Reader 只是为了产生一些输出)。此代码重新发明了io.TeeReader,但使用了更具体但更容易出错的 API。
      【解决方案4】:

      如果您想同时通过 sha1 推送来自 blob 的所有读取,我建议使用 io.TeeReader

      bodyReader := io.TeeReader(body, hash)
      

      现在由于在上传过程中消耗了 bodyReader,哈希值会自动更新。

      【讨论】:

        【解决方案5】:

        你有两个选择。

        最直接的方法是使用io.MultiWriter

        但如果您需要哈希来生成多部分输出,那么您必须复制到bytes.Buffer,然后将缓冲区写回每个写入器。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2018-04-07
          • 1970-01-01
          • 2017-08-18
          • 2015-10-02
          • 1970-01-01
          • 1970-01-01
          • 2015-12-22
          • 1970-01-01
          相关资源
          最近更新 更多