在Ruby on Rails 6中使用活动存储上传文件
将文件上传到web应用程序是一个相当常见的功能。随着Rails 5的到来,活动存储作为Rails代码库的一部分被添加。在主动存储之前,文件上传功能通过添加Ruby gems(最著名的是CarrierWave、Silence或曲别针)添加到Rails应用程序中。
我最近花了几天时间,试图让文件上传在带有活动存储的Rails中作为一个完整的CRUD操作来工作。为了了解Active Storage的工作原理,我做了一些教程,并开发了几个小应用程序。和许多教程一样,有几个边缘案例没有被教程涵盖,所以我决定写这篇文章。如果你打算在Rails应用程序中添加文件上传,希望这能对你有所帮助。
将文件上载到Rails中的基本博客帖子
为了演示活动存储的工作原理,让我们构建一个带有附件的简单博客。我们将此示例称为active_博客。让我们创建我们的博客:
$ rails new active_blog
$ cd active_blog
$ rails active_storage:install
$ rails g scaffold Post title:string body:text
$ rails db:migrate
$ rails s
通过上传表单向我们的帖子添加附件
现在,我们有了一个基本的博客文章,通过脚手架,让我们深入到通过主动存储添加上传功能。
首先,让我们将数据库逻辑添加到Post模型中。使用活动存储,我们可以选择每篇文章附加一个文件或每篇文章附加多个文件。根据我们想要的,我们添加到Post模型中的语句略有不同:
在model 新建以下文件
class Post < ApplicationRecord
has_one_attached :header_image # Use has_one_attached for only one file allowed
has_many_attached :files # Use has_many_attached for multiple files allowed
end
在controller 新建以下文件
class PostsController < ApplicationController
before_action :set_post, only: %i[ show edit update destroy ]
# GET /posts or /posts.json
def index
@posts = Post.all
end
# GET /posts/1 or /posts/1.json
def show
end
# GET /posts/new
def new
@post = Post.new
end
# GET /posts/1/edit
def edit
end
# POST /posts or /posts.json
def create
@post = Post.new(post_params)
respond_to do |format|
if @post.save
format.html { redirect_to post_url(@post), notice: "Post was successfully created." }
format.json { render :show, status: :created, location: @post }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /posts/1 or /posts/1.json
def update
respond_to do |format|
if @post.update(post_params)
format.html { redirect_to post_url(@post), notice: "Post was successfully updated." }
format.json { render :show, status: :ok, location: @post }
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @post.errors, status: :unprocessable_entity }
end
end
end
# DELETE /posts/1 or /posts/1.json
def destroy
@post.destroy
respond_to do |format|
format.html { redirect_to posts_url, notice: "Post was successfully destroyed." }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_post
@post = Post.find(params[:id])
end
# Only allow a list of trusted parameters through.
def post_params
params.require(:post).permit(:title, :body)
end
def post_params
params.require(:post).permit(:title, :body, :header_image)
end
# Use a Ruby symbol with brackets (array) for many attachments
def post_params
params.require(:post).permit(:title, :body, files: [])
end
end
最后在views 写下
<%= form_with(model: post) do |form| %>
<% if post.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>
<ul>
<% post.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :body %>
<%= form.text_area :body %>
</div>
<div class="field">
<%= form.label :header_image %>
<%= form.file_field :header_image %>
</div>
# use this for many attachments, note we need multiple: true
<div class="field">
<%= form.label :files %>
<%= form.file_field :files, multiple: true %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
显示上传的文件
要查看我们上传的文件,我们需要在Posts show页面上访问它们。在最简单的形式中,我们可以通过迭代文件并链接到它们来简单地链接到文件。这将是我们的许多文件上传:
<% @post.files.each do |file| %>
<%= link_to file.filename, rails_blob_path(file, disposition: :attachment) %>
<% end %>
注意:disposition::attachment参数会在单击文件时下载该文件。如果要在浏览器中打开它,请使用disposition::inline语句。
如果我们上传文件、保存帖子并查看帖子显示页面,我们将看到指向文件的链接。在本例中,这些是图像,但可以是任何文件类型
编辑上传的文件
对于我们附加的多个文件,如果编辑它们并上载新文件,默认行为是覆盖现有图像并用新图像替换它们。如果这是你想要做的,这很好。但是,如果你只是想在已经上传的现有文件中添加其他文件,该怎么办?
在默认的活动存储配置下,无法执行此操作。除了现有的文件之外,你还必须选择你想要上传的新文件,这样每次上传都会很麻烦。
这有个解决办法。在config/environments/development下添加以下:
require "active_support/core_ext/integer/time"
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded any time
# it changes. This slows down response time but is perfect for development
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Do not eager load code on boot.
config.eager_load = false
# Show full error reports.
config.consider_all_requests_local = true
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
else
config.action_controller.perform_caching = false
config.cache_store = :null_store
end
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
config.active_storage.replace_on_assign_to_many = false
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
config.action_mailer.perform_caching = false
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
# Raise exceptions for disallowed deprecations.
config.active_support.disallowed_deprecation = :raise
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
# Highlight code that triggered database queries in logs.
config.active_record.verbose_query_logs = true
# Debug mode disables concatenation and preprocessing of assets.
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.
# config.action_view.annotate_rendered_view_with_filenames = true
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
# Uncomment if you wish to allow Action Cable access from any origin.
# config.action_cable.disable_request_forgery_protection = true
end
删除上传的文件
因为我们每个帖子只有一个标题图片,所以我们永远不需要删除这个图片。我们可以通过编辑标题图像并上传不同的图像来切换标题图像。
对于我们的许多附件,我们可能希望在某个时候删除其中一个甚至几个。因为删除活动存储附件超出了Rails中RESTful操作的默认约定,所以我们需要向posts控制器添加一个新操作,以及向路由添加一个新路。
首先,让我们在destroy方法下面的posts控制器中添加一个新操作,但首先是控制器中的所有私有方法。此操作称为delete_file,包含从数据库中删除活动存储附件所需的清除方法
def delete_file
file = ActiveStorage::Attachment.find(params[:id])
file.purge
redirect_back(fallback_location: posts_path)
end