store.rb 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Store < ApplicationModel
  3. PREFERENCES_SIZE_MAX = 2400
  4. belongs_to :store_object, class_name: 'Store::Object', optional: true
  5. belongs_to :store_file, class_name: 'Store::File', optional: true
  6. delegate :content, to: :store_file
  7. delegate :provider, to: :store_file
  8. validates :filename, presence: true
  9. store :preferences
  10. before_validation :set_object_id
  11. before_create :set_store_file, :oversized_preferences_check
  12. after_create :generate_previews
  13. before_update :oversized_preferences_check
  14. attr_accessor :object, :data
  15. def set_object_id
  16. return if object.blank?
  17. self.store_object_id = Store::Object.create_if_not_exists(name: object).id
  18. end
  19. def set_store_file
  20. file = Store::File.add(data)
  21. self.size = data.to_s.bytesize
  22. self.store_file_id = file.id
  23. end
  24. =begin
  25. get attachment of object
  26. list = Store.list(
  27. object: 'Ticket::Article',
  28. o_id: 4711,
  29. )
  30. returns
  31. result = [store1, store2]
  32. store1 = {
  33. size: 94123,
  34. filename: 'image.png',
  35. preferences: {
  36. content_type: 'image/png',
  37. content_id: 234,
  38. }
  39. }
  40. store1.content # binary_string
  41. =end
  42. def self.list(data)
  43. # search
  44. store_object_id = Store::Object.lookup(name: data[:object])
  45. Store.where(store_object_id: store_object_id, o_id: data[:o_id])
  46. .reorder(created_at: :asc)
  47. end
  48. =begin
  49. remove attachments of object from storage
  50. result = Store.remove(
  51. object: 'Ticket::Article',
  52. o_id: 4711,
  53. )
  54. returns
  55. result = true
  56. =end
  57. def self.remove(data)
  58. # search
  59. store_object_id = Store::Object.lookup(name: data[:object])
  60. stores = Store.where(store_object_id: store_object_id)
  61. .where(o_id: data[:o_id])
  62. .reorder(created_at: :asc)
  63. stores.each do |store|
  64. # check backend for references
  65. Store.remove_item(store.id)
  66. end
  67. true
  68. end
  69. =begin
  70. remove one attachment from storage
  71. Store.remove_item(store_id)
  72. =end
  73. def self.remove_item(store_id)
  74. store = Store.find(store_id)
  75. file_id = store.store_file_id
  76. # check backend for references
  77. files = Store.where(store_file_id: file_id)
  78. if files.count > 1 || files.first.id != store.id
  79. store.destroy!
  80. return true
  81. end
  82. store.destroy!
  83. Store::File.find(file_id).destroy!
  84. end
  85. =begin
  86. get content of file in preview size
  87. store = Store.find(store_id)
  88. content_as_string = store.content_preview
  89. returns
  90. content_as_string
  91. =end
  92. def content_preview(options = {})
  93. file = Store::File.find_by(id: store_file_id)
  94. if !file
  95. raise "No such file #{store_file_id}!"
  96. end
  97. raise __('Content preview could not be generated.') if options[:silence] != true && preferences[:content_preview] != true
  98. image_resize(file.content, 200)
  99. end
  100. =begin
  101. get content of file in inline size
  102. store = Store.find(store_id)
  103. content_as_string = store.content_inline
  104. returns
  105. content_as_string
  106. =end
  107. def content_inline(options = {})
  108. file = Store::File.find_by(id: store_file_id)
  109. if !file
  110. raise "No such file #{store_file_id}!"
  111. end
  112. raise __('Inline content could not be generated.') if options[:silence] != true && preferences[:content_inline] != true
  113. image_resize(file.content, 1800)
  114. end
  115. def attributes_for_display
  116. slice :id, :store_file_id, :filename, :size, :preferences
  117. end
  118. RESIZABLE_MIME_REGEXP = %r{image/(jpeg|jpg|png)}i
  119. def self.resizable_mime?(input)
  120. input.match? RESIZABLE_MIME_REGEXP
  121. end
  122. private
  123. def generate_previews
  124. return true if Setting.get('import_mode')
  125. resizable = preferences
  126. .slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
  127. .values
  128. .any? { |mime| self.class.resizable_mime?(mime) }
  129. begin
  130. if resizable
  131. if content_preview(silence: true)
  132. preferences[:resizable] = true
  133. preferences[:content_preview] = true
  134. end
  135. if content_inline(silence: true)
  136. preferences[:resizable] = true
  137. preferences[:content_inline] = true
  138. end
  139. if preferences[:resizable]
  140. save!
  141. end
  142. end
  143. rescue => e
  144. logger.error e
  145. preferences[:resizable] = false
  146. save!
  147. end
  148. end
  149. def image_resize(content, width)
  150. local_sha = Digest::SHA256.hexdigest(content)
  151. Rails.cache.fetch("#{self.class}/image-resize-#{local_sha}_#{width}", expires_in: 6.months) do
  152. temp_file = ::Tempfile.new
  153. temp_file.binmode
  154. temp_file.write(content)
  155. temp_file.close
  156. image = Rszr::Image.load(temp_file.path)
  157. # do not resize image if image is smaller or already same size
  158. return if image.width <= width
  159. # do not resize image if new height is smaller then 7px (images
  160. # with small height are usually useful to resize)
  161. ratio = image.width / width
  162. return if image.height / ratio <= 6
  163. original_format = image.format
  164. image.resize!(width, :auto)
  165. temp_file_resize = ::Tempfile.new.path
  166. image.save(temp_file_resize, format: original_format)
  167. ::File.binread(temp_file_resize)
  168. end
  169. end
  170. def oversized_preferences_check
  171. [[600, 100], [300, 60], [150, 30], [75, 15]].each do |row|
  172. return true if oversized_preferences_removed_by_content?(row[0])
  173. return true if oversized_preferences_removed_by_key?(row[1])
  174. end
  175. true
  176. end
  177. def oversized_preferences_removed_by_content?(max_char)
  178. oversized_preferences_removed? do |_key, content|
  179. content.try(:size).to_i > max_char
  180. end
  181. end
  182. def oversized_preferences_removed_by_key?(max_char)
  183. oversized_preferences_removed? do |key, _content|
  184. key.try(:size).to_i > max_char
  185. end
  186. end
  187. def oversized_preferences_removed?
  188. return true if !oversized_preferences_present?
  189. preferences&.each do |key, content|
  190. next if !yield(key, content)
  191. preferences.delete(key)
  192. Rails.logger.info "Removed oversized #{self.class.name} preference: '#{key}', '#{content}'"
  193. break if !oversized_preferences_present?
  194. end
  195. !oversized_preferences_present?
  196. end
  197. def oversized_preferences_present?
  198. preferences.to_yaml.size > PREFERENCES_SIZE_MAX
  199. end
  200. end