Class: Upload
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
extracted_markdown :text
note :text
position :integer
resource_type :string(255)
source_layout :jsonb
template :string
thumbnail_asset :jsonb
type :string(30)
typst_source :text
created_at :timestamp
updated_at :timestamp
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)
rubocop:disable Metrics/ClassLength -- pre-existing god-class (370+ lines on master); decompose separately
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 declaration_232_pdf declaration_232_xlsm 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],
rma_item: %i[photo not_returned_photo],
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],
warranty: %i[warranty_card warranty_card_pdf photo],
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
Models::Auditable::ALWAYS_IGNORED
Models::Schedulable::SIMPLE_FORM_OPTIONS
Instance Attribute Summary collapse
#creator, #updater
Has and belongs to many
collapse
Class Method Summary
collapse
-
.all_categories_for_select ⇒ Object
-
.custom_ship_labels ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are custom ship labels.
-
.has_document_date?(resource_type) ⇒ Boolean
-
.has_role_restrictions?(resource_type) ⇒ Boolean
-
.in_category ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are in category.
-
.invalid ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are invalid.
-
.literatures ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are literatures.
-
.most_recent_first ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are most recent first.
-
.options_for_categories_select(class_name, include_other: true, exclude: []) ⇒ Object
-
.sorted_by_position ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are sorted by position.
-
.temp_location(filename) ⇒ Pathname
A scratch path for +filename+, unique per call.
-
.under_temp_storage?(path) ⇒ Boolean
True when +path+ is already inside the scratch tree, so it can be cached in place rather than copied.
-
.uploadify(path, category, resource = nil, filename = nil, _do_not_delete = false, expiration_date = nil) ⇒ Object
-
.uploadify_from_data(file_name:, data:, category:, resource: nil) ⇒ Object
-
.uploadify_from_url(file_name:, url:, category:, resource: nil, _expiration_date: nil, headers: nil) ⇒ Object
-
.valid ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are valid.
Instance Method Summary
collapse
#all_skipped_columns, #audit_reference_data, #should_not_save_version, #stamp_record
ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation
config
#after_commit
#publish_event
Instance Attribute Details
#attachment ⇒ Object
148
|
# File 'app/models/upload.rb', line 148
validates :attachment, presence: true
|
#import ⇒ Object
Returns the value of attribute import.
136
137
138
|
# File 'app/models/upload.rb', line 136
def import
@import
end
|
#local_file_path ⇒ Object
Returns the value of attribute local_file_path.
136
137
138
|
# File 'app/models/upload.rb', line 136
def local_file_path
@local_file_path
end
|
#should_destroy ⇒ Object
Returns the value of attribute should_destroy.
136
137
138
|
# File 'app/models/upload.rb', line 136
def should_destroy
@should_destroy
end
|
#template ⇒ Object
154
|
# File 'app/models/upload.rb', line 154
validates :template, presence: true, if: proc { |u| u.category == 'installation_plan_image' }
|
Class Method Details
.all_categories_for_select ⇒ Object
187
188
189
|
# File 'app/models/upload.rb', line 187
def self.all_categories_for_select
CATEGORIES.sort.map { |c| [c.to_s.humanize.titleize, c.to_s] }
end
|
.custom_ship_labels ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are custom ship labels. Active Record Scope
161
|
# File 'app/models/upload.rb', line 161
scope :custom_ship_labels, -> { where(category: %w[master_carton_label manual_ship_label]) }
|
.has_document_date?(resource_type) ⇒ Boolean
671
672
673
|
# File 'app/models/upload.rb', line 671
def self.has_document_date?(resource_type)
%w[StatementOfAccount CreditMemo Invoice].include?(resource_type)
end
|
.has_role_restrictions?(resource_type) ⇒ Boolean
663
664
665
|
# File 'app/models/upload.rb', line 663
def self.has_role_restrictions?(resource_type)
resource_type == 'Employee'
end
|
.in_category ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are in category. Active Record Scope
158
|
# File 'app/models/upload.rb', line 158
scope :in_category, ->(cat) { where(category: cat) }
|
.invalid ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are invalid. Active Record Scope
157
|
# File 'app/models/upload.rb', line 157
scope :invalid, -> { where("category LIKE 'invalid_%'") }
|
.literatures ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are literatures. Active Record Scope
162
|
# File 'app/models/upload.rb', line 162
scope :literatures, -> { where(type: 'Literature').joins(:publication) }
|
.most_recent_first ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are most recent first. Active Record Scope
159
|
# File 'app/models/upload.rb', line 159
scope :most_recent_first, -> { reorder('uploads.created_at DESC') }
|
.options_for_categories_select(class_name, include_other: true, exclude: []) ⇒ Object
448
449
450
451
452
453
454
455
456
457
458
459
460
|
# File 'app/models/upload.rb', line 448
def self.options_for_categories_select(class_name, 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_position ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are sorted by position. Active Record Scope
160
|
# File 'app/models/upload.rb', line 160
scope :sorted_by_position, -> { reorder('uploads.position, uploads.created_at DESC') }
|
.temp_location(filename) ⇒ Pathname
A scratch path for +filename+, unique per call.
The BASENAME IS LEFT ALONE and the uniqueness lives in a fresh parent
directory, because Dragonfly derives +attachment_name+ from the file it is
assigned — prefixing the file itself would surface a1b2c3_invoice.pdf to
anyone downloading it.
Previously this returned tmp/storage/<filename>: one directory keyed on
the filename alone, shared by every process on the box. Two uploads with the
same basename got the same path, so one overwrote the other's bytes between
write and read — and #set_local_file_path then cached that path, so #to_file
served the wrong file for the rest of the process. Carrier label names are
formulaic, so concurrent shipments could swap PDFs. It also made the test
suite flaky: five tests used 'spec.pdf' and clobbered each other across
parallel workers (CI run 31016240508).
The name is reduced to a BASENAME and sanitized before use. Isolation is
the whole point of this method, and a caller passing ../label.pdf would
climb straight back out of its private directory — uploadify_from_url takes
its file_name from a remote source and never sanitized it (only
uploadify_from_data did), so the guard belongs here, at the one choke point
every caller shares.
425
426
427
428
429
430
431
432
433
434
435
|
# File 'app/models/upload.rb', line 425
def self.temp_location(filename)
safe_name = ActiveStorage::Filename.new(File.basename(filename.to_s)).sanitized
safe_name = 'file' if safe_name.blank? || ['.', '..'].include?(safe_name)
tmp_dir = Rails.application.config.x.temp_storage_path.join(SecureRandom.uuid)
FileUtils.mkdir_p(tmp_dir)
tmp_dir.join(safe_name)
end
|
.under_temp_storage?(path) ⇒ Boolean
True when +path+ is already inside the scratch tree, so it can be cached
in place rather than copied. Containment, not an exact dirname match —
temp_location nests each file in its own directory. Both sides are expanded
first so .. segments and symlinked roots can't fake containment.
443
444
445
446
|
# File 'app/models/upload.rb', line 443
def self.under_temp_storage?(path)
root = File.expand_path(Rails.application.config.x.temp_storage_path)
File.expand_path(path.to_s).start_with?("#{root}#{File::SEPARATOR}")
end
|
.uploadify(path, category, resource = nil, filename = nil, _do_not_delete = false, expiration_date = nil) ⇒ Object
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
|
# File 'app/models/upload.rb', line 359
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!
upload.set_local_file_path(path)
upload
else
msg = "File is missing at #{path}, category: #{category}, resource: #{resource&.class&.name} #{resource&.id}"
raise StandardError, msg
end
end
|
.uploadify_from_data(file_name:, data:, category:, resource: nil) ⇒ Object
378
379
380
381
382
383
384
385
386
387
|
# File 'app/models/upload.rb', line 378
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
389
390
391
392
393
394
395
396
397
398
|
# File 'app/models/upload.rb', line 389
def self.uploadify_from_url(file_name:, url:, category:, resource: nil, _expiration_date: nil, headers: nil)
require 'down'
file_path = temp_location(file_name)
= .stringify_keys if .present?
||= {}
Retryable.retryable(tries: 3, sleep: ->(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
|
.valid ⇒ ActiveRecord::Relation<Upload>
A relation of Uploads that are valid. Active Record Scope
156
|
# File 'app/models/upload.rb', line 156
scope :valid, -> { where("category NOT LIKE 'invalid_%'") }
|
Instance Method Details
#active_download_token(expires_at = nil) ⇒ Object
195
196
197
198
199
|
# File 'app/models/upload.rb', line 195
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
|
#articles ⇒ ActiveRecord::Relation<Article>
114
|
# File 'app/models/upload.rb', line 114
has_and_belongs_to_many :articles
|
#attachment_ext ⇒ Object
528
529
530
|
# File 'app/models/upload.rb', line 528
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.
542
543
544
|
# File 'app/models/upload.rb', line 542
def browser_previewable?
is_pdf? || is_image?
end
|
#cad_category? ⇒ Boolean
659
660
661
|
# File 'app/models/upload.rb', line 659
def cad_category?
category.in?(%w[room_layout_cad installation_plan_cad tracer_solution_cad])
end
|
#clone_with_file(new_resource = nil) ⇒ Object
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
|
# File 'app/models/upload.rb', line 305
def clone_with_file(new_resource = nil)
new_upload = nil
return if attachment.blank?
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
|
#communications ⇒ ActiveRecord::Relation<Communication>
113
|
# File 'app/models/upload.rb', line 113
has_and_belongs_to_many :communications
|
#create_new_download_token(expires_at = nil) ⇒ Object
191
192
193
|
# File 'app/models/upload.rb', line 191
def create_new_download_token(expires_at = nil)
download_tokens.create(expires_at:)
end
|
#deletable? ⇒ Boolean
485
486
487
488
489
490
491
|
# File 'app/models/upload.rb', line 485
def deletable?
if resource_type == 'RoomConfiguration'
!resource.complete?
else
false
end
end
|
#determine_orientation ⇒ Object
225
226
227
228
229
230
231
232
|
# File 'app/models/upload.rb', line 225
def determine_orientation
case template
when /landscape/
'Landscape'
else
'Portrait'
end
end
|
#determine_page_size ⇒ Object
210
211
212
213
214
215
216
217
218
219
|
# File 'app/models/upload.rb', line 210
def determine_page_size
case template
when /tabloid/
'Tabloid'
when /plotter/
'Plotter'
else
'Letter'
end
end
|
#download_tokens ⇒ ActiveRecord::Relation<DownloadToken>
111
|
# File 'app/models/upload.rb', line 111
has_many :download_tokens, inverse_of: :upload
|
#dup_with_attachment ⇒ Upload
STI-preserving duplicate with its own stored copy of the attachment: the
bytes are re-stored under a new uid, so the copy never aliases the
original's dragonfly uid (which would let one row's destroy pull the file
from under the other). Used to keep a publication's literature 1:1 when the
publication is duplicated (see Item#deep_dup).
The copy is built from +attributes+, NOT #dup: #dup copies the memoized
dragonfly attachment ivar, and with the source's proxy aboard, assignment
on the copy mis-stores — a second dup_with_attachment on the same source
produced rows carrying the source's uid (or no stored bytes at all) in
production. self.class.new(attrs) carries no instance state.
+extracted_markdown+ is intentionally not carried: clear_stale_extracted_markdown
nils it on save (attachment_uid changed), and the PublicationPdfChanged
pipeline re-extracts asynchronously.
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
|
# File 'app/models/upload.rb', line 341
def dup_with_attachment
copy = self.class.new(attributes.except('id', 'attachment_uid', 'created_at', 'updated_at'))
if attachment_stored?
dir = self.class.temp_location(SecureRandom.hex(8))
FileUtils.mkdir_p(dir)
file_path = dir.join(attachment_name.presence || 'attachment')
File.binwrite(file_path, attachment.data)
copy.attachment = File.new(file_path)
end
copy
end
|
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
|
# File 'app/models/upload.rb', line 276
def (force: false)
return unless .nil? || force
path = to_file
markdown = DoclingClient.convert(path, filename: attachment_name.presence || File.basename(path))
markdown = markdown.delete("\u0000") if markdown&.include?("\u0000")
update_column(:extracted_markdown, markdown)
markdown
end
|
#file_exists? ⇒ Boolean
475
476
477
478
479
480
481
482
483
|
# File 'app/models/upload.rb', line 475
def file_exists?
return false if attachment.nil?
signed_url = presigned_url(expires_in: 60.minutes)
response = HTTP.('Range' => 'bytes=0-0').get(signed_url)
response.status.success?
rescue HTTP::Error, Dragonfly::Job::Fetch::NotFound
false
end
|
#file_name_for_download ⇒ Object
221
222
223
|
# File 'app/models/upload.rb', line 221
def file_name_for_download
attachment_name end
|
#get_local_file_path ⇒ Object
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.
682
683
684
685
686
687
688
689
690
691
692
|
# File 'app/models/upload.rb', line 682
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
675
676
677
|
# File 'app/models/upload.rb', line 675
def has_document_date?
self.class.has_document_date?(resource_type)
end
|
#has_role_restrictions? ⇒ Boolean
667
668
669
|
# File 'app/models/upload.rb', line 667
def has_role_restrictions?
self.class.has_role_restrictions?(resource_type)
end
|
#has_thumbnail? ⇒ Boolean
546
547
548
|
# File 'app/models/upload.rb', line 546
def has_thumbnail?
is_image_or_pdf?
end
|
#image_attachment(dimensions) ⇒ Object
550
551
552
553
554
555
556
|
# File 'app/models/upload.rb', line 550
def image_attachment(dimensions)
if is_image?
attachment.thumb(dimensions)
elsif is_pdf? pdf_thumbnail_attachment(dimensions)
end
end
|
#image_category? ⇒ Boolean
655
656
657
|
# File 'app/models/upload.rb', line 655
def image_category?
category.in?(%w[installation_plan_image tracer_solution_image])
end
|
#image_url(dimensions) ⇒ Object
558
559
560
|
# File 'app/models/upload.rb', line 558
def image_url(dimensions)
image_attachment(dimensions)&.url
end
|
#is_audio? ⇒ Boolean
512
513
514
|
# File 'app/models/upload.rb', line 512
def is_audio?
mime_type&.start_with?('audio/')
end
|
#is_image? ⇒ Boolean
520
521
522
|
# File 'app/models/upload.rb', line 520
def is_image?
mime_type&.start_with?('image/') || (undefined_mime_type? && IMAGE_EXTENSIONS.include?(attachment_ext))
end
|
#is_image_or_pdf? ⇒ Boolean
524
525
526
|
# File 'app/models/upload.rb', line 524
def is_image_or_pdf?
is_image? || is_pdf?
end
|
#is_mp3? ⇒ Boolean
507
508
509
510
|
# File 'app/models/upload.rb', line 507
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
536
537
538
|
# File 'app/models/upload.rb', line 536
def is_pdf?
mime_type == 'application/pdf' || (undefined_mime_type? && attachment_ext == 'pdf')
end
|
#is_video? ⇒ Boolean
516
517
518
|
# File 'app/models/upload.rb', line 516
def is_video?
mime_type&.start_with?('video/')
end
|
#is_wav? ⇒ Boolean
502
503
504
505
|
# File 'app/models/upload.rb', line 502
def is_wav?
['audio/wav', 'audio/x-wav'].include?(mime_type) ||
(undefined_mime_type? && attachment_ext == 'wav')
end
|
#items ⇒ ActiveRecord::Relation<Item>
116
|
# File 'app/models/upload.rb', line 116
has_and_belongs_to_many :items
|
#mime_type ⇒ Object
493
494
495
496
497
498
499
500
|
# File 'app/models/upload.rb', line 493
def mime_type
return attachment_mime_type if attachment_mime_type.present?
attachment_mime_type
end
|
#mime_type_symbol ⇒ Object
639
640
641
642
643
|
# File 'app/models/upload.rb', line 639
def mime_type_symbol
attachment.ext.to_sym
rescue StandardError
nil
end
|
#ok_to_delete? ⇒ Boolean
645
646
647
648
649
|
# File 'app/models/upload.rb', line 645
def ok_to_delete?
!Item.exists?(literature_id: id) && resource.nil?
end
|
#options_for_categories_select(include_other: true, exclude: []) ⇒ Object
468
469
470
471
472
473
|
# File 'app/models/upload.rb', line 468
def options_for_categories_select(include_other: true, exclude: [])
class_name_to_use = resource&.class&.name
class_name_to_use ||= :item if items.present?
self.class.options_for_categories_select(class_name_to_use, include_other:, exclude:)
end
|
#pdf_category? ⇒ Boolean
651
652
653
|
# File 'app/models/upload.rb', line 651
def pdf_category?
category.in?(%w[custom_packing_slip_pdf purchase_order])
end
|
#pdf_thumbnail_attachment(dimensions = '600x600>', density: 150) ⇒ Object
562
563
564
565
566
567
568
569
|
# File 'app/models/upload.rb', line 562
def pdf_thumbnail_attachment(dimensions = '600x600>', density: 150)
return unless is_pdf?
attachment.thumb(dimensions, format: :avif, input_options: { page: 0, dpi: density })
end
|
#pdf_thumbnail_url(dimensions = '600x600>', density: 400) ⇒ Object
571
572
573
574
575
|
# File 'app/models/upload.rb', line 571
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.
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
|
# File 'app/models/upload.rb', line 592
def presigned_url(expires_in: 1.day, download: false, file_name: nil)
return nil unless attachment_uid.present? && attachment.present?
max_expires_in = 7.days - 1.hour
expires_in = max_expires_in if expires_in.to_i > max_expires_in.to_i
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
attachment.app.datastore.url_for(
attachment_uid,
scheme: 'https',
expires: expires_in.from_now.to_i,
query: query_params.presence
)
end
|
#publication ⇒ Item
98
|
# File 'app/models/upload.rb', line 98
has_one :publication, class_name: 'Item', inverse_of: :literature, foreign_key: :literature_id
|
#recipient_party_id ⇒ Object
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
|
# File 'app/models/upload.rb', line 620
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
|
#resource ⇒ Resource
97
|
# File 'app/models/upload.rb', line 97
belongs_to :resource, polymorphic: true, optional: true
|
#resource_combo ⇒ Object
293
294
295
|
# File 'app/models/upload.rb', line 293
def resource_combo
"#{resource_type}|#{resource_id}" if resource
end
|
#resource_combo=(combo_val) ⇒ Object
297
298
299
300
301
302
303
|
# File 'app/models/upload.rb', line 297
def resource_combo=(combo_val)
return if combo_val.blank?
values = combo_val.split('|')
self.resource_type = values[0]
self.resource_id = values[1]
end
|
#roles ⇒ ActiveRecord::Relation<Role>
115
|
# File 'app/models/upload.rb', line 115
has_and_belongs_to_many :roles
|
#scope_condition ⇒ Object
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
170
171
172
173
174
175
176
177
178
179
180
181
|
# File 'app/models/upload.rb', line 170
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.
697
698
699
700
701
702
703
704
705
706
707
708
709
710
|
# File 'app/models/upload.rb', line 697
def set_local_file_path(file_path)
if self.class.under_temp_storage?(file_path)
self.local_file_path = file_path
else
self.local_file_path = self.class.temp_location(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_select ⇒ Object
462
463
464
465
466
|
# File 'app/models/upload.rb', line 462
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
583
584
585
586
587
|
# File 'app/models/upload.rb', line 583
def thumbnail_2x_url(dimensions: '184x122>')
return unless has_thumbnail?
image_attachment(dimensions)&.url
end
|
#thumbnail_url(dimensions: '92x61>') ⇒ Object
577
578
579
580
581
|
# File 'app/models/upload.rb', line 577
def thumbnail_url(dimensions: '92x61>')
return unless has_thumbnail?
image_attachment(dimensions)&.url
end
|
#title ⇒ Object
205
206
207
208
|
# File 'app/models/upload.rb', line 205
def title
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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
|
# File 'app/models/upload.rb', line 235
def to_file(file_path: nil, file_name: nil)
if get_local_file_path
Rails.logger.info "to_file getting upload #{id} from local path: #{get_local_file_path}"
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}"
remote_file_name = attachment_name.presence || URI.parse(url).path
ext = begin
File.extname(remote_file_name)
rescue StandardError
nil
end
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: ->(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) end
file_path.to_s
end
|
#to_s ⇒ Object
201
202
203
|
# File 'app/models/upload.rb', line 201
def to_s
attachment_name || "Upload id #{id}"
end
|
#undefined_mime_type? ⇒ Boolean
532
533
534
|
# File 'app/models/upload.rb', line 532
def undefined_mime_type?
mime_type.blank? || mime_type == 'application/octet-stream'
end
|