Rails - 使用Jquery文件上载(在Heroku上托管)将大文件直接上传到S3

时间:2015-09-24 23:59:30

标签: ruby-on-rails-3 heroku amazon-s3 jquery-file-upload

我正在使用Heroku,这意味着我必须直接将多个大文件上传到S3 ..我使用的是Rails 3.2.11和Ruby 1.9.3。我不想使用carrierwave或回形针宝石,或者在这一点上真的改变很多 - 我只需要得到我的工作。

在尝试迁移到S3之前,如果我在本地运行我的应用程序,我可以将多个大文件上传到本地文件系统。当我在Heroku上运行它时,小文件上传但大文件失败。因此切换到S3 ..

我尝试了几个调整,以及下面的这个链接,但这只是对我已经已经使用本地服务器的文件系统(和Heroku,但是Heroku)的更改只是无法处理大文件..)

尝试:https://devcenter.heroku.com/articles/direct-to-s3-image-uploads-in-rails

我在Stack Overflow上尝试了其他一些例子,但是对于本地工作的东西来说,它们太多了,而且,我并没有掌握他们正在做的所有事情。

现在,当我尝试上传图片时会发生什么?

好像文件上传工作 - 预览图像已成功创建,但没有任何内容上传到亚马逊s3,我没有收到任何类型的错误消息(如s3身份验证失败或任何事情......没有)< / p>

为了将文件传输到我的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">

帮助!

1 个答案:

答案 0 :(得分:2)

使用S3实际上没有简单易用的上传文件解决方案,因为亚马逊是一个相当复杂的工具。

我在当天遇到了类似的问题,并花了两周的时间试图弄清楚S3是如何工作的,现在使用一种有效的解决方案将文件上传到S3。我可以告诉你一个适合我的解决方案,我从未尝试过Heroku提出的解决方案。 我使用的插件是Plupload,因为它是我实际设法工作的唯一组件,除了通过XHR简单的直接S3上传,并提供百分比指标的使用和浏览器中的图像大小调整,我发现完全是强制性的对于生产应用程序,一些用户有20mb图像,他们想要上传为他们的头像。

S3中的一些基础知识:

第1步

亚马逊存储桶需要在其CORS文件中进行正确配置,以便首先进行外部上传。 Heroku totorial已经告诉你如何将配置放在正确的位置。 http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html

第2步

需要策略数据,否则您的客户端将无法访问相应的存储桶文件。我发现通过Ajax调用可以更好地完成生成策略,因此,例如,admin可以将文件上载到不同用户的文件夹中。 在我的示例中,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步

前端,获取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回调来做同样的事情会很棒,但是使用web钩子,这是我将来要做的事情。

同样,这不是一个完美的解决方案,但是根据我的经验,将图像和视频上传到亚马逊上的效果非常好。

注意如果有人问为什么我有这个复杂的面向对象的上传器对象结构,原因是我的应用程序有各种不同类型的上传者,他们需要有一个初始化器有共同的行为。我这样做的方式,我可以用最少量的代码为视频编写一个初始化器,它将与现有的图像上传器做类似的事情。