rails文件上传_使用Rails和ActionCable上传文件

rails文件上传

This is the second part of the tutorial that explains how to enable real-time communication and file uploading with Rails and ActionCable. In the previous part we have created a Rails 5.1 chatting application powered by Clearance (for authentication), ActionCable (for real-time updates) and ActiveJob (for broadcasting). In this part you will learn how to integrate the Shrine gem and utilize the FileReader API to enable file uploading over web sockets.

这是本教程的第二部分,介绍了如何使用Rails和ActionCable启用实时通信和文件上传。 在上一部分中,我们创建了一个由Clearance (用于身份验证),ActionCable(用于实时更新)和ActiveJob (用于广播)支持的Rails 5.1聊天应用程序。 在这一部分中,您将学习如何集成Shrine gem和如何使用FileReader API启用通过Web套接字上传文件。

As a result, you will get an application similar to this one:

The source code for the tutorial is available at
GitHub.

结果,您将获得与此应用程序类似的应用程序: GitHub获得

整合神社 ( Integrating Shrine )

First things first: we need a solution to enable file uploading functionality for our application. There are a handful of gems that can solve this task, including Paperclip, Dragonfly and Carrierwave, but I'd suggest sticking with a solution called Shrine that I've stumbled upon a couple of months ago. I like this gem because of its modular approach and vast array of supported features: it works with ActiveRecord and Sequel, supports Rails and non-Rails environments, provides background processing and more. Another very important thing is that this gem enables easy uploading of files sent as data URIs which is cruical for our today's task (later you will see why). You may read this introductory article by Janko Marohnić, author of the gem, explaining the motivation behind creating Shrine. By the way, I wanted to thank Janko for helping me out and answering some questions.

首先,我们需要一个解决方案来为我们的应用程序启用文件上传功能。 有很多宝石可以解决此任务,包括PaperclipDragonflyCarrierwave ,但我建议坚持使用几个月前偶然发现的名为Shrine的解决方案。 我喜欢这个gem,因为它的模块化方法和受支持的功能广泛:它与ActiveRecord和Sequel一起使用,支持Rails和non-Rails环境,提供后台处理等等。 另一个非常重要的事情是,该gem可以轻松上传作为数据URI发送的文件,这对于我们今天的任务至关重要(稍后您将了解原因)。 您可以阅读宝石作者JankoMarohnić的介绍性文章 ,其中解释了创建神社的动机。 顺便说一句,我要感谢Janko帮助我并回答了一些问题。

So, you know what to do—drop a new gem into the Gemfile:

因此,您知道该怎么做–将新的gem放入Gemfile中

gem'shrine', '~> 2.6'

Then run:

然后运行:

bundleinstall

Next you will require at least basic setup for Shrine that is stored inside an initializer file:

接下来,您将至少需要对Shrine进行基本设置,并将其存储在初始化程序文件中:

# config/initializers/shrine.rb

require "shrine" # core
require "shrine/storage/file_system" # plugin to save files using file system

Shrine.storages = {
    cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), 
    store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"),
}

Shrine.plugin :activerecord # enable ActiveRecord support

Here we simply provide paths where the uploaded files and cached data will be stored. Also, we enable support for ActiveRecord by using the appropriate plugin. Most of the Shrine's functionality is packed in a form plugins, and you can configure a totally different setup by using the ones listed on the gem's homepage.

在这里,我们仅提供将存储上传文件和缓存数据的路径。 此外,我们通过使用适当的插件启用对ActiveRecord的支持。 Shrine的大多数功能都包装在一个表单插件中,您可以使用gem主页上列出的设置来配置完全不同的设置。

Please note that it is probably a good idea to exclude public/uploads directory from version control to avoid pushing images uploaded for testing purposes to GitHub. Add the following line to the .gitignore file:

请注意,从版本控制中排除public / uploads目录可能是个好主意,以避免将出于测试目的上传的图像推送到GitHub。 将以下行添加到.gitignore文件中:

public/uploads

An attachment attribute is required for our model as well. It should have a name of <some_name>_data and a text type. This attribute stores all the information about the uploaded file, so you don't have to add separate fields for a name, metadata etc. Create and apply a new migration:

模型的附件属性也是必需的。 它的名称应为<some_name>_datatext类型。 此属性存储有关上载文件的所有信息,因此您不必为名称,元数据等添加单独的字段。创建并应用新的迁移:

rails g migration add_attachment_data_to_messages attachment_data:text
rails db:migrate

All attachment-specific logic and individual plugins are listed inside the uploader class:

所有特定于附件的逻辑和各个插件均在uploader类中列出:

class AttachmentUploader < Shrine
  # app/uploaders/attachment_uploader.rb
end

Later we will hook up some necessary plugins inside this class. Now the uploader can be included in the model:

稍后,我们将在此类中连接一些必要的插件。 现在可以将上传器包含在模型中:

# models/message.rb
include AttachmentUploader[:attachment]

Note that the same uploader may be included for multiple models.

请注意,多个模型可能包含相同的上传器。

添加文件验证 (Adding File Validations)

Before proceeding, let's also secure our uploads a bit. Currently any file with any size is accepted which is not particularly great, so we are going to introduce some restrictions. This can be done using the validation_helpers plugin. I will define validations globally inside the initializer, but this can also be done per-uploader:

在继续之前,我们还需要确保上传的内容安全。 当前,任何大小的文件都可以被接受,这并不是特别好,因此我们将引入一些限制。 这可以使用validation_helpers插件来完成。 我将在初始化程序中全局定义验证,但是也可以按每个上载器进行验证:

# config/initializers/shrine.rb
Shrine.plugin :determine_mime_type # check MIME TYPE
Shrine.plugin :validation_helpers, default_messages: {
    mime_type_inclusion: ->(whitelist) { # you may use whitelist variable to display allowed types
      "isn't of allowed type. It must be an image."
    }
}

Shrine::Attacher.validate do
  validate_mime_type_inclusion [ # whitelist only these MIME types
                                   'image/jpeg',
                                   'image/png',
                                   'image/gif'
                               ]
  validate_max_size 1.megabyte # limit file size to 1MB
end

This code is pretty much self-explaining. Apart from validation helpers, we use determine_mime_type plugin that, well, checks MIME types for the uploaded files based on their content. The default analyzer for this plugin is file utility which is not available on Windows by default. You may use some third-party solution instead (or even multiple solutions at once) as explained by the official guide. Do test, however, that all whitelisted file types are really accepted because recently I had troubles when uploading .txt files with mimemagic set as an analyzer.

此代码几乎是不言自明的。 除了验证帮助程序之外,我们还使用define_mime_type插件 ,该插件根据文件的内容检查MIME类型。 此插件的默认分析器是file Utility ,默认情况下Windows上不提供该工具 。 如官方指南所述,您可以改用某些第三方解决方案(甚至一次使用多个解决方案)。 但是,请进行测试以确保所有列入白名单的文件类型都确实被接受,因为最近我在上载带有mimemagic设置为分析器的.txt文件时遇到了麻烦。

Next, inside the Shrine::Attacher.validate do we allow PNG, JPEG and GIF images to be uploaded and restrict their size to 1 megabyte.

接下来,在Shrine::Attacher.validate do内部,我们允许上传PNG,JPEG和GIF图像并将其大小限制为1 MB。

Lastly, I'd like to change our validation rule for the body attribute inside the Message model. Currently it looks like

最后,我想更改Message模型中body属性的验证规则。 目前看起来像

# models/message.rb
validates :body, presence: true

but I'd like to modify it:

但我想修改它:

# models/message.rb
validates :body, presence: true, unless: :attachment_data

This way we allow messages to have no text if they have an attached file. Great!

这样,如果邮件具有附件,我们将允许它们不包含任何文本。 大!

使用FileReader上传 ( Uploading with FileReader )

观看次数 (Views)

Shrine is now integrated into the application and we can proceed to the client-side and add a file field to the form rendered at the views/chats/index.html.erb view:

Shrine现在已集成到应用程序中,我们可以继续进行客户端操作,并将文件字段添加到在views / chats / index.html.erb视图中呈现的表单中:

<%= form_with url: '#', html: {id: 'new-message'} do |f| %>
  <%= f.label :body %>
  <%= f.text_area :body, id: 'message-body' %><div class="form-group">
    <%= f.file_field :attachment, id: 'message-attachment' %>
    <br>
    <small>Only PNG, JPG and GIF images are allowed</small>
  </div>

  <br>
  <%= f.submit %>
<% end %>

Remember that the form_with helper does not add any ids automatically, so I've specified them explicitly.

请记住, form_with帮助器不会自动添加任何ID,因此我已明确指定了它们。

Also let's tweak the views/messages/_message.html.erb partial to display an attachment if it is present:

另外,让我们调整views / messages / _message.html.erb部分以显示附件(如果存在):

<div class="message">
  <strong><%= message.user.email %></strong> says:
  <%= message.body %>
  <br>
  <small>at <%= l message.created_at, format: :short %></small>
  <% if message.attachment_data? %>
    <br>
    <%= link_to "View #{message.attachment.original_filename}",
                message.attachment.url, target: '_blank' %>
  <% end %>
  <hr>
</div>

The link will simply display the file's original name and open the attachment in a new tab when clicked. Your page should now look like this:

链接将仅显示文件的原始名称,并在单击时在新选项卡中打开附件。 您的页面现在应如下所示:

CoffeeScript (CoffeeScript)

Great, the views are prepared as well and we can write some CoffeeScript. It may be a good idea to make yourself a coffee because this is where things start to get a bit more complex. But fear not, I'll proceed slowly and explain what is going on.

太好了,视图也已经准备好了,我们可以编写一些CoffeeScript。 自己煮一杯咖啡可能是个好主意,因为这会使事情变得更加复杂。 但是不要担心,我会慢慢进行并解释发生了什么。

The first thing to mention is that we have to send the chosen file over web sockets along with the entered message. Therefore, the file has to be processed with JavaScript. Luckily, there is a FileReader API available that allows us to read the contents of files. It works with all major browsers, including support for IE 10+ (by the way, web sockets are also supported in IE 10+).

首先要提到的是,我们必须通过Web套接字将所选文件与输入的消息一起发送。 因此,该文件必须使用JavaScript处理。 幸运的是,有一个FileReader API可以使我们读取文件的内容。 它适用于所有主流浏览器,包括对IE 10+的支持(顺便说一句, IE 10+中也支持 Web套接字)。

To start off, let's change the condition inside the submit event handler. Here is its current version:

首先,让我们更改submit事件处理程序中的条件。 这是它的当前版本:

# app/assets/javascripts/channels/chat.coffee
# ...

$new_message_form.submit (e) ->
  $this = $(this)
  message_body = $new_message_body.val()
  if $.trim(message_body).length > 0
    App.chat.send_message message_body
  e.preventDefault()
  return false

I want to send the message if it has some text or if an attachment is selected (remember that we've added server-side validation inside the Message model that follows the same principle). Therefore, the condition turns to:

我想发送包含某些文本或选择了附件的消息(请记住,我们已经在遵循相同原理的Message模型内添加了服务器端验证)。 因此,条件变为:

# We check that either the body has some contents or a file is chosen
if $.trim(message_body).length > 0 or $new_message_attachment.get(0).files.length > 0

Now, if the attachment is chosen, we need to instantiate a new FileReader object, so add another condition inside:

现在,如果选择了附件,我们需要实例化一个新的FileReader对象,因此在其中添加另一个条件:

# ...
if $.trim(message_body).length > 0 or $new_message_attachment.get(0).files.length > 0
    if $new_message_attachment.get(0).files.length > 0 # if file is chosen
      reader = new FileReader()  # use FileReader API
      file_name = $new_message_attachment.get(0).files[0].name # get the name of the first chosen file
    else
      App.chat.send_message message_body

Here I am also storing the original file name as it has to be sent separately—there will be no way to determine it on the server-side otherwise.

我还在这里存储原始文件名,因为它必须单独发送-否则无法在服务器端确定它。

The reader can now be used to carry out its main purpose: read the contents of a file. We'll use the readAsDataURL method that returns a base64 encoded string:

现在可以使用reader执行其主要目的:读取文件的内容。 我们将使用readAsDataURL方法 ,该方法返回base64编码的字符串:

if $.trim(message_body).length > 0 or $new_message_attachment.get(0).files.length > 0
    if $new_message_attachment.get(0).files.length > 0 # if file is chosen
      reader = new FileReader() 
      file_name = $new_message_attachment.get(0).files[0].name
      reader.readAsDataURL $new_message_attachment.get(0).files[0] # read the chosen file
    else
      App.chat.send_message message_body

Of course, reading a file may take some time, therefore we must wait for this process to finish before sending the message. This can be done with the help of the loadend callback that is fired as soon as the file is read. Inside this callback we have access to the reader.result that contains the actual base64 encoded string. So, apply this knowledge to practice:

当然,读取文件可能需要一些时间,因此在发送消息之前,我们必须等待此过程完成。 可以在读取文件后立即触发的loadend回调的帮助下完成此操作。 在此回调中,我们可以访问reader.result ,其中包含实际的base64编码的字符串。 因此,将这些知识应用于实践:

if $.trim(message_body).length > 0 or $new_message_attachment.get(0).files.length > 0
    if $new_message_attachment.get(0).files.length > 0 
      reader = new FileReader() 
      file_name = $new_message_attachment.get(0).files[0].name
      reader.addEventListener "loadend", -> # perform the following action after the file is loaded
        App.chat.send_message message_body, reader.result, file_name 

      reader.readAsDataURL $new_message_attachment.get(0).files[0] # read file in base 64 format
    else
      App.chat.send_message message_body

That's pretty much it. Our file reader is finished!

就是这样。 我们的文件阅读器完成了!

The send_message function requires some changes as we wish to pass the file's contents and the original filename to it:

send_message函数需要进行一些更改,因为我们希望将文件的内容和原始文件名传递给它:

# ...
send_message: (message, file_uri, original_name) ->
# send the message to the server along with file in base64 encoding 
        @perform 'send_message', message: message, file_uri: file_uri, original_name: original_name

All that is left to do is enable support for base64 encoded strings on the server-side, so let's move on to the next section.

剩下要做的就是在服务器端启用对base64编码字符串的支持,因此让我们继续下一节。

将文件上传为数据URI ( Uploading Files as Data URIs )

At the beginning of this tutorial I mentioned that Shrine supports uploading files in the form of data URIs. We're at the point where it becomes really important, as we need to process the base64 encoded string sent by the client and save the file properly.

在本教程的开头,我提到Shrine支持以数据URI形式上传文件。 我们正变得非常重要,因为我们需要处理客户端发送的base64编码的字符串并正确保存文件。

To achieve this, enable the data_uri plugin for the uploader (though this can also be done globally inside the initializer):

为此,请为上载器启用data_uri插件 (尽管这也可以在初始化程序中全局进行):

# uploaders/attachment_uploader.rb
plugin :data_uri

It appears that having this plugin in place, uploading files as data URIs means simply assigning the proper string to the attachment_data_uri attribute:

看来,有了此插件,将文件作为数据URI上传就意味着只需将正确的字符串分配给attachment_data_uri属性即可:

# channels/chat_channel.rb

def send_message(data)
    message = current_user.messages.build(body: data['message'])
    if data['file_uri']
      message.attachment_data_uri = data['file_uri']
    end
    message.save
end

That's pretty much it, the uploading should now be carried out properly. Note that we do not have to make any changes to the after_create_commit callback or to the background job.

差不多了,现在应该正确执行上传了。 注意,我们不必对after_create_commit回调或后台作业进行任何更改。

存储原始文件名 (Storing the Original Filename)

Another thing we need to take care of is storing the original file's name. Strictly speaking, it is not required, but this way users will be able to quickly understand which file they've received (including its original extension).

我们需要注意的另一件事是存储原始文件的名称。 严格来说,这不是必需的,但是通过这种方式,用户将能够快速了解​​他们收到的文件(包括其原始扩展名)。

The original name can be stored inside the attachment's metadata, so another plugin called add_metadata is needed. As the name implies, it allows to provide or extract meta information from the uploaded file. This process is performed inside the uploader or the initializer. At this point, however, we do not have access to the original filename sent by the client anymore:

原始名称可以存储在附件的元数据中,因此需要另一个名为add_metadata的插件 。 顾名思义,它允许从上载的文件中提供或提取元信息。 此过程在上载器或初始化器中执行。 但是,在这一点上,我们不再可以访问客户端发送的原始文件名:

# uploaders/attachment_uploader.rb

plugin :add_metadata

add_metadata do |io, context|
    {'filename' => ???}
end

Luckily, we do have access to the context (that is, the actual record), so the filename can be saved inside the virtual attribute called, for instance, attachment_name:

幸运的是,我们确实可以访问context (即实际记录),因此可以将文件名保存在称为attachment_name的虚拟属性中:

# uploaders/attachment_uploader.rb

add_metadata do |io, context|
    {'filename' => context[:record].attachment_name}
end

All we need to do is set this virtual attribute:

我们需要做的就是设置这个虚拟属性:

# channels/chat_channel.rb

def send_message(data)
    message = current_user.messages.build(body: data['message'])
    if data['file_uri']
      message.attachment_name = data['original_name']
      message.attachment_data_uri = data['file_uri']
    end
    message.save
end

Don't forget to define the getter and setter for this attribute inside the model (or use attr_accessor):

不要忘记在模型中为此属性定义getter和setter(或使用attr_accessor ):

# models/mesage.rb

def attachment_name=(name)
    @attachment_name = name
end

def attachment_name
    @attachment_name
end

And—guess what—we are done! You may now boot the server and try to upload some image (remember that we've restricted its size to 1MB though). The link to the file should be displayed under the message. Note that the original filename is preserved:

You will see an output in the terminal similar to this one:

并且-猜怎么着-我们完成了! 现在,您可以启动服务器并尝试上传一些图像(请记住,尽管我们将其大小限制为1MB)。 该文件的链接应显示在消息下方。 请注意,原始文件名被保留:

然后,该消息将广播到所有客户端:

结论 ( Conclusion )

We've come to the end of the tutorial. The chat application is finished and the user can now communicate and exchange files with ease in real-time which is quite convenient. In this tutorial you have learned how to:

我们到了教程的结尾。 聊天应用程序完成后,用户现在可以轻松,实时地交流和交换文件,这非常方便。 在本教程中,您学习了如何:

  • Integrate Shrine gem to enable file uploads

    集成Shrine gem以启用文件上传
  • Add file validations

    添加文件验证
  • Work with Shrine's plugins

    使用Shrine的插件
  • Utilize FileReader Web API

    利用FileReader Web API
  • Process files as Data URIs

    将文件作为数据URI处理
  • Store file's metadata

    存储文件的元数据

Of course, there is more to this app that can be done: for example, it would be great to display validation errors if the message cannot be saved. However, this will be left for you as an exercise. All in all, this task is not that complex: you can, for example, set a condition inside the ChatChannel#send_message and send an error back to the author if the saving of the message failed.

当然,此应用程序还有很多可以做的事情:例如,如果无法保存消息,则显示验证错误会很棒。 但是,这将留给您作为练习。 总而言之,此任务并不那么复杂:例如,您可以在ChatChannel#send_message内设置一个条件,并在消息保存失败时将错误发送给作者。

Share your solutions in the comments and don't hesitate to post your questions as well. I thank you for staying with me and happy coding!

在评论中分享您的解决方案,也不要犹豫发表您的问题。 感谢您与我在一起并祝您编程愉快!

翻译自: https://scotch.io/tutorials/uploading-files-with-rails-and-actioncable

rails文件上传

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值