Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Active Storage for Picture and File attachments #2968

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
93c8704
Add activestorage
tvdeyen Apr 12, 2022
a1e2065
Add image_processing
tvdeyen Apr 12, 2022
a09ac6a
Add dragonfly to image processing converter
tvdeyen Apr 12, 2022
eafe65f
Use vips as image processor in dummy app
tvdeyen Apr 12, 2022
42c9250
[ci] install libvips
tvdeyen Apr 12, 2022
5ad0360
Ignore active storage files in dummy app
tvdeyen Apr 12, 2022
00dee77
Use custom validations for picture size and format
tvdeyen Apr 12, 2022
add7123
Use activestorage for picture file handling
tvdeyen Apr 12, 2022
007da40
Do not sharpen images by default
tvdeyen Apr 12, 2022
46865f9
Add image_file_extension method
tvdeyen Apr 12, 2022
20ed1bd
Delegate convertible format check to activestorage
tvdeyen Apr 12, 2022
91aac5a
Eager load attachments in admin pictures controller
tvdeyen Apr 12, 2022
2197a16
Delegate image_file_* methods to attached file
tvdeyen Apr 12, 2022
72808eb
Remove custom picture variant classes
tvdeyen Apr 12, 2022
3199621
Move can_be_cropped_to? into picture_thumbnails
tvdeyen Apr 12, 2022
dd2dd79
Use deletaged image_file methods for validations
tvdeyen Apr 12, 2022
f401da3
Use ActiveStorage for Attachments as well
tvdeyen Apr 12, 2022
61b3230
Use file mime type for picture by format select
tvdeyen Apr 12, 2022
c1ab0b1
Remove Dragonfly
tvdeyen Apr 12, 2022
67a9faa
Add support for animated gifs
tvdeyen Aug 2, 2023
e1c9cc0
Always return image format
tvdeyen Aug 4, 2023
b70ab1c
Use ActiveStorage redirect path
tvdeyen Jan 23, 2024
21777e8
Fix picture and attachment search
tvdeyen Jan 23, 2024
a07ff57
Preprocess thumbnails after upload
tvdeyen Jan 29, 2024
fb61906
Use image_file_extension as original format
tvdeyen Jun 11, 2024
853d5ad
Allow webp as image format for ActiveStorage
tvdeyen Jun 11, 2024
38b4972
Add upgrader for active storage
tvdeyen Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ jobs:
sudo apt update -qq
sudo apt install -qq --fix-missing libmysqlclient-dev -o dir::cache::archives="/home/runner/apt/cache"
sudo chown -R runner /home/runner/apt/cache
- name: Install libvips
env:
DEBIAN_FRONTEND: noninteractive
run: sudo apt install --fix-missing libvips -o dir::cache::archives="/home/runner/apt/cache"
- uses: actions/download-artifact@v4
if: needs.check_bun_lock.outputs.bun_lock_changed == 'true'
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ yarn-debug.log*
/spec/dummy/public/pictures
.byebug_history
.vscode/
/spec/dummy/storage
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ gem "pg", "~> 1.0" if ENV["DB"] == "postgresql"

gem "alchemy_i18n", github: "AlchemyCMS/alchemy_i18n", branch: "download-flatpickr-locales"

gem "ruby-vips"

group :development, :test do
gem "execjs", "~> 2.9.1"
gem "rubocop", require: false
Expand Down
2 changes: 2 additions & 0 deletions alchemy_cms.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Gem::Specification.new do |gem|
activejob
activemodel
activerecord
activestorage
activesupport
railties
].each do |rails_gem|
Expand All @@ -45,6 +46,7 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency "dragonfly", ["~> 1.4"]
gem.add_runtime_dependency "dragonfly_svg", ["~> 0.0.4"]
gem.add_runtime_dependency "gutentag", ["~> 2.2", ">= 2.2.1"]
gem.add_runtime_dependency "image_processing", ["~> 1.13"]
gem.add_runtime_dependency "importmap-rails", ["~> 2.0"]
gem.add_runtime_dependency "kaminari", ["~> 1.1"]
gem.add_runtime_dependency "originator", ["~> 3.1"]
Expand Down
4 changes: 2 additions & 2 deletions app/components/alchemy/ingredients/picture_view.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class PictureView < BaseView
# @param disable_link [Boolean] (false) Whether to disable the link even if the picture has a link.
# @param srcset [Array<String>] An array of srcset sizes that will generate variants of the picture.
# @param sizes [Array<String>] An array of sizes that will be passed to the img tag.
# @param picture_options [Hash] Options that will be passed to the picture url. See {Alchemy::PictureVariant} for options.
# @param picture_options [Hash] Options that will be passed to the picture url. See {Alchemy::Picture#url} for options.
# @param html_options [Hash] Options that will be passed to the img tag.
# @see Alchemy::PictureVariant
# @see Alchemy::Picture#url
def initialize(
ingredient,
show_caption: nil,
Expand Down
8 changes: 0 additions & 8 deletions app/controllers/alchemy/admin/attachments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,6 @@ def destroy
flash[:notice] = Alchemy.t("File deleted successfully", name: name)
end

def download
@attachment = Attachment.find(params[:id])
send_file @attachment.file.path, {
filename: @attachment.file_name,
type: @attachment.file_mime_type
}
end

private

def search_filter_params
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/alchemy/admin/pictures_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PicturesController < Alchemy::Admin::ResourcesController

def index
@query = Picture.ransack(search_filter_params[:q])
@pictures = filtered_pictures.includes(:thumbs)
@pictures = filtered_pictures.with_attached_image_file

if in_overlay?
archive_overlay
Expand Down
41 changes: 24 additions & 17 deletions app/controllers/alchemy/attachments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,44 @@

module Alchemy
class AttachmentsController < BaseController
include ActiveStorage::Streaming

before_action :load_attachment
authorize_resource class: Alchemy::Attachment

self.etag_with_template_digest = false

# sends file inline. i.e. for viewing pdfs/movies in browser
def show
response.headers["Content-Length"] = @attachment.file.size.to_s
send_file(
@attachment.file.path,
{
filename: @attachment.file_name,
type: @attachment.file_mime_type,
disposition: "inline"
}
)
authorize! :show, @attachment
send_blob disposition: :inline
end

# sends file as attachment. aka download
def download
response.headers["Content-Length"] = @attachment.file.size.to_s
send_file(
@attachment.file.path, {
filename: @attachment.file_name,
type: @attachment.file_mime_type
}
)
authorize! :download, @attachment
send_blob disposition: :attachment
end

private

def load_attachment
@attachment = Attachment.find(params[:id])
end

def send_blob(disposition: :inline)
@blob = @attachment.file.blob

if request.headers["Range"].present?
send_blob_byte_range_data @blob, request.headers["Range"], disposition: disposition

Check warning on line 33 in app/controllers/alchemy/attachments_controller.rb

View check run for this annotation

Codecov / codecov/patch

app/controllers/alchemy/attachments_controller.rb#L33

Added line #L33 was not covered by tests
else
http_cache_forever public: true do
response.headers["Accept-Ranges"] = "bytes"
send_blob_stream @blob, disposition: disposition
# Rails ActionController::Live removes the Content-Length header,
# but browsers need that to be able to show a progress bar during download.
response.headers["Content-Length"] = @blob.byte_size.to_s
end
end
end
end
end
95 changes: 78 additions & 17 deletions app/models/alchemy/attachment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,21 @@
include Alchemy::Taggable
include Alchemy::TouchElements

dragonfly_accessor :file, app: :alchemy_attachments do
after_assign { |f| write_attribute(:file_mime_type, f.mime_type) }
end
attr_readonly(
:legacy_image_file_name,
:legacy_image_file_size,
:legacy_image_file_uid
)

deprecate(
:legacy_image_file_name,
:legacy_image_file_size,
:legacy_image_file_uid,
deprecator: Alchemy::Deprecation
)

# Use ActiveStorage file attachments
has_one_attached :file, service: :alchemy_cms

stampable stamper_class_name: Alchemy.user_class.name

Expand All @@ -38,7 +50,11 @@
has_many :elements, through: :file_ingredients
has_many :pages, through: :elements

scope :by_file_type, ->(file_type) { where(file_mime_type: file_type) }
scope :by_file_type,
->(file_type) {
with_attached_file.joins(:file_blob).where(active_storage_blobs: {content_type: file_type})
}

scope :recent, -> { where("#{table_name}.created_at > ?", Time.current - 24.hours).order(:created_at) }
scope :without_tag, -> { left_outer_joins(:taggings).where(gutentag_taggings: {id: nil}) }

Expand All @@ -62,7 +78,7 @@
[
{
name: :by_file_type,
values: distinct.pluck(:file_mime_type).map { |type| [Alchemy.t(type, scope: "mime_types"), type] }.sort_by(&:first)
values: file_types
},
{
name: :misc,
Expand All @@ -78,32 +94,48 @@
where(id: last_id)
end

# Used by Alchemy::Resource#search_field_name to build the search query
def searchable_alchemy_resource_attributes
%w[name file_name]
%w[name file_blob_filename]
end

def ransackable_attributes(_auth_object = nil)
%w[name]
end

def ransackable_associations(_auth_object = nil)
%w[file_blob]
end

def allowed_filetypes
Config.get(:uploader).fetch("allowed_filetypes", {}).fetch("alchemy/attachments", [])
end

private

def file_types
ActiveStorage::Blob.joins(:attachments).merge(
ActiveStorage::Attachment.where(record_type: name)
).distinct.pluck(:content_type)
end
end

validates_presence_of :file
validates_size_of :file, maximum: Config.get(:uploader)["file_size_limit"].megabytes
validates_property :ext,
of: :file,
in: allowed_filetypes,
case_sensitive: false,
message: Alchemy.t("not a valid file"),
unless: -> { self.class.allowed_filetypes.include?("*") }

before_save :set_name, if: :file_name_changed?
validate :file_not_too_big, if: -> { file.present? }

validate :file_type_allowed,
unless: -> { self.class.allowed_filetypes.include?("*") },
if: -> { file.present? }

before_save :set_name, if: -> { file.changed? }

scope :with_file_type, ->(file_type) { where(file_mime_type: file_type) }

# Instance methods

def url(options = {})
if file
if file.present?
self.class.url_class.new(self).call(options)
end
end
Expand All @@ -118,9 +150,23 @@
pages.any? && pages.not_restricted.blank?
end

# File name
def file_name
file&.filename&.to_s
end

# File size
def file_size
file&.byte_size
end

def file_mime_type
super || file&.content_type
end

# File format suffix
def extension
file_name.split(".").last
file&.filename&.extension
end

alias_method :suffix, :extension
Expand Down Expand Up @@ -156,8 +202,23 @@

private

def file_type_allowed
unless extension&.in?(self.class.allowed_filetypes)
errors.add(:file, Alchemy.t("not a valid file"))
end
end

def file_not_too_big
maximum = Config.get(:uploader)["file_size_limit"]&.megabytes
return true unless maximum

if file_size > maximum
errors.add(:file, :too_big)

Check warning on line 216 in app/models/alchemy/attachment.rb

View check run for this annotation

Codecov / codecov/patch

app/models/alchemy/attachment.rb#L216

Added line #L216 was not covered by tests
end
end

def set_name
self.name = convert_to_humanized_name(file_name, file.ext)
self.name = convert_to_humanized_name(file_name, extension)
end
end
end
Loading
Loading