Class: Upload

Inherits:
ApplicationRecord show all
Includes:
Models::Auditable
Defined in:
app/models/upload.rb

Overview

== Schema Information

Table name: uploads
Database name: primary

id :integer not null, primary key
asset :jsonb
attachment_mime_type :string
attachment_name :string
attachment_processing :boolean
attachment_size :integer
attachment_uid :string
category :string(255)
document_date :date
expiration_date :date
note :text
position :integer
resource_type :string(255)
template :string
thumbnail_asset :jsonb
type :string(30)
created_at :datetime
updated_at :datetime
resource_id :integer

Indexes

idx_resource_type_category (resource_type,category) WHERE (resource_id IS NULL)
idx_resource_type_category_resource_id (resource_type,category,resource_id)
index_uploads_on_category (category)
index_uploads_on_resource_type_and_resource_id (resource_type,resource_id)
index_uploads_on_type (type) WHERE (type IS NOT NULL)

Direct Known Subclasses

Literature

Defined Under Namespace

Classes: CurrentPaths

Constant Summary collapse

MODEL_TO_CATEGORIES =

.each_with_object({}) {|cat, hsh| hsh[cat] = cat.to_s.humanize.titleize }.freeze

{
  activity: %i[activity_attachment msrp_quote_pdf_transmission organization_quote_pdf_transmission room_layout],
  call_record: %i[call_record],
  certification: %i[agreement liability_insurance],
  check: %i[check],
  credit_memo: %i[credit_memo_pdf statement_of_account_pdf],
  contact: %i[liability_insurance contract],
  customer: %i[liability_insurance contract],
  delivery: %i[all_intl_forms_pdf all_labels_pdf manually_generated_combined_pdf manual_ship_label pick_slip_pdf serial_numbers_pdf ship_bol_pdf ship_ci_pdf electronic_ship_ci_pdf split_pick_slip_pdf],
  employee: %i[certificate contract resume form],
  item: %i[usmca_certificate other_certificate],
  invoice: %i[invoice_pdf statement_of_account_pdf],
  mail_activity: %i[all_labels_pdf],
  manifest: %i[manifest_pdf],
  speedee_manifest: %i[summary_pdf manifest_csv],
  opportunity: %i[installation_plan_cad installation_plan_image room_layout room_layout_image],
  order: %i[custom_order_agreement custom_packing_slip_pdf early_ship_label installation_plan_pdf invalid_pick_slip_pdf manual_ship_label manual_smart_preset_form master_carton_label other pick_slip_pdf purchase_order ship_bol_pdf],
  purchase_order: %i[purchase_order],
  quote: %i[copy_of_plans_pdf msrp_quote_pdf organization_quote_pdf],
  rma: %i[letter_ship_label_pdf photo rma_return_instructions_pdf],
  room_configuration: %i[design_guideline electrical_plan_pdf installation_plan_cad installation_plan_image installation_plan_pdf installation_heated_area other photo room_layout room_layout_image warranty_card warranty_card_pdf
                         heat_loss_customer_form heat_loss_report multizone_plan multizone_plan_pdf],
  service_job: %i[room_layout tstat_job_result],
  shipment: %i[container_label letter_ship_label_pdf manual_ship_label ship_bol_pdf ship_label_image ship_label_pdf],
  statement_of_account: %i[statement_of_account_pdf],
  store: %i[super_pick_slip],
  supplier: %i[price_list contract],
  edi_communication_log: %i[custom_packing_slip_pdf feed_document_result_xml feed_document_result_json feed_document_result_gzip],
  exported_catalog_item_packet: %i[exported_packet],
  catalog_item: %i[amalytix_snapshot],
  video: %i[audio_extraction captions],
  assistant_conversation: %i[assistant_attachment],
  tax_exemption: %i[tax_exemption]
}.freeze
UNCLASSIFIED_CATEGORIES =
%i[accounting_documents all_postage_labels_path_pdf article_pdf fedex_manifest google_local_inventory_feed misc preview_organization_quote_pdf price_list_pdf reviews_io_product_feed super_invoice super_pick_slip super_pick_slip_pdf super_serial_number_label_pdf super_soa
agreement_pdf order_review archive declaration_of_steel_aluminimum_copper].freeze
EXCLUDE_CATEGORIES_FOR_MANUAL_UPLOAD =

we don't want people accidentally manually uploading the wrong category when these are only generated by HW

%i[all_postage_labels_path_pdf article_pdf preview_organization_quote_pdf price_list_pdf super_pick_slip_pdf super_serial_number_label_pdf super_soa agreement_pdf electrical_plan_pdf installation_plan_pdf multizone_plan_pdf].freeze
CATEGORIES =
(MODEL_TO_CATEGORIES.values.flatten + UNCLASSIFIED_CATEGORIES).uniq
IMAGE_EXTENSIONS =
Marcel::EXTENSIONS.select { |_, mime_type| mime_type.start_with?('image/') }.keys.freeze
RESOURCES_WITH_UPLOADS_COUNT =

Counter cache for uploads_count on polymorphic resources
Only increment for resource types that have an uploads_count column

%w[Activity Customer Contact Employee Order Quote Rma Shipment Supplier].freeze

Constants included from Models::Auditable

Models::Auditable::ALWAYS_IGNORED

Instance Attribute Summary collapse

Belongs to collapse

Methods included from Models::Auditable

#creator, #updater

Has one collapse

Has many collapse

Has and belongs to many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Models::Auditable

#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#attachmentObject (readonly)



132
# File 'app/models/upload.rb', line 132

validates :attachment, presence: true

#importObject

Returns the value of attribute import.



123
124
125
# File 'app/models/upload.rb', line 123

def import
  @import
end

#local_file_pathObject

Returns the value of attribute local_file_path.



123
124
125
# File 'app/models/upload.rb', line 123

def local_file_path
  @local_file_path
end

#should_destroyObject

Returns the value of attribute should_destroy.



123
124
125
# File 'app/models/upload.rb', line 123

def should_destroy
  @should_destroy
end

#templateObject (readonly)



138
# File 'app/models/upload.rb', line 138

validates :template, presence: true, if: proc { |u| u.category == 'installation_plan_image' }

Class Method Details

.all_categories_for_selectObject



171
172
173
# File 'app/models/upload.rb', line 171

def self.all_categories_for_select
  CATEGORIES.sort.map { |c| [c.to_s.humanize.titleize, c.to_s] }
end

.custom_ship_labelsActiveRecord::Relation<Upload>

A relation of Uploads that are custom ship labels. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



145
# File 'app/models/upload.rb', line 145

scope :custom_ship_labels, -> { where(category: %w[master_carton_label manual_ship_label]) }

.has_document_date?(resource_type) ⇒ Boolean

Returns:

  • (Boolean)


544
545
546
# File 'app/models/upload.rb', line 544

def self.has_document_date?(resource_type)
  %w[StatementOfAccount CreditMemo Invoice].include?(resource_type)
end

.has_role_restrictions?(resource_type) ⇒ Boolean

Returns:

  • (Boolean)


536
537
538
# File 'app/models/upload.rb', line 536

def self.has_role_restrictions?(resource_type)
  resource_type == 'Employee'
end

.in_categoryActiveRecord::Relation<Upload>

A relation of Uploads that are in category. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



142
# File 'app/models/upload.rb', line 142

scope :in_category, ->(cat) { where(category: cat) }

.invalidActiveRecord::Relation<Upload>

A relation of Uploads that are invalid. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



141
# File 'app/models/upload.rb', line 141

scope :invalid, -> { where("category LIKE 'invalid_%'") }

.literaturesActiveRecord::Relation<Upload>

A relation of Uploads that are literatures. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



146
# File 'app/models/upload.rb', line 146

scope :literatures, -> { where(type: 'Literature').joins(:publication) }

.most_recent_firstActiveRecord::Relation<Upload>

A relation of Uploads that are most recent first. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



143
# File 'app/models/upload.rb', line 143

scope :most_recent_first, -> { reorder('uploads.created_at DESC') }

.options_for_categories_select(class_name, user: nil, include_other: true, exclude: []) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'app/models/upload.rb', line 329

def self.options_for_categories_select(class_name, user: nil, include_other: true, exclude: [])
  categories = []
  if class_name.present?
    symbolized_class_name = class_name if class_name.is_a?(Symbol)
    symbolized_class_name ||= class_name.tableize.singularize.to_sym
    new_categories = MODEL_TO_CATEGORIES[symbolized_class_name]
    categories += new_categories if new_categories.present?
  end
  categories += [:other] if include_other
  categories.reject! { |cat| cat.in?(exclude) }
  categories.reject! { |cat| cat.in?(EXCLUDE_CATEGORIES_FOR_MANUAL_UPLOAD) }
  categories.uniq.map { |cat| [cat.to_s.humanize.titleize.gsub('Custom', 'This is public and always enclosed: * Custom'), cat] }.sort_by { |cat_arr| cat_arr.last == :purchase_order ? '0' : cat_arr.first }
end

.sorted_by_positionActiveRecord::Relation<Upload>

A relation of Uploads that are sorted by position. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



144
# File 'app/models/upload.rb', line 144

scope :sorted_by_position, -> { reorder('uploads.position, uploads.created_at DESC') }

.temp_location(filename) ⇒ Object



323
324
325
326
327
# File 'app/models/upload.rb', line 323

def self.temp_location(filename)
  tmp_dir = Rails.application.config.x.temp_storage_path
  FileUtils.mkdir(tmp_dir) unless File.directory?(tmp_dir)
  tmp_dir.join(filename)
end

.uploadify(path, category, resource = nil, filename = nil, _do_not_delete = false, expiration_date = nil) ⇒ Object

Raises:

  • (ArgumentError)


282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'app/models/upload.rb', line 282

def self.uploadify(path, category, resource = nil, filename = nil, _do_not_delete = false, expiration_date = nil)
  raise ArgumentError, "Upload path is nil (category: #{category}, resource: #{resource&.class&.name} ##{resource&.id})" if path.nil?

  if URI(path.to_s).host.present?
    uploadify_from_url(file_name: filename, url: path, category:, resource:, expiration_date:)
  elsif File.exist?(path)
    upload = new(category:, resource:, expiration_date:)
    upload.attachment_name ||= filename
    upload.attachment = File.new(path)
    upload.save!
    # We store a local path to speed up when the upload is called within the same thread
    upload.set_local_file_path(path)
    upload
  else
    msg = "File is missing at #{path}, category: #{category}, resource: #{resource&.class&.name} #{resource&.id}"
    raise StandardError.new(msg)
  end
end

.uploadify_from_data(file_name:, data:, category:, resource: nil) ⇒ Object



301
302
303
304
305
306
307
308
309
310
# File 'app/models/upload.rb', line 301

def self.uploadify_from_data(file_name:, data:, category:, resource: nil)
  file_name = ActiveStorage::Filename.new(file_name).sanitized
  file_path = temp_location(file_name)
  File.open(file_path, 'wb') do |file|
    file.write data
    file.flush
    file.fsync
  end
  uploadify(file_path, category, resource, file_name)
end

.uploadify_from_url(file_name:, url:, category:, resource: nil, expiration_date: nil, headers: nil) ⇒ Object



312
313
314
315
316
317
318
319
320
321
# File 'app/models/upload.rb', line 312

def self.uploadify_from_url(file_name:, url:, category:, resource: nil, expiration_date: nil, headers: nil)
  require 'down'
  file_path = temp_location(file_name)
  headers = headers.stringify_keys if headers.present?
  headers ||= {}
  Retryable.retryable(tries: 3, sleep: lambda { |n| 4**n }, on: Retryable::TIMEOUT_CLASSES) do |_attempt_number, _exception|
    Down.download(url, headers:, destination: file_path) { |client| client.timeout(read: 60) }
  end
  uploadify(file_path, category, resource, file_name)
end

.validActiveRecord::Relation<Upload>

A relation of Uploads that are valid. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Upload>)

See Also:



140
# File 'app/models/upload.rb', line 140

scope :valid, -> { where("category NOT LIKE 'invalid_%'") }

Instance Method Details

#active_download_token(expires_at = nil) ⇒ Object



179
180
181
182
183
# File 'app/models/upload.rb', line 179

def active_download_token(expires_at = nil)
  dt = download_tokens.where('expires_at IS NULL or expires_at > ?', Time.current).order(Arel.sql('expires_at IS NOT NULL, expires_at DESC')).first
  dt ||= create_new_download_token(expires_at)
  dt.token
end

#articlesActiveRecord::Relation<Article>

Returns:

  • (ActiveRecord::Relation<Article>)

See Also:



101
# File 'app/models/upload.rb', line 101

has_and_belongs_to_many :articles

#attachment_extObject



409
410
411
# File 'app/models/upload.rb', line 409

def attachment_ext
  File.extname(attachment_name).downcase.delete_prefix('.') if attachment_name.present?
end

#browser_previewable?Boolean

True for file types modern browsers can render natively (PDFs and images).
Used by the show action to decide whether to serve inline or force download.

Returns:

  • (Boolean)


423
424
425
# File 'app/models/upload.rb', line 423

def browser_previewable?
  is_pdf? || is_image?
end

#cad_category?Boolean

Returns:

  • (Boolean)


532
533
534
# File 'app/models/upload.rb', line 532

def cad_category?
  category.in?(%w[room_layout_cad installation_plan_cad tracer_solution_cad])
end

#clone_with_file(new_resource = nil) ⇒ Object



263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'app/models/upload.rb', line 263

def clone_with_file(new_resource = nil)
  new_upload = nil
  return unless attachment.present?

  begin
    new_upload = Upload.new(category:,
                            resource: new_resource,
                            template:,
                            note:)
    new_upload.attachment_url = presigned_url(expires_in: 10.minutes)
    new_upload.attachment_name = attachment_name
    new_upload.attachment_mime_type = mime_type
  rescue StandardError => e
    Rails.logger.error "File could not be found for #{id}, #{e}"
    new_upload = nil
  end
  new_upload
end

#communicationsActiveRecord::Relation<Communication>

Returns:

See Also:



100
# File 'app/models/upload.rb', line 100

has_and_belongs_to_many :communications

#create_new_download_token(expires_at = nil) ⇒ Object



175
176
177
# File 'app/models/upload.rb', line 175

def create_new_download_token(expires_at = nil)
  download_tokens.create(expires_at:)
end

#deletable?Boolean

Returns:

  • (Boolean)


366
367
368
369
370
371
372
# File 'app/models/upload.rb', line 366

def deletable?
  if resource_type == 'RoomConfiguration'
    !resource.complete?
  else
    false
  end
end

#determine_orientationObject



209
210
211
212
213
214
215
216
# File 'app/models/upload.rb', line 209

def determine_orientation
  case template
  when /landscape/
    'Landscape'
  else
    'Portrait'
  end
end

#determine_page_sizeObject



194
195
196
197
198
199
200
201
202
203
# File 'app/models/upload.rb', line 194

def determine_page_size
  case template
  when /tabloid/
    'Tabloid'
  when /plotter/
    'Plotter'
  else
    'Letter'
  end
end

#download_tokensActiveRecord::Relation<DownloadToken>

Returns:

See Also:



98
# File 'app/models/upload.rb', line 98

has_many :download_tokens, inverse_of: :upload

#file_exists?Boolean

Returns:

  • (Boolean)


356
357
358
359
360
361
362
363
364
# File 'app/models/upload.rb', line 356

def file_exists?
  return false if attachment.nil?

  signed_url = presigned_url(expires_in: 60.minutes)
  response = HTTP.headers('Range' => 'bytes=0-0').get(signed_url)
  response.status.success?
rescue HTTP::Error, Dragonfly::Job::Fetch::NotFound
  false
end

#file_name_for_downloadObject



205
206
207
# File 'app/models/upload.rb', line 205

def file_name_for_download
  attachment_name # Literature subclass specializes this
end

#get_local_file_pathObject

Returns the local file path but only if the file still exists on disk.
Checks the instance attr first, then falls back to the process-level cache
so that freshly loaded instances of the same record can reuse temp files.



555
556
557
558
559
560
561
562
563
564
565
# File 'app/models/upload.rb', line 555

def get_local_file_path
  path = local_file_path || (id && CurrentPaths[id])
  if path.present? && File.exist?(path.to_s)
    self.local_file_path = path
    path
  else
    self.local_file_path = nil
    CurrentPaths.delete(id) if id
    nil
  end
end

#has_document_date?Boolean

Returns:

  • (Boolean)


548
549
550
# File 'app/models/upload.rb', line 548

def has_document_date?
  self.class.has_document_date?(resource_type)
end

#has_role_restrictions?Boolean

Returns:

  • (Boolean)


540
541
542
# File 'app/models/upload.rb', line 540

def has_role_restrictions?
  self.class.has_role_restrictions?(resource_type)
end

#has_thumbnail?Boolean

Returns:

  • (Boolean)


427
428
429
# File 'app/models/upload.rb', line 427

def has_thumbnail?
  is_image_or_pdf?
end

#image_attachment(dimensions) ⇒ Object



431
432
433
434
435
436
437
# File 'app/models/upload.rb', line 431

def image_attachment(dimensions)
  if is_image?
    attachment.thumb(dimensions)
  elsif is_pdf? # pdf
    pdf_thumbnail_attachment(dimensions)
  end
end

#image_category?Boolean

Returns:

  • (Boolean)


528
529
530
# File 'app/models/upload.rb', line 528

def image_category?
  category.in?(%w[installation_plan_image tracer_solution_image])
end

#image_url(dimensions) ⇒ Object



439
440
441
# File 'app/models/upload.rb', line 439

def image_url(dimensions)
  image_attachment(dimensions)&.url
end

#is_audio?Boolean

Returns:

  • (Boolean)


393
394
395
# File 'app/models/upload.rb', line 393

def is_audio?
  mime_type&.start_with?('audio/')
end

#is_image?Boolean

Returns:

  • (Boolean)


401
402
403
# File 'app/models/upload.rb', line 401

def is_image?
  mime_type&.start_with?('image/') || (undefined_mime_type? && IMAGE_EXTENSIONS.include?(attachment_ext))
end

#is_image_or_pdf?Boolean

Returns:

  • (Boolean)


405
406
407
# File 'app/models/upload.rb', line 405

def is_image_or_pdf?
  is_image? || is_pdf?
end

#is_mp3?Boolean

Returns:

  • (Boolean)


388
389
390
391
# File 'app/models/upload.rb', line 388

def is_mp3?
  (mime_type =~ %r{^audio/(?:mp3|mpeg|mpeg3|mpg|x-mp3|x-mpeg|x-mpeg3|x-mpegaudio|x-mpg)$}).present? ||
    (undefined_mime_type? && attachment_ext == 'mp3')
end

#is_pdf?Boolean

Returns:

  • (Boolean)


417
418
419
# File 'app/models/upload.rb', line 417

def is_pdf?
  mime_type == 'application/pdf' || (undefined_mime_type? && attachment_ext == 'pdf')
end

#is_video?Boolean

Returns:

  • (Boolean)


397
398
399
# File 'app/models/upload.rb', line 397

def is_video?
  mime_type&.start_with?('video/')
end

#is_wav?Boolean

Returns:

  • (Boolean)


383
384
385
386
# File 'app/models/upload.rb', line 383

def is_wav?
  ['audio/wav', 'audio/x-wav'].include?(mime_type) ||
    (undefined_mime_type? && attachment_ext == 'wav')
end

#itemsActiveRecord::Relation<Item>

Returns:

  • (ActiveRecord::Relation<Item>)

See Also:



103
# File 'app/models/upload.rb', line 103

has_and_belongs_to_many :items

#mime_typeObject



374
375
376
377
378
379
380
381
# File 'app/models/upload.rb', line 374

def mime_type
  return attachment_mime_type if attachment_mime_type.present?

  # Since we disabled automatic MIME type detection in Dragonfly,
  # we should rely on the stored attachment_mime_type field
  # This avoids triggering file downloads from S3
  attachment_mime_type
end

#mime_type_symbolObject



512
513
514
515
516
# File 'app/models/upload.rb', line 512

def mime_type_symbol
  attachment.ext.to_sym
rescue StandardError
  nil
end

#ok_to_delete?Boolean

Returns:

  • (Boolean)


518
519
520
521
522
# File 'app/models/upload.rb', line 518

def ok_to_delete?
  # Check if this upload is used as literature for any publication (Item)
  # Use direct database query to avoid association caching issues
  !Item.exists?(literature_id: id) && resource.nil?
end

#options_for_categories_select(user: nil, include_other: true, exclude: []) ⇒ Object



349
350
351
352
353
354
# File 'app/models/upload.rb', line 349

def options_for_categories_select(user: nil, include_other: true, exclude: [])
  class_name_to_use = resource&.class&.name
  # When upload are used with Item, the resource is nil
  class_name_to_use ||= :item if items.present?
  self.class.options_for_categories_select(class_name_to_use, user:, include_other:, exclude:)
end

#pdf_category?Boolean

Returns:

  • (Boolean)


524
525
526
# File 'app/models/upload.rb', line 524

def pdf_category?
  category.in?(%w[custom_packing_slip_pdf purchase_order])
end

#pdf_thumbnail_attachment(dimensions = '600x600>', density: 150) ⇒ Object



443
444
445
446
447
448
449
450
# File 'app/models/upload.rb', line 443

def pdf_thumbnail_attachment(dimensions = '600x600>', density: 150)
  return unless is_pdf?

  # Using libvips options: page (first page), dpi for resolution
  # 150 DPI is sufficient for thumbnails up to ~1200px (8.5" * 150 = 1275px)
  # AVIF output for best compression/quality ratio; Cloudflare handles browser fallback
  attachment.thumb(dimensions, format: :avif, input_options: { page: 0, dpi: density })
end

#pdf_thumbnail_url(dimensions = '600x600>', density: 400) ⇒ Object



452
453
454
455
456
# File 'app/models/upload.rb', line 452

def pdf_thumbnail_url(dimensions = '600x600>', density: 400)
  return unless is_pdf?

  pdf_thumbnail_attachment(dimensions, density:).url
end

#presigned_url(expires_in: 1.day, download: false, file_name: nil) ⇒ Object

Generate a presigned URL for the attachment locally without S3 network calls.
Uses Dragonfly's datastore.url_for which is purely local AWS Sig V4 computation.
Use this for preview/download links where you need a signed URL quickly.



473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'app/models/upload.rb', line 473

def presigned_url(expires_in: 1.day, download: false, file_name: nil)
  return nil unless attachment_uid.present? && attachment.present?

  query_params = {}
  if download || file_name.present?
    safe_filename = (file_name || attachment_name).to_s.parameterize(separator: '_')
    safe_filename = "#{safe_filename}#{File.extname(attachment_name)}" unless safe_filename.include?('.')
    disposition = download ? 'attachment' : 'inline'
    query_params['response-content-disposition'] = "#{disposition}; filename=\"#{safe_filename}\""
  end

  # Get datastore from the attachment's app (not hardcoded)
  attachment.app.datastore.url_for(
    attachment_uid,
    scheme: 'https',
    expires: expires_in.from_now.to_i,
    query: query_params.presence
  )
end

#publicationItem

Returns:

See Also:



85
# File 'app/models/upload.rb', line 85

has_one :publication, class_name: 'Item', inverse_of: :literature, foreign_key: :literature_id

#recipient_party_idObject



493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'app/models/upload.rb', line 493

def recipient_party_id
  p = begin
    resource.customer_id
  rescue StandardError
    nil
  end
  p ||= begin
    resource.party_id
  rescue StandardError
    nil
  end
  p ||= begin
    upload.resource.supplier_id
  rescue StandardError
    nil
  end
  p
end

#resourceResource

Returns:

  • (Resource)

See Also:



84
# File 'app/models/upload.rb', line 84

belongs_to :resource, polymorphic: true, optional: true

#resource_comboObject



251
252
253
# File 'app/models/upload.rb', line 251

def resource_combo
  "#{resource_type}|#{resource_id}" if resource
end

#resource_combo=(combo_val) ⇒ Object



255
256
257
258
259
260
261
# File 'app/models/upload.rb', line 255

def resource_combo=(combo_val)
  return unless combo_val.present?

  values = combo_val.split('|')
  self.resource_type = values[0]
  self.resource_id = values[1]
end

#rolesActiveRecord::Relation<Role>

Returns:

  • (ActiveRecord::Relation<Role>)

See Also:



102
# File 'app/models/upload.rb', line 102

has_and_belongs_to_many :roles

#scope_conditionObject

Uploads should only be sorted based on the presence of resource id of category
otherwise if those are null then the whole upload table gets a shuffle. big
performance hit and deadlocks
:resource_id, :category



154
155
156
157
158
159
160
161
162
163
164
165
# File 'app/models/upload.rb', line 154

def scope_condition
  if resource_type && resource_id
    cnds = {
      resource_type:,
      resource_id:
    }
    cnds[:category] = category if category.present?
    cnds
  else
    { id: -1 }
  end
end

#set_local_file_path(file_path) ⇒ Object

Ensures our local file path is stored in the temp storage directory (for
sendfile header acceleration) and registers it in the request-scoped cache
so other instances of the same record can find it without hitting S3.



570
571
572
573
574
575
576
577
578
579
# File 'app/models/upload.rb', line 570

def set_local_file_path(file_path)
  if File.dirname(file_path.to_s) == Rails.application.config.x.temp_storage_path.to_s
    self.local_file_path = file_path
  else
    self.local_file_path = Rails.application.config.x.temp_storage_path.join(File.basename(file_path))
    FileUtils.cp(file_path, local_file_path.to_s)
  end
  CurrentPaths[id] = local_file_path.to_s if id.present?
  local_file_path
end

#template_options_for_selectObject



343
344
345
346
347
# File 'app/models/upload.rb', line 343

def template_options_for_select
  return [] unless resource.respond_to?(:template_options_for_select)

  resource.template_options_for_select
end

#thumbnail_2x_url(dimensions: '184x122>') ⇒ Object



464
465
466
467
468
# File 'app/models/upload.rb', line 464

def thumbnail_2x_url(dimensions: '184x122>')
  return unless has_thumbnail?

  image_attachment(dimensions)&.url
end

#thumbnail_url(dimensions: '92x61>') ⇒ Object



458
459
460
461
462
# File 'app/models/upload.rb', line 458

def thumbnail_url(dimensions: '92x61>')
  return unless has_thumbnail?

  image_attachment(dimensions)&.url
end

#titleObject



189
190
191
192
# File 'app/models/upload.rb', line 189

def title
  # this is to support serialization of Installation Plan Vignettes
  category.humanize.titleize.to_s
end

#to_file(file_path: nil, file_name: nil) ⇒ Object

Downloads the file using the Down library to a temporary place



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
# File 'app/models/upload.rb', line 219

def to_file(file_path: nil, file_name: nil)
  # If we still have a local copy of the file we will return this right away
  if get_local_file_path
    Rails.logger.info "to_file getting upload #{id} from local path: #{get_local_file_path}"
    # If file_path specified, copy the file to that location
    return get_local_file_path unless file_path.present? && file_path.to_s != get_local_file_path

    FileUtils.cp get_local_file_path, file_path
    file_path

  else
    remote_url = presigned_url(expires_in: 15.minutes)
    Rails.logger.info "to_file getting upload #{id} from remote path #{remote_url}"
    # Is there an extension to this upload?
    remote_file_name = attachment_name.presence || URI.parse(url).path
    ext = begin
      File.extname(remote_file_name)
    rescue StandardError
      nil
    end
    # Future you can also look at the response headers using a chunked io download
    file_name ||= "tmp_#{id}_#{Time.current.strftime('%Y-%m-%d-%H%M%S')}#{ext}"
    file_path ||= Rails.application.config.x.temp_storage_path.join(file_name)
    Retryable.retryable(tries: 3, sleep: lambda { |n| 4**n }, on: Retryable::TIMEOUT_CLASSES) do |attempt_number, exception|
      ErrorReporting.error(exception, message: "Exception on download attempt #{attempt_number} for #{remote_url}") if exception
      Down::Http.download(remote_url, destination: file_path) { |client| client.timeout(read: 30) }
    end
    set_local_file_path(file_path) # Speed up further retrievals
  end
  file_path.to_s
end

#to_sObject



185
186
187
# File 'app/models/upload.rb', line 185

def to_s
  attachment_name || "Upload id #{id}"
end

#undefined_mime_type?Boolean

Returns:

  • (Boolean)


413
414
415
# File 'app/models/upload.rb', line 413

def undefined_mime_type?
  mime_type.blank? || mime_type == 'application/octet-stream'
end