summaryrefslogtreecommitdiff
path: root/app/models/concerns/file_attachment.rb
blob: 0e99fa2e1135156a28a9277df27fbac9bcbdb590 (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
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