summaryrefslogtreecommitdiff
path: root/app/models/concerns/file_attachment.rb
blob: b3ff0f14538967ed92634c911d7b9d10374424e8 (plain)
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
# 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

  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

    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)
    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("convert", original_path, "-resize", options[:geometry], 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