Rails - загрузка больших файлов прямо в S3 с помощью JQuery File Upload (размещено на Heroku)

Я использую Heroku, а это значит, что мне нужно загрузить несколько больших файлов напрямую на S3. Я использую Rails 3.2.11 и Ruby 1.9.3. Я не хочу использовать драгоценные камни несущей волны или скрепок, или действительно сильно менять на этом этапе - мне просто нужно получить то, что у меня работает.

Прежде чем пытаться перейти на S3, если бы я запускал свое приложение локально, я мог бы загрузить несколько больших файлов в локальную файловую систему. Когда я запускал его на Heroku, небольшие файлы загружались, а большие не удавалось. Отсюда и переход на S3 ..

Я пробовал несколько настроек, а также эту ссылку ниже, но это слишком большое изменение того, что у меня есть, которое уже работает с файловой системой локального сервера (и Heroku тоже, но Heroku просто может ' t обрабатывать большие файлы ..)

Пробовал: https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails

Я пробовал некоторые другие примеры здесь, на Stack Overflow, но они слишком сильно меняют то, что работает локально, и что ж, я не понимаю всего, что они делают.

Что происходит, когда я пытаюсь загрузить изображения?

Как будто загрузка файла работает - изображения для предварительного просмотра успешно созданы, но на Amazon s3 никогда ничего не загружается, и я не получаю никаких сообщений об ошибках (например, сбой аутентификации s3 или что-то в этом роде ... ничего)

Что мне нужно изменить, чтобы передать файлы в мое хранилище s3, и что я могу записать на консоль, чтобы обнаружить проблемы, если таковые имеются, при подключении к моему s3?

Моя форма:

        <%= form_for @status  do |f| %>

        {A FEW HTML FIELDS USED FOR A DESCRIPTION OF THE FILES - NOT IMPORTANT FOR THE QUESTION}

        File:<input id="fileupload"  multiple="multiple"  name="image" 
            type="file"  data-form-data = <%= @s3_direct_post.fields%> 
            data-url= <%= @s3_direct_post.url %> 
            data-host =<%=URI.parse(@s3_direct_post.url).host%> >   
        <%= link_to 'submit', "#", :id=>'submit' , :remote=>true%>

        <% end %>

Мой jquery:

....
  $('#fileupload').fileupload({
      formData: {
                 batch: createUUID(),
                  authenticity_token:$('meta[name="csrf-token"]').attr('content')
                    },
      dataType: 'json',
      acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i,
              maxFileSize: 5000000, // 5 MB
              previewMaxWidth: 400,
              previewMaxHeight: 400,
              previewCrop: true,
      add: function (e, data) {

      tmpImg.src = URL.createObjectURL(data.files[0]) ; // create image preview 
      $('#'+ fn + '_inner' ).append(tmpImg);

    ...

Мой контроллер:

def index
#it's in the index just to simplify getting it working 

 @s3_direct_post = S3_BUCKET.presigned_post(key: "uploads/#{SecureRandom.uuid}/${filename}", success_action_status: '201', acl: 'public-read')

end

Элемент, который создается для формы (через Inspect Element):

        <input id="fileupload" multiple="multiple" name="image" 
    data-form-data="{&quot;key&quot;=>&quot;uploads/34a64607-8d1b-4704-806b-159ecc47745e/${filename}&quot;," &quot;success_action_status&quot;="
    >&quot;201&quot;," &quot;acl&quot;=">&quot;public-read&quot;," &quot;policy&quot;=">&quot;[encryped stuff - no need to post]&quot;,"
     &quot;x-amz-credential&quot;=">&quot;
[AWS access key]/[some number]/us-east-1/s3/aws4_request&quot;
," &quot;x-amz-algorithm&quot;=">&quot;AWS4-HMAC-SHA256&quot;
," &quot;x-amz-date&quot;=">&quot;20150924T234656Z&quot;
," &quot;x-amz-signature&quot;=">&quot;[some encrypted stuff]&quot;}"
data-url="https://nunyabizness.s3.amazonaws.com" data-host="nunyabizness.s3.amazonaws.com" type="file">

Помощь!


person Ruben Obregon    schedule 24.09.2015    source источник


Ответы (1)


С S3 на самом деле нет простых готовых решений для загрузки файлов, потому что Amazon - довольно сложный инструмент.

Раньше у меня была аналогичная проблема, и я потратил две недели, пытаясь понять, как работает S3, а теперь использую рабочее решение для загрузки файлов на S3. Я могу сказать вам решение, которое работает для меня, я никогда не пробовал то, что было предложено Heroku. Я предпочитаю плагин Plupload, поскольку это единственный компонент, который мне действительно удалось заставить работать, помимо простой прямой загрузки S3 через XHR, и предлагает использование процентных индикаторов и изменение размера изображения в браузере, что я считаю совершенно обязательным. для производственных приложений, где у некоторых пользователей есть изображения размером 20 МБ, которые они хотят загрузить в качестве аватара.

Некоторые основы S3:

Шаг 1

Корзина Amazon нуждается в правильной конфигурации в своем CORS-файле, чтобы в первую очередь разрешить внешние загрузки. Тотториал Heroku уже рассказал вам, как разместить конфигурацию в нужном месте. http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html

Шаг 2

Требуются данные политики, иначе ваш клиент не сможет получить доступ к соответствующему файлу корзины. Я считаю, что создание политик лучше выполнять с помощью вызовов Ajax, чтобы, например, администратор мог загружать файлы в папки разных пользователей. В моем примере cancan используется для управления безопасностью данного пользователя, а figaro используется для управления переменными ENV.

def aws_policy_image
  user = User.find_by_id(params[:user_id])
  authorize! :upload_image, current_user
  options = {}
  bucket = Rails.configuration.bucket
  access_key_id = ENV["AWS_ACCESS_KEY_ID"]
  secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
  options[:key] ||= "users/" + params[:user_id] # folder on AWS to store file in
  options[:acl] ||= 'private'
  options[:expiration_date] ||= 10.hours.from_now.utc.iso8601
  options[:max_filesize] ||= 10.megabytes
  options[:content_type] ||= 'image/' # Videos would be binary/octet-stream
  options[:filter_title] ||= 'Images'
  options[:filter_extentions] ||= 'jpg,jpeg,gif,png,bmp'
  policy = Base64.encode64(
    "{'expiration': '#{options[:expiration_date]}',
      'conditions': [
        {'x-amz-server-side-encryption': 'AES256'},
        {'bucket': '#{bucket}'},
        {'acl': '#{options[:acl]}'},
        {'success_action_status': '201'},
        ['content-length-range', 0, #{options[:max_filesize]}],
        ['starts-with', '$key', '#{options[:key]}'],
        ['starts-with', '$Content-Type', ''],
        ['starts-with', '$name', ''],
        ['starts-with', '$Filename', '']
      ]
    }").gsub(/\n|\r/, '')

  signature = Base64.encode64(
    OpenSSL::HMAC.digest(
      OpenSSL::Digest::Digest.new('sha1'),
      secret_access_key, policy)).gsub("\n", "")
  render :json => {:access_key_id => access_key_id, :policy => policy, :signature => signature, :bucket => bucket}
end

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

Шаг 3

Frontend, получите plupload: http://www.plupload.com/ создайте ссылку, которая будет действовать как кнопка загрузки :

<a id="upload_button" href="#">Upload</a>

Создайте сценарий, который настраивает инициализацию plupload.

function Plupload(config_x, access_key_id, policy, signature, bucket) {
  var $this = this;
  $this.config = $.extend({
  key: 'error',
  acl: 'private',
  content_type: '',
  filter_title: 'Images',
  filter_extentions: 'jpg,jpeg,gif,png,bmp',
  select_button: "upload_button",
  multi_selection: true,
  callback: function (params) {
  },
  add_files_callback: function (up, files) {
  },
  complete_callback: function (params) {
  }
}, config_x);
$this.params = {
  runtimes: 'html5',
  browse_button: $this.config.select_button,
  max_file_size: $this.config.max_file_size,
  url: 'https://' + bucket + '.s3.amazonaws.com/',
  flash_swf_url: '/assets/plupload/js/Moxie.swf',
  silverlight_xap_url: '/assets/plupload/js/Moxie.xap',
  init: {
    FilesRemoved: function (up, files) {
      /*if (up.files.length < 1) {
       $('#' + config.select_button).fadeIn('slow');
       }*/
    }
  },
  multi_selection: $this.config.multi_selection,
  multipart: true,
  // resize: {width: 1000, height: 1000}, // currently causes "blob" problem
  multipart_params: {
    'acl': $this.config.acl,
    'Content-Type': $this.config.content_type,
    'success_action_status': '201',
    'AWSAccessKeyId': access_key_id,
    'x-amz-server-side-encryption': "AES256",
    'policy': policy,
    'signature': signature
  },
// Resize images on clientside if we can
  resize: {
    preserve_headers: false, // (!)
    width: 1200,
    height: 1200,
    quality: 70
  },
  filters: [
    {
      title: $this.config.filter_title,
      extensions: $this.config.filter_extentions
    }
  ],
  file_data_name: 'file'
};
$this.uploader = new plupload.Uploader($this.params);
$this.uploader.init();

$this.uploader.bind('UploadProgress', function (up, file) {
  $('#' + file.id + ' .percent').text(file.percent + '%');
});

// before upload
$this.uploader.bind('BeforeUpload', function (up, file) {
  // optional: regen the filename, otherwise the user will upload image.jpg that will overwrite each other
  var extension = file.name.split('.').pop();
  var file_name = extension + "_" + (+new Date);
  up.settings.multipart_params.key = $this.config.key + '/' + file_name + '.' + extension;
  up.settings.multipart_params.Filename = $this.config.key + '/' + file_name + '.' + extension;
  file.name = file_name + '.' + extension;
});

// shows error object in the browser console (for now)
$this.uploader.bind('Error', function (up, error) {
  console.log('Expand the error object below to see the error. Use WireShark to debug.');
  alert_x(".validation-error", error.message);
});

// files added
$this.uploader.bind('FilesAdded', function (up, files) {
  $this.config.add_files_callback(up, files, $this.uploader);
  // p(uploader);
  // uploader.start();
});

// when file gets uploaded
$this.uploader.bind('FileUploaded', function (up, file) {
  $this.config.callback(file);
  up.refresh();
});

// when all files are uploaded
$this.uploader.bind('UploadComplete', function (up, file) {
  $this.config.complete_callback(file);
  up.refresh();
});
}
Plupload.prototype.init = function () {
  //
}

Шаг 4

Реализация универсальной функции загрузчика файлов:

ImageUploader = {
  init: function (user_id, config, callback) {
  $.ajax({
      type: "get",
      url: "/aws_policy_image",
      data: {user_id: user_id},
      error: function (request, status, error) {
      alert(request.responseText);
    },
    success: function (msg) {
      // set aws credentials
      callback(config, msg);
    }
  });
},
},
// local functions
photo_uploader: function (user_id) {
  var container = "#photos .unverified_images" // for example;
  var can_render = false;
  this.init(user_id,
    {
      select_button: "upload_photos",
      callback: function (file) {
        file.aws_id = file.id;
        file.id = "0";
        file.album_title = "userpics"; // I use this param to manage photo directory
        file.user_id = user_id;
        //console.log(file);
        [** your ajax code here that saves the image object in the database via file variable you get here **]
      });
    },
    add_files_callback: function (up, files, uploader) {
      $.each(files, function (index, value) {
        // do something like adding a progress bar html
      });
      uploader.start();
    },
    complete_callback: function (files) {
      can_render = true;
    }
  }, function (config, msg) {
    config.key = "users/" + user_id;
    // Most important part:
    window.photo_uploader = new Plupload(config, msg.access_key_id, msg.policy, msg.signature, msg.bucket);
  });
}

can_render переменная полезна для того, чтобы вы могли создать приложение только после того, как повторно отрендерили страницу, когда загрузчик на самом деле закончил.

А чтобы кнопка работала откуда-то еще, позвоните:

ImageUploader.photo_uploader(user_id);

И кнопка будет действовать как кнопка загрузчика Plupload. Важно то, что Политика составлена ​​таким образом, чтобы никто не мог загрузить фотографию в чужой каталог. Было бы здорово иметь версию, которая делает то же самое не через обратные вызовы ajax, а с веб-хуками, это то, чем я хочу заниматься в будущем.

Опять же, это не идеальное решение, но, судя по моему опыту, оно работает достаточно хорошо для загрузки изображений и видео на Amazon.

Примечание. Если кто-то спросит, почему у меня такая сложная объектно-ориентированная структура объектов загрузчика, причина в том, что в моем приложении есть все виды загрузчиков, которые ведут себя по-разному, и им нужен инициализатор с обычным поведением. . Как я это сделал, я могу написать инициализатор, скажем, для видео, с минимальным количеством кода, который будет делать то же самое, что и существующий загрузчик изображений.

person Vitaly Stanchits    schedule 30.09.2015
comment
круто .. не уверен, что это помогает мне, но я разберусь с этим .. большое спасибо, и мне нравится ваше объектно-ориентированное решение .. - person Ruben Obregon; 01.10.2015