使用send_file从Amazon S3下载文件?

65
我在我的应用程序中有一个下载链接,用户应该能够从中下载在s3上存储的文件。这些文件将在类似以下的URL上公开访问:
https://s3.amazonaws.com/:bucket_name/:path/:to/:file.png

下载链接触发了我控制器中的一个操作:

class AttachmentsController < ApplicationController
  def show
    @attachment = Attachment.find(params[:id])
    send_file(@attachment.file.url, disposition: 'attachment')
  end
end

但是,当我尝试下载文件时,我收到以下错误:

ActionController::MissingFile in AttachmentsController#show

Cannot read file https://s3.amazonaws.com/:bucket_name/:path/:to/:file.png
Rails.root: /Users/user/dev/rails/print

Application Trace | Framework Trace | Full Trace
app/controllers/attachments_controller.rb:9:in `show'

错误信息中提供的url地址可以确认该文件确实存在且公开可访问。

我该如何允许用户下载S3文件?

7个回答

92
你也可以使用send_data
我喜欢这个选项,因为你有更好的控制权。你不会把用户发送到S3,这可能会让一些用户感到困惑。
我只是会在AttachmentsController中添加一个下载方法。
def download
  data = open("https://s3.amazonaws.com/PATTH TO YOUR FILE") 
  send_data data.read, filename: "NAME YOU WANT.pdf", type: "application/pdf", disposition: 'inline', stream: 'true', buffer_size: '4096' 
end 

并添加路由

get "attachments/download"

4
在这个解决方案中,“open”方法是否不会先下载整个文件?send_data可以从亚马逊流式传输文件到用户,而用户无需知道真实的S3文件路径吗? - Homan
毫无疑问,这是一种方法,但似乎不需要streambuffer_size选项 https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/data_streaming.rb http://api.rubyonrails.org/classes/ActionController/DataStreaming.html - equivalent8
4
对于大文件来说,这会让用户感到困惑,因为他们需要等待服务器先下载文件,然后再将其流式传输给用户。此外,这比直接从S3下载用户要慢两倍。 - Joshua Pinter
2
使用Carrierwave+S3文件,我像这样使其工作: article = Article.find params[:id] file_data = open(article.file.url) send_data file_data.read, filename: article.filename, type: article.file.content_type, disposition: 'attachment' - ArnoHolo
3
除了@JoshPinter的观察到它比直接下载慢一些(因为数据经过中介而不是直接传输),还会对您的服务器造成额外负载并且是阻塞式操作,尽管您可以将其转移到后台任务进行处理。直接从S3下载文件更加高效,而且S3具有良好的错误页面 - 但是,当访问私有文件时,您的AWS凭据将可见。 - Dennis
显示剩余4条评论

38

让用户操作简单

我认为处理这个问题的最佳方式是使用具有过期时间的S3 URL。其他方法存在以下问题:

  • 文件首先下载到服务器,然后再下载到用户端。
  • 使用send_data无法产生预期的“浏览器下载”效果。
  • 占用Ruby进程。
  • 需要额外的download控制器动作。

我的实现看起来像这样:

在你的attachment.rb

def download_url
  S3 = AWS::S3.new.buckets[ 'bucket_name' ] # This can be done elsewhere as well,
                                            # e.g config/environments/development.rb
  url_options = { 
    expires_in:                   60.minutes, 
    use_ssl:                      true, 
    response_content_disposition: "attachment; filename=\"#{attachment_file_name}\""
  }

  S3.objects[ self.path ].url_for( :read, url_options ).to_s
end

在您的观点中

<%= link_to 'Download Avicii by Avicii', attachment.download_url %>

就是这样了。


如果您出于某种原因仍想保留download操作,那么只需使用以下内容:

在您的attachments_controller.rb文件中:

def download
  redirect_to @attachment.download_url
end

感谢guilleva的指导。


这会下载整个存储桶吗?我有一个类似的方法,根据它们的键下载单个存储桶对象。 - BigRon
@BigRon 现在看一下。已经添加了使用 S3 存储桶获取对象的部分。 - Joshua Pinter
不错的更正,看起来没问题。我得尝试一下你使用 self.path 的方法。它似乎比我当前的方法更简单。 - BigRon
顺便说一下,我编辑了这个代码,使用双引号来包含头文件名。我们遇到了一个问题,某些版本的Internet Explorer(当然)会将单引号作为下载文件名的一部分。使用双引号似乎解决了这个问题。 - Joshua Pinter
@Maxence 如果你只是为模型的 download_url 方法添加一个参数,允许你改变它的处理方式,会怎么样? - Joshua Pinter
显示剩余4条评论

32
为了从您的Web服务器发送文件,
  • 您需要从S3下载它(参见@ nzajt的答案)或者

  • 您可以redirect_to @attachment.file.expiring_url(10)


4
我如何在S3上使用此功能来处理非公共文件? - mehulkar
1
在这种情况下,您需要使用“@attachment.file.expiring_url”。 - dgilperez
2
请注意,当像这里所示访问S3上的私有文件时,S3 URL将包含您的秘密AWS凭据,以便浏览器可以进行身份验证请求。如果下载成功并且用户流程停留在您的页面上,则不会明显,但是当它无法正常工作(例如文件不存在)时,S3上的错误页面将在URL上显示凭据。这是一个相当大的安全风险。 - Dennis
重定向到URL并不会自动下载文件,它只是将您带到浏览器视图中,在那里您可以然后下载它。 - kittyminky
我知道这篇文章很旧,但因为这个评论被多次引用:,过期的URL不会泄露您的凭据。它们显示您的访问ID,这不是一个秘密,并且有一个预签名的哈希进行身份验证。如果预签名的URL泄露了您的凭据,人们可以在URL过期后一遍又一遍地重新创建URL。 - Sampson Crowley

6

我刚将我的public/system文件夹迁移到了Amazon S3。上面的解决方案很有帮助,但我的应用程序接受不同类型的文档。因此,如果您需要相同的行为,这对我很有帮助:

@document = DriveDocument.where(id: params[:id])
if @document.present?
  @document.track_downloads(current_user) if current_user
  data = open(@document.attachment.expiring_url)
  send_data data.read, filename: @document.attachment_file_name, type: @document.attachment_content_type, disposition: 'attachment'
end

该文件正在保存在DriveDocument对象的attachment字段中。希望这可以帮到您。

5
以下是对我非常有效的解决方案。先从S3对象获取原始数据,然后使用send_data将其传递给浏览器。
使用此处找到的aws-sdk gem文档:http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/S3Object.html 完整控制器方法:
def download
  AWS.config({
    access_key_id: "SECRET_KEY",
    secret_access_key: "SECRET_ACCESS_KEY"
  })

  send_data( 
    AWS::S3.new.buckets["S3_BUCKET"].objects["FILENAME"].read, {
      filename: "NAME_YOUR_FILE.pdf", 
      type: "application/pdf", 
      disposition: 'attachment', 
      stream: 'true', 
      buffer_size: '4096'
    }
  )
end

4
在发送给用户之前,这需要先将文件下载到服务器上,对吗? - Joshua Pinter
正确,这取决于您的文件有多大,可能并不是一个选项。在我的情况下,它们是小型PDF文件,这是可以接受的。 - David Morrow

0
如何允许用户下载S3文件?
如果您能够在上传到S3之前为文件设置一些元数据,而不是在用户稍后想要下载它时尝试修补它,那么这个解决方案就简单得多了:

https://dev59.com/AGAg5IYBdhLWcg3w1t5a#24297799

If you are using fog then you can do something like this:

has_attached_file :report,
  fog_file: lambda { |attachment|
    {
      content_type: 'text/csv',
      content_disposition: "attachment; filename=#{attachment.original_filename}",
    }
  }

If you are using Amazon S3 as your storage provider, then something like this should work:

has_attached_file :report
  s3_headers: lambda { |attachment|
    { 
      'Content-Type' => 'text/csv',
      'Content-Disposition' => "attachment; filename=#{attachment.original_filename}",
    }
  }

-1

def download_pdf @post= @post.avatar.service_url

定义下载PDF函数 @post = @post.avatar.service_url

send_data(

    "#{Rails.root}/public/#{@post}",
    filename: "#{@post}",
    type: "image/*",
    disposition: 'inline', stream: 'true', buffer_size: '4096'
)

结束


2
尽可能地,请努力提供额外的解释,而不仅仅是代码。这样的答案往往更有用,因为它们帮助社区成员,特别是新开发人员更好地理解解决方案的原因,并可以帮助防止需要回答后续问题的需要。 - Rajan
如果可能,尽量提供额外的解释而不仅仅是代码。这样的答案通常更有用,因为它们可以帮助社区成员,特别是新的开发人员更好地理解解决方案的原理,并且可以帮助防止需要回答后续问题。 - Rajan

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