Прямая (и простая!) Загрузка AJAX в AWS S3 из (AngularJS).

Я знаю, что было много внимания при загрузке в AWS S3. Тем не менее, я боролся с этим около 24 часов, и я не нашел ответа, который бы соответствовал моей ситуации.

Что я пытаюсь сделать

Загрузите файл в AWS S3 прямо из моего клиента в мой ведро S3. Ситуация такова:

  • Это приложение с одной страницей, поэтому запрос на загрузку должен быть в AJAX
  • Мой сервер и мой клиент не в том же домене
  • Ведро S3 имеет самый новый тип (Франкфурт), для которого некоторые библиотеки, генерирующие подпись, не работают (см. ниже).
  • Клиент находится в AngularJS
  • Сервер находится в ExpressJS

Что я пробовал

  • статья Heroku о прямой загрузке на S3. Не соответствует моей конфигурации клиент/сервер (плюс это действительно не гармонично сочетается с Angular)
  • готовые директивы, такие как ng-s3upload. Не работает, потому что их алгоритм генерации сигнатур не принимается последними ведрами s3.
  • Вручную создайте директиву загрузки и логику для клиента, как в этой статье (используя FormData и Angular $http). Он состоял в получении подписанного URL-адреса от AWS на сервере (и эта часть работала), затем AJAX-загрузка на этот URL-адрес. Это не удалось с каким-то загадочным сообщением, связанным с CORS (хотя я установил конфигурацию CORS на Heroku)

Кажется, я столкнулся с двумя трудностями: с файлом, который работает в моем приложении с одной страницей, и с правильной обработкой рабочего процесса AWS.

Вид решения, которое я ищу

Если возможно, я бы хотел избежать "всех включенных" решений, которые управляют всем процессом, скрывая всю сложность, затрудняя адаптацию к особым случаям. У меня было бы довольно простое объяснение, разрушающее поток данных между различными задействованными компонентами, даже если это требует от меня больше сантехники.

Ответы

Ответ 1

Наконец-то мне удалось. Ключевыми моментами были:

  • Отпустите Angular $http и вместо этого используйте native XMLHttpRequest.
  • Используйте getSignedUrl функцию AWS SDK, вместо этого реализуя мой собственный процесс генерации сигнатур, как это делают многие библиотеки.
  • Задайте конфигурацию AWS для использования правильной версии подписи (v4 на момент написания) и области ('eu-central-1' в случае Франкфурта).

Ниже приводится пошаговое руководство по тому, что я сделал; он использует AngularJS и NodeJS на сервере, но их довольно легко адаптировать к другим стекам, особенно потому, что он имеет дело с наиболее патологическими случаями (SPA в другом домене, который сервер с ведром в последнее время - во время запись - регион).


Краткое описание рабочего процесса

  • Пользователь выбирает файл в браузере; ваш JavaScript сохраняет ссылку на него.
  • клиент отправляет запрос на ваш сервер для получения подписанного URL-адреса для загрузки.
  • Ваш сервер выбирает имя для объекта, помещаемого в ведро (обязательно избегайте коллизий имен!).
  • Сервер получает подписанный URL-адрес для вашего объекта, используя AWS SDK, и отправляет его клиенту. Это включает имя объекта и учетные данные AWS.
  • Учитывая файл и подписанный URL, клиент отправляет запрос PUT непосредственно в ваш S3 Bucket.

Перед началом

Убедитесь, что:

  • На вашем сервере есть AWS SDK
  • У вашего сервера есть учетные данные AWS с правильными правами доступа к вашему ведру.
  • Ваш ведро S3 имеет правильную конфигурацию CORS для вашего клиента.

Шаг 1: настройте форму/виджет для загрузки в формате SPA.

Все, что имеет значение, - это иметь рабочий процесс, который в конечном итоге дает вам программный доступ к объекту File - без его загрузки.

В моем случае я использовал директивы ng-file-select и ng-file-drop отличной библиотеки angular-file-upload. Но есть и другие способы сделать это (например, этот пост.).

Обратите внимание, что вы можете получить доступ к полезной информации в своем файловом объекте, таком как file.name, file.type и т.д.

Шаг 2: Получите подписанный URL-адрес для файла на вашем сервере

На вашем сервере вы можете использовать AWS SDK для получения защищенного временного URL-адреса PUT вашего файла из другого места (например, ваш интерфейс).

В 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, он может отправить запрос PUT на S3. Мой совет в случае 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 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 и Уилл Вебберли, публикации которого были очень ценны для меня для этой проблемы.

Ответ 2

Вы можете использовать метод ng-file-upload $upload.http в сочетании с aws-sdk getSignedUrl, чтобы выполнить это. После того, как вы вернете подписанный аккаунт с сервера, это код клиента:

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);
});

Ответ 3

Чтобы делать многостраничные загрузки или более 5 ГБ, этот процесс становится несколько более сложным, так как каждая часть нуждается в собственной подписи. Удобно, для этого есть библиотека JS:

https://github.com/TTLabs/EvaporateJS

через https://github.com/aws/aws-sdk-js/issues/468

Ответ 4

Использовать директиву с открытым исходным кодом s3-file-upload с функциями динамической привязки данных и автоматического обратного вызова - https://github.com/vinayvnvv/s3FileUpload