1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
|
# FileAttachment — minimal drop-in replacement for Paperclip's has_attached_file.
#
# Provides the same interface used throughout this codebase:
# asset.upload.url -> "/system/uploads/:id/original/:filename"
# asset.upload.url(:thumb) -> "/system/uploads/:id/thumb/:filename"
# asset.upload.content_type -> string
# asset.upload.size -> integer (bytes)
#
# Files are stored at:
# Rails.root/public/system/uploads/:id/:style/:filename
#
# Image variants are generated via ImageMagick (convert) on upload.
# Non-image files get only an original, no variants.
#
# To replace an asset: assign a new file to asset.upload= and save.
# The filename is fixed on first upload and preserved on replacement,
# keeping all public URLs stable.
#
# Future: if more sophisticated asset management is needed (versioning,
# S3, on-demand resizing), replace this module and keep the interface.
module FileAttachment
extend ActiveSupport::Concern
STYLES = {
medium: { geometry: "300x300>", format: nil },
thumb: { geometry: "100x100>", format: nil },
headline: { geometry: "460x250!", format: nil }
}.freeze
IMAGE_CONTENT_TYPES = %w[image/jpeg image/gif image/png image/webp].freeze
VECTOR_CONTENT_TYPES = %w[image/svg+xml].freeze
DISPLAYABLE_AS_IMAGE = IMAGE_CONTENT_TYPES + VECTOR_CONTENT_TYPES
included do
attr_reader :upload
after_initialize :build_upload_proxy
after_save :process_upload
before_destroy :delete_upload_files
end
def upload=(uploaded_file)
return if uploaded_file.blank?
@pending_upload = uploaded_file
# Populate the database columns immediately so validations can use them
self.upload_file_name = sanitize_filename(uploaded_file.original_filename)
self.upload_content_type = uploaded_file.content_type.to_s.split(';').first.strip
self.upload_file_size = uploaded_file.size
self.upload_updated_at = Time.current
build_upload_proxy
end
private
def build_upload_proxy
@upload = UploadProxy.new(self)
end
def process_upload
return unless @pending_upload
uploaded_file = @pending_upload
@pending_upload = nil
old_dir = Rails.root.join("public", "system", "uploads", id.to_s)
FileUtils.rm_rf(old_dir) if Dir.exist?(old_dir)
original_path = file_path(:original)
FileUtils.mkdir_p(File.dirname(original_path))
FileUtils.cp(uploaded_file.tempfile.path, original_path)
if IMAGE_CONTENT_TYPES.include?(upload_content_type)
generate_variants(original_path)
elsif VECTOR_CONTENT_TYPES.include?(upload_content_type)
generate_svg_variants(original_path)
end
end
def generate_variants(original_path)
STYLES.each do |style, options|
dest_path = file_path(style)
FileUtils.mkdir_p(File.dirname(dest_path))
system("magick", original_path, "-resize", options[:geometry], dest_path)
end
end
def generate_svg_variants(original_path)
STYLES.each do |style, _|
dest_path = file_path(style)
FileUtils.mkdir_p(File.dirname(dest_path))
FileUtils.cp(original_path, dest_path)
end
end
def delete_upload_files
dir = Rails.root.join("public", "system", "uploads", id.to_s)
FileUtils.rm_rf(dir) if Dir.exist?(dir)
end
def file_path(style)
Rails.root.join(
"public", "system", "uploads",
id.to_s, style.to_s, upload_file_name
).to_s
end
def sanitize_filename(filename)
File.basename(filename).gsub(/[^\w\.\-]/, '_')
end
# Proxy object returned by asset.upload, providing the Paperclip-compatible
# interface used in views: .url, .url(:style), .content_type, .size
class UploadProxy
def initialize(record)
@record = record
end
def url(style = :original)
return "" if @record.upload_file_name.blank?
"/system/uploads/#{@record.id}/#{style}/#{@record.upload_file_name}"
end
def content_type
@record.upload_content_type.to_s
end
def size
@record.upload_file_size.to_i
end
def present?
@record.upload_file_name.present?
end
def blank?
!present?
end
end
end
|