【问题标题】:Rails direct upload to Amazon S3Rails 直接上传到 Amazon S3
【发布时间】:2011-09-01 21:28:31
【问题描述】:

我希望向我的 Rails 应用程序添加功能,以将文件直接上传到 Amazon S3。根据我的研究,普遍的共识似乎是使用s3-swf-upload-plugin。我已经使用该 gem 设置了一个示例应用程序,但是仅允许选择单个文件,我无法让它很好地发挥作用。我还想创建一个记录上传并使用回形针创建一个缩略图,我可以找到一些指导。

所以我的问题是:

(1) 我是在使用该 gem 的正确轨道上还是应该采取另一种方法?

(2) 有没有我可以参考的样本?

任何帮助将不胜感激。

克里斯

【问题讨论】:

  • 请记住,如果您在 Heroku 上托管并打算上传大文件,他们会有 30 秒的请求超时,这将终止任何大型上传。我在下面发布了一些示例项目的答案,可以直接上传到 S3,从而绕过这个问题。

标签: ruby-on-rails upload amazon-s3


【解决方案1】:

尝试一个名为CarrierWaveDirect 的新 Gem,它允许您使用 html 表单将文件直接上传到 S3,并轻松地将图像处理移动到后台进程中

【讨论】:

  • +1 伟大的宝石,一直在等待这样的东西,它将集成到carrierwave。谢谢
  • 非常感谢那个引导 dwilkie - 一定会看看那个宝石
  • 这很好,但是任何关于如何在 Ruby 之外构建 POST 的示例,即:iOS?
【解决方案2】:

不确定您是否可以轻松地将其修改为一次只上传一个文件,但这个 gem 对我来说非常有用。它基于Ryan Bates' Railscast之一:

https://github.com/waynehoover/s3_direct_upload

【讨论】:

  • 是的!这颗宝石是最好的。非常感谢 Ryan Bates 和该 gem 的维护者。
【解决方案3】:

【讨论】:

    【解决方案4】:

    如果您使用的是 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

    【讨论】:

      【解决方案5】:

      我已经在 Rails 中调整了 Heroku's direct to S3 upload solution(使用 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 内的 info 类的 div 中,而不是在每个帖子表单中。这意味着无论页面上的表单数量如何,任何时候页面上都只会出现一个@s3_direct_post@s3_direct_post 中的数据只有在单击文件上传按钮时才会被抓取(通过调用 prepareUpload())。提交后,新的@s3_direct_post 会在posts 控制器中生成,.info 中的信息由 create.js.erb 更新。将@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。这个错误是一个真正难以识别的问题(参见here)。 original Heroku solution 直到我更改它才起作用。

      【讨论】:

        【解决方案6】:

        您可以使用 Paperclip 上传到 S3(参见 documentation)并创建缩略图,虽然它首先上传到临时文件夹,但在将文件上传到 S3 之前可以应用图像处理。

        关于这种配置的示例,在整个博客圈和 StackOverflow 上都有很多,例如this.

        【讨论】:

        • 我认为 Paperclip 不会直接上传到 S3。 Paperclip 和 Carrierwave 一样,上传到服务器的 tmp 目录进行处理,然后将文件上传到 S3。这意味着,服务器将占用整个 http 请求处理程序,直到上传完成。在某些情况下,可能会“冻结”应用程序
        • 嗯,它首先上传到 tmp 文件夹以允许在上传到 S3 之前进行图像处理(例如调整大小、创建适当的缩略图等)。由于 topicstarter 需要图像后处理,因此该解决方案似乎比“将原始文件直接上传到 S3 -> 下载它 -> 创建缩略图 -> 将缩略图上传到 S3”更好。应用程序“冻结”的问题可以通过将上传到 S3 到后台进程来解决。
        • 感谢你们的 cmets 伙计们。回形针上传到 tmp 文件夹的方法的另一个问题是托管在 Heroku 这样的网站上,因为它们限制了上传文件的大小。
        • Paperclip 不允许您直接上传到 S3,您应该编辑您的答案
        • 编辑了一个答案。将其留在这里以防有人寻找间接上传到 S3 的方法(通过临时文件夹),但不知何故偶然发现了此页面。
        猜你喜欢
        • 2014-10-23
        • 1970-01-01
        • 2011-05-12
        • 2015-11-09
        • 2013-09-06
        • 2011-06-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多