salebot_uploader 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +0 -0
  3. data/lib/generators/templates/uploader.rb.erb +9 -0
  4. data/lib/generators/uploader_generator.rb +7 -0
  5. data/lib/salebot_uploader/compatibility/paperclip.rb +104 -0
  6. data/lib/salebot_uploader/downloader/base.rb +101 -0
  7. data/lib/salebot_uploader/downloader/remote_file.rb +68 -0
  8. data/lib/salebot_uploader/error.rb +8 -0
  9. data/lib/salebot_uploader/locale/en.yml +17 -0
  10. data/lib/salebot_uploader/mount.rb +446 -0
  11. data/lib/salebot_uploader/mounter.rb +255 -0
  12. data/lib/salebot_uploader/orm/activerecord.rb +68 -0
  13. data/lib/salebot_uploader/processing/mini_magick.rb +194 -0
  14. data/lib/salebot_uploader/processing/rmagick.rb +402 -0
  15. data/lib/salebot_uploader/processing/vips.rb +284 -0
  16. data/lib/salebot_uploader/processing.rb +3 -0
  17. data/lib/salebot_uploader/sanitized_file.rb +357 -0
  18. data/lib/salebot_uploader/storage/abstract.rb +41 -0
  19. data/lib/salebot_uploader/storage/file.rb +124 -0
  20. data/lib/salebot_uploader/storage/fog.rb +547 -0
  21. data/lib/salebot_uploader/storage.rb +3 -0
  22. data/lib/salebot_uploader/test/matchers.rb +398 -0
  23. data/lib/salebot_uploader/uploader/cache.rb +223 -0
  24. data/lib/salebot_uploader/uploader/callbacks.rb +33 -0
  25. data/lib/salebot_uploader/uploader/configuration.rb +184 -0
  26. data/lib/salebot_uploader/uploader/content_type_allowlist.rb +61 -0
  27. data/lib/salebot_uploader/uploader/content_type_denylist.rb +62 -0
  28. data/lib/salebot_uploader/uploader/default_url.rb +17 -0
  29. data/lib/salebot_uploader/uploader/dimension.rb +66 -0
  30. data/lib/salebot_uploader/uploader/download.rb +24 -0
  31. data/lib/salebot_uploader/uploader/extension_allowlist.rb +63 -0
  32. data/lib/salebot_uploader/uploader/extension_denylist.rb +64 -0
  33. data/lib/salebot_uploader/uploader/file_size.rb +43 -0
  34. data/lib/salebot_uploader/uploader/mountable.rb +44 -0
  35. data/lib/salebot_uploader/uploader/processing.rb +125 -0
  36. data/lib/salebot_uploader/uploader/proxy.rb +99 -0
  37. data/lib/salebot_uploader/uploader/remove.rb +21 -0
  38. data/lib/salebot_uploader/uploader/serialization.rb +28 -0
  39. data/lib/salebot_uploader/uploader/store.rb +142 -0
  40. data/lib/salebot_uploader/uploader/url.rb +44 -0
  41. data/lib/salebot_uploader/uploader/versions.rb +350 -0
  42. data/lib/salebot_uploader/uploader.rb +53 -0
  43. data/lib/salebot_uploader/utilities/file_name.rb +47 -0
  44. data/lib/salebot_uploader/utilities/uri.rb +26 -0
  45. data/lib/salebot_uploader/utilities.rb +7 -0
  46. data/lib/salebot_uploader/validations/active_model.rb +76 -0
  47. data/lib/salebot_uploader/version.rb +3 -0
  48. data/lib/salebot_uploader.rb +62 -0
  49. metadata +392 -0
@@ -0,0 +1,446 @@
1
+ module SalebotUploader
2
+
3
+ ##
4
+ # If a Class is extended with this module, it gains the mount_uploader
5
+ # method, which is used for mapping attributes to uploaders and allowing
6
+ # easy assignment.
7
+ #
8
+ # You can use mount_uploader with pretty much any class, however it is
9
+ # intended to be used with some kind of persistent storage, like an ORM.
10
+ # If you want to persist the uploaded files in a particular Class, it
11
+ # needs to implement a `read_uploader` and a `write_uploader` method.
12
+ #
13
+ module Mount
14
+
15
+ ##
16
+ # === Returns
17
+ #
18
+ # [Hash{Symbol => SalebotUploader}] what uploaders are mounted on which columns
19
+ #
20
+ def uploaders
21
+ @uploaders ||= superclass.respond_to?(:uploaders) ? superclass.uploaders.dup : {}
22
+ end
23
+
24
+ def uploader_options
25
+ @uploader_options ||= superclass.respond_to?(:uploader_options) ? superclass.uploader_options.dup : {}
26
+ end
27
+
28
+ ##
29
+ # Return a particular option for a particular uploader
30
+ #
31
+ # === Parameters
32
+ #
33
+ # [column (Symbol)] The column the uploader is mounted at
34
+ # [option (Symbol)] The option, e.g. validate_integrity
35
+ #
36
+ # === Returns
37
+ #
38
+ # [Object] The option value
39
+ #
40
+ def uploader_option(column, option)
41
+ if uploader_options[column].has_key?(option)
42
+ uploader_options[column][option]
43
+ else
44
+ uploaders[column].send(option)
45
+ end
46
+ end
47
+
48
+ ##
49
+ # Mounts the given uploader on the given column. This means that assigning
50
+ # and reading from the column will upload and retrieve files. Supposing
51
+ # that a User class has an uploader mounted on image, you can assign and
52
+ # retrieve files like this:
53
+ #
54
+ # @user.image # => <Uploader>
55
+ # @user.image.store!(some_file_object)
56
+ #
57
+ # @user.image.url # => '/some_url.png'
58
+ #
59
+ # It is also possible (but not recommended) to omit the uploader, which
60
+ # will create an anonymous uploader class.
61
+ #
62
+ # Passing a block makes it possible to customize the uploader. This can be
63
+ # convenient for brevity, but if there is any significant logic in the
64
+ # uploader, you should do the right thing and have it in its own file.
65
+ #
66
+ # === Added instance methods
67
+ #
68
+ # Supposing a class has used +mount_uploader+ to mount an uploader on a column
69
+ # named +image+, in that case the following methods will be added to the class:
70
+ #
71
+ # [image] Returns an instance of the uploader only if anything has been uploaded
72
+ # [image=] Caches the given file
73
+ #
74
+ # [image_url] Returns the url to the uploaded file
75
+ #
76
+ # [image_cache] Returns a string that identifies the cache location of the file
77
+ # [image_cache=] Retrieves the file from the cache based on the given cache name
78
+ #
79
+ # [remote_image_url] Returns previously cached remote url
80
+ # [remote_image_url=] Retrieve the file from the remote url
81
+ #
82
+ # [remove_image] An attribute reader that can be used with a checkbox to mark a file for removal
83
+ # [remove_image=] An attribute writer that can be used with a checkbox to mark a file for removal
84
+ # [remove_image?] Whether the file should be removed when store_image! is called.
85
+ #
86
+ # [store_image!] Stores a file that has been assigned with +image=+
87
+ # [remove_image!] Removes the uploaded file from the filesystem.
88
+ #
89
+ # [image_integrity_error] Returns an error object if the last file to be assigned caused an integrity error
90
+ # [image_processing_error] Returns an error object if the last file to be assigned caused a processing error
91
+ # [image_download_error] Returns an error object if the last file to be remotely assigned caused a download error
92
+ #
93
+ # [image_identifier] Reads out the identifier of the file
94
+ #
95
+ # === Parameters
96
+ #
97
+ # [column (Symbol)] the attribute to mount this uploader on
98
+ # [uploader (SalebotUploader::Uploader)] the uploader class to mount
99
+ # [options (Hash{Symbol => Object})] a set of options
100
+ # [&block (Proc)] customize anonymous uploaders
101
+ #
102
+ # === Options
103
+ #
104
+ # [:mount_on => Symbol] if the name of the column to be serialized to differs you can override it using this option
105
+ # [:ignore_integrity_errors => Boolean] if set to true, integrity errors will result in caching failing silently
106
+ # [:ignore_processing_errors => Boolean] if set to true, processing errors will result in caching failing silently
107
+ #
108
+ # === Examples
109
+ #
110
+ # Mounting uploaders on different columns.
111
+ #
112
+ # class Song
113
+ # mount_uploader :lyrics, LyricsUploader
114
+ # mount_uploader :alternative_lyrics, LyricsUploader
115
+ # mount_uploader :file, SongUploader
116
+ # end
117
+ #
118
+ # This will add an anonymous uploader with only the default settings:
119
+ #
120
+ # class Data
121
+ # mount_uploader :csv
122
+ # end
123
+ #
124
+ # this will add an anonymous uploader overriding the store_dir:
125
+ #
126
+ # class Product
127
+ # mount_uploader :blueprint do
128
+ # def store_dir
129
+ # 'blueprints'
130
+ # end
131
+ # end
132
+ # end
133
+ #
134
+ def mount_uploader(column, uploader=nil, options={}, &block)
135
+ mount_base(column, uploader, options.merge(multiple: false), &block)
136
+
137
+ mod = Module.new
138
+ include mod
139
+ mod.class_eval <<-RUBY, __FILE__, __LINE__+1
140
+
141
+ def #{column}
142
+ _mounter(:#{column}).uploaders[0] ||= _mounter(:#{column}).blank_uploader
143
+ end
144
+
145
+ def #{column}=(new_file)
146
+ _mounter(:#{column}).cache([new_file])
147
+ end
148
+
149
+ def #{column}_url(*args)
150
+ #{column}.url(*args)
151
+ end
152
+
153
+ def #{column}_cache
154
+ _mounter(:#{column}).cache_names[0]
155
+ end
156
+
157
+ def #{column}_cache=(cache_name)
158
+ _mounter(:#{column}).cache_names = [cache_name]
159
+ end
160
+
161
+ def remote_#{column}_url
162
+ [_mounter(:#{column}).remote_urls].flatten[0]
163
+ end
164
+
165
+ def remote_#{column}_url=(url)
166
+ _mounter(:#{column}).remote_urls = url
167
+ end
168
+
169
+ def remote_#{column}_request_header=(header)
170
+ _mounter(:#{column}).remote_request_headers = [header]
171
+ end
172
+
173
+ def #{column}_identifier
174
+ _mounter(:#{column}).read_identifiers[0]
175
+ end
176
+
177
+ def #{column}_integrity_error
178
+ #{column}_integrity_errors.last
179
+ end
180
+
181
+ def #{column}_processing_error
182
+ #{column}_processing_errors.last
183
+ end
184
+
185
+ def #{column}_download_error
186
+ #{column}_download_errors.last
187
+ end
188
+ RUBY
189
+ end
190
+
191
+ ##
192
+ # Mounts the given uploader on the given array column. This means that
193
+ # assigning and reading from the array column will upload and retrieve
194
+ # multiple files. Supposing that a User class has an uploader mounted on
195
+ # images, and that images can store an array, for example it could be a
196
+ # PostgreSQL JSON column. You can assign and retrieve files like this:
197
+ #
198
+ # @user.images # => []
199
+ # @user.images = [some_file_object]
200
+ # @user.images # => [<Uploader>]
201
+ #
202
+ # @user.images[0].url # => '/some_url.png'
203
+ #
204
+ # It is also possible (but not recommended) to omit the uploader, which
205
+ # will create an anonymous uploader class.
206
+ #
207
+ # Passing a block makes it possible to customize the uploader. This can be
208
+ # convenient for brevity, but if there is any significant logic in the
209
+ # uploader, you should do the right thing and have it in its own file.
210
+ #
211
+ # === Added instance methods
212
+ #
213
+ # Supposing a class has used +mount_uploaders+ to mount an uploader on a column
214
+ # named +images+, in that case the following methods will be added to the class:
215
+ #
216
+ # [images] Returns an array of uploaders for each uploaded file
217
+ # [images=] Caches the given files
218
+ #
219
+ # [images_urls] Returns the urls to the uploaded files
220
+ #
221
+ # [images_cache] Returns a string that identifies the cache location of the files
222
+ # [images_cache=] Retrieves the files from the cache based on the given cache name
223
+ #
224
+ # [remote_image_urls] Returns previously cached remote urls
225
+ # [remote_image_urls=] Retrieve files from the given remote urls
226
+ #
227
+ # [remove_images] An attribute reader that can be used with a checkbox to mark the files for removal
228
+ # [remove_images=] An attribute writer that can be used with a checkbox to mark the files for removal
229
+ # [remove_images?] Whether the files should be removed when store_image! is called.
230
+ #
231
+ # [store_images!] Stores all files that have been assigned with +images=+
232
+ # [remove_images!] Removes the uploaded file from the filesystem.
233
+ #
234
+ # [image_integrity_errors] Returns error objects of files which failed to pass integrity check
235
+ # [image_processing_errors] Returns error objects of files which failed to be processed
236
+ # [image_download_errors] Returns error objects of files which failed to be downloaded
237
+ #
238
+ # [image_identifiers] Reads out the identifiers of the files
239
+ #
240
+ # === Parameters
241
+ #
242
+ # [column (Symbol)] the attribute to mount this uploader on
243
+ # [uploader (SalebotUploader::Uploader)] the uploader class to mount
244
+ # [options (Hash{Symbol => Object})] a set of options
245
+ # [&block (Proc)] customize anonymous uploaders
246
+ #
247
+ # === Options
248
+ #
249
+ # [:mount_on => Symbol] if the name of the column to be serialized to differs you can override it using this option
250
+ # [:ignore_integrity_errors => Boolean] if set to true, integrity errors will result in caching failing silently
251
+ # [:ignore_processing_errors => Boolean] if set to true, processing errors will result in caching failing silently
252
+ #
253
+ # === Examples
254
+ #
255
+ # Mounting uploaders on different columns.
256
+ #
257
+ # class Song
258
+ # mount_uploaders :lyrics, LyricsUploader
259
+ # mount_uploaders :alternative_lyrics, LyricsUploader
260
+ # mount_uploaders :files, SongUploader
261
+ # end
262
+ #
263
+ # This will add an anonymous uploader with only the default settings:
264
+ #
265
+ # class Data
266
+ # mount_uploaders :csv_files
267
+ # end
268
+ #
269
+ # this will add an anonymous uploader overriding the store_dir:
270
+ #
271
+ # class Product
272
+ # mount_uploaders :blueprints do
273
+ # def store_dir
274
+ # 'blueprints'
275
+ # end
276
+ # end
277
+ # end
278
+ #
279
+ def mount_uploaders(column, uploader=nil, options={}, &block)
280
+ mount_base(column, uploader, options.merge(multiple: true), &block)
281
+
282
+ mod = Module.new
283
+ include mod
284
+ mod.class_eval <<-RUBY, __FILE__, __LINE__+1
285
+
286
+ def #{column}
287
+ _mounter(:#{column}).uploaders
288
+ end
289
+
290
+ def #{column}=(new_files)
291
+ _mounter(:#{column}).cache(new_files)
292
+ end
293
+
294
+ def #{column}_urls(*args)
295
+ _mounter(:#{column}).urls(*args)
296
+ end
297
+
298
+ def #{column}_cache
299
+ names = _mounter(:#{column}).cache_names
300
+ names.to_json if names.present?
301
+ end
302
+
303
+ def #{column}_cache=(cache_name)
304
+ _mounter(:#{column}).cache_names = JSON.parse(cache_name) if cache_name.present?
305
+ end
306
+
307
+ def remote_#{column}_urls
308
+ _mounter(:#{column}).remote_urls
309
+ end
310
+
311
+ def remote_#{column}_urls=(urls)
312
+ _mounter(:#{column}).remote_urls = urls
313
+ end
314
+
315
+ def remote_#{column}_request_headers=(headers)
316
+ _mounter(:#{column}).remote_request_headers = headers
317
+ end
318
+
319
+ def #{column}_identifiers
320
+ _mounter(:#{column}).read_identifiers
321
+ end
322
+ RUBY
323
+ end
324
+
325
+ private
326
+
327
+ def mount_base(column, uploader=nil, options={}, &block)
328
+ include SalebotUploader::Mount::Extension
329
+
330
+ uploader = build_uploader(uploader, column, &block)
331
+ uploaders[column.to_sym] = uploader
332
+ uploader_options[column.to_sym] = options
333
+
334
+ # Make sure to write over accessors directly defined on the class.
335
+ # Simply super to the included module below.
336
+ class_eval <<-RUBY, __FILE__, __LINE__+1
337
+ def #{column}; super; end
338
+ def #{column}=(new_file); super; end
339
+ RUBY
340
+
341
+ mod = Module.new
342
+ include mod
343
+ mod.class_eval <<-RUBY, __FILE__, __LINE__+1
344
+
345
+ def #{column}?
346
+ _mounter(:#{column}).present?
347
+ end
348
+
349
+ def remove_#{column}
350
+ _mounter(:#{column}).remove
351
+ end
352
+
353
+ def remove_#{column}!
354
+ _mounter(:#{column}).remove!
355
+ self.remove_#{column} = true
356
+ write_#{column}_identifier
357
+ end
358
+
359
+ def remove_#{column}=(value)
360
+ _mounter(:#{column}).remove = value
361
+ end
362
+
363
+ def remove_#{column}?
364
+ _mounter(:#{column}).remove?
365
+ end
366
+
367
+ def store_#{column}!
368
+ _mounter(:#{column}).store!
369
+ end
370
+
371
+ def #{column}_integrity_errors
372
+ _mounter(:#{column}).integrity_errors
373
+ end
374
+
375
+ def #{column}_processing_errors
376
+ _mounter(:#{column}).processing_errors
377
+ end
378
+
379
+ def #{column}_download_errors
380
+ _mounter(:#{column}).download_errors
381
+ end
382
+
383
+ def write_#{column}_identifier
384
+ _mounter(:#{column}).write_identifier
385
+ end
386
+
387
+ def mark_remove_#{column}_false
388
+ _mounter(:#{column}).remove = false
389
+ end
390
+
391
+ def reset_previous_changes_for_#{column}
392
+ _mounter(:#{column}).reset_changes!
393
+ end
394
+
395
+ def remove_previously_stored_#{column}
396
+ _mounter(:#{column}).remove_previous
397
+ end
398
+
399
+ def remove_rolled_back_#{column}
400
+ _mounter(:#{column}).remove_added
401
+ end
402
+ RUBY
403
+ end
404
+
405
+ def build_uploader(uploader, column, &block)
406
+ uploader ||= SalebotUploader::Uploader::Base
407
+ return uploader unless block_given?
408
+
409
+ uploader = Class.new(uploader)
410
+ const_set("SalebotUploader#{column.to_s.camelize}Uploader", uploader)
411
+
412
+ uploader.class_eval(&block)
413
+
414
+ uploader
415
+ end
416
+
417
+ module Extension
418
+
419
+ ##
420
+ # overwrite this to read from a serialized attribute
421
+ #
422
+ def read_uploader(column); end
423
+
424
+ ##
425
+ # overwrite this to write to a serialized attribute
426
+ #
427
+ def write_uploader(column, identifier); end
428
+
429
+ private
430
+
431
+ def initialize_dup(other)
432
+ @_mounters = @_mounters.dup
433
+ super
434
+ end
435
+
436
+ def _mounter(column)
437
+ # We cannot memoize in frozen objects :(
438
+ return Mounter.build(self, column) if frozen?
439
+ @_mounters ||= {}
440
+ @_mounters[column] ||= Mounter.build(self, column)
441
+ end
442
+
443
+ end # Extension
444
+
445
+ end # Mount
446
+ end # SalebotUploader
@@ -0,0 +1,255 @@
1
+ module SalebotUploader
2
+
3
+ # this is an internal class, used by SalebotUploader::Mount so that
4
+ # we don't pollute the model with a lot of methods.
5
+ class Mounter # :nodoc:
6
+ class Single < Mounter # :nodoc
7
+ def identifier
8
+ uploaders.first&.identifier
9
+ end
10
+
11
+ def temporary_identifier
12
+ temporary_identifiers.first
13
+ end
14
+ end
15
+
16
+ class Multiple < Mounter # :nodoc
17
+ def identifier
18
+ uploaders.map(&:identifier).presence
19
+ end
20
+
21
+ def temporary_identifier
22
+ temporary_identifiers.presence
23
+ end
24
+ end
25
+
26
+ def self.build(record, column)
27
+ if record.class.uploader_options[column][:multiple]
28
+ Multiple.new(record, column)
29
+ else
30
+ Single.new(record, column)
31
+ end
32
+ end
33
+
34
+ attr_reader :column, :record, :remote_urls, :remove,
35
+ :integrity_errors, :processing_errors, :download_errors
36
+ attr_accessor :remote_request_headers, :uploader_options
37
+
38
+ def initialize(record, column)
39
+ @record = record
40
+ @column = column
41
+ @options = record.class.uploader_options[column]
42
+ @download_errors = []
43
+ @processing_errors = []
44
+ @integrity_errors = []
45
+
46
+ @removed_uploaders = []
47
+ @added_uploaders = []
48
+ end
49
+
50
+ def uploader_class
51
+ record.class.uploaders[column]
52
+ end
53
+
54
+ def blank_uploader
55
+ uploader_class.new(record, column)
56
+ end
57
+
58
+ def identifiers
59
+ uploaders.map(&:identifier)
60
+ end
61
+
62
+ def read_identifiers
63
+ [record.read_uploader(serialization_column)].flatten.reject(&:blank?)
64
+ end
65
+
66
+ def uploaders
67
+ @uploaders ||= read_identifiers.map do |identifier|
68
+ uploader = blank_uploader
69
+ uploader.retrieve_from_store!(identifier)
70
+ uploader
71
+ end
72
+ end
73
+
74
+ def cache(new_files)
75
+ return if !new_files.is_a?(Array) && new_files.blank?
76
+ old_uploaders = uploaders
77
+ @uploaders = new_files.map do |new_file|
78
+ handle_error do
79
+ if new_file.is_a?(String)
80
+ if (uploader = old_uploaders.detect { |old_uploader| old_uploader.identifier == new_file })
81
+ uploader.staged = true
82
+ uploader
83
+ else
84
+ begin
85
+ uploader = blank_uploader
86
+ uploader.retrieve_from_cache!(new_file)
87
+ uploader
88
+ rescue SalebotUploader::InvalidParameter
89
+ nil
90
+ end
91
+ end
92
+ else
93
+ uploader = blank_uploader
94
+ uploader.cache!(new_file)
95
+ uploader
96
+ end
97
+ end
98
+ end.reject(&:blank?)
99
+ @removed_uploaders += (old_uploaders - @uploaders)
100
+ write_temporary_identifier
101
+ end
102
+
103
+ def cache_names
104
+ uploaders.map(&:cache_name).compact
105
+ end
106
+
107
+ def cache_names=(cache_names)
108
+ cache_names = cache_names.reject(&:blank?)
109
+ return if cache_names.blank?
110
+ clear_unstaged
111
+ cache_names.each do |cache_name|
112
+ uploader = blank_uploader
113
+ uploader.retrieve_from_cache!(cache_name)
114
+ @uploaders << uploader
115
+ rescue SalebotUploader::InvalidParameter
116
+ # ignore
117
+ end
118
+ write_temporary_identifier
119
+ end
120
+
121
+ def remote_urls=(urls)
122
+ if urls.nil?
123
+ urls = []
124
+ else
125
+ urls = Array.wrap(urls).reject(&:blank?)
126
+ return if urls.blank?
127
+ end
128
+ @remote_urls = urls
129
+
130
+ clear_unstaged
131
+ @remote_urls.zip(remote_request_headers || []) do |url, header|
132
+ handle_error do
133
+ uploader = blank_uploader
134
+ uploader.download!(url, header || {})
135
+ @uploaders << uploader
136
+ end
137
+ end
138
+ write_temporary_identifier
139
+ end
140
+
141
+ def store!
142
+ uploaders.each(&:store!)
143
+ end
144
+
145
+ def write_identifier
146
+ return if record.frozen?
147
+
148
+ clear! if remove?
149
+
150
+ additions, remains = uploaders.partition(&:cached?)
151
+ existing_identifiers = (@removed_uploaders + remains).map(&:identifier)
152
+ additions.each do |uploader|
153
+ uploader.deduplicate(existing_identifiers)
154
+ existing_identifiers << uploader.identifier
155
+ end
156
+ @added_uploaders += additions
157
+
158
+ record.write_uploader(serialization_column, identifier)
159
+ end
160
+
161
+ def urls(*args)
162
+ uploaders.map { |u| u.url(*args) }
163
+ end
164
+
165
+ def blank?
166
+ uploaders.none?(&:present?)
167
+ end
168
+
169
+ def remove=(value)
170
+ @remove = value
171
+ write_temporary_identifier
172
+ end
173
+
174
+ def remove?
175
+ remove.present? && (remove.to_s !~ /\A0|false$\z/)
176
+ end
177
+
178
+ def remove!
179
+ uploaders.each(&:remove!)
180
+ clear!
181
+ end
182
+
183
+ def clear!
184
+ @removed_uploaders += uploaders
185
+ @remove = nil
186
+ @uploaders = []
187
+ end
188
+
189
+ def reset_changes!
190
+ @removed_uploaders = []
191
+ @added_uploaders = []
192
+ end
193
+
194
+ def serialization_column
195
+ option(:mount_on) || column
196
+ end
197
+
198
+ def remove_previous
199
+ current_paths = uploaders.map(&:path)
200
+ @removed_uploaders
201
+ .reject {|uploader| current_paths.include?(uploader.path) }
202
+ .each { |uploader| uploader.remove! if uploader.remove_previously_stored_files_after_update }
203
+ reset_changes!
204
+ end
205
+
206
+ def remove_added
207
+ current_paths = (@removed_uploaders + uploaders.select(&:staged)).map(&:path)
208
+ @added_uploaders
209
+ .reject {|uploader| current_paths.include?(uploader.path) }
210
+ .each { |uploader| uploader.remove! }
211
+ reset_changes!
212
+ end
213
+
214
+ private
215
+
216
+ def option(name)
217
+ self.uploader_options ||= {}
218
+ self.uploader_options[name] ||= record.class.uploader_option(column, name)
219
+ end
220
+
221
+ def clear_unstaged
222
+ @uploaders ||= []
223
+ staged, unstaged = @uploaders.partition(&:staged)
224
+ @uploaders = staged
225
+ @removed_uploaders += unstaged
226
+ end
227
+
228
+ def handle_error
229
+ yield
230
+ rescue SalebotUploader::DownloadError => e
231
+ @download_errors << e
232
+ raise e unless option(:ignore_download_errors)
233
+ rescue SalebotUploader::ProcessingError => e
234
+ @processing_errors << e
235
+ raise e unless option(:ignore_processing_errors)
236
+ rescue SalebotUploader::IntegrityError => e
237
+ @integrity_errors << e
238
+ raise e unless option(:ignore_integrity_errors)
239
+ end
240
+
241
+ def write_temporary_identifier
242
+ return if record.frozen?
243
+
244
+ record.write_uploader(serialization_column, temporary_identifier)
245
+ end
246
+
247
+ def temporary_identifiers
248
+ if remove?
249
+ []
250
+ else
251
+ uploaders.map { |uploader| uploader.temporary_identifier }
252
+ end
253
+ end
254
+ end # Mounter
255
+ end # SalebotUploader