即使在验证失败后,Active Storage 仍会检测到附件

Active Storage still detects attachement even after validation failure

这个问题已经持续了很长时间,直到今天,我仍然没有找到解决方案。我已经看到了一些类似的问题,但并不完全是我遇到的问题。

自从 Rails Active Storage 在 Rails 5 中出现以来,我一直在体验它,但由于这个特殊问题,我从未在生产中实际使用过它。 Rails 6 出现时解决的主要问题是,如果您对文件附件实施任何验证,则记录不会保存,但附件 (blob) 仍会保存,并且如果您的验证开启内容类型(例如确保它是 JPEG 图像)然后您最终得到一个无效的附件(例如,如果您上传了一个文本文件)。例如,如果您尝试使用 image_tag "display" 此附件,这将导致问题。 Rails 6 通过仅在记录实际保存到数据库中时才保存附件来解决此问题。这对我来说只解决了一半的问题。

这是我仍然遇到的问题,尚未找到解决方案。

假设您有一个非常基本的设置。具有姓名、电子邮件和附加头像的 Person 模型

Class Person < ApplicationRecord
  has_one_attached :avatar
  #Check that image type is jpg or png
  validate :check_image_type

  #Remove avatar flag needed for form
  attr_accessor :remove_avatar

  #purge picture if remove picture flag was ticked on form
  after_save :purge_avatar, if: :purge_requested?

  #Returns a thumbnail version of the property picture
  def thumbnail
    return self.avatar.variant(resize:'100x100').processed 
  end

  private 
    #Validates the image type being uploaded
    def check_image_type
      if avatar.attached? && !avatar.content_type.in?(%("image/jpeg image/png"))
        errors.add(:avatar, "Invalid Avatar Format")
      end
    end

    #Was a purge of the picture requested 
    def purge_requested?
      remove_avatar == "1"
    end

    def purge_avatar
      avatar.purge_later
    end
end 

正如您在上面的代码中看到的,Person 有一个附加的头像。保存后,我们验证图像类型,确保它是 jpep 或 png,如果不是,我们只需向记录添加错误,这将阻止记录被保存。

这是控制器代码(我省略了索引、编辑、更新和销毁操作)

class PeopleController < ApplicationController
  before_action :set_person, only: [:show, :edit, :update, :destroy]

  def new
    @person = Person.new
  end

  def create
    @person = Person.new(person_params)

    respond_to do |format|
      if @person.save
        format.html { redirect_to @person, notice: 'Person was successfully created.' }
        format.json { render :show, status: :created, location: @person }
      else
        format.html { render :new }
        format.json { render json: @person.errors, status: :unprocessable_entity }
      end
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_person
      @person = Person.find(params[:id])
    end

    # Never trust parameters from the scary internet, only allow the white list through.
    def person_params
      params.require(:person).permit(:name, :email, :avatar, :remove_avatar)
    end
end

然后在表单中,如果头像存在于表单顶部,我会显示头像,然后让用户create/edit信息和select另一个头像,如果他们想要的话。

<%= form_with(model: person, local: true) do |form| %>
  <% if person.errors.any? %>
    <div id="error_explanation">
      <h2>
        <%= pluralize(person.errors.count, "error") %> prohibited this person from being saved:
      </h2>

      <ul>
        <% person.errors.full_messages.each do |message| %>
          <li><%= message %></li>
        <% end %>
      </ul>
    </div>
  <% end %>
  <!-- Display avatar if one attach -->
  <%if person.avatar.attached?%>
    <%=image_tag(person.avatar)%>
    Remove <%=form.check_box :remove_avatar%>
  <%end%>

  <div class="field">
    <%= form.label :name %>
    <%= form.text_field :name %>
  </div>

  <div class="field">
    <%= form.label :email %>
    <%= form.text_field :email %>
  </div>

  <!-- Select Picture -->
  <div class = "field">
    <%=form.label :avatar %>
    <%= form.file_field :avatar, accept: 'image/*'%>
  </div>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>

预期的行为是

当您 select 错误的文件类型时,它会阻止记录被保存,这很好,但它仍然 "sees" 非持久记录上的附件。因此,尽管在创建操作中最初显示表单时没有显示头像,但在验证失败后重新呈现表单时会显示 "empty" 头像。这是由于附?方法返回 true,即使记录中没有附件(因为它被拒绝了)。

空白的头像看起来有点古怪,看起来像一张破损的图片 link。但是,如果您要对头像本身进行任何操作,例如 Person class 中的缩略图方法,则会产生以下错误:ActiveStorage::InvariableError

这是附件的原因?属性为真且头像属性有效,但它没有关联的 blob(图片)。因此,尝试将其调整为缩略图失败并出现错误。

我正在尝试找到 "clear" 或重置附加头像 and/or 的方法?当验证阻止记录保存时的属性。在内部,Rails 执行它应该执行的操作(不保存文件并保留当前文件(如果有))。

但是表单上显示的内容(如果您显示头像)一定会让用户感到困惑,尤其是当用户在您 select 新头像之前拥有头像时。如果您 select 一个无效的头像并且表单拒绝了它,则在重新渲染时您的初始头像不会显示并被替换为损坏的 link 图标的图片。这可能会让用户误以为他们之前的头像已被删除,但事实并非如此。在这一点上,我不确定如何在不深入研究活动存储内容的情况下解决这个问题(我现在对这样做不感兴趣)。

我曾尝试自己调用 purge 或将 null 分配给头像属性,但这没有用

def check_image_type
  if avatar.attached? && !avatar.content_type.in?(%("image/jpeg image/png"))
    errors.add(:avatar, "Invalid Avatar Format")
    avatar.purge    <---- Not working
    avatar = nil <--- Not working
    avatar = '' <--- Not working
  end
end

编辑: 我不一定要显示我刚刚上传的预览。事实上,我不想那样做,但 Rails 似乎是自己做的。

在我的示例中,当您创建用户时,没有头像,因此显示 none,但是当您尝试上传错误文件类型的头像并且表单重新加载以显示错误时,它会尝试显示加载失败的头像。如果上传的文件类型正确,它会保存用户信息并重定向到用户列表或我们可以显示加载头像的另一个屏幕。

用户已有头像,而您想更改头像。您首先打开表单(使用编辑操作),它会显示当前头像。如果我尝试更改它并再次上传无效文件,表单将重新加载并出现错误,但会再次将当前有效头像替换为空头像(即使在数据库中,旧头像仍然存在)。同样的,如果我确实上传了一个有效的文件,那么表单就会提交,头像就会改变,我们会在下一个屏幕看到它。

总而言之,(我觉得)正确的行为应该是 如果我尝试上传一个基于验证(文件类型、大小等)被拒绝的文件,那么 Rails 应该就像我什至没有尝试上传文件一样。它应该废弃 "tentative attachment" 的任何遗迹。

如果是新资源,仍然不会显示头像,但是如果是已经存在的资源,它仍然会显示当前头像。

要防止错误,您可以使用 persisted?

<% if person.avatar.attached? && person.avatar.persisted? %>
  <%= image_tag(person.avatar)%>
  Remove <%= form.check_box :remove_avatar%>
<%end%>

并且您可以使用 this gem 进行 ActiveStorage 验证。

像这样:

validates :avatar, content_type: %w[image/png image/jpg image/jpeg]

我发现另一种解决此问题的方法是改用 variable?

Returns true if ImageMagick can transform the blob (its content type is in ActiveStorage.variable_content_types).

如果您尝试使用无效文件覆盖现有图像,它仍然不起作用 - 正如其他人在其他答案中指出的那样,旧文件的预览仍然会消失。