123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class Store < ApplicationModel
- PREFERENCES_SIZE_MAX = 2400
- belongs_to :store_object, class_name: 'Store::Object', optional: true
- belongs_to :store_file, class_name: 'Store::File', optional: true
- delegate :content, to: :store_file
- delegate :provider, to: :store_file
- validates :filename, presence: true
- store :preferences
- before_validation :set_object_id
- before_create :set_store_file, :oversized_preferences_check
- after_create :generate_previews
- before_update :oversized_preferences_check
- attr_accessor :object, :data
- def set_object_id
- return if object.blank?
- self.store_object_id = Store::Object.create_if_not_exists(name: object).id
- end
- def set_store_file
- file = Store::File.add(data)
- self.size = data.to_s.bytesize
- self.store_file_id = file.id
- end
- =begin
- get attachment of object
- list = Store.list(
- object: 'Ticket::Article',
- o_id: 4711,
- )
- returns
- result = [store1, store2]
- store1 = {
- size: 94123,
- filename: 'image.png',
- preferences: {
- content_type: 'image/png',
- content_id: 234,
- }
- }
- store1.content # binary_string
- =end
- def self.list(data)
- # search
- store_object_id = Store::Object.lookup(name: data[:object])
- Store.where(store_object_id: store_object_id, o_id: data[:o_id])
- .reorder(created_at: :asc)
- end
- =begin
- remove attachments of object from storage
- result = Store.remove(
- object: 'Ticket::Article',
- o_id: 4711,
- )
- returns
- result = true
- =end
- def self.remove(data)
- # search
- store_object_id = Store::Object.lookup(name: data[:object])
- stores = Store.where(store_object_id: store_object_id)
- .where(o_id: data[:o_id])
- .reorder(created_at: :asc)
- stores.each do |store|
- # check backend for references
- Store.remove_item(store.id)
- end
- true
- end
- =begin
- remove one attachment from storage
- Store.remove_item(store_id)
- =end
- def self.remove_item(store_id)
- store = Store.find(store_id)
- file_id = store.store_file_id
- # check backend for references
- files = Store.where(store_file_id: file_id)
- if files.count > 1 || files.first.id != store.id
- store.destroy!
- return true
- end
- store.destroy!
- Store::File.find(file_id).destroy!
- end
- =begin
- get content of file in preview size
- store = Store.find(store_id)
- content_as_string = store.content_preview
- returns
- content_as_string
- =end
- def content_preview(options = {})
- file = Store::File.find_by(id: store_file_id)
- if !file
- raise "No such file #{store_file_id}!"
- end
- raise __('Content preview could not be generated.') if options[:silence] != true && preferences[:content_preview] != true
- image_resize(file.content, 200)
- end
- =begin
- get content of file in inline size
- store = Store.find(store_id)
- content_as_string = store.content_inline
- returns
- content_as_string
- =end
- def content_inline(options = {})
- file = Store::File.find_by(id: store_file_id)
- if !file
- raise "No such file #{store_file_id}!"
- end
- raise __('Inline content could not be generated.') if options[:silence] != true && preferences[:content_inline] != true
- image_resize(file.content, 1800)
- end
- def attributes_for_display
- slice :id, :store_file_id, :filename, :size, :preferences
- end
- RESIZABLE_MIME_REGEXP = %r{image/(jpeg|jpg|png)}i
- def self.resizable_mime?(input)
- input.match? RESIZABLE_MIME_REGEXP
- end
- def inline?
- preferences['Content-Disposition'] == 'inline'
- end
- private
- def generate_previews
- return true if Setting.get('import_mode')
- resizable = preferences
- .slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
- .values
- .any? { |mime| self.class.resizable_mime?(mime) }
- begin
- if resizable
- if content_preview(silence: true)
- preferences[:resizable] = true
- preferences[:content_preview] = true
- end
- if content_inline(silence: true)
- preferences[:resizable] = true
- preferences[:content_inline] = true
- end
- if preferences[:resizable]
- save!
- end
- end
- rescue => e
- logger.error e
- preferences[:resizable] = false
- save!
- end
- end
- def image_resize(content, width)
- local_sha = Digest::SHA256.hexdigest(content)
- Rails.cache.fetch("#{self.class}/image-resize-#{local_sha}_#{width}", expires_in: 6.months) do
- temp_file = ::Tempfile.new
- temp_file.binmode
- temp_file.write(content)
- temp_file.close
- image = Rszr::Image.load(temp_file.path)
- # do not resize image if image is smaller or already same size
- return if image.width <= width
- # do not resize image if new height is smaller then 7px (images
- # with small height are usually useful to resize)
- ratio = image.width / width
- return if image.height / ratio <= 6
- original_format = image.format
- image.resize!(width, :auto)
- temp_file_resize = ::Tempfile.new.path
- image.save(temp_file_resize, format: original_format)
- ::File.binread(temp_file_resize)
- end
- end
- def oversized_preferences_check
- [[600, 100], [300, 60], [150, 30], [75, 15]].each do |row|
- return true if oversized_preferences_removed_by_content?(row[0])
- return true if oversized_preferences_removed_by_key?(row[1])
- end
- true
- end
- def oversized_preferences_removed_by_content?(max_char)
- oversized_preferences_removed? do |_key, content|
- content.try(:size).to_i > max_char
- end
- end
- def oversized_preferences_removed_by_key?(max_char)
- oversized_preferences_removed? do |key, _content|
- key.try(:size).to_i > max_char
- end
- end
- def oversized_preferences_removed?
- return true if !oversized_preferences_present?
- preferences&.each do |key, content|
- next if !yield(key, content)
- preferences.delete(key)
- Rails.logger.info "Removed oversized #{self.class.name} preference: '#{key}', '#{content}'"
- break if !oversized_preferences_present?
- end
- !oversized_preferences_present?
- end
- def oversized_preferences_present?
- preferences.to_yaml.size > PREFERENCES_SIZE_MAX
- end
- end
|