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,398 @@
1
+ module SalebotUploader
2
+ module Test
3
+
4
+ ##
5
+ # These are some matchers that can be used in RSpec specs, to simplify the testing
6
+ # of uploaders.
7
+ #
8
+ module Matchers
9
+
10
+ class BeIdenticalTo # :nodoc:
11
+ def initialize(expected)
12
+ @expected = expected
13
+ end
14
+
15
+ def matches?(actual)
16
+ @actual = actual
17
+ FileUtils.identical?(@actual, @expected)
18
+ end
19
+
20
+ def failure_message
21
+ "expected #{@actual.inspect} to be identical to #{@expected.inspect}"
22
+ end
23
+
24
+ def failure_message_when_negated
25
+ "expected #{@actual.inspect} to not be identical to #{@expected.inspect}"
26
+ end
27
+
28
+ def description
29
+ "be identical to #{@expected.inspect}"
30
+ end
31
+
32
+ # RSpec 2 compatibility:
33
+ alias_method :negative_failure_message, :failure_message_when_negated
34
+ end
35
+
36
+ def be_identical_to(expected)
37
+ BeIdenticalTo.new(expected)
38
+ end
39
+
40
+ class HavePermissions # :nodoc:
41
+ def initialize(expected)
42
+ @expected = expected
43
+ end
44
+
45
+ def matches?(actual)
46
+ @actual = actual
47
+ # Satisfy expectation here. Return false or raise an error if it's not met.
48
+ (File.stat(@actual.path).mode & 0o777) == @expected
49
+ end
50
+
51
+ def failure_message
52
+ "expected #{@actual.current_path.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0o777).to_s(8)}"
53
+ end
54
+
55
+ def failure_message_when_negated
56
+ "expected #{@actual.current_path.inspect} not to have permissions #{@expected.to_s(8)}, but it did"
57
+ end
58
+
59
+ def description
60
+ "have permissions #{@expected.to_s(8)}"
61
+ end
62
+
63
+ # RSpec 2 compatibility:
64
+ alias_method :negative_failure_message, :failure_message_when_negated
65
+ end
66
+
67
+ def have_permissions(expected)
68
+ HavePermissions.new(expected)
69
+ end
70
+
71
+ class HaveDirectoryPermissions # :nodoc:
72
+ def initialize(expected)
73
+ @expected = expected
74
+ end
75
+
76
+ def matches?(actual)
77
+ @actual = actual
78
+ # Satisfy expectation here. Return false or raise an error if it's not met.
79
+ (File.stat(File.dirname(@actual.path)).mode & 0o777) == @expected
80
+ end
81
+
82
+ def failure_message
83
+ "expected #{File.dirname @actual.current_path.inspect} to have permissions #{@expected.to_s(8)}, but they were #{(File.stat(@actual.path).mode & 0o777).to_s(8)}"
84
+ end
85
+
86
+ def failure_message_when_negated
87
+ "expected #{File.dirname @actual.current_path.inspect} not to have permissions #{@expected.to_s(8)}, but it did"
88
+ end
89
+
90
+ def description
91
+ "have permissions #{@expected.to_s(8)}"
92
+ end
93
+
94
+ # RSpec 2 compatibility:
95
+ alias_method :negative_failure_message, :failure_message_when_negated
96
+ end
97
+
98
+ def have_directory_permissions(expected)
99
+ HaveDirectoryPermissions.new(expected)
100
+ end
101
+
102
+ class BeNoLargerThan # :nodoc:
103
+ def initialize(width, height)
104
+ @width, @height = width, height
105
+ end
106
+
107
+ def matches?(actual)
108
+ @actual = actual
109
+ # Satisfy expectation here. Return false or raise an error if it's not met.
110
+ image = ImageLoader.load_image(@actual.current_path)
111
+ @actual_width = image.width
112
+ @actual_height = image.height
113
+ @actual_width <= @width && @actual_height <= @height
114
+ end
115
+
116
+ def failure_message
117
+ "expected #{@actual.current_path.inspect} to be no larger than #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}."
118
+ end
119
+
120
+ def failure_message_when_negated
121
+ "expected #{@actual.current_path.inspect} to be larger than #{@width} by #{@height}, but it wasn't."
122
+ end
123
+
124
+ def description
125
+ "be no larger than #{@width} by #{@height}"
126
+ end
127
+
128
+ # RSpec 2 compatibility:
129
+ alias_method :negative_failure_message, :failure_message_when_negated
130
+ end
131
+
132
+ def be_no_larger_than(width, height)
133
+ BeNoLargerThan.new(width, height)
134
+ end
135
+
136
+ class HaveDimensions # :nodoc:
137
+ def initialize(width, height)
138
+ @width, @height = width, height
139
+ end
140
+
141
+ def matches?(actual)
142
+ @actual = actual
143
+ # Satisfy expectation here. Return false or raise an error if it's not met.
144
+ image = ImageLoader.load_image(@actual.current_path)
145
+ @actual_width = image.width
146
+ @actual_height = image.height
147
+ @actual_width == @width && @actual_height == @height
148
+ end
149
+
150
+ def failure_message
151
+ "expected #{@actual.current_path.inspect} to have an exact size of #{@width} by #{@height}, but it was #{@actual_width} by #{@actual_height}."
152
+ end
153
+
154
+ def failure_message_when_negated
155
+ "expected #{@actual.current_path.inspect} not to have an exact size of #{@width} by #{@height}, but it did."
156
+ end
157
+
158
+ def description
159
+ "have an exact size of #{@width} by #{@height}"
160
+ end
161
+
162
+ # RSpec 2 compatibility:
163
+ alias_method :negative_failure_message, :failure_message_when_negated
164
+ end
165
+
166
+ def have_dimensions(width, height)
167
+ HaveDimensions.new(width, height)
168
+ end
169
+
170
+ class HaveHeight # :nodoc:
171
+ def initialize(height)
172
+ @height = height
173
+ end
174
+
175
+ def matches?(actual)
176
+ @actual = actual
177
+ # Satisfy expectation here. Return false or raise an error if it's not met.
178
+ image = ImageLoader.load_image(@actual.current_path)
179
+ @actual_height = image.height
180
+ @actual_height == @height
181
+ end
182
+
183
+ def failure_message
184
+ "expected #{@actual.current_path.inspect} to have an exact size of #{@height}, but it was #{@actual_height}."
185
+ end
186
+
187
+ def failure_message_when_negated
188
+ "expected #{@actual.current_path.inspect} not to have an exact size of #{@height}, but it did."
189
+ end
190
+
191
+ def description
192
+ "have an exact height of #{@height}"
193
+ end
194
+
195
+ # RSpec 2 compatibility:
196
+ alias_method :negative_failure_message, :failure_message_when_negated
197
+ end
198
+
199
+ def have_height(height)
200
+ HaveHeight.new(height)
201
+ end
202
+
203
+ class HaveWidth # :nodoc:
204
+ def initialize(width)
205
+ @width = width
206
+ end
207
+
208
+ def matches?(actual)
209
+ @actual = actual
210
+ # Satisfy expectation here. Return false or raise an error if it's not met.
211
+ image = ImageLoader.load_image(@actual.current_path)
212
+ @actual_width = image.width
213
+ @actual_width == @width
214
+ end
215
+
216
+ def failure_message
217
+ "expected #{@actual.current_path.inspect} to have an exact size of #{@width}, but it was #{@actual_width}."
218
+ end
219
+
220
+ def failure_message_when_negated
221
+ "expected #{@actual.current_path.inspect} not to have an exact size of #{@width}, but it did."
222
+ end
223
+
224
+ def description
225
+ "have an exact width of #{@width}"
226
+ end
227
+
228
+ # RSpec 2 compatibility:
229
+ alias_method :negative_failure_message, :failure_message_when_negated
230
+ end
231
+
232
+ def have_width(width)
233
+ HaveWidth.new(width)
234
+ end
235
+
236
+ class BeNoWiderThan # :nodoc:
237
+ def initialize(width)
238
+ @width = width
239
+ end
240
+
241
+ def matches?(actual)
242
+ @actual = actual
243
+ # Satisfy expectation here. Return false or raise an error if it's not met.
244
+ image = ImageLoader.load_image(@actual.current_path)
245
+ @actual_width = image.width
246
+ @actual_width <= @width
247
+ end
248
+
249
+ def failure_message
250
+ "expected #{@actual.current_path.inspect} to be no wider than #{@width}, but it was #{@actual_width}."
251
+ end
252
+
253
+ def failure_message_when_negated
254
+ "expected #{@actual.current_path.inspect} not to be wider than #{@width}, but it is."
255
+ end
256
+
257
+ def description
258
+ "have a width less than or equal to #{@width}"
259
+ end
260
+
261
+ # RSpec 2 compatibility:
262
+ alias_method :negative_failure_message, :failure_message_when_negated
263
+ end
264
+
265
+ def be_no_wider_than(width)
266
+ BeNoWiderThan.new(width)
267
+ end
268
+
269
+ class BeNoTallerThan # :nodoc:
270
+ def initialize(height)
271
+ @height = height
272
+ end
273
+
274
+ def matches?(actual)
275
+ @actual = actual
276
+ # Satisfy expectation here. Return false or raise an error if it's not met.
277
+ image = ImageLoader.load_image(@actual.current_path)
278
+ @actual_height = image.height
279
+ @actual_height <= @height
280
+ end
281
+
282
+ def failure_message
283
+ "expected #{@actual.current_path.inspect} to be no taller than #{@height}, but it was #{@actual_height}."
284
+ end
285
+
286
+ def failure_message_when_negated
287
+ "expected #{@actual.current_path.inspect} not to be taller than #{@height}, but it is."
288
+ end
289
+
290
+ def description
291
+ "have a height less than or equal to #{@height}"
292
+ end
293
+
294
+ # RSpec 2 compatibility:
295
+ alias_method :negative_failure_message, :failure_message_when_negated
296
+ end
297
+
298
+ def be_no_taller_than(height)
299
+ BeNoTallerThan.new(height)
300
+ end
301
+
302
+ class BeFormat # :nodoc:
303
+ def initialize(expected)
304
+ @expected = expected
305
+ end
306
+
307
+ def matches?(actual)
308
+ @actual = actual
309
+ # Satisfy expectation here. Return false or raise an error if it's not met.
310
+ image = ImageLoader.load_image(@actual.current_path)
311
+ @actual_expected = image.format
312
+ !@expected.nil? && @actual_expected.casecmp(@expected).zero?
313
+ end
314
+
315
+ def failure_message
316
+ "expected #{@actual.current_path.inspect} to have #{@expected} format, but it was #{@actual_expected}."
317
+ end
318
+
319
+ def failure_message_when_negated
320
+ "expected #{@actual.current_path.inspect} not to have #{@expected} format, but it did."
321
+ end
322
+
323
+ def description
324
+ "have #{@expected} format"
325
+ end
326
+
327
+ # RSpec 2 compatibility:
328
+ alias_method :negative_failure_message, :failure_message_when_negated
329
+ end
330
+
331
+ def be_format(expected)
332
+ BeFormat.new(expected)
333
+ end
334
+
335
+ class ImageLoader # :nodoc:
336
+ def self.load_image(filename)
337
+ if defined? ::MiniMagick
338
+ MiniMagickWrapper.new(filename)
339
+ else
340
+ unless defined? ::Magick
341
+ begin
342
+ require 'rmagick'
343
+ rescue LoadError
344
+ begin
345
+ require 'RMagick'
346
+ rescue LoadError
347
+ puts "WARNING: Failed to require rmagick, image processing may fail!"
348
+ end
349
+ end
350
+ end
351
+ MagickWrapper.new(filename)
352
+ end
353
+ end
354
+ end
355
+
356
+ class MagickWrapper # :nodoc:
357
+ attr_reader :image
358
+
359
+ def width
360
+ image.columns
361
+ end
362
+
363
+ def height
364
+ image.rows
365
+ end
366
+
367
+ def format
368
+ image.format
369
+ end
370
+
371
+ def initialize(filename)
372
+ @image = ::Magick::Image.read(filename).first
373
+ end
374
+ end
375
+
376
+ class MiniMagickWrapper # :nodoc:
377
+ attr_reader :image
378
+
379
+ def width
380
+ image[:width]
381
+ end
382
+
383
+ def height
384
+ image[:height]
385
+ end
386
+
387
+ def format
388
+ image[:format]
389
+ end
390
+
391
+ def initialize(filename)
392
+ @image = ::MiniMagick::Image.open(filename)
393
+ end
394
+ end
395
+
396
+ end # Matchers
397
+ end # Test
398
+ end # SalebotUploader
@@ -0,0 +1,223 @@
1
+ require 'securerandom'
2
+
3
+ module SalebotUploader
4
+
5
+ class FormNotMultipart < UploadError
6
+ def message
7
+ "You tried to assign a String or a Pathname to an uploader, for security reasons, this is not allowed.\n\n If this is a file upload, please check that your upload form is multipart encoded."
8
+ end
9
+ end
10
+
11
+ class CacheCounter
12
+ @@counter = 0
13
+
14
+ def self.increment
15
+ @@counter += 1
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Generates a unique cache id for use in the caching system
21
+ #
22
+ # === Returns
23
+ #
24
+ # [String] a cache id in the format TIMEINT-PID-COUNTER-RND
25
+ #
26
+ def self.generate_cache_id
27
+ [
28
+ Time.now.utc.to_i,
29
+ SecureRandom.random_number(1_000_000_000_000_000),
30
+ '%04d' % (SalebotUploader::CacheCounter.increment % 10_000),
31
+ '%04d' % SecureRandom.random_number(10_000)
32
+ ].map(&:to_s).join('-')
33
+ end
34
+
35
+ module Uploader
36
+ module Cache
37
+ extend ActiveSupport::Concern
38
+
39
+ include SalebotUploader::Uploader::Callbacks
40
+ include SalebotUploader::Uploader::Configuration
41
+
42
+ included do
43
+ prepend Module.new {
44
+ def initialize(*)
45
+ super
46
+ @staged = false
47
+ end
48
+ }
49
+ attr_accessor :staged
50
+ end
51
+
52
+ module ClassMethods
53
+
54
+ ##
55
+ # Removes cached files which are older than one day. You could call this method
56
+ # from a rake task to clean out old cached files.
57
+ #
58
+ # You can call this method directly on the module like this:
59
+ #
60
+ # SalebotUploader.clean_cached_files!
61
+ #
62
+ # === Note
63
+ #
64
+ # This only works as long as you haven't done anything funky with your cache_dir.
65
+ # It's recommended that you keep cache files in one place only.
66
+ #
67
+ def clean_cached_files!(seconds=60*60*24)
68
+ (cache_storage || storage).new(new).clean_cache!(seconds)
69
+ end
70
+ end
71
+
72
+ ##
73
+ # Returns true if the uploader has been cached
74
+ #
75
+ # === Returns
76
+ #
77
+ # [Bool] whether the current file is cached
78
+ #
79
+ def cached?
80
+ !!@cache_id
81
+ end
82
+
83
+ ##
84
+ # Caches the remotely stored file
85
+ #
86
+ # This is useful when about to process images. Most processing solutions
87
+ # require the file to be stored on the local filesystem.
88
+ #
89
+ def cache_stored_file!
90
+ cache!
91
+ end
92
+
93
+ def sanitized_file
94
+ ActiveSupport::Deprecation.warn('#sanitized_file is deprecated, use #file instead.')
95
+ file
96
+ end
97
+
98
+ ##
99
+ # Returns a String which uniquely identifies the currently cached file for later retrieval
100
+ #
101
+ # === Returns
102
+ #
103
+ # [String] a cache name, in the format TIMEINT-PID-COUNTER-RND/filename.txt
104
+ #
105
+ def cache_name
106
+ File.join(cache_id, original_filename) if cache_id && original_filename
107
+ end
108
+
109
+ ##
110
+ # Caches the given file. Calls process! to trigger any process callbacks.
111
+ #
112
+ # By default, cache!() uses copy_to(), which operates by copying the file
113
+ # to the cache, then deleting the original file. If move_to_cache() is
114
+ # overridden to return true, then cache!() uses move_to(), which simply
115
+ # moves the file to the cache. Useful for large files.
116
+ #
117
+ # === Parameters
118
+ #
119
+ # [new_file (File, IOString, Tempfile)] any kind of file object
120
+ #
121
+ # === Raises
122
+ #
123
+ # [SalebotUploader::FormNotMultipart] if the assigned parameter is a string
124
+ #
125
+ def cache!(new_file = file)
126
+ new_file = SalebotUploader::SanitizedFile.new(new_file)
127
+ return if new_file.empty?
128
+
129
+ raise SalebotUploader::FormNotMultipart if new_file.is_path? && ensure_multipart_form
130
+
131
+ self.cache_id = SalebotUploader.generate_cache_id unless cache_id
132
+
133
+ @identifier = nil
134
+ @staged = true
135
+ @filename = new_file.filename
136
+ self.original_filename = new_file.filename
137
+
138
+ begin
139
+ # first, create a workfile on which we perform processings
140
+ if move_to_cache
141
+ @file = new_file.move_to(File.expand_path(workfile_path, root), permissions, directory_permissions)
142
+ else
143
+ @file = new_file.copy_to(File.expand_path(workfile_path, root), permissions, directory_permissions)
144
+ end
145
+
146
+ with_callbacks(:cache, @file) do
147
+ @file = cache_storage.cache!(@file)
148
+ end
149
+ ensure
150
+ FileUtils.rm_rf(workfile_path(''))
151
+ end
152
+ end
153
+
154
+ ##
155
+ # Retrieves the file with the given cache_name from the cache.
156
+ #
157
+ # === Parameters
158
+ #
159
+ # [cache_name (String)] uniquely identifies a cache file
160
+ #
161
+ # === Raises
162
+ #
163
+ # [SalebotUploader::InvalidParameter] if the cache_name is incorrectly formatted.
164
+ #
165
+ def retrieve_from_cache!(cache_name)
166
+ with_callbacks(:retrieve_from_cache, cache_name) do
167
+ self.cache_id, self.original_filename = cache_name.to_s.split('/', 2)
168
+ @staged = true
169
+ @filename = original_filename
170
+ @file = cache_storage.retrieve_from_cache!(full_original_filename)
171
+ end
172
+ end
173
+
174
+ ##
175
+ # Calculates the path where the cache file should be stored.
176
+ #
177
+ # === Parameters
178
+ #
179
+ # [for_file (String)] name of the file <optional>
180
+ #
181
+ # === Returns
182
+ #
183
+ # [String] the cache path
184
+ #
185
+ def cache_path(for_file=full_original_filename)
186
+ File.join(*[cache_dir, @cache_id, for_file].compact)
187
+ end
188
+
189
+ protected
190
+
191
+ attr_reader :cache_id
192
+
193
+ private
194
+
195
+ def workfile_path(for_file=original_filename)
196
+ File.join(SalebotUploader.tmp_path, @cache_id, version_name.to_s, for_file)
197
+ end
198
+
199
+ attr_reader :original_filename
200
+
201
+ def cache_id=(cache_id)
202
+ # Earlier version used 3 part cache_id. Thus we should allow for
203
+ # the cache_id to have both 3 part and 4 part formats.
204
+ raise SalebotUploader::InvalidParameter, "invalid cache id" unless cache_id =~ /\A(-)?[\d]+\-[\d]+(\-[\d]{4})?\-[\d]{4}\z/
205
+ @cache_id = cache_id
206
+ end
207
+
208
+ def original_filename=(filename)
209
+ raise SalebotUploader::InvalidParameter, "invalid filename" if filename =~ SalebotUploader::SanitizedFile.sanitize_regexp
210
+ @original_filename = filename
211
+ end
212
+
213
+ def cache_storage
214
+ @cache_storage ||= (self.class.cache_storage || self.class.storage).new(self)
215
+ end
216
+
217
+ # We can override the full_original_filename method in other modules
218
+ def full_original_filename
219
+ forcing_extension(original_filename)
220
+ end
221
+ end # Cache
222
+ end # Uploader
223
+ end # SalebotUploader
@@ -0,0 +1,33 @@
1
+ module SalebotUploader
2
+ module Uploader
3
+ module Callbacks
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :_before_callbacks, :_after_callbacks,
8
+ :instance_writer => false
9
+ self._before_callbacks = Hash.new []
10
+ self._after_callbacks = Hash.new []
11
+ end
12
+
13
+ def with_callbacks(kind, *args)
14
+ self.class._before_callbacks[kind].each { |c| send c, *args }
15
+ yield
16
+ self.class._after_callbacks[kind].each { |c| send c, *args }
17
+ end
18
+
19
+ module ClassMethods
20
+ def before(kind, callback)
21
+ self._before_callbacks = self._before_callbacks.
22
+ merge kind => _before_callbacks[kind] + [callback]
23
+ end
24
+
25
+ def after(kind, callback)
26
+ self._after_callbacks = self._after_callbacks.
27
+ merge kind => _after_callbacks[kind] + [callback]
28
+ end
29
+ end # ClassMethods
30
+
31
+ end # Callbacks
32
+ end # Uploader
33
+ end # SalebotUploader