PageRenderTime 60ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/paperclip/attachment.rb

https://github.com/aeldaly/paperclip
Ruby | 416 lines | 294 code | 60 blank | 62 comment | 52 complexity | 805ab5b379687060da284c8eee5a6ed8 MD5 | raw file
Possible License(s): MIT
  1. # encoding: utf-8
  2. module Paperclip
  3. # The Attachment class manages the files for a given attachment. It saves
  4. # when the model saves, deletes when the model is destroyed, and processes
  5. # the file upon assignment.
  6. class Attachment
  7. def self.default_options
  8. @default_options ||= {
  9. :url => "/system/:attachment/:id/:style/:filename",
  10. :path => ":rails_root/public:url",
  11. :styles => {},
  12. :default_url => "/:attachment/:style/missing.png",
  13. :default_style => :original,
  14. :validations => [],
  15. :storage => :filesystem,
  16. :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails]
  17. }
  18. end
  19. attr_reader :name, :instance, :styles, :default_style, :convert_options, :queued_for_write, :options
  20. # Creates an Attachment object. +name+ is the name of the attachment,
  21. # +instance+ is the ActiveRecord object instance it's attached to, and
  22. # +options+ is the same as the hash passed to +has_attached_file+.
  23. def initialize name, instance, options = {}
  24. @name = name
  25. @instance = instance
  26. options = self.class.default_options.merge(options)
  27. @url = options[:url]
  28. @url = @url.call(self) if @url.is_a?(Proc)
  29. @path = options[:path]
  30. @path = @path.call(self) if @path.is_a?(Proc)
  31. @styles = options[:styles]
  32. @styles = @styles.call(self) if @styles.is_a?(Proc)
  33. @default_url = options[:default_url]
  34. @validations = options[:validations]
  35. @default_style = options[:default_style]
  36. @storage = options[:storage]
  37. @whiny = options[:whiny_thumbnails] || options[:whiny]
  38. @convert_options = options[:convert_options] || {}
  39. @processors = options[:processors] || [:thumbnail]
  40. @options = options
  41. @queued_for_delete = []
  42. @queued_for_write = {}
  43. @errors = {}
  44. @validation_errors = nil
  45. @dirty = false
  46. @commands = options[:commands] || {}
  47. normalize_style_definition
  48. initialize_storage
  49. end
  50. # What gets called when you call instance.attachment = File. It clears
  51. # errors, assigns attributes, processes the file, and runs validations. It
  52. # also queues up the previous file for deletion, to be flushed away on
  53. # #save of its host. In addition to form uploads, you can also assign
  54. # another Paperclip attachment:
  55. # new_user.avatar = old_user.avatar
  56. # If the file that is assigned is not valid, the processing (i.e.
  57. # thumbnailing, etc) will NOT be run.
  58. def assign uploaded_file
  59. ensure_required_accessors!
  60. if uploaded_file.is_a?(Paperclip::Attachment)
  61. uploaded_file = uploaded_file.to_file(:original)
  62. close_uploaded_file = uploaded_file.respond_to?(:close)
  63. end
  64. return nil unless valid_assignment?(uploaded_file)
  65. uploaded_file.binmode if uploaded_file.respond_to? :binmode
  66. self.clear
  67. return nil if uploaded_file.nil?
  68. @queued_for_write[:original] = uploaded_file.to_tempfile
  69. instance_write(:file_name, uploaded_file.original_filename.strip.gsub(/[^A-Za-z\d\.\-_]+/, '_'))
  70. instance_write(:content_type, uploaded_file.content_type.to_s.strip)
  71. instance_write(:file_size, uploaded_file.size.to_i)
  72. instance_write(:updated_at, Time.now)
  73. @dirty = true
  74. post_process if valid?
  75. # Reset the file size if the original file was reprocessed.
  76. instance_write(:file_size, @queued_for_write[:original].size.to_i)
  77. ensure
  78. uploaded_file.close if close_uploaded_file
  79. validate
  80. end
  81. # Returns the public URL of the attachment, with a given style. Note that
  82. # this does not necessarily need to point to a file that your web server
  83. # can access and can point to an action in your app, if you need fine
  84. # grained security. This is not recommended if you don't need the
  85. # security, however, for performance reasons. set
  86. # include_updated_timestamp to false if you want to stop the attachment
  87. # update time appended to the url
  88. def url style = default_style, include_updated_timestamp = true
  89. url = original_filename.nil? ? interpolate(@default_url, style) : interpolate(@url, style)
  90. include_updated_timestamp && updated_at ? [url, updated_at].compact.join(url.include?("?") ? "&" : "?") : url
  91. end
  92. # Returns the path of the attachment as defined by the :path option. If the
  93. # file is stored in the filesystem the path refers to the path of the file
  94. # on disk. If the file is stored in S3, the path is the "key" part of the
  95. # URL, and the :bucket option refers to the S3 bucket.
  96. def path style = default_style
  97. original_filename.nil? ? nil : interpolate(@path, style)
  98. end
  99. # Alias to +url+
  100. def to_s style = nil
  101. url(style)
  102. end
  103. # Returns true if there are no errors on this attachment.
  104. def valid?
  105. validate
  106. errors.empty?
  107. end
  108. # Returns an array containing the errors on this attachment.
  109. def errors
  110. @errors
  111. end
  112. # Returns true if there are changes that need to be saved.
  113. def dirty?
  114. @dirty
  115. end
  116. # Saves the file, if there are no errors. If there are, it flushes them to
  117. # the instance's errors and returns false, cancelling the save.
  118. def save
  119. if valid?
  120. flush_deletes
  121. flush_writes
  122. @dirty = false
  123. true
  124. else
  125. flush_errors
  126. false
  127. end
  128. end
  129. # Clears out the attachment. Has the same effect as previously assigning
  130. # nil to the attachment. Does NOT save. If you wish to clear AND save,
  131. # use #destroy.
  132. def clear
  133. queue_existing_for_delete
  134. @errors = {}
  135. @validation_errors = nil
  136. end
  137. # Destroys the attachment. Has the same effect as previously assigning
  138. # nil to the attachment *and saving*. This is permanent. If you wish to
  139. # wipe out the existing attachment but not save, use #clear.
  140. def destroy
  141. clear
  142. save
  143. end
  144. # Returns the name of the file as originally assigned, and lives in the
  145. # <attachment>_file_name attribute of the model.
  146. def original_filename
  147. instance_read(:file_name)
  148. end
  149. # Returns the size of the file as originally assigned, and lives in the
  150. # <attachment>_file_size attribute of the model.
  151. def size
  152. instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
  153. end
  154. # Returns the content_type of the file as originally assigned, and lives
  155. # in the <attachment>_content_type attribute of the model.
  156. def content_type
  157. instance_read(:content_type)
  158. end
  159. # Returns the last modified time of the file as originally assigned, and
  160. # lives in the <attachment>_updated_at attribute of the model.
  161. def updated_at
  162. time = instance_read(:updated_at)
  163. time && time.to_f.to_i
  164. end
  165. # Paths and URLs can have a number of variables interpolated into them
  166. # to vary the storage location based on name, id, style, class, etc.
  167. # This method is a deprecated access into supplying and retrieving these
  168. # interpolations. Future access should use either Paperclip.interpolates
  169. # or extend the Paperclip::Interpolations module directly.
  170. def self.interpolations
  171. warn('[DEPRECATION] Paperclip::Attachment.interpolations is deprecated ' +
  172. 'and will be removed from future versions. ' +
  173. 'Use Paperclip.interpolates instead')
  174. Paperclip::Interpolations
  175. end
  176. # This method really shouldn't be called that often. It's expected use is
  177. # in the paperclip:refresh rake task and that's it. It will regenerate all
  178. # thumbnails forcefully, by reobtaining the original file and going through
  179. # the post-process again.
  180. def reprocess!
  181. new_original = Tempfile.new("paperclip-reprocess")
  182. new_original.binmode
  183. if old_original = to_file(:original)
  184. new_original.write( old_original.read )
  185. new_original.rewind
  186. @queued_for_write = { :original => new_original }
  187. post_process
  188. old_original.close if old_original.respond_to?(:close)
  189. save
  190. else
  191. true
  192. end
  193. end
  194. # Returns true if a file has been assigned.
  195. def file?
  196. !original_filename.blank?
  197. end
  198. # Writes the attachment-specific attribute on the instance. For example,
  199. # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
  200. # "avatar_file_name" field (assuming the attachment is called avatar).
  201. def instance_write(attr, value)
  202. setter = :"#{name}_#{attr}="
  203. responds = instance.respond_to?(setter)
  204. self.instance_variable_set("@_#{setter.to_s.chop}", value)
  205. instance.send(setter, value) if responds || attr.to_s == "file_name"
  206. end
  207. # Reads the attachment-specific attribute on the instance. See instance_write
  208. # for more details.
  209. def instance_read(attr)
  210. getter = :"#{name}_#{attr}"
  211. responds = instance.respond_to?(getter)
  212. cached = self.instance_variable_get("@_#{getter}")
  213. return cached if cached
  214. instance.send(getter) if responds || attr.to_s == "file_name"
  215. end
  216. private
  217. def ensure_required_accessors! #:nodoc:
  218. %w(file_name).each do |field|
  219. unless @instance.respond_to?("#{name}_#{field}") && @instance.respond_to?("#{name}_#{field}=")
  220. raise PaperclipError.new("#{@instance.class} model missing required attr_accessor for '#{name}_#{field}'")
  221. end
  222. end
  223. end
  224. def log message #:nodoc:
  225. Paperclip.log(message)
  226. end
  227. def valid_assignment? file #:nodoc:
  228. file.nil? || (file.respond_to?(:original_filename) && file.respond_to?(:content_type))
  229. end
  230. def validate #:nodoc:
  231. unless @validation_errors
  232. @validation_errors = @validations.inject({}) do |errors, validation|
  233. name, options = validation
  234. errors[name] = send(:"validate_#{name}", options) if allow_validation?(options)
  235. errors
  236. end
  237. @validation_errors.reject!{|k,v| v == nil }
  238. @errors.merge!(@validation_errors)
  239. end
  240. @validation_errors
  241. end
  242. def allow_validation? options #:nodoc:
  243. (options[:if].nil? || check_guard(options[:if])) && (options[:unless].nil? || !check_guard(options[:unless]))
  244. end
  245. def check_guard guard #:nodoc:
  246. if guard.respond_to? :call
  247. guard.call(instance)
  248. elsif ! guard.blank?
  249. instance.send(guard.to_s)
  250. end
  251. end
  252. def validate_size options #:nodoc:
  253. if file? && !options[:range].include?(size.to_i)
  254. options[:message].gsub(/:min/, options[:min].to_s).gsub(/:max/, options[:max].to_s)
  255. end
  256. end
  257. def validate_presence options #:nodoc:
  258. options[:message] unless file?
  259. end
  260. def validate_content_type options #:nodoc:
  261. valid_types = [options[:content_type]].flatten
  262. unless original_filename.blank?
  263. unless valid_types.blank?
  264. content_type = instance_read(:content_type)
  265. unless valid_types.any?{|t| content_type.nil? || t === content_type }
  266. options[:message] || "is not one of the allowed file types."
  267. end
  268. end
  269. end
  270. end
  271. def normalize_style_definition #:nodoc:
  272. @styles.each do |name, args|
  273. unless args.is_a? Hash
  274. dimensions, format = [args, nil].flatten[0..1]
  275. format = nil if format.blank?
  276. @styles[name] = {
  277. :processors => @processors,
  278. :geometry => dimensions,
  279. :format => format,
  280. :whiny => @whiny,
  281. :convert_options => extra_options_for(name),
  282. :commands => @commands[name]
  283. }
  284. else
  285. @styles[name] = {
  286. :processors => @processors,
  287. :whiny => @whiny,
  288. :convert_options => extra_options_for(name)
  289. }.merge(@styles[name])
  290. end
  291. end
  292. end
  293. def solidify_style_definitions #:nodoc:
  294. @styles.each do |name, args|
  295. @styles[name][:geometry] = @styles[name][:geometry].call(instance) if @styles[name][:geometry].respond_to?(:call)
  296. @styles[name][:processors] = @styles[name][:processors].call(instance) if @styles[name][:processors].respond_to?(:call)
  297. end
  298. end
  299. def initialize_storage #:nodoc:
  300. @storage_module = Paperclip::Storage.const_get(@storage.to_s.capitalize)
  301. self.extend(@storage_module)
  302. end
  303. def extra_options_for(style) #:nodoc:
  304. all_options = convert_options[:all]
  305. all_options = all_options.call(instance) if all_options.respond_to?(:call)
  306. style_options = convert_options[style]
  307. style_options = style_options.call(instance) if style_options.respond_to?(:call)
  308. [ style_options, all_options ].compact.join(" ")
  309. end
  310. def post_process #:nodoc:
  311. return if @queued_for_write[:original].nil?
  312. solidify_style_definitions
  313. return if fire_events(:before)
  314. post_process_styles
  315. return if fire_events(:after)
  316. end
  317. def fire_events(which) #:nodoc:
  318. return true if callback(:"#{which}_post_process") == false
  319. return true if callback(:"#{which}_#{name}_post_process") == false
  320. end
  321. def callback which #:nodoc:
  322. instance.run_callbacks(which, @queued_for_write){|result, obj| result == false }
  323. end
  324. def post_process_styles #:nodoc:
  325. @styles.each do |name, args|
  326. begin
  327. raise RuntimeError.new("Style #{name} has no processors defined.") if args[:processors].blank?
  328. @queued_for_write[name] = args[:processors].inject(@queued_for_write[:original]) do |file, processor|
  329. Paperclip.processor(processor).make(file, args, self)
  330. end
  331. rescue PaperclipError => e
  332. log("An error was received while processing: #{e.inspect}")
  333. (@errors[:processing] ||= []) << e.message if @whiny
  334. end
  335. end
  336. end
  337. def interpolate pattern, style = default_style #:nodoc:
  338. Paperclip::Interpolations.interpolate(pattern, self, style)
  339. end
  340. def queue_existing_for_delete #:nodoc:
  341. return unless file?
  342. @queued_for_delete += [:original, *@styles.keys].uniq.map do |style|
  343. path(style) if exists?(style)
  344. end.compact
  345. instance_write(:file_name, nil)
  346. instance_write(:content_type, nil)
  347. instance_write(:file_size, nil)
  348. instance_write(:updated_at, nil)
  349. end
  350. def flush_errors #:nodoc:
  351. @errors.each do |error, message|
  352. [message].flatten.each {|m| instance.errors.add(name, m) }
  353. end
  354. end
  355. end
  356. end