store.rb 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. # Copyright (C) 2012-2023 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, content_type: preferences['Content-Type'], filename: filename)
  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].to_i)
  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. =begin
  116. get content of file
  117. store = Store.find(store_id)
  118. location_of_file = store.save_to_file
  119. returns
  120. location_of_file
  121. =end
  122. def save_to_file(path = nil)
  123. content
  124. file = Store::File.find_by(id: store_file_id)
  125. if !file
  126. raise "No such file #{store_file_id}!"
  127. end
  128. if !path
  129. path = Rails.root.join('tmp', filename)
  130. end
  131. ::File.binwrite(path, file.content)
  132. path
  133. end
  134. def attributes_for_display
  135. slice :id, :store_file_id, :filename, :size, :preferences
  136. end
  137. RESIZABLE_MIME_REGEXP = %r{image/(jpeg|jpg|png)}i
  138. def self.resizable_mime?(input)
  139. input.match? RESIZABLE_MIME_REGEXP
  140. end
  141. private
  142. def generate_previews
  143. return true if Setting.get('import_mode')
  144. resizable = preferences
  145. .slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
  146. .values
  147. .any? { |mime| self.class.resizable_mime?(mime) }
  148. begin
  149. if resizable
  150. if content_preview(silence: true)
  151. preferences[:resizable] = true
  152. preferences[:content_preview] = true
  153. end
  154. if content_inline(silence: true)
  155. preferences[:resizable] = true
  156. preferences[:content_inline] = true
  157. end
  158. if preferences[:resizable]
  159. save!
  160. end
  161. end
  162. rescue => e
  163. logger.error e
  164. preferences[:resizable] = false
  165. save!
  166. end
  167. end
  168. def image_resize(content, width)
  169. local_sha = Digest::SHA256.hexdigest(content)
  170. Rails.cache.fetch("#{self.class}/image-resize-#{local_sha}_#{width}", expires_in: 6.months) do
  171. temp_file = ::Tempfile.new
  172. temp_file.binmode
  173. temp_file.write(content)
  174. temp_file.close
  175. image = Rszr::Image.load(temp_file.path)
  176. # do not resize image if image is smaller or already same size
  177. return if image.width <= width
  178. # do not resize image if new height is smaller then 7px (images
  179. # with small height are usually useful to resize)
  180. ratio = image.width / width
  181. return if image.height / ratio <= 6
  182. image.resize!(width, :auto)
  183. temp_file_resize = ::Tempfile.new.path
  184. image.save(temp_file_resize)
  185. ::File.binread(temp_file_resize)
  186. end
  187. end
  188. def oversized_preferences_check
  189. [[600, 100], [300, 60], [150, 30], [75, 15]].each do |row|
  190. return true if oversized_preferences_removed_by_content?(row[0])
  191. return true if oversized_preferences_removed_by_key?(row[1])
  192. end
  193. true
  194. end
  195. def oversized_preferences_removed_by_content?(max_char)
  196. oversized_preferences_removed? do |_key, content|
  197. content.try(:size).to_i > max_char
  198. end
  199. end
  200. def oversized_preferences_removed_by_key?(max_char)
  201. oversized_preferences_removed? do |key, _content|
  202. key.try(:size).to_i > max_char
  203. end
  204. end
  205. def oversized_preferences_removed?
  206. return true if !oversized_preferences_present?
  207. preferences&.each do |key, content|
  208. next if !yield(key, content)
  209. preferences.delete(key)
  210. Rails.logger.info "Removed oversized #{self.class.name} preference: '#{key}', '#{content}'"
  211. break if !oversized_preferences_present?
  212. end
  213. !oversized_preferences_present?
  214. end
  215. def oversized_preferences_present?
  216. preferences.to_yaml.size > PREFERENCES_SIZE_MAX
  217. end
  218. end