Module: Dscf::Core::FileUploadable

Extended by:
ActiveSupport::Concern
Included in:
FilesController
Defined in:
app/controllers/concerns/dscf/core/file_uploadable.rb

Overview

Include this concern in controllers that need to handle file uploads Works seamlessly with Dscf::Core::Common for automatic file handling in CRUD operations

File validation happens BEFORE save, and if it fails, the entire operation is rolled back. This ensures atomic behavior - either everything succeeds or nothing is saved.

Examples:

With Common module (recommended - zero config!)

class ProductsController < ApplicationController
  include Dscf::Core::Common
  include Dscf::Core::FileUploadable  # Add after Common

  # That's it! Files are auto-attached from params on create/update
  # Just make sure your model has: has_one_file :avatar or has_many_files :images
end

With custom file param names

class ProductsController < ApplicationController
  include Dscf::Core::Common
  include Dscf::Core::FileUploadable

  # Override to specify which params are file uploads
  def file_upload_params
    { avatar: params[:product][:avatar], images: params[:product][:images] }
  end
end

Soft failure mode (don’t fail on upload errors)

class ProductsController < ApplicationController
  include Dscf::Core::Common
  include Dscf::Core::FileUploadable

  def file_upload_strict_mode?
    false  # Record will be saved even if file upload fails
  end
end

Instance Method Summary collapse

Instance Method Details

#after_save_hook(record) ⇒ Object

Hook called by Common module after create/update Automatically attaches files from params with strict validation

Raises:



50
51
52
53
54
55
56
57
58
59
60
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 50

def after_save_hook(record)
  super if defined?(super)

  success = auto_attach_files(record)

  # In strict mode, raise error to trigger rollback if upload failed
  return unless !success && file_upload_strict_mode?

  error_message = @file_upload_errors&.first || "File upload failed"
  raise FileUploadError, error_message
end

#attach_files(record, attachment_name, files, **options) ⇒ Boolean

Attach files to a model’s attachment field

Parameters:

  • record (ActiveRecord::Base)

    the record to attach files to

  • attachment_name (Symbol)

    the attachment field name

  • files (ActionDispatch::Http::UploadedFile, Array)

    the file(s) to attach

  • options (Hash)

    additional options

Returns:

  • (Boolean)

    success status



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 75

def attach_files(record, attachment_name, files, **options)
  return true if files.blank?
  return true unless record.respond_to?(attachment_name)

  attachment = record.send(attachment_name)
  return true unless attachment.respond_to?(:attach)

  # Auto-set uploaded_by if available
  attachment = attachment.uploaded_by(current_user) if respond_to?(:current_user) && current_user

  result = attachment.attach(files, **options)

  unless result
    @file_upload_errors ||= []
    @file_upload_errors.concat(Array(attachment.errors))
  end

  result
end

#auto_attach_files(record, namespace: nil) ⇒ Boolean

Automatically attach file uploads from params to a record Detects file params and attaches them to matching model attachments

Parameters:

  • record (ActiveRecord::Base)

    the record to attach files to

  • namespace (Symbol) (defaults to: nil)

    the params namespace (default: model name underscored)

Returns:

  • (Boolean)

    success status (false if any attachment failed)



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 100

def auto_attach_files(record, namespace: nil)
  @file_upload_errors = []

  # Use custom file params if defined
  files_to_attach = if respond_to?(:file_upload_params, true)
                      file_upload_params
                    else
                      extract_file_params(record, namespace)
                    end

  all_success = true
  files_to_attach.each do |attachment_name, file_value|
    success = attach_files(record, attachment_name.to_sym, file_value)
    all_success = false unless success
  end

  all_success
end

#file_upload_strict_mode?Boolean

Whether to fail the entire operation if file upload fails Override in controller to change behavior

Returns:

  • (Boolean)

    true = fail on upload error, false = save record anyway



65
66
67
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 65

def file_upload_strict_mode?
  true
end

#process_base64_file(base64_data, filename:, content_type:) ⇒ Hash

Process base64 encoded file

Parameters:

  • base64_data (String)

    base64 encoded file data

  • filename (String)

    desired filename

  • content_type (String)

    MIME type

Returns:

  • (Hash)

    file hash suitable for uploading



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 202

def process_base64_file(base64_data, filename:, content_type:)
  return nil if base64_data.blank?

  # Handle data URL format: "data:image/png;base64,..."
  if base64_data.include?(",")
    content_type_match = base64_data.match(/data:([^;]+);base64/)
    content_type = content_type_match[1] if content_type_match
    base64_data = base64_data.split(",").last
  end

  decoded = Base64.decode64(base64_data)
  io = StringIO.new(decoded)

  {
    io: io,
    filename: filename,
    content_type: content_type
  }
end

#send_attachment(attachment, **options) ⇒ Object

Download a file from an attachment

Parameters:



183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 183

def send_attachment(attachment, **options)
  unless attachment&.attached?
    render json: {error: "File not found"}, status: :not_found
    return
  end

  send_stored_file(
    attachment.file_key,
    filename: options[:filename] || attachment.filename,
    content_type: options[:content_type] || attachment.content_type,
    disposition: options[:disposition] || "inline"
  )
end

#send_stored_file(file_key, **options) ⇒ Object

Download a file and send it as response

Parameters:

  • file_key (String)

    the file key in storage

  • options (Hash)

    download options



166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 166

def send_stored_file(file_key, **options)
  client = FileStorage::Client.new
  result = client.download(file_key)

  send_data(
    result[:data],
    filename: options[:filename] || result[:filename],
    type: options[:content_type] || result[:content_type],
    disposition: options[:disposition] || "inline"
  )
rescue FileStorage::Client::DownloadError => e
  render json: {error: e.message}, status: :not_found
end

#upload_base64_file(base64_data, filename:, content_type:, **options) ⇒ Hash?

Upload a base64 encoded file

Parameters:

  • base64_data (String)

    base64 encoded file data

  • filename (String)

    desired filename

  • content_type (String)

    MIME type

  • options (Hash)

    uploader options

Returns:

  • (Hash, nil)

    uploaded file hash



228
229
230
231
232
233
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 228

def upload_base64_file(base64_data, filename:, content_type:, **options)
  file_hash = process_base64_file(base64_data, filename: filename, content_type: content_type)
  return nil unless file_hash

  upload_file(file_hash, **options)
end

#upload_file(file, **options) ⇒ Dscf::Core::FileStorage::Attachment?

Upload a file directly (without attaching to a model) Useful for standalone upload endpoints

Parameters:

  • file (ActionDispatch::Http::UploadedFile)

    the file to upload

  • options (Hash)

    uploader options

Returns:

  • (Dscf::Core::FileStorage::Attachment, nil)


124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 124

def upload_file(file, **options)
  @file_upload_errors = []

  if file.blank?
    @file_upload_errors << "No file provided"
    return nil
  end

  uploader = FileStorage::Uploader.new(options)
  result = uploader.upload(file)

  unless result
    @file_upload_errors = uploader.errors
    return nil
  end

  result
end

#upload_files(files, **options) ⇒ Array<Hash>

Upload multiple files directly

Parameters:

  • files (Array<ActionDispatch::Http::UploadedFile>)

    the files to upload

  • options (Hash)

    uploader options

Returns:

  • (Array<Hash>)

    array of uploaded file hashes



147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'app/controllers/concerns/dscf/core/file_uploadable.rb', line 147

def upload_files(files, **options)
  @file_upload_errors = []

  if files.blank?
    @file_upload_errors << "No files provided"
    return []
  end

  uploader = FileStorage::Uploader.new(options)
  results = uploader.upload_many(files)

  @file_upload_errors = uploader.errors unless uploader.success?

  results
end