store.rb 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  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. after_create :generate_previews
  11. before_create :oversized_preferences_check
  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. stores = Store.where(store_object_id: store_object_id, o_id: data[:o_id].to_i)
  64. .order(created_at: :asc)
  65. stores
  66. end
  67. =begin
  68. remove attachments of object from storage
  69. result = Store.remove(
  70. object: 'Ticket::Article',
  71. o_id: 4711,
  72. )
  73. returns
  74. result = true
  75. =end
  76. def self.remove(data)
  77. # search
  78. store_object_id = Store::Object.lookup(name: data[:object])
  79. stores = Store.where(store_object_id: store_object_id)
  80. .where(o_id: data[:o_id])
  81. .order(created_at: :asc)
  82. stores.each do |store|
  83. # check backend for references
  84. Store.remove_item(store.id)
  85. end
  86. true
  87. end
  88. =begin
  89. remove one attachment from storage
  90. Store.remove_item(store_id)
  91. =end
  92. def self.remove_item(store_id)
  93. store = Store.find(store_id)
  94. file_id = store.store_file_id
  95. # check backend for references
  96. files = Store.where(store_file_id: file_id)
  97. if files.count > 1 || files.first.id != store.id
  98. store.destroy!
  99. return true
  100. end
  101. store.destroy!
  102. Store::File.find(file_id).destroy!
  103. end
  104. =begin
  105. get content of file
  106. store = Store.find(store_id)
  107. content_as_string = store.content
  108. returns
  109. content_as_string
  110. =end
  111. def content
  112. file = Store::File.find_by(id: store_file_id)
  113. if !file
  114. raise "No such file #{store_file_id}!"
  115. end
  116. file.content
  117. end
  118. =begin
  119. get content of file in preview size
  120. store = Store.find(store_id)
  121. content_as_string = store.content_preview
  122. returns
  123. content_as_string
  124. =end
  125. def content_preview(options = {})
  126. file = Store::File.find_by(id: store_file_id)
  127. if !file
  128. raise "No such file #{store_file_id}!"
  129. end
  130. raise 'Unable to generate preview' if options[:silence] != true && preferences[:content_preview] != true
  131. image_resize(file.content, 200)
  132. end
  133. =begin
  134. get content of file in inline size
  135. store = Store.find(store_id)
  136. content_as_string = store.content_inline
  137. returns
  138. content_as_string
  139. =end
  140. def content_inline(options = {})
  141. file = Store::File.find_by(id: store_file_id)
  142. if !file
  143. raise "No such file #{store_file_id}!"
  144. end
  145. raise 'Unable to generate inline' if options[:silence] != true && preferences[:content_inline] != true
  146. image_resize(file.content, 1800)
  147. end
  148. =begin
  149. get content of file
  150. store = Store.find(store_id)
  151. location_of_file = store.save_to_file
  152. returns
  153. location_of_file
  154. =end
  155. def save_to_file(path = nil)
  156. content
  157. file = Store::File.find_by(id: store_file_id)
  158. if !file
  159. raise "No such file #{store_file_id}!"
  160. end
  161. if !path
  162. path = Rails.root.join('tmp', filename)
  163. end
  164. ::File.open(path, 'wb') do |handle|
  165. handle.write file.content
  166. end
  167. path
  168. end
  169. def attributes_for_display
  170. slice :id, :filename, :size, :preferences
  171. end
  172. def provider
  173. file = Store::File.find_by(id: store_file_id)
  174. if !file
  175. raise "No such file #{store_file_id}!"
  176. end
  177. file.provider
  178. end
  179. RESIZABLE_MIME_REGEXP = %r{image/(jpeg|jpg|png)}i.freeze
  180. def self.resizable_mime?(input)
  181. input.match? RESIZABLE_MIME_REGEXP
  182. end
  183. private
  184. def generate_previews
  185. return true if Setting.get('import_mode')
  186. resizable = preferences
  187. .slice('Mime-Type', 'Content-Type', 'mime_type', 'content_type')
  188. .values
  189. .any? { |mime| self.class.resizable_mime?(mime) }
  190. begin
  191. if resizable
  192. if content_preview(silence: true)
  193. preferences[:resizable] = true
  194. preferences[:content_preview] = true
  195. end
  196. if content_inline(silence: true)
  197. preferences[:resizable] = true
  198. preferences[:content_inline] = true
  199. end
  200. if preferences[:resizable]
  201. save!
  202. end
  203. end
  204. rescue => e
  205. logger.error e
  206. preferences[:resizable] = false
  207. save!
  208. end
  209. end
  210. def image_resize(content, width)
  211. local_sha = Digest::SHA256.hexdigest(content)
  212. cache_key = "image-resize-#{local_sha}_#{width}"
  213. image = Cache.get(cache_key)
  214. return image if image
  215. temp_file = ::Tempfile.new
  216. temp_file.binmode
  217. temp_file.write(content)
  218. temp_file.close
  219. image = Rszr::Image.load(temp_file.path)
  220. # do not resize image if image is smaller or already same size
  221. return if image.width <= width
  222. # do not resize image if new height is smaller then 7px (images
  223. # with small height are usually useful to resize)
  224. ratio = image.width / width
  225. return if image.height / ratio <= 6
  226. image.resize!(width, :auto)
  227. temp_file_resize = ::Tempfile.new.path
  228. image.save(temp_file_resize)
  229. image_resized = ::File.binread(temp_file_resize)
  230. Cache.write(cache_key, image_resized, { expires_in: 6.months })
  231. image_resized
  232. end
  233. def oversized_preferences_check
  234. return true if oversized_preferences_removed_by_content?(600)
  235. return true if oversized_preferences_removed_by_key?(100)
  236. return true if oversized_preferences_removed_by_content?(300)
  237. return true if oversized_preferences_removed_by_key?(60)
  238. return true if oversized_preferences_removed_by_content?(150)
  239. return true if oversized_preferences_removed_by_key?(30)
  240. true
  241. end
  242. def oversized_preferences_removed_by_content?(max_char)
  243. oversized_preferences_removed? do |_key, content|
  244. content.try(:size).to_i > max_char
  245. end
  246. end
  247. def oversized_preferences_removed_by_key?(max_char)
  248. oversized_preferences_removed? do |key, _content|
  249. key.try(:size).to_i > max_char
  250. end
  251. end
  252. def oversized_preferences_removed?
  253. return true if !oversized_preferences_present?
  254. preferences&.each do |key, content|
  255. next if !yield(key, content)
  256. preferences.delete(key)
  257. Rails.logger.info "Removed oversized #{self.class.name} preference: '#{key}', '#{content}'"
  258. break if !oversized_preferences_present?
  259. end
  260. !oversized_preferences_present?
  261. end
  262. def oversized_preferences_present?
  263. preferences.to_yaml.size > PREFERENCES_SIZE_MAX
  264. end
  265. end