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 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0d7e35d
Add activestorage
tvdeyen Apr 12, 2022
e0991b1
Add image_processing
tvdeyen Apr 12, 2022
dca9dc9
Add dragonfly to image processing converter
tvdeyen Apr 12, 2022
1793a7e
Use vips as image processor in dummy app
tvdeyen Apr 12, 2022
a5f6143
[ci] install libvips
tvdeyen Apr 12, 2022
a13e936
Ignore active storage files in dummy app
tvdeyen Apr 12, 2022
2d71b01
Use custom validations for picture size and format
tvdeyen Apr 12, 2022
8e96461
Use activestorage for picture file handling
tvdeyen Apr 12, 2022
ec76c70
Do not sharpen images by default
tvdeyen Apr 12, 2022
a375642
Add image_file_extension method
tvdeyen Apr 12, 2022
a432155
Delegate convertible format check to activestorage
tvdeyen Apr 12, 2022
83631ee
Eager load attachments in admin pictures controller
tvdeyen Apr 12, 2022
f778479
Delegate image_file_* methods to attached file
tvdeyen Apr 12, 2022
a1dedb0
Remove custom picture variant classes
tvdeyen Apr 12, 2022
c357f0d
Move can_be_cropped_to? into picture_thumbnails
tvdeyen Apr 12, 2022
9e673f1
Use deletaged image_file methods for validations
tvdeyen Apr 12, 2022
589a9d7
Use ActiveStorage for Attachments as well
tvdeyen Apr 12, 2022
b821d2c
Use file mime type for picture by format select
tvdeyen Apr 12, 2022
5ed6854
Remove Dragonfly
tvdeyen Apr 12, 2022
614e67d
Add support for animated gifs
tvdeyen Aug 2, 2023
34a5bfb
Always return image format
tvdeyen Aug 4, 2023
cbfe074
Use ActiveStorage redirect path
tvdeyen Jan 23, 2024
e79d99e
Fix picture and attachment search
tvdeyen Jan 23, 2024
7d2ccf9
Add upgrader for active storage
tvdeyen Jan 23, 2024
324dd94
Preprocess thumbnails after upload
tvdeyen Jan 29, 2024
7f6b7c0
Use image_file_extension as original format
tvdeyen Jun 11, 2024
4b3c0ce
Allow webp as image format for ActiveStorage
tvdeyen Jun 11, 2024
9587e59
wip: append upgrader
tvdeyen Sep 18, 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
4 changes: 2 additions & 2 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 @@ -42,9 +43,8 @@ Gem::Specification.new do |gem|
gem.add_runtime_dependency "cancancan", [">= 2.1", "< 4.0"]
gem.add_runtime_dependency "coffee-rails", [">= 4.0", "< 6.0"]
gem.add_runtime_dependency "csv", ["~> 3.3"]
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.2"]
gem.add_runtime_dependency "importmap-rails", ["~> 1.2", ">= 1.2.1"]
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
82 changes: 65 additions & 17 deletions app/models/alchemy/attachment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@
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
# Use ActiveStorage file attachments
has_one_attached :file, service: :alchemy_cms

stampable stamper_class_name: Alchemy.user_class.name

Expand All @@ -38,7 +37,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 +65,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 +81,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 +137,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 +189,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 203 in app/models/alchemy/attachment.rb

View check run for this annotation

Codecov / codecov/patch

app/models/alchemy/attachment.rb#L203

Added line #L203 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