salebot_uploader 1.0.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 (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