paperclip-fix 4.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.hound.yml +1066 -0
  4. data/.rubocop.yml +1 -0
  5. data/.travis.yml +22 -0
  6. data/Appraisals +11 -0
  7. data/CONTRIBUTING.md +75 -0
  8. data/Gemfile +21 -0
  9. data/LICENSE +24 -0
  10. data/NEWS +420 -0
  11. data/README.md +979 -0
  12. data/RELEASING.md +17 -0
  13. data/Rakefile +44 -0
  14. data/UPGRADING +14 -0
  15. data/cucumber/paperclip_steps.rb +6 -0
  16. data/features/basic_integration.feature +80 -0
  17. data/features/migration.feature +94 -0
  18. data/features/rake_tasks.feature +62 -0
  19. data/features/step_definitions/attachment_steps.rb +110 -0
  20. data/features/step_definitions/html_steps.rb +15 -0
  21. data/features/step_definitions/rails_steps.rb +236 -0
  22. data/features/step_definitions/s3_steps.rb +14 -0
  23. data/features/step_definitions/web_steps.rb +107 -0
  24. data/features/support/env.rb +11 -0
  25. data/features/support/fakeweb.rb +13 -0
  26. data/features/support/file_helpers.rb +34 -0
  27. data/features/support/fixtures/boot_config.txt +15 -0
  28. data/features/support/fixtures/gemfile.txt +5 -0
  29. data/features/support/fixtures/preinitializer.txt +20 -0
  30. data/features/support/paths.rb +28 -0
  31. data/features/support/rails.rb +63 -0
  32. data/features/support/selectors.rb +19 -0
  33. data/gemfiles/3.2.gemfile +19 -0
  34. data/gemfiles/4.1.gemfile +19 -0
  35. data/gemfiles/4.2.gemfile +19 -0
  36. data/lib/generators/paperclip/USAGE +8 -0
  37. data/lib/generators/paperclip/paperclip_generator.rb +30 -0
  38. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +15 -0
  39. data/lib/paperclip/attachment.rb +608 -0
  40. data/lib/paperclip/attachment_registry.rb +59 -0
  41. data/lib/paperclip/callbacks.rb +40 -0
  42. data/lib/paperclip/content_type_detector.rb +79 -0
  43. data/lib/paperclip/deprecations.rb +42 -0
  44. data/lib/paperclip/errors.rb +32 -0
  45. data/lib/paperclip/file_command_content_type_detector.rb +30 -0
  46. data/lib/paperclip/filename_cleaner.rb +16 -0
  47. data/lib/paperclip/geometry.rb +158 -0
  48. data/lib/paperclip/geometry_detector_factory.rb +48 -0
  49. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  50. data/lib/paperclip/glue.rb +17 -0
  51. data/lib/paperclip/has_attached_file.rb +109 -0
  52. data/lib/paperclip/helpers.rb +56 -0
  53. data/lib/paperclip/interpolations/plural_cache.rb +18 -0
  54. data/lib/paperclip/interpolations.rb +197 -0
  55. data/lib/paperclip/io_adapters/abstract_adapter.rb +47 -0
  56. data/lib/paperclip/io_adapters/attachment_adapter.rb +36 -0
  57. data/lib/paperclip/io_adapters/data_uri_adapter.rb +22 -0
  58. data/lib/paperclip/io_adapters/empty_string_adapter.rb +18 -0
  59. data/lib/paperclip/io_adapters/file_adapter.rb +22 -0
  60. data/lib/paperclip/io_adapters/http_url_proxy_adapter.rb +15 -0
  61. data/lib/paperclip/io_adapters/identity_adapter.rb +12 -0
  62. data/lib/paperclip/io_adapters/nil_adapter.rb +34 -0
  63. data/lib/paperclip/io_adapters/registry.rb +32 -0
  64. data/lib/paperclip/io_adapters/stringio_adapter.rb +33 -0
  65. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +42 -0
  66. data/lib/paperclip/io_adapters/uri_adapter.rb +63 -0
  67. data/lib/paperclip/locales/de.yml +18 -0
  68. data/lib/paperclip/locales/en.yml +18 -0
  69. data/lib/paperclip/locales/es.yml +18 -0
  70. data/lib/paperclip/locales/ja.yml +18 -0
  71. data/lib/paperclip/locales/pt-BR.yml +18 -0
  72. data/lib/paperclip/locales/zh-CN.yml +18 -0
  73. data/lib/paperclip/locales/zh-HK.yml +18 -0
  74. data/lib/paperclip/locales/zh-TW.yml +18 -0
  75. data/lib/paperclip/logger.rb +21 -0
  76. data/lib/paperclip/matchers/have_attached_file_matcher.rb +54 -0
  77. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +100 -0
  78. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +59 -0
  79. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +96 -0
  80. data/lib/paperclip/matchers.rb +64 -0
  81. data/lib/paperclip/media_type_spoof_detector.rb +89 -0
  82. data/lib/paperclip/missing_attachment_styles.rb +79 -0
  83. data/lib/paperclip/processor.rb +48 -0
  84. data/lib/paperclip/processor_helpers.rb +50 -0
  85. data/lib/paperclip/rails_environment.rb +25 -0
  86. data/lib/paperclip/railtie.rb +31 -0
  87. data/lib/paperclip/schema.rb +83 -0
  88. data/lib/paperclip/storage/filesystem.rb +90 -0
  89. data/lib/paperclip/storage/fog.rb +241 -0
  90. data/lib/paperclip/storage/s3.rb +440 -0
  91. data/lib/paperclip/storage.rb +3 -0
  92. data/lib/paperclip/style.rb +109 -0
  93. data/lib/paperclip/tempfile.rb +43 -0
  94. data/lib/paperclip/tempfile_factory.rb +23 -0
  95. data/lib/paperclip/thumbnail.rb +121 -0
  96. data/lib/paperclip/url_generator.rb +72 -0
  97. data/lib/paperclip/validators/attachment_content_type_validator.rb +88 -0
  98. data/lib/paperclip/validators/attachment_file_name_validator.rb +80 -0
  99. data/lib/paperclip/validators/attachment_file_type_ignorance_validator.rb +29 -0
  100. data/lib/paperclip/validators/attachment_presence_validator.rb +30 -0
  101. data/lib/paperclip/validators/attachment_size_validator.rb +115 -0
  102. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +27 -0
  103. data/lib/paperclip/validators.rb +74 -0
  104. data/lib/paperclip/version.rb +3 -0
  105. data/lib/paperclip.rb +213 -0
  106. data/lib/tasks/paperclip.rake +127 -0
  107. data/paperclip.gemspec +51 -0
  108. data/shoulda_macros/paperclip.rb +134 -0
  109. data/spec/database.yml +4 -0
  110. data/spec/paperclip/attachment_definitions_spec.rb +13 -0
  111. data/spec/paperclip/attachment_processing_spec.rb +82 -0
  112. data/spec/paperclip/attachment_registry_spec.rb +130 -0
  113. data/spec/paperclip/attachment_spec.rb +1494 -0
  114. data/spec/paperclip/content_type_detector_spec.rb +48 -0
  115. data/spec/paperclip/deprecations_spec.rb +65 -0
  116. data/spec/paperclip/file_command_content_type_detector_spec.rb +26 -0
  117. data/spec/paperclip/filename_cleaner_spec.rb +14 -0
  118. data/spec/paperclip/geometry_detector_spec.rb +39 -0
  119. data/spec/paperclip/geometry_parser_spec.rb +73 -0
  120. data/spec/paperclip/geometry_spec.rb +255 -0
  121. data/spec/paperclip/glue_spec.rb +44 -0
  122. data/spec/paperclip/has_attached_file_spec.rb +142 -0
  123. data/spec/paperclip/integration_spec.rb +667 -0
  124. data/spec/paperclip/interpolations_spec.rb +262 -0
  125. data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +78 -0
  126. data/spec/paperclip/io_adapters/attachment_adapter_spec.rb +139 -0
  127. data/spec/paperclip/io_adapters/data_uri_adapter_spec.rb +83 -0
  128. data/spec/paperclip/io_adapters/empty_string_adapter_spec.rb +17 -0
  129. data/spec/paperclip/io_adapters/file_adapter_spec.rb +131 -0
  130. data/spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb +104 -0
  131. data/spec/paperclip/io_adapters/identity_adapter_spec.rb +8 -0
  132. data/spec/paperclip/io_adapters/nil_adapter_spec.rb +25 -0
  133. data/spec/paperclip/io_adapters/registry_spec.rb +35 -0
  134. data/spec/paperclip/io_adapters/stringio_adapter_spec.rb +64 -0
  135. data/spec/paperclip/io_adapters/uploaded_file_adapter_spec.rb +146 -0
  136. data/spec/paperclip/io_adapters/uri_adapter_spec.rb +127 -0
  137. data/spec/paperclip/matchers/have_attached_file_matcher_spec.rb +19 -0
  138. data/spec/paperclip/matchers/validate_attachment_content_type_matcher_spec.rb +99 -0
  139. data/spec/paperclip/matchers/validate_attachment_presence_matcher_spec.rb +69 -0
  140. data/spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb +88 -0
  141. data/spec/paperclip/media_type_spoof_detector_spec.rb +79 -0
  142. data/spec/paperclip/meta_class_spec.rb +30 -0
  143. data/spec/paperclip/paperclip_missing_attachment_styles_spec.rb +84 -0
  144. data/spec/paperclip/paperclip_spec.rb +222 -0
  145. data/spec/paperclip/plural_cache_spec.rb +37 -0
  146. data/spec/paperclip/processor_helpers_spec.rb +57 -0
  147. data/spec/paperclip/processor_spec.rb +26 -0
  148. data/spec/paperclip/rails_environment_spec.rb +33 -0
  149. data/spec/paperclip/rake_spec.rb +103 -0
  150. data/spec/paperclip/schema_spec.rb +248 -0
  151. data/spec/paperclip/storage/filesystem_spec.rb +79 -0
  152. data/spec/paperclip/storage/fog_spec.rb +535 -0
  153. data/spec/paperclip/storage/s3_live_spec.rb +182 -0
  154. data/spec/paperclip/storage/s3_spec.rb +1526 -0
  155. data/spec/paperclip/style_spec.rb +255 -0
  156. data/spec/paperclip/tempfile_factory_spec.rb +33 -0
  157. data/spec/paperclip/thumbnail_spec.rb +500 -0
  158. data/spec/paperclip/url_generator_spec.rb +211 -0
  159. data/spec/paperclip/validators/attachment_content_type_validator_spec.rb +322 -0
  160. data/spec/paperclip/validators/attachment_file_name_validator_spec.rb +160 -0
  161. data/spec/paperclip/validators/attachment_presence_validator_spec.rb +85 -0
  162. data/spec/paperclip/validators/attachment_size_validator_spec.rb +229 -0
  163. data/spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb +52 -0
  164. data/spec/paperclip/validators_spec.rb +164 -0
  165. data/spec/spec_helper.rb +43 -0
  166. data/spec/support/assertions.rb +71 -0
  167. data/spec/support/deprecations.rb +9 -0
  168. data/spec/support/fake_model.rb +25 -0
  169. data/spec/support/fake_rails.rb +12 -0
  170. data/spec/support/fixtures/12k.png +0 -0
  171. data/spec/support/fixtures/50x50.png +0 -0
  172. data/spec/support/fixtures/5k.png +0 -0
  173. data/spec/support/fixtures/animated +0 -0
  174. data/spec/support/fixtures/animated.gif +0 -0
  175. data/spec/support/fixtures/animated.unknown +0 -0
  176. data/spec/support/fixtures/bad.png +1 -0
  177. data/spec/support/fixtures/empty.html +1 -0
  178. data/spec/support/fixtures/empty.xlsx +0 -0
  179. data/spec/support/fixtures/fog.yml +8 -0
  180. data/spec/support/fixtures/rotated.jpg +0 -0
  181. data/spec/support/fixtures/s3.yml +8 -0
  182. data/spec/support/fixtures/spaced file.jpg +0 -0
  183. data/spec/support/fixtures/spaced file.png +0 -0
  184. data/spec/support/fixtures/text.txt +1 -0
  185. data/spec/support/fixtures/twopage.pdf +0 -0
  186. data/spec/support/fixtures/uppercase.PNG +0 -0
  187. data/spec/support/matchers/accept.rb +5 -0
  188. data/spec/support/matchers/exist.rb +5 -0
  189. data/spec/support/matchers/have_column.rb +23 -0
  190. data/spec/support/mock_attachment.rb +22 -0
  191. data/spec/support/mock_interpolator.rb +24 -0
  192. data/spec/support/mock_url_generator_builder.rb +27 -0
  193. data/spec/support/model_reconstruction.rb +60 -0
  194. data/spec/support/rails_helpers.rb +7 -0
  195. data/spec/support/test_data.rb +13 -0
  196. data/spec/support/version_helper.rb +9 -0
  197. metadata +606 -0
@@ -0,0 +1,608 @@
1
+ # encoding: utf-8
2
+ require 'uri'
3
+ require 'paperclip/url_generator'
4
+ require 'active_support/deprecation'
5
+
6
+ module Paperclip
7
+ # The Attachment class manages the files for a given attachment. It saves
8
+ # when the model saves, deletes when the model is destroyed, and processes
9
+ # the file upon assignment.
10
+ class Attachment
11
+ def self.default_options
12
+ @default_options ||= {
13
+ :convert_options => {},
14
+ :default_style => :original,
15
+ :default_url => "/:attachment/:style/missing.png",
16
+ :escape_url => true,
17
+ :restricted_characters => /[&$+,\/:;=?@<>\[\]\{\}\|\\\^~%# ]/,
18
+ :filename_cleaner => nil,
19
+ :hash_data => ":class/:attachment/:id/:style/:updated_at",
20
+ :hash_digest => "SHA1",
21
+ :interpolator => Paperclip::Interpolations,
22
+ :only_process => [],
23
+ :path => ":rails_root/public:url",
24
+ :preserve_files => false,
25
+ :processors => [:thumbnail],
26
+ :source_file_options => {},
27
+ :storage => :filesystem,
28
+ :styles => {},
29
+ :url => "/system/:class/:attachment/:id_partition/:style/:filename",
30
+ :url_generator => Paperclip::UrlGenerator,
31
+ :use_default_time_zone => true,
32
+ :use_timestamp => true,
33
+ :whiny => Paperclip.options[:whiny] || Paperclip.options[:whiny_thumbnails],
34
+ :validate_media_type => true,
35
+ :check_validity_before_processing => true
36
+ }
37
+ end
38
+
39
+ attr_reader :name, :instance, :default_style, :convert_options, :queued_for_write, :whiny,
40
+ :options, :interpolator, :source_file_options
41
+ attr_accessor :post_processing
42
+
43
+ # Creates an Attachment object. +name+ is the name of the attachment,
44
+ # +instance+ is the model object instance it's attached to, and
45
+ # +options+ is the same as the hash passed to +has_attached_file+.
46
+ #
47
+ # Options include:
48
+ #
49
+ # +url+ - a relative URL of the attachment. This is interpolated using +interpolator+
50
+ # +path+ - where on the filesystem to store the attachment. This is interpolated using +interpolator+
51
+ # +styles+ - a hash of options for processing the attachment. See +has_attached_file+ for the details
52
+ # +only_process+ - style args to be run through the post-processor. This defaults to the empty list
53
+ # +default_url+ - a URL for the missing image
54
+ # +default_style+ - the style to use when an argument is not specified e.g. #url, #path
55
+ # +storage+ - the storage mechanism. Defaults to :filesystem
56
+ # +use_timestamp+ - whether to append an anti-caching timestamp to image URLs. Defaults to true
57
+ # +whiny+, +whiny_thumbnails+ - whether to raise when thumbnailing fails
58
+ # +use_default_time_zone+ - related to +use_timestamp+. Defaults to true
59
+ # +hash_digest+ - a string representing a class that will be used to hash URLs for obfuscation
60
+ # +hash_data+ - the relative URL for the hash data. This is interpolated using +interpolator+
61
+ # +hash_secret+ - a secret passed to the +hash_digest+
62
+ # +convert_options+ - flags passed to the +convert+ command for processing
63
+ # +source_file_options+ - flags passed to the +convert+ command that controls how the file is read
64
+ # +processors+ - classes that transform the attachment. Defaults to [:thumbnail]
65
+ # +preserve_files+ - whether to keep files on the filesystem when deleting or clearing the attachment. Defaults to false
66
+ # +filename_cleaner+ - An object that responds to #call(filename) that will strip unacceptable charcters from filename
67
+ # +interpolator+ - the object used to interpolate filenames and URLs. Defaults to Paperclip::Interpolations
68
+ # +url_generator+ - the object used to generate URLs, using the interpolator. Defaults to Paperclip::UrlGenerator
69
+ # +escape_url+ - Perform URI escaping to URLs. Defaults to true
70
+ def initialize(name, instance, options = {})
71
+ @name = name.to_sym
72
+ @name_string = name.to_s
73
+ @instance = instance
74
+
75
+ options = self.class.default_options.deep_merge(options)
76
+
77
+ @options = options
78
+ @post_processing = true
79
+ @queued_for_delete = []
80
+ @queued_for_write = {}
81
+ @errors = {}
82
+ @dirty = false
83
+ @interpolator = options[:interpolator]
84
+ @url_generator = options[:url_generator].new(self, @options)
85
+ @source_file_options = options[:source_file_options]
86
+ @whiny = options[:whiny]
87
+
88
+ initialize_storage
89
+ end
90
+
91
+ # What gets called when you call instance.attachment = File. It clears
92
+ # errors, assigns attributes, and processes the file. It also queues up the
93
+ # previous file for deletion, to be flushed away on #save of its host. In
94
+ # addition to form uploads, you can also assign another Paperclip
95
+ # attachment:
96
+ # new_user.avatar = old_user.avatar
97
+ def assign(uploaded_file)
98
+ @file = Paperclip.io_adapters.for(uploaded_file)
99
+ ensure_required_accessors!
100
+ ensure_required_validations!
101
+
102
+ if @file.assignment?
103
+ clear(*only_process)
104
+
105
+ if @file.nil?
106
+ nil
107
+ else
108
+ assign_attributes
109
+ post_process_file
110
+ reset_file_if_original_reprocessed
111
+ end
112
+ else
113
+ nil
114
+ end
115
+ end
116
+
117
+ # Returns the public URL of the attachment with a given style. This does
118
+ # not necessarily need to point to a file that your Web server can access
119
+ # and can instead point to an action in your app, for example for fine grained
120
+ # security; this has a serious performance tradeoff.
121
+ #
122
+ # Options:
123
+ #
124
+ # +timestamp+ - Add a timestamp to the end of the URL. Default: true.
125
+ # +escape+ - Perform URI escaping to the URL. Default: true.
126
+ #
127
+ # Global controls (set on has_attached_file):
128
+ #
129
+ # +interpolator+ - The object that fills in a URL pattern's variables.
130
+ # +default_url+ - The image to show when the attachment has no image.
131
+ # +url+ - The URL for a saved image.
132
+ # +url_generator+ - The object that generates a URL. Default: Paperclip::UrlGenerator.
133
+ #
134
+ # As mentioned just above, the object that generates this URL can be passed
135
+ # in, for finer control. This object must respond to two methods:
136
+ #
137
+ # +#new(Paperclip::Attachment, options_hash)+
138
+ # +#for(style_name, options_hash)+
139
+
140
+ def url(style_name = default_style, options = {})
141
+ if options == true || options == false # Backwards compatibility.
142
+ @url_generator.for(style_name, default_options.merge(:timestamp => options))
143
+ else
144
+ @url_generator.for(style_name, default_options.merge(options))
145
+ end
146
+ end
147
+
148
+ def default_options
149
+ {
150
+ :timestamp => @options[:use_timestamp],
151
+ :escape => @options[:escape_url]
152
+ }
153
+ end
154
+
155
+ # Alias to +url+ that allows using the expiring_url method provided by the cloud
156
+ # storage implementations, but keep using filesystem storage for development and
157
+ # testing.
158
+ def expiring_url(time = 3600, style_name = default_style)
159
+ url(style_name)
160
+ end
161
+
162
+ # Returns the path of the attachment as defined by the :path option. If the
163
+ # file is stored in the filesystem the path refers to the path of the file
164
+ # on disk. If the file is stored in S3, the path is the "key" part of the
165
+ # URL, and the :bucket option refers to the S3 bucket.
166
+ def path(style_name = default_style)
167
+ path = original_filename.nil? ? nil : interpolate(path_option, style_name)
168
+ path.respond_to?(:unescape) ? path.unescape : path
169
+ end
170
+
171
+ # :nodoc:
172
+ def staged_path(style_name = default_style)
173
+ if staged?
174
+ @queued_for_write[style_name].path
175
+ end
176
+ end
177
+
178
+ # :nodoc:
179
+ def staged?
180
+ ! @queued_for_write.empty?
181
+ end
182
+
183
+ # Alias to +url+
184
+ def to_s style_name = default_style
185
+ url(style_name)
186
+ end
187
+
188
+ def as_json(options = nil)
189
+ to_s((options && options[:style]) || default_style)
190
+ end
191
+
192
+ def default_style
193
+ @options[:default_style]
194
+ end
195
+
196
+ def styles
197
+ if @options[:styles].respond_to?(:call) || @normalized_styles.nil?
198
+ styles = @options[:styles]
199
+ styles = styles.call(self) if styles.respond_to?(:call)
200
+
201
+ @normalized_styles = styles.dup
202
+ styles.each_pair do |name, options|
203
+ @normalized_styles[name.to_sym] = Paperclip::Style.new(name.to_sym, options.dup, self)
204
+ end
205
+ end
206
+ @normalized_styles
207
+ end
208
+
209
+ def only_process
210
+ only_process = @options[:only_process].dup
211
+ only_process = only_process.call(self) if only_process.respond_to?(:call)
212
+ only_process.map(&:to_sym)
213
+ end
214
+
215
+ def processors
216
+ processing_option = @options[:processors]
217
+
218
+ if processing_option.respond_to?(:call)
219
+ processing_option.call(instance)
220
+ else
221
+ processing_option
222
+ end
223
+ end
224
+
225
+ # Returns an array containing the errors on this attachment.
226
+ def errors
227
+ @errors
228
+ end
229
+
230
+ # Returns true if there are changes that need to be saved.
231
+ def dirty?
232
+ @dirty
233
+ end
234
+
235
+ # Saves the file, if there are no errors. If there are, it flushes them to
236
+ # the instance's errors and returns false, cancelling the save.
237
+ def save
238
+ flush_deletes unless @options[:keep_old_files]
239
+ flush_writes
240
+ @dirty = false
241
+ true
242
+ end
243
+
244
+ # Clears out the attachment. Has the same effect as previously assigning
245
+ # nil to the attachment. Does NOT save. If you wish to clear AND save,
246
+ # use #destroy.
247
+ def clear(*styles_to_clear)
248
+ if styles_to_clear.any?
249
+ queue_some_for_delete(*styles_to_clear)
250
+ else
251
+ queue_all_for_delete
252
+ @queued_for_write = {}
253
+ @errors = {}
254
+ end
255
+ end
256
+
257
+ # Destroys the attachment. Has the same effect as previously assigning
258
+ # nil to the attachment *and saving*. This is permanent. If you wish to
259
+ # wipe out the existing attachment but not save, use #clear.
260
+ def destroy
261
+ clear
262
+ save
263
+ end
264
+
265
+ # Returns the uploaded file if present.
266
+ def uploaded_file
267
+ instance_read(:uploaded_file)
268
+ end
269
+
270
+ # Returns the name of the file as originally assigned, and lives in the
271
+ # <attachment>_file_name attribute of the model.
272
+ def original_filename
273
+ instance_read(:file_name)
274
+ end
275
+
276
+ # Returns the size of the file as originally assigned, and lives in the
277
+ # <attachment>_file_size attribute of the model.
278
+ def size
279
+ instance_read(:file_size) || (@queued_for_write[:original] && @queued_for_write[:original].size)
280
+ end
281
+
282
+ # Returns the fingerprint of the file, if one's defined. The fingerprint is
283
+ # stored in the <attachment>_fingerprint attribute of the model.
284
+ def fingerprint
285
+ instance_read(:fingerprint)
286
+ end
287
+
288
+ # Returns the content_type of the file as originally assigned, and lives
289
+ # in the <attachment>_content_type attribute of the model.
290
+ def content_type
291
+ instance_read(:content_type)
292
+ end
293
+
294
+ # Returns the creation time of the file as originally assigned, and
295
+ # lives in the <attachment>_created_at attribute of the model.
296
+ def created_at
297
+ if able_to_store_created_at?
298
+ time = instance_read(:created_at)
299
+ time && time.to_f.to_i
300
+ end
301
+ end
302
+
303
+ # Returns the last modified time of the file as originally assigned, and
304
+ # lives in the <attachment>_updated_at attribute of the model.
305
+ def updated_at
306
+ time = instance_read(:updated_at)
307
+ time && time.to_f.to_i
308
+ end
309
+
310
+ # The time zone to use for timestamp interpolation. Using the default
311
+ # time zone ensures that results are consistent across all threads.
312
+ def time_zone
313
+ @options[:use_default_time_zone] ? Time.zone_default : Time.zone
314
+ end
315
+
316
+ # Returns a unique hash suitable for obfuscating the URL of an otherwise
317
+ # publicly viewable attachment.
318
+ def hash_key(style_name = default_style)
319
+ raise ArgumentError, "Unable to generate hash without :hash_secret" unless @options[:hash_secret]
320
+ require 'openssl' unless defined?(OpenSSL)
321
+ data = interpolate(@options[:hash_data], style_name)
322
+ OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@options[:hash_digest]).new, @options[:hash_secret], data)
323
+ end
324
+
325
+ # This method really shouldn't be called that often. It's expected use is
326
+ # in the paperclip:refresh rake task and that's it. It will regenerate all
327
+ # thumbnails forcefully, by reobtaining the original file and going through
328
+ # the post-process again.
329
+ # NOTE: Calling reprocess WILL NOT delete existing files. This is due to
330
+ # inconsistencies in timing of S3 commands. It's possible that calling
331
+ # #reprocess! will lose data if the files are not kept.
332
+ def reprocess!(*style_args)
333
+ saved_only_process, @options[:only_process] = @options[:only_process], style_args
334
+ saved_preserve_files, @options[:preserve_files] = @options[:preserve_files], true
335
+ begin
336
+ assign(self)
337
+ save
338
+ instance.save
339
+ rescue Errno::EACCES => e
340
+ warn "#{e} - skipping file."
341
+ false
342
+ ensure
343
+ @options[:only_process] = saved_only_process
344
+ @options[:preserve_files] = saved_preserve_files
345
+ end
346
+ end
347
+
348
+ # Returns true if a file has been assigned.
349
+ def file?
350
+ !original_filename.blank?
351
+ end
352
+
353
+ alias :present? :file?
354
+
355
+ def blank?
356
+ not present?
357
+ end
358
+
359
+ # Determines whether the instance responds to this attribute. Used to prevent
360
+ # calculations on fields we won't even store.
361
+ def instance_respond_to?(attr)
362
+ instance.respond_to?(:"#{name}_#{attr}")
363
+ end
364
+
365
+ # Writes the attachment-specific attribute on the instance. For example,
366
+ # instance_write(:file_name, "me.jpg") will write "me.jpg" to the instance's
367
+ # "avatar_file_name" field (assuming the attachment is called avatar).
368
+ def instance_write(attr, value)
369
+ setter = :"#{@name_string}_#{attr}="
370
+ if instance.respond_to?(setter)
371
+ instance.send(setter, value)
372
+ end
373
+ end
374
+
375
+ # Reads the attachment-specific attribute on the instance. See instance_write
376
+ # for more details.
377
+ def instance_read(attr)
378
+ getter = :"#{@name_string}_#{attr}"
379
+ if instance.respond_to?(getter)
380
+ instance.send(getter)
381
+ end
382
+ end
383
+
384
+ private
385
+
386
+ def path_option
387
+ @options[:path].respond_to?(:call) ? @options[:path].call(self) : @options[:path]
388
+ end
389
+
390
+ def active_validator_classes
391
+ @instance.class.validators.map(&:class)
392
+ end
393
+
394
+ def missing_required_validator?
395
+ (active_validator_classes.flat_map(&:ancestors) & Paperclip::REQUIRED_VALIDATORS).empty?
396
+ end
397
+
398
+ def ensure_required_validations!
399
+ if missing_required_validator?
400
+ raise Paperclip::Errors::MissingRequiredValidatorError
401
+ end
402
+ end
403
+
404
+ def ensure_required_accessors! #:nodoc:
405
+ %w(file_name).each do |field|
406
+ unless @instance.respond_to?("#{@name_string}_#{field}") && @instance.respond_to?("#{@name_string}_#{field}=")
407
+ raise Paperclip::Error.new("#{@instance.class} model missing required attr_accessor for '#{@name_string}_#{field}'")
408
+ end
409
+ end
410
+ end
411
+
412
+ def log message #:nodoc:
413
+ Paperclip.log(message)
414
+ end
415
+
416
+ def initialize_storage #:nodoc:
417
+ storage_class_name = @options[:storage].to_s.downcase.camelize
418
+ begin
419
+ storage_module = Paperclip::Storage.const_get(storage_class_name)
420
+ rescue NameError
421
+ raise Errors::StorageMethodNotFound, "Cannot load storage module '#{storage_class_name}'"
422
+ end
423
+ self.extend(storage_module)
424
+ end
425
+
426
+ def assign_attributes
427
+ @queued_for_write[:original] = @file
428
+ assign_file_information
429
+ assign_fingerprint(@file.fingerprint)
430
+ assign_timestamps
431
+ end
432
+
433
+ def assign_file_information
434
+ instance_write(:file_name, cleanup_filename(@file.original_filename))
435
+ instance_write(:content_type, @file.content_type.to_s.strip)
436
+ instance_write(:file_size, @file.size)
437
+ end
438
+
439
+ def assign_fingerprint(fingerprint)
440
+ if instance_respond_to?(:fingerprint)
441
+ instance_write(:fingerprint, fingerprint)
442
+ end
443
+ end
444
+
445
+ def assign_timestamps
446
+ if has_enabled_but_unset_created_at?
447
+ instance_write(:created_at, Time.now)
448
+ end
449
+
450
+ instance_write(:updated_at, Time.now)
451
+ end
452
+
453
+ def post_process_file
454
+ dirty!
455
+
456
+ if post_processing
457
+ post_process(*only_process)
458
+ end
459
+ end
460
+
461
+ def dirty!
462
+ @dirty = true
463
+ end
464
+
465
+ def reset_file_if_original_reprocessed
466
+ instance_write(:file_size, @queued_for_write[:original].size)
467
+ assign_fingerprint(@queued_for_write[:original].fingerprint)
468
+ reset_updater
469
+ end
470
+
471
+ def reset_updater
472
+ if instance.respond_to?(updater)
473
+ instance.send(updater)
474
+ end
475
+ end
476
+
477
+ def updater
478
+ :"#{name}_file_name_will_change!"
479
+ end
480
+
481
+ def extra_options_for(style) #:nodoc:
482
+ process_options(:convert_options, style)
483
+ end
484
+
485
+ def extra_source_file_options_for(style) #:nodoc:
486
+ process_options(:source_file_options, style)
487
+ end
488
+
489
+ def process_options(options_type, style) #:nodoc:
490
+ all_options = @options[options_type][:all]
491
+ all_options = all_options.call(instance) if all_options.respond_to?(:call)
492
+ style_options = @options[options_type][style]
493
+ style_options = style_options.call(instance) if style_options.respond_to?(:call)
494
+
495
+ [ style_options, all_options ].compact.join(" ")
496
+ end
497
+
498
+ def post_process(*style_args) #:nodoc:
499
+ return if @queued_for_write[:original].nil?
500
+
501
+ instance.run_paperclip_callbacks(:post_process) do
502
+ instance.run_paperclip_callbacks(:"#{name}_post_process") do
503
+ unless @options[:check_validity_before_processing] && instance.errors.any?
504
+ post_process_styles(*style_args)
505
+ end
506
+ end
507
+ end
508
+ end
509
+
510
+ def post_process_styles(*style_args) #:nodoc:
511
+ post_process_style(:original, styles[:original]) if styles.include?(:original) && process_style?(:original, style_args)
512
+ styles.reject{ |name, style| name == :original }.each do |name, style|
513
+ post_process_style(name, style) if process_style?(name, style_args)
514
+ end
515
+ end
516
+
517
+ def post_process_style(name, style) #:nodoc:
518
+ begin
519
+ raise RuntimeError.new("Style #{name} has no processors defined.") if style.processors.blank?
520
+ intermediate_files = []
521
+
522
+ @queued_for_write[name] = style.processors.inject(@queued_for_write[:original]) do |file, processor|
523
+ file = Paperclip.processor(processor).make(file, style.processor_options, self)
524
+ intermediate_files << file
525
+ file
526
+ end
527
+
528
+ unadapted_file = @queued_for_write[name]
529
+ @queued_for_write[name] = Paperclip.io_adapters.for(@queued_for_write[name])
530
+ unadapted_file.close if unadapted_file.respond_to?(:close)
531
+ @queued_for_write[name]
532
+ rescue Paperclip::Errors::NotIdentifiedByImageMagickError => e
533
+ log("An error was received while processing: #{e.inspect}")
534
+ (@errors[:processing] ||= []) << e.message if @options[:whiny]
535
+ ensure
536
+ unlink_files(intermediate_files)
537
+ end
538
+ end
539
+
540
+ def process_style?(style_name, style_args) #:nodoc:
541
+ style_args.empty? || style_args.include?(style_name)
542
+ end
543
+
544
+ def interpolate(pattern, style_name = default_style) #:nodoc:
545
+ interpolator.interpolate(pattern, self, style_name)
546
+ end
547
+
548
+ def queue_some_for_delete(*styles)
549
+ @queued_for_delete += styles.uniq.map do |style|
550
+ path(style) if exists?(style)
551
+ end.compact
552
+ end
553
+
554
+ def queue_all_for_delete #:nodoc:
555
+ return if !file?
556
+ unless @options[:preserve_files]
557
+ @queued_for_delete += [:original, *styles.keys].uniq.map do |style|
558
+ path(style) if exists?(style)
559
+ end.compact
560
+ end
561
+ instance_write(:file_name, nil)
562
+ instance_write(:content_type, nil)
563
+ instance_write(:file_size, nil)
564
+ instance_write(:fingerprint, nil)
565
+ instance_write(:created_at, nil) if has_enabled_but_unset_created_at?
566
+ instance_write(:updated_at, nil)
567
+ end
568
+
569
+ def flush_errors #:nodoc:
570
+ @errors.each do |error, message|
571
+ [message].flatten.each {|m| instance.errors.add(name, m) }
572
+ end
573
+ end
574
+
575
+ # called by storage after the writes are flushed and before @queued_for_write is cleared
576
+ def after_flush_writes
577
+ unlink_files(@queued_for_write.values)
578
+ end
579
+
580
+ def unlink_files(files)
581
+ Array(files).each do |file|
582
+ file.close unless file.closed?
583
+ file.unlink if file.respond_to?(:unlink) && file.path.present? && File.exist?(file.path)
584
+ end
585
+ end
586
+
587
+ # You can either specifiy :restricted_characters or you can define your own
588
+ # :filename_cleaner object. This object needs to respond to #call and takes
589
+ # the filename that will be cleaned. It should return the cleaned filenme.
590
+ def filename_cleaner
591
+ @options[:filename_cleaner] || FilenameCleaner.new(@options[:restricted_characters])
592
+ end
593
+
594
+ def cleanup_filename(filename)
595
+ filename_cleaner.call(filename)
596
+ end
597
+
598
+ # Check if attachment database table has a created_at field
599
+ def able_to_store_created_at?
600
+ @instance.respond_to?("#{name}_created_at".to_sym)
601
+ end
602
+
603
+ # Check if attachment database table has a created_at field which is not yet set
604
+ def has_enabled_but_unset_created_at?
605
+ able_to_store_created_at? && !instance_read(:created_at)
606
+ end
607
+ end
608
+ end
@@ -0,0 +1,59 @@
1
+ require 'singleton'
2
+
3
+ module Paperclip
4
+ class AttachmentRegistry
5
+ include Singleton
6
+
7
+ def self.register(klass, attachment_name, attachment_options)
8
+ instance.register(klass, attachment_name, attachment_options)
9
+ end
10
+
11
+ def self.clear
12
+ instance.clear
13
+ end
14
+
15
+ def self.names_for(klass)
16
+ instance.names_for(klass)
17
+ end
18
+
19
+ def self.each_definition(&block)
20
+ instance.each_definition(&block)
21
+ end
22
+
23
+ def self.definitions_for(klass)
24
+ instance.definitions_for(klass)
25
+ end
26
+
27
+ def initialize
28
+ clear
29
+ end
30
+
31
+ def register(klass, attachment_name, attachment_options)
32
+ @attachments ||= {}
33
+ @attachments[klass] ||= {}
34
+ @attachments[klass][attachment_name] = attachment_options
35
+ end
36
+
37
+ def clear
38
+ @attachments = Hash.new { |h,k| h[k] = {} }
39
+ end
40
+
41
+ def names_for(klass)
42
+ @attachments[klass].keys
43
+ end
44
+
45
+ def each_definition
46
+ @attachments.each do |klass, attachments|
47
+ attachments.each do |name, options|
48
+ yield klass, name, options
49
+ end
50
+ end
51
+ end
52
+
53
+ def definitions_for(klass)
54
+ klass.ancestors.each_with_object({}) do |ancestor, inherited_definitions|
55
+ inherited_definitions.deep_merge! @attachments[ancestor]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,40 @@
1
+ module Paperclip
2
+ module Callbacks
3
+ def self.included(base)
4
+ base.extend(Defining)
5
+ base.send(:include, Running)
6
+ end
7
+
8
+ module Defining
9
+ def define_paperclip_callbacks(*callbacks)
10
+ define_callbacks(*[callbacks, {:terminator => callback_terminator}].flatten)
11
+ callbacks.each do |callback|
12
+ eval <<-end_callbacks
13
+ def before_#{callback}(*args, &blk)
14
+ set_callback(:#{callback}, :before, *args, &blk)
15
+ end
16
+ def after_#{callback}(*args, &blk)
17
+ set_callback(:#{callback}, :after, *args, &blk)
18
+ end
19
+ end_callbacks
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def callback_terminator
26
+ if ::ActiveSupport::VERSION::STRING >= '4.1'
27
+ lambda { |target, result| result == false }
28
+ else
29
+ 'result == false'
30
+ end
31
+ end
32
+ end
33
+
34
+ module Running
35
+ def run_paperclip_callbacks(callback, &block)
36
+ run_callbacks(callback, &block)
37
+ end
38
+ end
39
+ end
40
+ end