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.rb124
1 files changed, 124 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..b3ff0f1
--- /dev/null
+++ b/app/models/concerns/file_attachment.rb
@@ -0,0 +1,124 @@
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
33 included do
34 attr_reader :upload
35
36 after_initialize :build_upload_proxy
37 after_save :process_upload
38 before_destroy :delete_upload_files
39 end
40
41 def upload=(uploaded_file)
42 return if uploaded_file.blank?
43 @pending_upload = uploaded_file
44 # Populate the database columns immediately so validations can use them
45 self.upload_file_name = sanitize_filename(uploaded_file.original_filename)
46 self.upload_content_type = uploaded_file.content_type.to_s.split(';').first.strip
47 self.upload_file_size = uploaded_file.size
48 self.upload_updated_at = Time.current
49 build_upload_proxy
50 end
51
52 private
53
54 def build_upload_proxy
55 @upload = UploadProxy.new(self)
56 end
57
58 def process_upload
59 return unless @pending_upload
60 uploaded_file = @pending_upload
61 @pending_upload = nil
62
63 original_path = file_path(:original)
64 FileUtils.mkdir_p(File.dirname(original_path))
65 FileUtils.cp(uploaded_file.tempfile.path, original_path)
66
67 if IMAGE_CONTENT_TYPES.include?(upload_content_type)
68 generate_variants(original_path)
69 end
70 end
71
72 def generate_variants(original_path)
73 STYLES.each do |style, options|
74 dest_path = file_path(style)
75 FileUtils.mkdir_p(File.dirname(dest_path))
76 system("convert", original_path, "-resize", options[:geometry], dest_path)
77 end
78 end
79
80 def delete_upload_files
81 dir = Rails.root.join("public", "system", "uploads", id.to_s)
82 FileUtils.rm_rf(dir) if Dir.exist?(dir)
83 end
84
85 def file_path(style)
86 Rails.root.join(
87 "public", "system", "uploads",
88 id.to_s, style.to_s, upload_file_name
89 ).to_s
90 end
91
92 def sanitize_filename(filename)
93 File.basename(filename).gsub(/[^\w\.\-]/, '_')
94 end
95
96 # Proxy object returned by asset.upload, providing the Paperclip-compatible
97 # interface used in views: .url, .url(:style), .content_type, .size
98 class UploadProxy
99 def initialize(record)
100 @record = record
101 end
102
103 def url(style = :original)
104 return "" if @record.upload_file_name.blank?
105 "/system/uploads/#{@record.id}/#{style}/#{@record.upload_file_name}"
106 end
107
108 def content_type
109 @record.upload_content_type.to_s
110 end
111
112 def size
113 @record.upload_file_size.to_i
114 end
115
116 def present?
117 @record.upload_file_name.present?
118 end
119
120 def blank?
121 !present?
122 end
123 end
124end