从(AngularJS)单页应用程序直接(简单!)AJAX上传到AWS S3



我知道有很多关于上传到AWS S3的报道。然而,我已经为此挣扎了大约24个小时,我还没有找到任何适合我情况的答案。

我想做什么

将一个文件直接从我的客户端上传到我的S3存储桶中。情况是:

  1. 这是一个单页应用程序,所以上传请求必须是AJAX
  2. 我的服务器和客户端不在同一域上
  3. S3 bucket是最新的类型(Frankfurt),一些生成签名的库不起作用(见下文)
  4. 客户端位于AngularJS
  5. 服务器在ExpressJS中

我尝试过的

  • Heroku关于直接上传到S3的文章。不适合我的客户端/服务器配置(此外,它确实不适合Angular)
  • 像ng-supload这样的现成指令。不起作用,因为他们的签名生成算法不被最近的s3 bucket接受
  • 在客户端上手动创建文件上传指令和逻辑,如本文所述(使用FormData和Angular的$http)。它包括从服务器上的AWS获得一个签名的URL(这部分工作正常),然后AJAX上传到该URL。它失败了,出现了一些神秘的CORS相关消息(尽管我确实在Heroku上设置了CORS配置)

我似乎面临着两个困难:在我的单页应用程序中使用文件输入,以及正确处理AWS的工作流程。

我正在寻找的解决方案

如果可能的话,我希望避免"包罗万象"的解决方案,这些解决方案管理整个过程,同时隐藏所有复杂性,使其难以适应特殊情况。我宁愿有一个简单的解释来分解所涉及的各个组件之间的数据流,即使这需要我更多的管道。

我终于成功了。关键点是:

  1. 放弃Angular的$http,转而使用原生XMLHttpRequest
  2. 使用AWS SDK的getSignedUrl功能,而不是像许多库那样实现我自己的签名生成工作流
  3. 将AWS配置设置为使用正确的签名版本(在编写时为v4)和区域(在法兰克福的情况下为'eu-central-1'

下面是我所做工作的分步指南;它在服务器上使用AngularJS和NodeJS,但应该很容易适应其他堆栈,特别是因为它处理的是最病态的情况(SPA在服务器的不同域上,在撰写本文时,在最近的区域中有一个bucket)。


工作流摘要

  1. 用户在浏览器中选择一个文件;您的JavaScript会保留对它的引用
  2. 客户端向您的服务器发送请求以获得签名的上传URL
  3. 您的服务器为要放入bucket的对象选择一个名称(确保避免名称冲突!)
  4. 服务器使用AWS SDK获取对象的签名URL,并将其发送回客户端。这涉及到对象的名称和AWS凭据
  5. 给定文件和签名的URL,客户端会直接向S3 Bucket发送PUT请求

开始之前

确保:

  • 您的服务器具有AWS SDK
  • 您的服务器具有对您的bucket具有适当访问权限的AWS凭据
  • 您的S3存储桶为您的客户端提供了适当的CORS配置

步骤1:设置一个SPA友好的文件上传表单/小部件

重要的是要有一个工作流程,它最终可以让您对File对象进行编程访问,而无需上传

在我的案例中,我使用了优秀的angular文件上传库的ng-file-selectng-file-drop指令。但还有其他方法可以做到(例如,请参阅本文)

请注意,您可以访问文件对象中的有用信息,如file.namefile.type等。

步骤2:获取服务器上文件的签名URL

在您的服务器上,您可以使用AWS SDK从其他地方(如前端)获得PUT文件的安全临时URL。

在NodeJS中,我是这样做的:

// ---------------------------------
// some initial configuration
var aws = require('aws-sdk');
aws.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY,
  secretAccessKey: process.env.AWS_SECRET_KEY,
  signatureVersion: 'v4',
  region: 'eu-central-1'
});
// ---------------------------------
// now say you want fetch a URL for an object named `objectName`
var s3 = new aws.S3();
var s3_params = {
  Bucket: MY_BUCKET_NAME,
  Key: objectName,
  Expires: 60,
  ACL: 'public-read'
};
s3.getSignedUrl('putObject', s3_params, function (err, signedUrl) {
  // send signedUrl back to client
  // [...]
});

您可能想知道获取对象的URL(通常是图像)。为了做到这一点,我简单地从URL中删除了查询字符串:

var url = require('url');
// ...
var parsedUrl = url.parse(signedUrl);
parsedUrl.search = null;
var objectUrl = url.format(parsedUrl);

步骤3:从客户端发送PUT请求

现在您的客户端已经有了File对象和签名的URL,它可以向S3发送PUT请求。在Angular的案例中,我的建议是只使用XMLHttpRequest而不是$http服务:

var signedUrl, file;
// ...
var d_completed = $q.defer(); // since I'm working with Angular, I use $q for asynchronous control flow, but it's not mandatory
var xhr = new XMLHttpRequest();
xhr.file = file; // not necessary if you create scopes like this
xhr.onreadystatechange = function(e) {
  if ( 4 == this.readyState ) {
    // done uploading! HURRAY!
    d_completed.resolve(true);
  }
};
xhr.open('PUT', signedUrl, true);
xhr.setRequestHeader("Content-Type","application/octet-stream");
xhr.send(file);

鸣谢

我要感谢emil10001和Will Webberley,他们的出版物在本期对我来说非常有价值。

您可以将ng文件upload$upload.http方法与aws-sdk getSignedUrl结合使用来实现这一点。从服务器取回签名的URL后,这是客户端代码:

var fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = function(e) {
$upload.http({
  method: 'PUT',
  headers: {'Content-Type': file.type != '' ? file.type : 'application/octet-stream'},
  url: signedUrl,
  data: e.target.result
}).progress(function (evt) {
  var progressPercentage = parseInt(100.0 * evt.loaded / evt.total);
  console.log('progress: ' + progressPercentage + '% ' + file.name);
}).success(function (data, status, headers, config) {
  console.log('file ' + file.name + 'uploaded. Response: ' + data);
});

要进行多部分上传,或者大于5GB的上传,这个过程会变得有点复杂,因为每个部分都需要自己的签名。方便的是,有一个JS库:

https://github.com/TTLabs/EvaporateJS

通过https://github.com/aws/aws-sdk-js/issues/468

使用具有动态数据绑定和自动回调功能的s3文件上传开源指令-https://github.com/vinayvnvv/s3FileUpload

最新更新