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