Skip to content

Commit c72a016

Browse files
committed
fix: Use Ruby/Rails internals to create human readable file names
Use Ruby's `File.basename` and Rails' `humanize` to create the first initial human readable `name` of `Attachment`s and `Picture`s. Before we where using our own brittle Regexp, that expected the file suffix to be known from it's mime type. This is not the case for unknown mimetypes. We also move the name conversion before filename sanitization in order to get a nice human readable name and not escape characters. Since the name is not used to load the file and all output is escaped in Rails by default, this is fine. Signed-off-by: Thomas von Deyen <vondeyen@blish.cloud>
1 parent bddb4cd commit c72a016

12 files changed

Lines changed: 66 additions & 41 deletions

File tree

app/controllers/alchemy/admin/pictures_controller.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def url
6464

6565
def create
6666
@picture = Picture.new(picture_params)
67-
@picture.name = @picture.humanized_name
6867
if @picture.save
6968
render successful_uploader_response(file: @picture)
7069
else

app/models/alchemy/attachment.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class Attachment < BaseRecord
2525
include Alchemy::TouchElements
2626
include Alchemy::RelatableResource
2727

28+
before_save :set_name, if: -> { Alchemy.storage_adapter.set_attachment_name?(self) }
29+
2830
include Alchemy.storage_adapter.attachment_class_methods
2931

3032
stampable stamper_class_name: Alchemy.config.user_class_name
@@ -97,8 +99,6 @@ def ransackable_scopes(_auth_object = nil)
9799
validate :file_type_allowed,
98100
unless: -> { self.class.allowed_filetypes.include?("*") }
99101

100-
before_save :set_name, if: -> { Alchemy.storage_adapter.set_attachment_name?(self) }
101-
102102
scope :with_file_type, ->(file_type) { where(file_mime_type: file_type) }
103103

104104
# Instance methods
@@ -179,7 +179,7 @@ def file_type_allowed
179179
end
180180

181181
def set_name
182-
self.name ||= convert_to_humanized_name(file_name, extension)
182+
self.name ||= Alchemy.storage_adapter.file_basename(self).humanize
183183
end
184184
end
185185
end

app/models/alchemy/picture.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ def self.preprocessor_class=(klass)
6262
@_preprocessor_class = klass
6363
end
6464

65+
before_create :set_name, if: :image_file_name
66+
6567
include Alchemy.storage_adapter.picture_class_methods
6668

6769
# We need to define this method here to have it available in the validations below.
@@ -195,14 +197,6 @@ def urlname
195197
end
196198
end
197199

198-
# Returns a humanized, readable name from image filename.
199-
#
200-
def humanized_name
201-
return "" if image_file_name.blank?
202-
203-
convert_to_humanized_name(image_file_name, image_file_extension)
204-
end
205-
206200
# Returns the format the image should be rendered with
207201
#
208202
# Only returns a format differing from original if an +image_output_format+
@@ -287,6 +281,12 @@ def image_file_dimensions
287281

288282
private
289283

284+
# Returns a humanized, readable name from image filename.
285+
#
286+
def set_name
287+
self.name ||= Alchemy.storage_adapter.image_file_basename(self).humanize
288+
end
289+
290290
def image_file_type_allowed
291291
unless image_file_extension&.in?(self.class.allowed_filetypes)
292292
errors.add(:image_file, Alchemy.t("not a valid image"))

app/models/alchemy/storage_adapter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ class UnknownAdapterError < StandardError; end
99
:by_file_format_scope,
1010
:by_file_type_scope,
1111
:not_file_type_scope,
12+
:file_basename,
1213
:file_extension,
1314
:file_formats,
1415
:file_mime_type,
1516
:file_name,
1617
:file_size,
1718
:has_convertible_format?,
19+
:image_file_basename,
1820
:image_file_extension,
1921
:image_file_format,
2022
:image_file_height,

app/models/alchemy/storage_adapter/active_storage.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ def not_file_type_scope(file_type)
9696
Attachment.with_attached_file.joins(:file_blob).where.not(active_storage_blobs: {content_type: file_type})
9797
end
9898

99+
# @param [Alchemy::Attachment]
100+
# @return [String]
101+
def file_basename(attachment)
102+
filename = attachment.file&.filename.to_s
103+
File.basename(filename, File.extname(filename))
104+
end
105+
99106
# @param [Alchemy::Attachment]
100107
# @return [String]
101108
def file_name(attachment)
@@ -126,6 +133,13 @@ def has_convertible_format?(picture)
126133
picture.image_file&.variable?
127134
end
128135

136+
# @param [Alchemy::Picture]
137+
# @return [String]
138+
def image_file_basename(picture)
139+
filename = picture.image_file&.filename.to_s
140+
File.basename(filename, File.extname(filename))
141+
end
142+
129143
# @param [Alchemy::Picture]
130144
# @return [String]
131145
def image_file_name(picture)

app/models/alchemy/storage_adapter/dragonfly.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ def file_name(attachment)
127127
attachment.read_attribute(:file_name)
128128
end
129129

130+
# @param [Alchemy::Attachment]
131+
# @return [String]
132+
def file_basename(attachment)
133+
attachment.file&.basename&.to_s
134+
end
135+
130136
# @param [Alchemy::Attachment]
131137
# @return [Integer]
132138
def file_size(attachment)
@@ -152,6 +158,12 @@ def has_convertible_format?(picture)
152158
image_file_extension(picture).in?(CONVERTIBLE_FILE_FORMATS)
153159
end
154160

161+
# @param [Alchemy::Picture]
162+
# @return [String]
163+
def image_file_basename(picture)
164+
picture.image_file&.basename&.to_s
165+
end
166+
155167
# @param [Alchemy::Picture]
156168
# @return [String]
157169
def image_file_name(picture)

lib/alchemy/name_conversions.rb

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ def convert_to_urlname(name)
1717
.parameterize
1818
end
1919

20-
# Converts a filename and suffix into a human readable name.
21-
#
22-
def convert_to_humanized_name(name, suffix)
23-
name.gsub(/\.#{::Regexp.quote(suffix)}$/i, "").tr("_", " ").strip
24-
end
25-
2620
# Sanitizes a given filename by removing directory traversal attempts and HTML entities.
2721
def sanitized_filename(file_name)
2822
file_name = File.basename(file_name)

spec/controllers/alchemy/admin/pictures_controller_spec.rb

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,15 +141,14 @@ module Alchemy
141141
describe "#create" do
142142
subject { post :create, params: params }
143143

144-
let(:params) { {picture: {name: ""}} }
145-
let(:picture) { mock_model("Picture", humanized_name: "Cute kittens") }
146-
147144
context "with passing validations" do
148-
before do
149-
expect(Picture).to receive(:new).and_return(picture)
150-
expect(picture).to receive(:name=).and_return("Cute kittens")
151-
expect(picture).to receive(:name).and_return("Cute kittens")
152-
expect(picture).to receive(:save).and_return(true)
145+
let(:params) do
146+
{picture: {image_file: fixture_file_upload("my FileNämü.png")}}
147+
end
148+
149+
it "creates a new picture with human friendly name" do
150+
expect { subject }.to change { Picture.count }.by(1)
151+
expect(Picture.last.name).to eq("My filenämü")
153152
end
154153

155154
it "renders json response with success message" do
@@ -162,6 +161,8 @@ module Alchemy
162161
end
163162

164163
context "with failing validations" do
164+
let(:params) { {picture: {name: ""}} }
165+
165166
it_behaves_like "having a json uploader error message"
166167
end
167168
end
70 Bytes
Loading
73 Bytes
Loading

0 commit comments

Comments
 (0)