Use Active Storage to handle cached attachments

This fixes a few issues we've had for years.

First, when attaching an image and then sending a form with validation
errors, the image preview would not be rendered when the form was
displayed once again. Now it's rendered as expected.

Second, when attaching an image, removing it, and attaching a new
one, browsers were displaying the image preview of the first one. That's
because Paperclip generated the same URL from both files (as they both
had the same hash data and prefix). Browsers usually cache images and
render the cached image when getting the same URL.

Since now we're storing each image in a different Blob, the images have
different URLs and so the preview of the second one is correctly
displayed.

Finally, when users downloaded a document, they were getting files with
a very long hexadecimal hash as filename. Now they get the original
filename.
This commit is contained in:
Javi Martín
2021-07-27 22:14:45 +02:00
parent 091abfc944
commit e0e35298d5
13 changed files with 123 additions and 98 deletions

View File

@@ -15,9 +15,9 @@ class DirectUploadsController < ApplicationController
@direct_upload.relation.set_cached_attachment_from_attachment
render json: { cached_attachment: @direct_upload.relation.cached_attachment,
filename: @direct_upload.relation.attachment.original_filename,
filename: @direct_upload.relation.storage_attachment.filename.to_s,
destroy_link: render_destroy_upload_link(@direct_upload),
attachment_url: @direct_upload.relation.attachment.url }
attachment_url: polymorphic_path(@direct_upload.relation.storage_attachment) }
else
render json: { errors: @direct_upload.errors[:attachment].join(", ") },
status: :unprocessable_entity

View File

@@ -9,14 +9,10 @@ module Attachable
# Paperclip do not allow to use Procs on valiations definition
do_not_validate_attachment_file_type :attachment
validate :attachment_presence
validate :validate_attachment_content_type, if: -> { attachment.present? }
validate :validate_attachment_size, if: -> { attachment.present? }
validate :validate_attachment_content_type, if: -> { storage_attachment.attached? }
validate :validate_attachment_size, if: -> { storage_attachment.attached? }
before_save :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? }
Paperclip.interpolates :prefix do |attachment, style|
attachment.instance.prefix(attachment, style)
end
before_validation :set_attachment_from_cached_attachment, if: -> { cached_attachment.present? }
end
def association_class
@@ -26,29 +22,37 @@ module Attachable
end
def set_cached_attachment_from_attachment
self.cached_attachment = if filesystem_storage?
attachment.path
else
attachment.url
end
self.cached_attachment = storage_attachment.signed_id
end
def set_attachment_from_cached_attachment
self.storage_attachment = cached_attachment
if filesystem_storage?
File.open(cached_attachment) { |file| self.attachment = file }
File.open(file_path) do |file|
self.paperclip_attachment = file
end
else
self.attachment = URI.parse(cached_attachment).open
self.paperclip_attachment = URI.parse(cached_attachment).open
end
end
def prefix(attachment, _style)
if attachment.instance.persisted?
":attachment/:id_partition"
def attachment_content_type
storage_attachment.blob.content_type if storage_attachment.attached?
end
def attachment_file_size
if storage_attachment.attached?
storage_attachment.blob.byte_size
else
"cached_attachments/user/#{attachment.instance.user_id}"
0
end
end
def file_path
ActiveStorage::Blob.service.path_for(storage_attachment.blob.key)
end
private
def filesystem_storage?
@@ -73,7 +77,7 @@ module Attachable
end
def attachment_presence
if attachment.blank? && cached_attachment.blank?
unless storage_attachment.attached?
errors.add(:attachment, I18n.t("errors.messages.blank"))
end
end

View File

@@ -5,18 +5,21 @@ module HasAttachment
def has_attachment(attribute, paperclip_options = {})
has_one_attached :"storage_#{attribute}"
has_attached_file attribute, paperclip_options
alias_method :"paperclip_#{attribute}=", :"#{attribute}="
define_method :"#{attribute}=" do |file|
if file.is_a?(IO) || file.is_a?(Tempfile) && !file.is_a?(Ckeditor::Http::QqFile)
define_method :"storage_#{attribute}=" do |file|
if file.is_a?(IO)
send(:"storage_#{attribute}").attach(io: file, filename: File.basename(file.path))
elsif file.nil?
send(:"storage_#{attribute}").detach
else
send(:"storage_#{attribute}").attach(file)
end
end
has_attached_file attribute, paperclip_options
alias_method :"paperclip_#{attribute}=", :"#{attribute}="
define_method :"#{attribute}=" do |file|
send(:"storage_#{attribute}=", file)
send(:"paperclip_#{attribute}=", file)
end
end

View File

@@ -34,7 +34,7 @@ class DirectUpload
end
def save_attachment
@relation.attachment.save
@relation.storage_attachment.blob.save!
end
def persisted?
@@ -53,7 +53,7 @@ class DirectUpload
def relation_attributtes
{
paperclip_attachment: @attachment,
storage_attachment: @attachment,
cached_attachment: @cached_attachment,
user: @user
}

View File

@@ -1,7 +1,7 @@
class Document < ApplicationRecord
include Attachable
has_attachment :attachment, url: "/system/:class/:prefix/:style/:hash.:extension",
has_attachment :attachment, url: "/system/:class/:attachment/:id_partition/:style/:hash.:extension",
hash_data: ":class/:style/:custom_hash_data",
use_timestamp: false,
hash_secret: Rails.application.secrets.secret_key_base

View File

@@ -6,7 +6,7 @@ class Image < ApplicationRecord
medium: "300x300#",
thumb: "140x245#"
},
url: "/system/:class/:prefix/:style/:hash.:extension",
url: "/system/:class/:attachment/:id_partition/:style/:hash.:extension",
hash_data: ":class/:style",
use_timestamp: false,
hash_secret: Rails.application.secrets.secret_key_base
@@ -27,7 +27,7 @@ class Image < ApplicationRecord
validates :user_id, presence: true
validates :imageable_id, presence: true, if: -> { persisted? }
validates :imageable_type, presence: true, if: -> { persisted? }
validate :validate_image_dimensions, if: -> { attachment.present? && attachment.dirty? }
validate :validate_image_dimensions, if: -> { storage_attachment.attached? && storage_attachment.new_record? }
def self.max_file_size
Setting["uploads.images.max_size"].to_i
@@ -68,14 +68,17 @@ class Image < ApplicationRecord
end
def validate_image_dimensions
if attachment_of_valid_content_type?
if accepted_content_types.include?(attachment_content_type)
return true if imageable_class == Widget::Card
dimensions = Paperclip::Geometry.from_file(attachment.queued_for_write[:original].path)
storage_attachment.analyze unless storage_attachment.analyzed?
width = storage_attachment.metadata[:width]
height = storage_attachment.metadata[:height]
min_width = Setting["uploads.images.min_width"].to_i
min_height = Setting["uploads.images.min_height"].to_i
errors.add(:attachment, :min_image_width, required_min_width: min_width) if dimensions.width < min_width
errors.add(:attachment, :min_image_height, required_min_height: min_height) if dimensions.height < min_height
errors.add(:attachment, :min_image_width, required_min_width: min_width) if width < min_width
errors.add(:attachment, :min_image_height, required_min_height: min_height) if height < min_height
end
end
@@ -93,8 +96,4 @@ class Image < ApplicationRecord
end
end
end
def attachment_of_valid_content_type?
attachment.present? && accepted_content_types.include?(attachment_content_type)
end
end