store.rb 6.2 KB

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