在不使用缓冲区或管道的情况下在Golang中上传大文件

我们最近决定使用Golang作为基础功能语言将其中一种文件传输服务从AWS EC2迁移到AWS Lambda。

EC2上的相关文件传输服务充当Unity Cloud Build的通知网络挂钩。 在Unity Cloud Build中成功完成构建后,它将使用基本构建信息将构建成功通知发布到服务webhook。 然后,该服务将从请求中提取构建信息,并执行以下步骤:

  1. 从Unity Could Build取回构建文件下载链接
  2. 将构建文件从Unity Cloud Build下载到EC2本地存储
  3. 将文件从本地存储上传到DeployGate,以在我们的团队中分发。

挑战性

为了将服务迁移到AWS Lambda,我们最初计划在Golang中实现相同的工作程。 实施步骤1非常简单,但是在将构建文件下载到本地存储时,我们在步骤2中遇到了问题。 默认情况下,AWS Lambda仅提供512 MB作为临时存储,但是我们的某些测试版本可能远远超过512 MB。 因此,将文件下载到本地存储不是一种选择。

如果我们决定将文件保留在内存中,则Lambda的内存使用量将非常高,从而增加成本。 Lambda还具有大约3000 MB的内存使用限制和大文件限制,该选项不能很好地工作。 因此,我们需要找到一种方法,将传入的下载流直接定向到我们的上传请求中。

在寻找解决方案之前,让我们了解一下浏览器或http客户端如何创建http文件上传请求。

分段文件上传

通常,文件上传请求不仅包含文件,还会发送一些其他元数据信息,例如文件名,文件类型等。在我们的情况下,我们需要发送DeployGate令牌,测试人员的发行说明和文件名以及构建文件。 要发送多个表单数据条目以及上载文件,我们使用HTTP Multipart表单数据请求。 该请求期望请求主体是由边界分隔的一系列部分,每个部分描述一个表单数据条目。 典型的多部分表单数据请求如下所示:

POST /files/upload HTTP/1.1 
.
.

Content-Length: 1234
Content-Type: multipart/form-data; boundary=abcd1234
  --abcd1234 
内容处置:表单数据; name =“ form_field_name”
 表格栏位值 
--abcd1234
内容处置:表单数据; name =“ file”; filename =“ build.apk”
内容类型:应用程序/八位字节流
   
--abcd1234--

在此示例中,我们将边界设置为abcd1234 。 边界可以是与表单数据值不冲突的任何唯一字符串。 在请求主体中,我们有一个名为form_field_name的表单数据字段,还有一个名为file的字段,其中包含我们要上传的文件名和主体。 这里要注意的一件事是,根据规范,在请求主体内使用的折线必须为\ r \ n

使用io.MultiReader实施分段上传

为了实现分段上传请求,我们将使用Golang的net / http包发送请求,并使用io.MultiReader函数构建请求主体。 基本流程如下:

  func 进程 (downloadURL字符串)错误{ 
//步骤1.从Unity Cloud Build下载文件
响应,错误:= DownloadFromUnity (downloadURL)
如果err!= nil {
返回错误
}
推迟响应。 身体关闭 ()

//步骤2。准备要上传的请求正文
contentType,uploadBody:= PrepareUploadBody (响应。正文)
  //步骤3.发送上传请求 
err = UploadToDeployGate (contentType,uploadBody)
返回错误
}

在这段代码中,我们有3个步骤。 让我们一一看一下。

第一步,我们从Unity Cloud Build下载构建文件。 响应变量包含GET请求的http响应。 可以从实现io.Reader接口的response.Body中读取文件。 下载功能定义如下所示:

  func DownloadFromUnity (downloadURL字符串)(* http。 响应 ,错误){ 
请求,错误:= http。 NewRequest (“ GET”,downloadURL,无)
如果err!= nil {
返回&http。 响应 {},错误
}
 客户端:=&http。  客户 {} 
返回客户。 (请求)
}

在第二步中,我们正在准备分段上传请求的请求正文。 contentType变量包含指定边界的Content-Type请求标头值。 函数定义如下:

  func PrepareUploadBody (fileReader io。Reader)(string,io。Reader){ 
边界:=“ MyMultiPartBoundary12345”
令牌:=“ DEPLOY_GATE_TOKEN”
消息:=“由AWS Lambda Webhook上载”
releaseNote:=“由Unity Cloud Build构建”
  fieldFormat:=“-%s \ r \ n内容处置:表格数据;名称= \”%s \“ \ r \ n \ r \ n%s \ r \ n” 
tokenPart:= fmt。 Sprintf(fieldFormat,边界,“令牌”,令牌)
messagePart:= fmt。 Sprintf(fieldFormat,边界,“消息”,消息)
releaseNotePart:= fmt。 Sprintf(fieldFormat,边界,“ release_note”,releaseNote)
  fileName:=“ build.apk” 
fileHeader:=“内容类型:应用程序/八位字节流”
fileFormat:=“-%s \ r \ n内容处置:表格数据;名称= \”文件\“;文件名= \”%s \“ \ r \ n%s \ r \ n \ r \ n”
filePart:= fmt。 Sprintf(文件格式,边界,文件名,文件头)
  bodyTop:= fmt。  Sprintf (“%s%s%s%s”,tokenPart,messagePart,releaseNotePart,filePart) 
bodyBottom:= fmt。 Sprintf (“ \ r \ n-%s-\ r \ n”,边界)
 正文:= io。  MultiReader (字符串。NewReader (bodyTop),fileReader,字符串。NewReader (bodyBottom)) 
  contentType:= fmt。  Sprintf (“多部分/表单数据;边界=%s”,边界) 
返回contentType,正文
}

该函数接受来自Unity Cloud Build的下载请求的http响应主体作为变量fileReader 。 我们使用字符串格式化来创建请求主体的上部和下部,并使用io.MultiReader函数将它们与fileReader组合在一起。 MultiReader可以像字符串串联一样组合多个字符串读取器。 最终的请求正文和内容标头如下所示:

 Content-Type: multipart/form-data; boundary=  Content-Type: multipart/form-data; boundary= MyMultiPartBoundary12345 
  --MyMultiPartBoundary12345 
内容处置:表单数据; name =“令牌”
  DEPLOY_GATE_TOKEN 
--MyMultiPartBoundary12345
内容处置:表单数据; name =“ message”
 由AWS Lambda Webhook上载 
--MyMultiPartBoundary12345
内容处置:表单数据; name =“ release_note”
 由Unity Cloud Build构建 
--MyMultiPartBoundary12345
内容处置:表单数据; name =“ file”; filename =“ build.apk”
内容类型:应用程序/八位字节流
   
--MyMultiPartBoundary12345--

现在,我们已经成功地为分段上传请求准备了请求正文。 我们可以看第三步,发送上传请求。

  func UploadToDeployGate (contentType字符串,主体io。Reader )错误{ 
响应,错误:= http。 发布 (“ DeployGatePostURL”,contentType,正文)
如果err!= nil {
返回错误
}
推迟响应。 身体关闭 ()
  //响应处理 
 返回零 
}

由于此方法使用io.Reader读取下载文件流,因此可以使用同一方法上载实现io.Reader的任何数据源,例如,要从文件系统上载文件,可以使用os.Open函数来获取文件的io.Reader句柄,并使用它而不是response.Body来为上载请求准备请求正文。

结论

在AWS Lambda上,此方法使用大约40 MB的内存将大小约为800 MB的文件从Unity Cloud Build流式传输到DeployGate,并且不使用任何本地存储。

请注意,Golang也有一个mime / multipart包来支持构建Multipart请求。 在线上有很多文章介绍了使用此软件包以及io.Bufferio.Pipe上载大文件的方法。 但是我想不出一种方法来使其与传入的下载流一起工作。