2021-06-01 12:20:20 +00:00
|
|
|
# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/
|
2013-06-12 15:59:58 +00:00
|
|
|
|
2018-04-26 08:55:53 +00:00
|
|
|
class Store < ApplicationModel
|
2019-09-02 16:03:10 +00:00
|
|
|
PREFERENCES_SIZE_MAX = 2400
|
2018-05-05 17:03:14 +00:00
|
|
|
|
2019-07-04 11:16:55 +00:00
|
|
|
belongs_to :store_object, class_name: 'Store::Object', optional: true
|
|
|
|
belongs_to :store_file, class_name: 'Store::File', optional: true
|
2018-04-12 14:57:37 +00:00
|
|
|
|
|
|
|
validates :filename, presence: true
|
|
|
|
|
|
|
|
store :preferences
|
2012-10-14 21:00:33 +00:00
|
|
|
|
2019-09-02 16:03:10 +00:00
|
|
|
before_create :oversized_preferences_check
|
2020-09-30 09:07:01 +00:00
|
|
|
after_create :generate_previews
|
2019-09-02 16:03:10 +00:00
|
|
|
before_update :oversized_preferences_check
|
2019-07-17 11:02:54 +00:00
|
|
|
|
2014-04-28 07:44:36 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
add an attachment to storage
|
|
|
|
|
|
|
|
result = Store.add(
|
2016-01-24 11:50:29 +00:00
|
|
|
object: 'Ticket::Article',
|
|
|
|
o_id: 4711,
|
|
|
|
data: binary_string,
|
2018-06-05 12:57:00 +00:00
|
|
|
filename: 'filename.txt',
|
2016-01-24 11:50:29 +00:00
|
|
|
preferences: {
|
2016-05-10 13:06:51 +00:00
|
|
|
content_type: 'image/png',
|
|
|
|
content_id: 234,
|
2014-04-28 07:44:36 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
result = true
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
2012-04-10 14:06:46 +00:00
|
|
|
def self.add(data)
|
2019-02-14 05:42:41 +00:00
|
|
|
data.deep_stringify_keys!
|
2012-04-10 14:06:46 +00:00
|
|
|
|
|
|
|
# lookup store_object.id
|
2016-01-24 11:50:29 +00:00
|
|
|
store_object = Store::Object.create_if_not_exists(name: data['object'])
|
2012-04-10 14:06:46 +00:00
|
|
|
data['store_object_id'] = store_object.id
|
2012-12-02 10:18:55 +00:00
|
|
|
|
2014-05-03 12:34:36 +00:00
|
|
|
# add to real store
|
2016-01-24 11:50:29 +00:00
|
|
|
file = Store::File.add(data['data'])
|
2012-12-02 10:18:55 +00:00
|
|
|
|
2013-01-24 08:18:29 +00:00
|
|
|
data['size'] = data['data'].to_s.bytesize
|
2012-04-10 14:06:46 +00:00
|
|
|
data['store_file_id'] = file.id
|
|
|
|
|
|
|
|
# not needed attributes
|
|
|
|
data.delete('data')
|
|
|
|
data.delete('object')
|
|
|
|
|
2019-07-17 11:02:54 +00:00
|
|
|
Store.create!(data)
|
2012-04-10 14:06:46 +00:00
|
|
|
end
|
2012-12-02 10:18:55 +00:00
|
|
|
|
2014-04-28 07:44:36 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
get attachment of object
|
|
|
|
|
|
|
|
list = Store.list(
|
2016-01-24 11:50:29 +00:00
|
|
|
object: 'Ticket::Article',
|
|
|
|
o_id: 4711,
|
2014-04-28 07:44:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
result = [store1, store2]
|
|
|
|
|
|
|
|
store1 = {
|
2016-01-24 11:50:29 +00:00
|
|
|
size: 94123,
|
|
|
|
filename: 'image.png',
|
|
|
|
preferences: {
|
2016-05-10 13:06:51 +00:00
|
|
|
content_type: 'image/png',
|
|
|
|
content_id: 234,
|
2014-04-28 07:44:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
store1.content # binary_string
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
2012-04-10 14:06:46 +00:00
|
|
|
def self.list(data)
|
|
|
|
# search
|
2016-01-24 11:50:29 +00:00
|
|
|
store_object_id = Store::Object.lookup(name: data[:object])
|
2020-07-07 06:30:20 +00:00
|
|
|
Store.where(store_object_id: store_object_id, o_id: data[:o_id].to_i)
|
2019-04-07 15:23:03 +00:00
|
|
|
.order(created_at: :asc)
|
2020-07-07 06:30:20 +00:00
|
|
|
|
2012-04-10 14:06:46 +00:00
|
|
|
end
|
|
|
|
|
2014-04-28 07:44:36 +00:00
|
|
|
=begin
|
|
|
|
|
2014-10-06 20:24:21 +00:00
|
|
|
remove attachments of object from storage
|
2014-04-28 07:44:36 +00:00
|
|
|
|
|
|
|
result = Store.remove(
|
2016-01-24 11:50:29 +00:00
|
|
|
object: 'Ticket::Article',
|
|
|
|
o_id: 4711,
|
2014-04-28 07:44:36 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
result = true
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
2012-04-10 14:06:46 +00:00
|
|
|
def self.remove(data)
|
|
|
|
# search
|
2016-01-24 11:50:29 +00:00
|
|
|
store_object_id = Store::Object.lookup(name: data[:object])
|
|
|
|
stores = Store.where(store_object_id: store_object_id)
|
|
|
|
.where(o_id: data[:o_id])
|
2019-04-07 15:23:03 +00:00
|
|
|
.order(created_at: :asc)
|
2012-04-10 14:06:46 +00:00
|
|
|
stores.each do |store|
|
2014-04-28 07:44:36 +00:00
|
|
|
|
|
|
|
# check backend for references
|
2016-01-24 11:50:29 +00:00
|
|
|
Store.remove_item(store.id)
|
2014-10-06 20:24:21 +00:00
|
|
|
end
|
2015-04-30 17:20:27 +00:00
|
|
|
true
|
2014-10-06 20:24:21 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
remove one attachment from storage
|
2014-04-28 07:44:36 +00:00
|
|
|
|
2017-06-06 15:49:49 +00:00
|
|
|
Store.remove_item(store_id)
|
2014-10-06 20:24:21 +00:00
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def self.remove_item(store_id)
|
2017-06-06 15:49:49 +00:00
|
|
|
store = Store.find(store_id)
|
|
|
|
file_id = store.store_file_id
|
|
|
|
|
2014-10-06 20:24:21 +00:00
|
|
|
# check backend for references
|
2017-06-06 15:49:49 +00:00
|
|
|
files = Store.where(store_file_id: file_id)
|
2017-11-17 10:41:44 +00:00
|
|
|
if files.count > 1 || files.first.id != store.id
|
|
|
|
store.destroy!
|
|
|
|
return true
|
|
|
|
end
|
2014-10-06 20:24:21 +00:00
|
|
|
|
2017-11-17 10:41:44 +00:00
|
|
|
store.destroy!
|
|
|
|
Store::File.find(file_id).destroy!
|
2012-04-10 14:06:46 +00:00
|
|
|
end
|
|
|
|
|
2017-06-06 12:11:28 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
get content of file
|
|
|
|
|
|
|
|
store = Store.find(store_id)
|
|
|
|
content_as_string = store.content
|
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
content_as_string
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
2014-04-28 07:44:36 +00:00
|
|
|
def content
|
2016-01-24 11:50:29 +00:00
|
|
|
file = Store::File.find_by(id: store_file_id)
|
2014-05-03 12:34:36 +00:00
|
|
|
if !file
|
2016-03-01 14:26:46 +00:00
|
|
|
raise "No such file #{store_file_id}!"
|
2014-04-28 07:44:36 +00:00
|
|
|
end
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2014-05-03 12:34:36 +00:00
|
|
|
file.content
|
2012-04-10 14:06:46 +00:00
|
|
|
end
|
|
|
|
|
2017-06-06 12:11:28 +00:00
|
|
|
=begin
|
|
|
|
|
2019-02-14 05:42:41 +00:00
|
|
|
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
|
|
|
|
|
2017-06-06 12:11:28 +00:00
|
|
|
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
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2017-06-06 12:11:28 +00:00
|
|
|
if !path
|
2017-11-23 08:09:44 +00:00
|
|
|
path = Rails.root.join('tmp', filename)
|
2017-06-06 12:11:28 +00:00
|
|
|
end
|
2017-10-01 12:25:52 +00:00
|
|
|
::File.open(path, 'wb') do |handle|
|
2017-06-06 12:11:28 +00:00
|
|
|
handle.write file.content
|
2017-10-01 12:25:52 +00:00
|
|
|
end
|
2017-06-06 12:11:28 +00:00
|
|
|
path
|
|
|
|
end
|
|
|
|
|
2018-05-03 14:09:33 +00:00
|
|
|
def attributes_for_display
|
|
|
|
slice :id, :filename, :size, :preferences
|
|
|
|
end
|
|
|
|
|
2014-05-03 12:34:36 +00:00
|
|
|
def provider
|
2016-01-24 11:50:29 +00:00
|
|
|
file = Store::File.find_by(id: store_file_id)
|
2014-05-03 12:34:36 +00:00
|
|
|
if !file
|
2016-03-01 14:26:46 +00:00
|
|
|
raise "No such file #{store_file_id}!"
|
2014-04-28 07:44:36 +00:00
|
|
|
end
|
2018-10-09 06:17:41 +00:00
|
|
|
|
2014-05-03 12:34:36 +00:00
|
|
|
file.provider
|
2014-04-28 07:44:36 +00:00
|
|
|
end
|
2019-02-14 05:42:41 +00:00
|
|
|
|
2020-04-08 06:23:58 +00:00
|
|
|
RESIZABLE_MIME_REGEXP = %r{image/(jpeg|jpg|png)}i.freeze
|
|
|
|
|
|
|
|
def self.resizable_mime?(input)
|
|
|
|
input.match? RESIZABLE_MIME_REGEXP
|
|
|
|
end
|
|
|
|
|
2019-02-14 05:42:41 +00:00
|
|
|
private
|
|
|
|
|
2019-07-17 11:02:54 +00:00
|
|
|
def generate_previews
|
|
|
|
return true if Setting.get('import_mode')
|
|
|
|
|
2020-04-08 06:23:58 +00:00
|
|
|
resizable = preferences
|
|
|
|
.slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
|
|
|
|
.values
|
|
|
|
.any? { |mime| self.class.resizable_mime?(mime) }
|
2019-07-17 11:02:54 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2019-02-14 05:42:41 +00:00
|
|
|
def image_resize(content, width)
|
|
|
|
local_sha = Digest::SHA256.hexdigest(content)
|
|
|
|
|
|
|
|
cache_key = "image-resize-#{local_sha}_#{width}"
|
2021-05-31 13:05:54 +00:00
|
|
|
image = Cache.read(cache_key)
|
2019-02-14 05:42:41 +00:00
|
|
|
return image if image
|
|
|
|
|
|
|
|
temp_file = ::Tempfile.new
|
|
|
|
temp_file.binmode
|
|
|
|
temp_file.write(content)
|
|
|
|
temp_file.close
|
2019-06-27 11:52:28 +00:00
|
|
|
image = Rszr::Image.load(temp_file.path)
|
2019-02-22 07:22:52 +00:00
|
|
|
|
2019-06-27 11:52:28 +00:00
|
|
|
# do not resize image if image is smaller or already same size
|
|
|
|
return if image.width <= width
|
2019-02-22 07:22:52 +00:00
|
|
|
|
2019-06-27 11:52:28 +00:00
|
|
|
# do not resize image if new height is smaller then 7px (images
|
2019-07-31 08:23:48 +00:00
|
|
|
# with small height are usually useful to resize)
|
2019-06-27 11:52:28 +00:00
|
|
|
ratio = image.width / width
|
|
|
|
return if image.height / ratio <= 6
|
2019-02-14 05:42:41 +00:00
|
|
|
|
2019-06-27 11:52:28 +00:00
|
|
|
image.resize!(width, :auto)
|
|
|
|
temp_file_resize = ::Tempfile.new.path
|
|
|
|
image.save(temp_file_resize)
|
2019-02-14 05:42:41 +00:00
|
|
|
image_resized = ::File.binread(temp_file_resize)
|
|
|
|
|
|
|
|
Cache.write(cache_key, image_resized, { expires_in: 6.months })
|
|
|
|
|
|
|
|
image_resized
|
|
|
|
end
|
|
|
|
|
2019-09-02 16:03:10 +00:00
|
|
|
def oversized_preferences_check
|
2020-12-02 08:44:49 +00:00
|
|
|
[[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
|
2019-09-02 16:03:10 +00:00
|
|
|
|
|
|
|
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
|
2015-04-27 14:15:29 +00:00
|
|
|
end
|