store.rb 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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. def inline?
  123. preferences['Content-Disposition'] == 'inline'
  124. end
  125. private
  126. def generate_previews
  127. return true if Setting.get('import_mode')
  128. resizable = preferences
  129. .slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
  130. .values
  131. .any? { |mime| self.class.resizable_mime?(mime) }
  132. begin
  133. if resizable
  134. if content_preview(silence: true)
  135. preferences[:resizable] = true
  136. preferences[:content_preview] = true
  137. end
  138. if content_inline(silence: true)
  139. preferences[:resizable] = true
  140. preferences[:content_inline] = true
  141. end
  142. if preferences[:resizable]
  143. save!
  144. end
  145. end
  146. rescue => e
  147. logger.error e
  148. preferences[:resizable] = false
  149. save!
  150. end
  151. end
  152. def image_resize(content, width)
  153. local_sha = Digest::SHA256.hexdigest(content)
  154. Rails.cache.fetch("#{self.class}/image-resize-#{local_sha}_#{width}", expires_in: 6.months) do
  155. temp_file = ::Tempfile.new
  156. temp_file.binmode
  157. temp_file.write(content)
  158. temp_file.close
  159. image = Rszr::Image.load(temp_file.path)
  160. # do not resize image if image is smaller or already same size
  161. return if image.width <= width
  162. # do not resize image if new height is smaller then 7px (images
  163. # with small height are usually useful to resize)
  164. ratio = image.width / width
  165. return if image.height / ratio <= 6
  166. original_format = image.format
  167. image.resize!(width, :auto)
  168. temp_file_resize = ::Tempfile.new.path
  169. image.save(temp_file_resize, format: original_format)
  170. ::File.binread(temp_file_resize)
  171. end
  172. end
  173. def oversized_preferences_check
  174. [[600, 100], [300, 60], [150, 30], [75, 15]].each do |row|
  175. return true if oversized_preferences_removed_by_content?(row[0])
  176. return true if oversized_preferences_removed_by_key?(row[1])
  177. end
  178. true
  179. end
  180. def oversized_preferences_removed_by_content?(max_char)
  181. oversized_preferences_removed? do |_key, content|
  182. content.try(:size).to_i > max_char
  183. end
  184. end
  185. def oversized_preferences_removed_by_key?(max_char)
  186. oversized_preferences_removed? do |key, _content|
  187. key.try(:size).to_i > max_char
  188. end
  189. end
  190. def oversized_preferences_removed?
  191. return true if !oversized_preferences_present?
  192. preferences&.each do |key, content|
  193. next if !yield(key, content)
  194. preferences.delete(key)
  195. Rails.logger.info "Removed oversized #{self.class.name} preference: '#{key}', '#{content}'"
  196. break if !oversized_preferences_present?
  197. end
  198. !oversized_preferences_present?
  199. end
  200. def oversized_preferences_present?
  201. preferences.to_yaml.size > PREFERENCES_SIZE_MAX
  202. end
  203. end