123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355 |
- # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
- require_dependency 'store/object'
- require_dependency 'store/file'
- 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
- validates :filename, presence: true
- store :preferences
- after_create :generate_previews
- before_create :oversized_preferences_check
- before_update :oversized_preferences_check
- =begin
- add an attachment to storage
- result = Store.add(
- object: 'Ticket::Article',
- o_id: 4711,
- data: binary_string,
- filename: 'filename.txt',
- preferences: {
- content_type: 'image/png',
- content_id: 234,
- }
- )
- returns
- result = true
- =end
- def self.add(data)
- data.deep_stringify_keys!
- # lookup store_object.id
- store_object = Store::Object.create_if_not_exists(name: data['object'])
- data['store_object_id'] = store_object.id
- # add to real store
- file = Store::File.add(data['data'])
- data['size'] = data['data'].to_s.bytesize
- data['store_file_id'] = file.id
- # not needed attributes
- data.delete('data')
- data.delete('object')
- Store.create!(data)
- 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])
- stores = Store.where(store_object_id: store_object_id, o_id: data[:o_id].to_i)
- .order(created_at: :asc)
- stores
- 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])
- .order(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
- store = Store.find(store_id)
- content_as_string = store.content
- returns
- content_as_string
- =end
- def content
- file = Store::File.find_by(id: store_file_id)
- if !file
- raise "No such file #{store_file_id}!"
- end
- file.content
- 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 'Unable to generate preview' 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 'Unable to generate inline' if options[:silence] != true && preferences[:content_inline] != true
- image_resize(file.content, 1800)
- end
- =begin
- get content of file
- store = Store.find(store_id)
- location_of_file = store.save_to_file
- returns
- location_of_file
- =end
- def save_to_file(path = nil)
- content
- file = Store::File.find_by(id: store_file_id)
- if !file
- raise "No such file #{store_file_id}!"
- end
- if !path
- path = Rails.root.join('tmp', filename)
- end
- ::File.open(path, 'wb') do |handle|
- handle.write file.content
- end
- path
- end
- def attributes_for_display
- slice :id, :filename, :size, :preferences
- end
- def provider
- file = Store::File.find_by(id: store_file_id)
- if !file
- raise "No such file #{store_file_id}!"
- end
- file.provider
- end
- private
- def generate_previews
- return true if Setting.get('import_mode')
- resizable = preferences.slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
- .values.grep(%r{image/(jpeg|jpg|png)}i).any?
- 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)
- cache_key = "image-resize-#{local_sha}_#{width}"
- image = Cache.get(cache_key)
- return image if image
- 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
- image.resize!(width, :auto)
- temp_file_resize = ::Tempfile.new.path
- image.save(temp_file_resize)
- image_resized = ::File.binread(temp_file_resize)
- Cache.write(cache_key, image_resized, { expires_in: 6.months })
- image_resized
- end
- def oversized_preferences_check
- return true if oversized_preferences_removed_by_content?(600)
- return true if oversized_preferences_removed_by_key?(100)
- return true if oversized_preferences_removed_by_content?(300)
- return true if oversized_preferences_removed_by_key?(60)
- 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
|