diff options
Diffstat (limited to 'app/models/concerns/file_attachment.rb')
| -rw-r--r-- | app/models/concerns/file_attachment.rb | 124 |
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 | |||
| 22 | module 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 | ||
| 124 | end | ||
