store.rb 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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. image.resize!(width, :auto)
  164. temp_file_resize = ::Tempfile.new.path
  165. image.save(temp_file_resize)
  166. ::File.binread(temp_file_resize)
  167. end
  168. end
  169. def oversized_preferences_check
  170. [[600, 100], [300, 60], [150, 30], [75, 15]].each do |row|
  171. return true if oversized_preferences_removed_by_content?(row[0])
  172. return true if oversized_preferences_removed_by_key?(row[1])
  173. end
  174. true
  175. end
  176. def oversized_preferences_removed_by_content?(max_char)
  177. oversized_preferences_removed? do |_key, content|
  178. content.try(:size).to_i > max_char
  179. end
  180. end
  181. def oversized_preferences_removed_by_key?(max_char)
  182. oversized_preferences_removed? do |key, _content|
  183. key.try(:size).to_i > max_char
  184. end
  185. end
  186. def oversized_preferences_removed?
  187. return true if !oversized_preferences_present?
  188. preferences&.each do |key, content|
  189. next if !yield(key, content)
  190. preferences.delete(key)
  191. Rails.logger.info "Removed oversized #{self.class.name} preference: '#{key}', '#{content}'"
  192. break if !oversized_preferences_present?
  193. end
  194. !oversized_preferences_present?
  195. end
  196. def oversized_preferences_present?
  197. preferences.to_yaml.size > PREFERENCES_SIZE_MAX
  198. end
  199. end