jr-paperclip 7.3.0

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