Rails直接上传到Amazon S3

29

我希望为我的Rails应用程序添加功能,以直接上传文件到Amazon S3。根据我的研究,普遍的共识似乎是使用 s3-swf-upload-plugin 插件。我已经使用这个gem设置了一个示例应用程序,但我无法仅允许选择单个文件。我还希望在上传后创建记录并使用paperclip创建缩略图,但关于此方面的指导很少。

因此,我的问题是:

(1) 我是否正确使用该gem,或者应该采取其他方法?

(2) 是否有任何可以用作参考的样本代码?

非常感谢您的任何帮助。

Chris


2
请注意,如果您在Heroku上托管并打算上传大文件,则会出现30秒请求超时的情况,这将终止任何大型上传。我在下面发布了一个答案,其中包含一些示例项目,可以直接上传到S3,从而避免此问题。 - iwasrobbed
6个回答

24

尝试使用名为CarrierWaveDirect的新宝石,它允许您使用HTML表单直接将文件上传到S3,并轻松将图像处理移入后台进程。


+1 太棒了,一直在等待这样的东西,可以集成到 CarrierWave 中。谢谢。 - Hishalv
非常感谢你的指引,dwilkie - 我一定会好好研究那个宝贝。 - Chris Hilton
1
这很棒,但有没有构建POST的示例是在Ruby之外,例如:iOS? - wprater

6

是的!这个宝石是最好的。非常感谢Ryan Bates和那个宝石的维护者。 - maletor

3

1

如果您正在使用Rails 3,请查看我的示例项目:

使用Rails 3、Flash和基于MooTools的FancyUploader的示例项目,直接上传到S3:https://github.com/iwasrobbed/Rails3-S3-Uploader-FancyUploader

使用Rails 3、Flash/Silverlight/GoogleGears/BrowserPlus和基于jQuery的Plupload的示例项目,直接上传到S3:https://github.com/iwasrobbed/Rails3-S3-Uploader-Plupload

顺便说一下,您可以使用类似于这篇博客文章描述的方法使用Paperclip进行后处理:

http://www.railstoolkit.com/posts/fancyupload-amazon-s3-uploader-with-paperclip


0

我在Rails中使用了Heroku的直接上传到S3解决方案(它使用jQuery-File-Uploadaws-sdk gem),以便可以通过ajax远程上传到S3。希望这对你有用:

posts_controller.rb

before_action :set_s3_direct_post, only: [:index, :create]
before_action :delete_picture_from_s3, only: [:destroy]

class PostsController < ApplicationController

  def index
    .
    .
  end

  def create
    @post = @user.posts.build(post_params)
    if @post.save
      format.html
      format.js
    end
  end

  def destroy
    Post.find(params[:id]).destroy
  end

  private

    def set_s3_direct_post
      return S3_BUCKET.presigned_post(key: "uploads/#{SecureRandom.uuid}/${filename}", success_action_status: '201', acl: 'public-read')
    end    

    def delete_picture_from_s3
      key = params[:picture_url].split('amazonaws.com/')[1]
      S3_BUCKET.object(key).delete
      return true
      rescue => e
        # If anyone knows a good way to deal with a defunct file sitting in the bucket, please speak up.
        return true
    end

    def post_params
      params.require(:post).permit(:content, :picture_url)
    end

end

posts.html.erb

<div class="info"      data-url="<%= @s3_direct_post.url %>"
                  data-formdata="<%= (@s3_direct_post.fields.to_json) %>"
                      data-host="<%= URI.parse(@s3_direct_post.url).host %>">
</div>

表单

<%= form_for(:post, url: :posts, method: :post,
              html: { class: "post_form", id: "post_form-#{post.id}" }
            ) do |f| %>
  <%= f.text_area :content, id: "postfield-#{post.id}", class: "postText" %>
  <%= f.button( :submit, name: "Post", title: "Post" ) do %>
    <span class="glyphicon glyphicon-ok" aria-hidden="true"></span>
  <% end %>
  <span class="postuploadbutton" id="postUp-<%= post.id %>" title="Add file" >
    <span class="glyphicon glyphicon-upload" aria-hidden="true"></span>
  </span>
  <span title="Cancel file" class="noticecancelupload" id="postCancel-<%= post.id %>" >
    <span class="glyphicon glyphicon-remove-circle" aria-hidden="true"></span>
  </span>
  <%= f.file_field :picture_url, accept: 'image/jpeg,image/gif,image/png', 
               class: "notice_file_field", id: "postFile-#{post.id}" %>
<% end %>

_post.html.erb

<%= button_to post_path(
                      params: {
                        id: post.id,
                        picture_url: post.picture_url
                      }
                    ),
                    class: 'btn btn-default btn-xs blurme',
                    data: { confirm: "Delete post: are you sure?" },
                    method: :delete do %>
        <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
<% end %>

_post.html.erb中的Javascript

$(document).off('click',"#postUp-<%= post.id %>");
$(document).on('click', '#postUp-<%= post.id %>', function(e) {
  prepareUpload("#post_form-<%= post.id %>");
  $('#postFile-<%= post.id %>').trigger("click");
});

$(document).off('click',"#postCancel-<%= post.id %>");
$(document).on('click', '#postCancel-<%= post.id %>', function(e) {
  $(".appendedInput").remove(); //  $('#postFile-<% post.id %>').val(""); doesn't work for me
  $('.progBar').css('background','white').text("");
});

$(document).off('submit',"#post_form-<%= post.id %>"); // without this the form submitted multiple times in production
$(document).on('submit', '#post_form-<%= post.id %>', function(e) { // don't use $('#post_form-<%= post.id %>').submit(function() { so it doesn't bind to the #post_form (so it still works after ajax loading)
  e.preventDefault(); // prevent normal form submission
  if ( validatePostForm('<%= post.id %>') ) {
    $.ajax({
      type: 'POST',
      url:  $(this).attr('action'),
      data: $(this).serialize(),
      dataType: 'script'
    });
    $('#postCancel-<%= post.id %>').trigger("click");
  }
});

function validatePostForm(postid) {
  if ( jQuery.isBlank($('#postfield-' + postid).val()) && jQuery.isBlank($('#postFile-' + postid).val()) ) {
    alert("Write something fascinating or add a picture.");
    return false;
  } else {
    return true;
  }
}

application.js 中的 Javascript

function prepareUpload(feckid) {
  $(feckid).find("input:file").each(function(i, elem) {
    var fileInput    = $(elem);
    var progressBar  = $("<div class='progBar'></div>");
    var barContainer = $("<div class='progress'></div>").append(progressBar);
    fileInput.after(barContainer);
    var maxFS = 10 * 1024 * 1024;

    var info             = $(".info");
    var urlnumbnuts      = info.attr("data-url");
    var formdatanumbnuts = jQuery.parseJSON(info.attr("data-formdata"));
    var hostnumbnuts     = info.attr("data-host");

    var form             = $(fileInput.parents('form:first'));

    fileInput.fileupload({
      fileInput:        fileInput,
      maxFileSize:      maxFS,
      url:              urlnumbnuts,
      type:             'POST',
      autoUpload:       true,
      formData:         formdatanumbnuts,
      paramName:        'file',
      dataType:         'XML',
      replaceFileInput: false,
      add: function (e, data) {
        $.each(data.files, function (index, file) {
          if (file.size > maxFS) {
            alert('Alas, the file exceeds the maximum file size of 10MB.');
            form[0].reset();
            return false;
          } else {
            data.submit();
            return true;
          }
        });
      },
      progressall: function (e, data) {
        var progress = parseInt(data.loaded / data.total * 100, 10);
        progressBar.css('width', progress + '%')
      },
      start: function (e) {
        progressBar.
          css('background', 'orange').
          css('display', 'block').
          css('width', '0%').
          text("Preparing...");
      },
      done: function(e, data) {
        var key   = $(data.jqXHR.responseXML).find("Key").text();
        var url   = '//' + hostnumbnuts + '/' + key;
        var input = $('<input />', { type:'hidden', class:'appendedInput', 
                     name: fileInput.attr('name'), value: url });
        form.append(input);
        progressBar.
          css('background', 'green').
          text("Ready");
      },
      fail: function(e, data) {
        progressBar.
          css("background", "red").
          css("color", "black").
          text("Failed");
      }
    });
  });
} // function prepareUpload()

create.js.erb

$(".info").attr("data-formdata",  '<%=raw @s3_direct_post.fields.to_json   %>'); // don't use .data() to set attributes 
$(".info").attr("data-url",       "<%= @s3_direct_post.url                 %>");
$(".info").attr("data-host",      "<%= URI.parse(@s3_direct_post.url).host %>");

$('.post_form')[0].reset();
$('.postText').val('');

application.js

//= require jquery-fileupload/basic

config/initializers/aws.rb

Aws.config.update({
  region: 'us-east-1',
  credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']),
})
S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET'])

注意:

此解决方案适用于 index.html.erb 页面上的多个帖子表单。这就是为什么将 @s3_direct_post 信息放置在 index.html.erb 中 class 为 info 的 div 中,而不是每个帖子表单中的原因。这意味着页面上只有一个 @s3_direct_post 在任何时候都会显示,而与页面上的表单数量无关。只有在单击文件上传按钮时才会抓取 @s3_direct_post 中的数据(使用 prepareUpload() 调用)。提交后,在 posts 控制器中生成一个新的 @s3_direct_post,并通过 create.js.erb 更新 .info 中的信息。将 @s3_direct_post 数据存储在表单中意味着可以同时存在许多不同的 @s3_direct_post 实例,从而导致文件名生成错误。

您需要在帖子控制器索引操作(准备第一次上传)和创建操作(准备第二次及随后的上传)中都设置 :set_s3_direct_post

通过e.preventDefault();阻止了普通表单提交,因此可以使用$.ajax({手动进行。为什么不在表单中使用remote: true呢?因为在Rails中,文件上传是通过HTML请求和页面刷新来完成的,即使您尝试远程上传也是如此。

使用info.attr()而不是info.data()来设置和检索@s3_direct_post属性,因为info.data不会得到更新(例如,请参见this问题)。这意味着您还必须手动使用jQuery.parseJSON()将属性解析为对象(.data()实际上会自动执行此操作)。

在application.js中不要使用//= require jquery-fileupload。这个bug真的很难识别(请参见这里)。直到我改变了这个,原始Heroku解决方案才起作用。


-1

您可以使用Paperclip上传到S3(请参见文档),并创建缩略图,尽管它首先上传到临时文件夹,之后可以应用图像处理,然后再将文件上传到S3。

至于此类配置的示例,在博客圈和StackOverflow上有很多,例如这里


我认为Paperclip不能直接上传到S3。与Carrierwave一样,Paperclip会将文件上传到服务器的tmp目录进行处理,然后再上传到S3。这意味着,服务器将占用整个http请求处理程序,直到上传完成。在某些情况下,这可能会“冻结”应用程序。 - Christian Fazzini
首先,它会先上传到 tmp 文件夹,以便进行图像处理(例如调整大小、创建适当的缩略图等),然后再上传到 S3。由于主题发起人需要图像后处理,因此这种解决方案似乎比“直接将原始文件上传到 S3 -> 下载它 -> 创建缩略图 -> 将缩略图上传到 S3”更好。将上传到 S3 的过程移至后台进程可能会解决应用程序“冻结”的问题。 - buru
谢谢大家的评论。Paperclip上传到tmp文件夹的另一个问题是,在像Heroku这样的网站上托管时,它们会限制上传文件的大小。 - Chris Hilton
Paperclip不允许直接上传到S3,你应该编辑你的答案。 - Yarin
编辑了一个答案。在这里留下它,以防有人寻找通过临时文件夹间接上传到S3的方法,但不知何故偶然发现了这个页面。 - buru

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接