Я изучал фреймворки JavaScript и начал свой первый проект с Ember.js, обслуживаемого API в бэкэнде Rails 5. На сайте много изображений, и он также размещен на Heroku, где хранение изображений на сервере не вариант - вместо этого я обратился к сервису Amazon AWS S3 для хранения файлов.

Rails предлагает несколько жемчужин для взаимодействия с AWS, таких как Paperclip или Carrierwave, но Ember дает возможность отказаться от них. Если вы используете эти гемы Rails, загрузка вашего изображения пройдет в несколько этапов: 1. Оно будет размещено на вашем сервере; 2. Rails загружает файл в AWS; 3. AWS отправляет ответ Rails; 4. Rails пересылает ответ Ember, и он, наконец, отображается во внешнем интерфейсе. К счастью, Ember позволяет нам обрабатывать загрузки на стороне клиента, безопасно и напрямую отправляя файлы в AWS, пропуская при этом нашу базу данных.

Установка Ember Uploader и базовой настройки формы

В этом уроке мы будем использовать Ember Uploader. К вашему сведению, это можно использовать во многих конфигурациях серверной части, помимо Rails, включая Node.js. Мы предполагаем, что у вас уже есть адаптер JSON API, работающий между вашими конфигурациями Ember и Rails.

Сначала установите загрузчик, затем сгенерируйте новый компонент для загрузки файла. Здесь это называется «s3-upload»:

$ ember install ember-uploader
$ ember g component s3-upload

На этом этапе Ember создаст для вас два файла: компонент и шаблон. Мы будем использовать только компонент, шаблон можно удалить.

Внесение двух изменений в файл компонента расширит его для использования Ember Uploader. Сначала добавьте оператор импорта (ниже, строка 2). Ваш компонент уже будет включать «Ember.Component.extend», вы захотите заменить его на «EmberUploader.FileField.extend» (ниже, строка 4).

# app/components/s3-upload.js
1  import Ember from 'ember';
2  import EmberUploader from 'ember-uploader';
3
4  export default EmberUploader.FileField.extend({
5 
6  });

Вы можете вызвать компонент в любом шаблоне формы. Если игнорировать любые другие поля, которые может включать форма вашей модели, шаблон Ember должен выглядеть примерно так:

# app/templates/your-form-template.js
1  <form>
2    {{s3-upload}}
3    <button {{action 'save'}} id="submit">Submit</button>
4  </form>

Просто заключив имя компонента в ручки, Ember Uploader отобразит кнопку ввода файла. Когда вы нажимаете «Выбрать файл», в браузере появляется диалоговое окно загрузки, в котором можно выбрать файл.

Добавление логики к контроллеру S3-Upload

Компоненту s3-upload нужно указать, как реагировать на новую загрузку, и это происходит в методе, называемом «filesDidChange». Внутри определены несколько переменных: создается новый экземпляр загрузчика, и этому экземпляру сообщается, что делать при выборе файла.

 # app/components/s3-upload.js
 1  import Ember from 'ember';
 2  import EmberUploader from 'ember-uploader';
 3
 4  export default EmberUploader.FileField.extend({
 5
 6    filesDidChange: function() {
 7      let uploadUrl = this.get('url'),
 8      files = this.get('files'),
 9      uploader = EmberUploader.S3Uploader.create();
10
11      if (!Ember.isEmpty(files))
12        uploader.upload(files[0]);
13    }.observes('files');
14
15  });

На этом этапе откройте консоль браузера, загрузите файл и отправьте форму. Если вы проверите вкладку сети, вы должны увидеть отклоненный запрос к вашему серверу rails по пути «/ sign» с ошибкой 404. Это связано с тем, что AWS требует подписи для загрузки в вашу корзину, также известную как дайджест JSON в кодировке base64 ваших секретных токенов S3. Хотя наши изображения будут поступать напрямую в Amazon, мы не хотим раскрывать фактические ключи браузеру клиента, поэтому в Rails отправляется запрос на получение закодированной подписи.

Создание корзины AWS и генерация подписи в Rails

Если вы еще не создали корзину в AWS, самое время! Зайдите на https://aws.amazon.com и зарегистрируйтесь.

Моя учетная запись ›Консоль управления AWS› Сервисы ›S3› Create Bucket

Как только ваша корзина будет создана, перейдите в раздел «Учетные данные безопасности» и запишите ключи доступа. Их должно быть два: идентификатор ключа доступа и секретный ключ доступа.

Следующие несколько шагов мы будем работать в Rails. Добавьте гем Figaro в Gemfile своего приложения и запустите в терминале следующее:

$ bundle install
$ rails generate figaro:install

Это создаст файл application.yml (в котором хранятся ключи AWS) и сообщит вашему Gitignore не публиковать файл в Github, тем самым избежав очень неприятного и дорогостоящего сценария взлома вашей учетной записи AWS. Содержимое файла application.yml должно быть следующим:

# config/application.yml
1  AWS_ACCESS_KEY_ID: MY-ACCESS-KEY-ID-HERE
2  AWS_SECRET_ACCESS_KEY: MY-SECRET-ACCESS-KEY-HERE

Помните, как Ember отправлял запрос по маршруту «/ sign»? Ошибка 404 связана с тем, что маршрута еще не существует - перейдите в файл routes.rb и добавьте его. Мой JSON обслуживается с http: // localhost: 3000 / api / v1, вы заметите, что файлы Rails ниже учитывают это пространство имен.

# config/routes.rb
1  Rails.application.routes.draw do
2    namespace :api do
3      namespace :v1 do
4
5        get 'sign', controller: 'sign'
6
7      end
8    end
9  end

Последним шагом в Rails является создание контроллера для маршрута / sign, который упоминается выше в строке 5. Эта часть в значительной степени скопирована и вставлена ​​из вики-страницы Ember Uploader. Вставьте все, что ниже, в контроллер знака и вставьте имя своей корзины AWS (ниже, строка 35).

# config/controllers/api/v1/sign_controller.rb
 1  module Api
 2    module V1
 3      class SignController < ApplicationController
 4      
 5        def sign
 6          @expires = 10.hours.from_now.utc.iso8601
 7          render json: {
 8          acl: 'public-read',
 9          awsaccesskeyid: ENV['AWS_ACCESS_KEY_ID'],
10          bucket: 'sump',
11          expires: @expires,
12          key: "uploads/#{params[:name]}",
13          policy: policy,
14          signature: signature,
15          success_action_status: '201',
16          'Content-Type' => params[:type],
17          'Cache-Control' => 'max-age=630720000, public'
18          }, status: :ok
19        end
20      
21        def signature
22          Base64.strict_encode64(
23          OpenSSL::HMAC.digest(
24            OpenSSL::Digest::Digest.new('sha1'),
25            ENV['AWS_SECRET_ACCESS_KEY'],
26            policy({ secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] })
27            )
28          )
29        end
30
31        def policy(options = {})
32          Base64.strict_encode64({
33            expiration: @expires,
34            conditions: [
35              { bucket: 'INSERT-BUCKET-NAME-HERE' },
36              { acl: 'public-read' },
37              { expires: @expires },
38              { success_action_status: '201' },
39              [ 'starts-with', '$key', '' ],
40              [ 'starts-with', '$Content-Type', '' ],
41              [ 'starts-with', '$Cache-Control', '' ],
42              [ 'content-length-range', 0, 524288000 ]
43              ]
44            }.to_json
45          )
46        end
47      end
48    end
49  end

Отправка наших файлов из Ember в AWS

На этом этапе мы вернулись в Ember и хотим подтвердить, что получаем правильную подпись от Rails. Сообщите Ember, куда отправить запрос, вставив адрес маршрута Rails «/ sign» в метод create загрузчика (ниже, строка 10).

# app/components/s3-upload.js
 1  import Ember from 'ember';
 2  import EmberUploader from 'ember-uploader';
 3
 4  export default EmberUploader.FileField.extend({
 5   
 6    filesDidChange: function() {
 7      let uploadUrl = this.get('url'),
 8      files = this.get('files'),
 9      uploader = EmberUploader.S3Uploader.create({
10        url: "http://localhost:3000/api/v1/sign" });         
11      if (!Ember.isEmpty(files))
12        uploader.upload(files[0]);
13    }.observes('files');
14
15  });

На этом этапе, если вы снова попытаетесь загрузить файл через браузер, консоль должна показать, что запрос подписи Rails выполнен успешно. Продвигается! Однако сразу после этого появится новый запрос, который будет отправлен на Amazon и завершится ошибкой «403 Forbidden». Причина - ошибка совместного использования ресурсов между источниками (CORS), потому что мы, ведро AWS, не знаем, разрешать запросы с сервера Ember.

Вернитесь к настройкам сегмента AWS и на вкладке разрешений отредактируйте конфигурацию CORS. Вам нужно будет отредактировать конфигурацию, чтобы разрешить запросы GET, PUT и POST от Ember. Ниже приведены настройки, которые у меня сработали, но обязательно ознакомьтесь с документацией, чтобы узнать больше о настройке CORS для вашей среды.

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>http://*.localhost:4200</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
    <CORSRule>
        <AllowedOrigin>http://localhost:4200</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <AllowedMethod>POST</AllowedMethod>
        <AllowedMethod>PUT</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>*</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

Помимо CORS, вам также может потребоваться добавить политику корзины. Конфигурация также находится на вкладке разрешений корзины AWS, проверьте «Изменить политику корзины». Есть варианты для автоматической генерации политик, но моя выглядела так:

{
 "Version": "2012-10-17",
 "Statement": [
  {
   "Sid": "",
   "Effect": "Allow",
   "Principal": "*",
   "Action": [
    "s3:ListBucket",
    "s3:PutObject",
    "s3:AbortMultipartUpload",
    "s3:PutObjectAcl",
    "s3:GetObject",
    "s3:DeleteObject",
    "s3:GetObjectVersion"
   ],
   "Resource": [
    "arn:aws:s3:::INSERT-BUCKET-NAME-HERE/*",
    "arn:aws:s3:::INSERT-BUCKET-NAME-HERE"
   ]
  }
 ]
}

На этом этапе файлы должны успешно загружаться в AWS. Попробуйте загрузить файл через форму Ember, а затем проверьте свою корзину, чтобы убедиться, что файл успешно попал на ваш сервер AWS.

Сохранение ассоциаций загрузок и записей

База данных должна хранить ссылку на URL-адрес AWS каждого файла, убедитесь, что есть строковое свойство для его сохранения в ваших моделях как в Ember, так и в Rails. Мой просто называется «имидж».

После того, как Ember отправит запрос POST в AWS и загрузит файл, в ваше приложение вернется ответ. Это может выглядеть примерно так:

Контроллер s3-upload будет анализировать ответ, захватив местоположение и ключ, которые затем могут быть проанализированы на предмет заголовка, который будет включен в параметры формы.

Сначала в поле загрузки файла добавляется свойство onComplete.

# app/templates/your-form-template.js
1  <form>
2    {{s3-upload onComplete="imageUploadComplete"}}
3    <button {{action 'save'}} id="submit">Submit</button>
4  </form>

Необходимо обновить контроллер s3-upload для учета onComplete (ниже, строка 5), а функцию filesDidComplete можно изменить для интерпретации ответа AWS.

# app/components/s3-upload.js
 1  import Ember from 'ember';
 2  import EmberUploader from 'ember-uploader';
 3
 4  export default EmberUploader.FileField.extend({
 5    onComplete: 'onComplete',
 6    filesDidChange: function() {
 7      let uploadUrl = this.get('url');
 8      let files = this.get('files');
 9      let uploader = EmberUploader.S3Uploader.create({
10        url: "http://localhost:3000/api/v1/sign" });
11      });
12      uploader.on('didUpload', function(response) {
13        let res = $(response);
14        let fullUrl = decodeURIComponent(
15                        res.find('Location')[0].textContent
16                      );
17        let key = decodeURIComponent(
18                    res.find('Key')[0].textContent
19                  );  
16        _this.sendAction('onComplete', {
17          fullUrl: fullUrl, key: key
18        }); 
19      });
20       
21      if (!Ember.isEmpty(files))
22        uploader.upload(files[0]);
23    }.observes('files');
24
25  });

Выше, в строке 12, экземпляру загрузчика сообщается, что он выполняет функцию в ответе AWS после его загрузки; didUpload - это бесплатная функция от Ember Uploader. Здесь он запускает действие onComplete, отправляя проанализированный ответ в виде хэша на контроллер нашей формы через imageUploadComplete.

# app/controllers/your-form-controller.js
...
actions: {
    imageUploadComplete: function(details) {
      this.get('model').set('image', details["fullUrl"])
    },
...

На этом последнем шаге ваши контроллеры Ember должны передавать URL-адрес файла на rails в форме params, пока ваш контроллер модели rails принимает данные для вашего поля загрузки файла.

Ресурсы:

Загрузчик Ember
https://github.com/benefitcloud/ember-uploader

Ember Uploader: настройка на стороне сервера в Rails
https://github.com/benefitcloud/ember-uploader/wiki/S3-Server-Setup

BuildLab: EmberJS Screencast
https://youtu.be/5MxJl4ZA0Us