store.rb 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. require_dependency 'store/object'
  3. require_dependency 'store/file'
  4. class Store < ApplicationModel
  5. PREFERENCES_SIZE_MAX = 2400
  6. belongs_to :store_object, class_name: 'Store::Object', optional: true
  7. belongs_to :store_file, class_name: 'Store::File', optional: true
  8. validates :filename, presence: true
  9. store :preferences
  10. before_create :oversized_preferences_check
  11. after_create :generate_previews
  12. before_update :oversized_preferences_check
  13. =begin
  14. add an attachment to storage
  15. result = Store.add(
  16. object: 'Ticket::Article',
  17. o_id: 4711,
  18. data: binary_string,
  19. filename: 'filename.txt',
  20. preferences: {
  21. content_type: 'image/png',
  22. content_id: 234,
  23. }
  24. )
  25. returns
  26. result = true
  27. =end
  28. def self.add(data)
  29. data.deep_stringify_keys!
  30. # lookup store_object.id
  31. store_object = Store::Object.create_if_not_exists(name: data['object'])
  32. data['store_object_id'] = store_object.id
  33. # add to real store
  34. file = Store::File.add(data['data'])
  35. data['size'] = data['data'].to_s.bytesize
  36. data['store_file_id'] = file.id
  37. # not needed attributes
  38. data.delete('data')
  39. data.delete('object')
  40. Store.create!(data)
  41. end
  42. =begin
  43. get attachment of object
  44. list = Store.list(
  45. object: 'Ticket::Article',
  46. o_id: 4711,
  47. )
  48. returns
  49. result = [store1, store2]
  50. store1 = {
  51. size: 94123,
  52. filename: 'image.png',
  53. preferences: {
  54. content_type: 'image/png',
  55. content_id: 234,
  56. }
  57. }
  58. store1.content # binary_string
  59. =end
  60. def self.list(data)
  61. # search
  62. store_object_id = Store::Object.lookup(name: data[:object])
  63. Store.where(store_object_id: store_object_id, o_id: data[:o_id].to_i)
  64. .order(created_at: :asc)
  65. end
  66. =begin
  67. remove attachments of object from storage
  68. result = Store.remove(
  69. object: 'Ticket::Article',
  70. o_id: 4711,
  71. )
  72. returns
  73. result = true
  74. =end
  75. def self.remove(data)
  76. # search
  77. store_object_id = Store::Object.lookup(name: data[:object])
  78. stores = Store.where(store_object_id: store_object_id)
  79. .where(o_id: data[:o_id])
  80. .order(created_at: :asc)
  81. stores.each do |store|
  82. # check backend for references
  83. Store.remove_item(store.id)
  84. end
  85. true
  86. end
  87. =begin
  88. remove one attachment from storage
  89. Store.remove_item(store_id)
  90. =end
  91. def self.remove_item(store_id)
  92. store = Store.find(store_id)
  93. file_id = store.store_file_id
  94. # check backend for references
  95. files = Store.where(store_file_id: file_id)
  96. if files.count > 1 || files.first.id != store.id
  97. store.destroy!
  98. return true
  99. end
  100. store.destroy!
  101. Store::File.find(file_id).destroy!
  102. end
  103. =begin
  104. get content of file
  105. store = Store.find(store_id)
  106. content_as_string = store.content
  107. returns
  108. content_as_string
  109. =end
  110. def content
  111. file = Store::File.find_by(id: store_file_id)
  112. if !file
  113. raise "No such file #{store_file_id}!"
  114. end
  115. file.content
  116. end
  117. =begin
  118. get content of file in preview size
  119. store = Store.find(store_id)
  120. content_as_string = store.content_preview
  121. returns
  122. content_as_string
  123. =end
  124. def content_preview(options = {})
  125. file = Store::File.find_by(id: store_file_id)
  126. if !file
  127. raise "No such file #{store_file_id}!"
  128. end
  129. raise 'Unable to generate preview' if options[:silence] != true && preferences[:content_preview] != true
  130. image_resize(file.content, 200)
  131. end
  132. =begin
  133. get content of file in inline size
  134. store = Store.find(store_id)
  135. content_as_string = store.content_inline
  136. returns
  137. content_as_string
  138. =end
  139. def content_inline(options = {})
  140. file = Store::File.find_by(id: store_file_id)
  141. if !file
  142. raise "No such file #{store_file_id}!"
  143. end
  144. raise 'Unable to generate inline' if options[:silence] != true && preferences[:content_inline] != true
  145. image_resize(file.content, 1800)
  146. end
  147. =begin
  148. get content of file
  149. store = Store.find(store_id)
  150. location_of_file = store.save_to_file
  151. returns
  152. location_of_file
  153. =end
  154. def save_to_file(path = nil)
  155. content
  156. file = Store::File.find_by(id: store_file_id)
  157. if !file
  158. raise "No such file #{store_file_id}!"
  159. end
  160. if !path
  161. path = Rails.root.join('tmp', filename)
  162. end
  163. ::File.open(path, 'wb') do |handle|
  164. handle.write file.content
  165. end
  166. path
  167. end
  168. def attributes_for_display
  169. slice :id, :filename, :size, :preferences
  170. end
  171. def provider
  172. file = Store::File.find_by(id: store_file_id)
  173. if !file
  174. raise "No such file #{store_file_id}!"
  175. end
  176. file.provider
  177. end
  178. RESIZABLE_MIME_REGEXP = %r{image/(jpeg|jpg|png)}i.freeze
  179. def self.resizable_mime?(input)
  180. input.match? RESIZABLE_MIME_REGEXP
  181. end
  182. private
  183. def generate_previews
  184. return true if Setting.get('import_mode')
  185. resizable = preferences
  186. .slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
  187. .values
  188. .any? { |mime| self.class.resizable_mime?(mime) }
  189. begin
  190. if resizable
  191. if content_preview(silence: true)
  192. preferences[:resizable] = true
  193. preferences[:content_preview] = true
  194. end
  195. if content_inline(silence: true)
  196. preferences[:resizable] = true
  197. preferences[:content_inline] = true
  198. end
  199. if preferences[:resizable]
  200. save!
  201. end
  202. end
  203. rescue => e
  204. logger.error e
  205. preferences[:resizable] = false
  206. save!
  207. end
  208. end
  209. def image_resize(content, width)
  210. local_sha = Digest::SHA256.hexdigest(content)
  211. cache_key = "image-resize-#{local_sha}_#{width}"
  212. image = Cache.get(cache_key)
  213. return image if image
  214. temp_file = ::Tempfile.new
  215. temp_file.binmode
  216. temp_file.write(content)
  217. temp_file.close
  218. image = Rszr::Image.load(temp_file.path)
  219. # do not resize image if image is smaller or already same size
  220. return if image.width <= width
  221. # do not resize image if new height is smaller then 7px (images
  222. # with small height are usually useful to resize)
  223. ratio = image.width / width
  224. return if image.height / ratio <= 6
  225. image.resize!(width, :auto)
  226. temp_file_resize = ::Tempfile.new.path
  227. image.save(temp_file_resize)
  228. image_resized = ::File.binread(temp_file_resize)
  229. Cache.write(cache_key, image_resized, { expires_in: 6.months })
  230. image_resized
  231. end
  232. def oversized_preferences_check
  233. [[600, 100], [300, 60], [150, 30], [75, 15]].each do |row|
  234. return true if oversized_preferences_removed_by_content?(row[0])
  235. return true if oversized_preferences_removed_by_key?(row[1])
  236. end
  237. true
  238. end
  239. def oversized_preferences_removed_by_content?(max_char)
  240. oversized_preferences_removed? do |_key, content|
  241. content.try(:size).to_i > max_char
  242. end
  243. end
  244. def oversized_preferences_removed_by_key?(max_char)
  245. oversized_preferences_removed? do |key, _content|
  246. key.try(:size).to_i > max_char
  247. end
  248. end
  249. def oversized_preferences_removed?
  250. return true if !oversized_preferences_present?
  251. preferences&.each do |key, content|
  252. next if !yield(key, content)
  253. preferences.delete(key)
  254. Rails.logger.info "Removed oversized #{self.class.name} preference: '#{key}', '#{content}'"
  255. break if !oversized_preferences_present?
  256. end
  257. !oversized_preferences_present?
  258. end
  259. def oversized_preferences_present?
  260. preferences.to_yaml.size > PREFERENCES_SIZE_MAX
  261. end
  262. end