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.
- checksums.yaml +7 -0
- data/README.md +0 -0
- data/lib/generators/templates/uploader.rb.erb +9 -0
- data/lib/generators/uploader_generator.rb +7 -0
- data/lib/salebot_uploader/compatibility/paperclip.rb +104 -0
- data/lib/salebot_uploader/downloader/base.rb +101 -0
- data/lib/salebot_uploader/downloader/remote_file.rb +68 -0
- data/lib/salebot_uploader/error.rb +8 -0
- data/lib/salebot_uploader/locale/en.yml +17 -0
- data/lib/salebot_uploader/mount.rb +446 -0
- data/lib/salebot_uploader/mounter.rb +255 -0
- data/lib/salebot_uploader/orm/activerecord.rb +68 -0
- data/lib/salebot_uploader/processing/mini_magick.rb +194 -0
- data/lib/salebot_uploader/processing/rmagick.rb +402 -0
- data/lib/salebot_uploader/processing/vips.rb +284 -0
- data/lib/salebot_uploader/processing.rb +3 -0
- data/lib/salebot_uploader/sanitized_file.rb +357 -0
- data/lib/salebot_uploader/storage/abstract.rb +41 -0
- data/lib/salebot_uploader/storage/file.rb +124 -0
- data/lib/salebot_uploader/storage/fog.rb +547 -0
- data/lib/salebot_uploader/storage.rb +3 -0
- data/lib/salebot_uploader/test/matchers.rb +398 -0
- data/lib/salebot_uploader/uploader/cache.rb +223 -0
- data/lib/salebot_uploader/uploader/callbacks.rb +33 -0
- data/lib/salebot_uploader/uploader/configuration.rb +184 -0
- data/lib/salebot_uploader/uploader/content_type_allowlist.rb +61 -0
- data/lib/salebot_uploader/uploader/content_type_denylist.rb +62 -0
- data/lib/salebot_uploader/uploader/default_url.rb +17 -0
- data/lib/salebot_uploader/uploader/dimension.rb +66 -0
- data/lib/salebot_uploader/uploader/download.rb +24 -0
- data/lib/salebot_uploader/uploader/extension_allowlist.rb +63 -0
- data/lib/salebot_uploader/uploader/extension_denylist.rb +64 -0
- data/lib/salebot_uploader/uploader/file_size.rb +43 -0
- data/lib/salebot_uploader/uploader/mountable.rb +44 -0
- data/lib/salebot_uploader/uploader/processing.rb +125 -0
- data/lib/salebot_uploader/uploader/proxy.rb +99 -0
- data/lib/salebot_uploader/uploader/remove.rb +21 -0
- data/lib/salebot_uploader/uploader/serialization.rb +28 -0
- data/lib/salebot_uploader/uploader/store.rb +142 -0
- data/lib/salebot_uploader/uploader/url.rb +44 -0
- data/lib/salebot_uploader/uploader/versions.rb +350 -0
- data/lib/salebot_uploader/uploader.rb +53 -0
- data/lib/salebot_uploader/utilities/file_name.rb +47 -0
- data/lib/salebot_uploader/utilities/uri.rb +26 -0
- data/lib/salebot_uploader/utilities.rb +7 -0
- data/lib/salebot_uploader/validations/active_model.rb +76 -0
- data/lib/salebot_uploader/version.rb +3 -0
- data/lib/salebot_uploader.rb +62 -0
- metadata +392 -0
@@ -0,0 +1,284 @@
|
|
1
|
+
module SalebotUploader
|
2
|
+
|
3
|
+
##
|
4
|
+
# This module simplifies manipulation with vips by providing a set
|
5
|
+
# of convenient helper methods. If you want to use them, you'll need to
|
6
|
+
# require this file:
|
7
|
+
#
|
8
|
+
# require 'SalebotUploader/processing/vips'
|
9
|
+
#
|
10
|
+
# And then include it in your uploader:
|
11
|
+
#
|
12
|
+
# class MyUploader < SalebotUploader::Uploader::Base
|
13
|
+
# include SalebotUploader::Vips
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# You can now use the provided helpers:
|
17
|
+
#
|
18
|
+
# class MyUploader < SalebotUploader::Uploader::Base
|
19
|
+
# include SalebotUploader::Vips
|
20
|
+
#
|
21
|
+
# process :resize_to_fit => [200, 200]
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# Or create your own helpers with the powerful vips! method, which
|
25
|
+
# yields an ImageProcessing::Builder object. Check out the ImageProcessing
|
26
|
+
# docs at http://github.com/janko-m/image_processing and the list of all
|
27
|
+
# available Vips options at
|
28
|
+
# https://libvips.github.io/libvips/API/current/using-cli.html for more info.
|
29
|
+
#
|
30
|
+
# class MyUploader < SalebotUploader::Uploader::Base
|
31
|
+
# include SalebotUploader::Vips
|
32
|
+
#
|
33
|
+
# process :radial_blur => 10
|
34
|
+
#
|
35
|
+
# def radial_blur(amount)
|
36
|
+
# vips! do |builder|
|
37
|
+
# builder.radial_blur(amount)
|
38
|
+
# builder = yield(builder) if block_given?
|
39
|
+
# builder
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# === Note
|
45
|
+
#
|
46
|
+
# The ImageProcessing gem uses ruby-vips, a binding for the vips image
|
47
|
+
# library. You can find more information here:
|
48
|
+
#
|
49
|
+
# https://github.com/libvips/ruby-vips
|
50
|
+
#
|
51
|
+
#
|
52
|
+
module Vips
|
53
|
+
extend ActiveSupport::Concern
|
54
|
+
|
55
|
+
included do
|
56
|
+
require "image_processing/vips"
|
57
|
+
# We need to disable caching since we're editing images in place.
|
58
|
+
::Vips.cache_set_max(0)
|
59
|
+
end
|
60
|
+
|
61
|
+
module ClassMethods
|
62
|
+
def convert(format)
|
63
|
+
process :convert => format
|
64
|
+
end
|
65
|
+
|
66
|
+
def resize_to_limit(width, height)
|
67
|
+
process :resize_to_limit => [width, height]
|
68
|
+
end
|
69
|
+
|
70
|
+
def resize_to_fit(width, height)
|
71
|
+
process :resize_to_fit => [width, height]
|
72
|
+
end
|
73
|
+
|
74
|
+
def resize_to_fill(width, height, gravity='centre')
|
75
|
+
process :resize_to_fill => [width, height, gravity]
|
76
|
+
end
|
77
|
+
|
78
|
+
def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil)
|
79
|
+
process :resize_and_pad => [width, height, background, gravity, alpha]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
##
|
84
|
+
# Changes the image encoding format to the given format
|
85
|
+
#
|
86
|
+
# See https://libvips.github.io/libvips/API/current/using-cli.html#using-command-line-conversion
|
87
|
+
#
|
88
|
+
# === Parameters
|
89
|
+
#
|
90
|
+
# [format (#to_s)] an abbreviation of the format
|
91
|
+
#
|
92
|
+
# === Yields
|
93
|
+
#
|
94
|
+
# [Vips::Image] additional manipulations to perform
|
95
|
+
#
|
96
|
+
# === Examples
|
97
|
+
#
|
98
|
+
# image.convert(:png)
|
99
|
+
#
|
100
|
+
def convert(format, page=nil)
|
101
|
+
vips! do |builder|
|
102
|
+
builder = builder.convert(format)
|
103
|
+
builder = builder.loader(page: page) if page
|
104
|
+
builder
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Resize the image to fit within the specified dimensions while retaining
|
110
|
+
# the original aspect ratio. Will only resize the image if it is larger than the
|
111
|
+
# specified dimensions. The resulting image may be shorter or narrower than specified
|
112
|
+
# in the smaller dimension but will not be larger than the specified values.
|
113
|
+
#
|
114
|
+
# === Parameters
|
115
|
+
#
|
116
|
+
# [width (Integer)] the width to scale the image to
|
117
|
+
# [height (Integer)] the height to scale the image to
|
118
|
+
# [combine_options (Hash)] additional Vips options to apply before resizing
|
119
|
+
#
|
120
|
+
# === Yields
|
121
|
+
#
|
122
|
+
# [Vips::Image] additional manipulations to perform
|
123
|
+
#
|
124
|
+
def resize_to_limit(width, height, combine_options: {})
|
125
|
+
width, height = resolve_dimensions(width, height)
|
126
|
+
|
127
|
+
vips! do |builder|
|
128
|
+
builder.resize_to_limit(width, height)
|
129
|
+
.apply(combine_options)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
##
|
134
|
+
# Resize the image to fit within the specified dimensions while retaining
|
135
|
+
# the original aspect ratio. The image may be shorter or narrower than
|
136
|
+
# specified in the smaller dimension but will not be larger than the specified values.
|
137
|
+
#
|
138
|
+
# === Parameters
|
139
|
+
#
|
140
|
+
# [width (Integer)] the width to scale the image to
|
141
|
+
# [height (Integer)] the height to scale the image to
|
142
|
+
# [combine_options (Hash)] additional Vips options to apply before resizing
|
143
|
+
#
|
144
|
+
# === Yields
|
145
|
+
#
|
146
|
+
# [Vips::Image] additional manipulations to perform
|
147
|
+
#
|
148
|
+
def resize_to_fit(width, height, combine_options: {})
|
149
|
+
width, height = resolve_dimensions(width, height)
|
150
|
+
|
151
|
+
vips! do |builder|
|
152
|
+
builder.resize_to_fit(width, height)
|
153
|
+
.apply(combine_options)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
##
|
158
|
+
# Resize the image to fit within the specified dimensions while retaining
|
159
|
+
# the aspect ratio of the original image. If necessary, crop the image in the
|
160
|
+
# larger dimension.
|
161
|
+
#
|
162
|
+
# === Parameters
|
163
|
+
#
|
164
|
+
# [width (Integer)] the width to scale the image to
|
165
|
+
# [height (Integer)] the height to scale the image to
|
166
|
+
# [combine_options (Hash)] additional vips options to apply before resizing
|
167
|
+
#
|
168
|
+
# === Yields
|
169
|
+
#
|
170
|
+
# [Vips::Image] additional manipulations to perform
|
171
|
+
#
|
172
|
+
def resize_to_fill(width, height, _gravity = nil, combine_options: {})
|
173
|
+
width, height = resolve_dimensions(width, height)
|
174
|
+
|
175
|
+
vips! do |builder|
|
176
|
+
builder.resize_to_fill(width, height).apply(combine_options)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
# Resize the image to fit within the specified dimensions while retaining
|
182
|
+
# the original aspect ratio. If necessary, will pad the remaining area
|
183
|
+
# with the given color, which defaults to transparent (for gif and png,
|
184
|
+
# white for jpeg).
|
185
|
+
#
|
186
|
+
# See https://libvips.github.io/libvips/API/current/libvips-conversion.html#VipsCompassDirection
|
187
|
+
# for gravity options.
|
188
|
+
#
|
189
|
+
# === Parameters
|
190
|
+
#
|
191
|
+
# [width (Integer)] the width to scale the image to
|
192
|
+
# [height (Integer)] the height to scale the image to
|
193
|
+
# [background (List, nil)] the color of the background as a RGB, like [0, 255, 255], nil indicates transparent
|
194
|
+
# [gravity (String)] how to position the image
|
195
|
+
# [alpha (Boolean, nil)] pad the image with the alpha channel if supported
|
196
|
+
# [combine_options (Hash)] additional vips options to apply before resizing
|
197
|
+
#
|
198
|
+
# === Yields
|
199
|
+
#
|
200
|
+
# [Vips::Image] additional manipulations to perform
|
201
|
+
#
|
202
|
+
def resize_and_pad(width, height, background=nil, gravity='centre', alpha=nil, combine_options: {})
|
203
|
+
width, height = resolve_dimensions(width, height)
|
204
|
+
|
205
|
+
vips! do |builder|
|
206
|
+
builder.resize_and_pad(width, height, background: background, gravity: gravity, alpha: alpha)
|
207
|
+
.apply(combine_options)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Returns the width of the image in pixels.
|
213
|
+
#
|
214
|
+
# === Returns
|
215
|
+
#
|
216
|
+
# [Integer] the image's width in pixels
|
217
|
+
#
|
218
|
+
def width
|
219
|
+
vips_image.width
|
220
|
+
end
|
221
|
+
|
222
|
+
##
|
223
|
+
# Returns the height of the image in pixels.
|
224
|
+
#
|
225
|
+
# === Returns
|
226
|
+
#
|
227
|
+
# [Integer] the image's height in pixels
|
228
|
+
#
|
229
|
+
def height
|
230
|
+
vips_image.height
|
231
|
+
end
|
232
|
+
|
233
|
+
# Process the image with vip, using the ImageProcessing gem. This
|
234
|
+
# method will build a "convert" vips command and execute it on the
|
235
|
+
# current image.
|
236
|
+
#
|
237
|
+
# === Gotcha
|
238
|
+
#
|
239
|
+
# This method assumes that the object responds to +current_path+.
|
240
|
+
# Any class that this module is mixed into must have a +current_path+ method.
|
241
|
+
# SalebotUploader::Uploader does, so you won't need to worry about this in
|
242
|
+
# most cases.
|
243
|
+
#
|
244
|
+
# === Yields
|
245
|
+
#
|
246
|
+
# [ImageProcessing::Builder] use it to define processing to be performed
|
247
|
+
#
|
248
|
+
# === Raises
|
249
|
+
#
|
250
|
+
# [SalebotUploader::ProcessingError] if processing failed.
|
251
|
+
def vips!
|
252
|
+
builder = ImageProcessing::Vips.source(current_path)
|
253
|
+
builder = yield(builder)
|
254
|
+
|
255
|
+
result = builder.call
|
256
|
+
result.close
|
257
|
+
|
258
|
+
FileUtils.mv result.path, current_path
|
259
|
+
|
260
|
+
if File.extname(result.path) != File.extname(current_path)
|
261
|
+
move_to = current_path.chomp(File.extname(current_path)) + File.extname(result.path)
|
262
|
+
file.content_type = Marcel::Magic.by_path(move_to).try(:type)
|
263
|
+
file.move_to(move_to, permissions, directory_permissions)
|
264
|
+
end
|
265
|
+
rescue ::Vips::Error
|
266
|
+
message = I18n.translate(:"errors.messages.processing_error")
|
267
|
+
raise SalebotUploader::ProcessingError, message
|
268
|
+
end
|
269
|
+
|
270
|
+
private
|
271
|
+
|
272
|
+
def resolve_dimensions(*dimensions)
|
273
|
+
dimensions.map do |value|
|
274
|
+
next value unless value.instance_of?(Proc)
|
275
|
+
value.arity >= 1 ? value.call(self) : value.call
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def vips_image
|
280
|
+
::Vips::Image.new_from_buffer(read, "")
|
281
|
+
end
|
282
|
+
|
283
|
+
end # Vips
|
284
|
+
end # SalebotUploader
|
@@ -0,0 +1,357 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'active_support/core_ext/string/multibyte'
|
3
|
+
require 'marcel'
|
4
|
+
|
5
|
+
module SalebotUploader
|
6
|
+
|
7
|
+
##
|
8
|
+
# SanitizedFile is a base class which provides a common API around all
|
9
|
+
# the different quirky Ruby File libraries. It has support for Tempfile,
|
10
|
+
# File, StringIO, Merb-style upload Hashes, as well as paths given as
|
11
|
+
# Strings and Pathnames.
|
12
|
+
#
|
13
|
+
# It's probably needlessly comprehensive and complex. Help is appreciated.
|
14
|
+
#
|
15
|
+
class SanitizedFile
|
16
|
+
include SalebotUploader::Utilities::FileName
|
17
|
+
|
18
|
+
attr_reader :file
|
19
|
+
|
20
|
+
class << self
|
21
|
+
attr_writer :sanitize_regexp
|
22
|
+
|
23
|
+
def sanitize_regexp
|
24
|
+
@sanitize_regexp ||= /[^[:word:]\.\-\+]/
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(file)
|
29
|
+
self.file = file
|
30
|
+
@content = @content_type = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
##
|
34
|
+
# Returns the filename as is, without sanitizing it.
|
35
|
+
#
|
36
|
+
# === Returns
|
37
|
+
#
|
38
|
+
# [String] the unsanitized filename
|
39
|
+
#
|
40
|
+
def original_filename
|
41
|
+
return @original_filename if @original_filename
|
42
|
+
|
43
|
+
if @file && @file.respond_to?(:original_filename)
|
44
|
+
@file.original_filename
|
45
|
+
elsif path
|
46
|
+
File.basename(path)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
##
|
51
|
+
# Returns the filename, sanitized to strip out any evil characters.
|
52
|
+
#
|
53
|
+
# === Returns
|
54
|
+
#
|
55
|
+
# [String] the sanitized filename
|
56
|
+
#
|
57
|
+
def filename
|
58
|
+
sanitize(original_filename) if original_filename
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :identifier, :filename
|
62
|
+
|
63
|
+
##
|
64
|
+
# Returns the file's size.
|
65
|
+
#
|
66
|
+
# === Returns
|
67
|
+
#
|
68
|
+
# [Integer] the file's size in bytes.
|
69
|
+
#
|
70
|
+
def size
|
71
|
+
if is_path?
|
72
|
+
exists? ? File.size(path) : 0
|
73
|
+
elsif @file.respond_to?(:size)
|
74
|
+
@file.size
|
75
|
+
elsif path
|
76
|
+
exists? ? File.size(path) : 0
|
77
|
+
else
|
78
|
+
0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
##
|
83
|
+
# Returns the full path to the file. If the file has no path, it will return nil.
|
84
|
+
#
|
85
|
+
# === Returns
|
86
|
+
#
|
87
|
+
# [String, nil] the path where the file is located.
|
88
|
+
#
|
89
|
+
def path
|
90
|
+
return if @file.blank?
|
91
|
+
|
92
|
+
if is_path?
|
93
|
+
File.expand_path(@file)
|
94
|
+
elsif @file.respond_to?(:path) && !@file.path.blank?
|
95
|
+
File.expand_path(@file.path)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
##
|
100
|
+
# === Returns
|
101
|
+
#
|
102
|
+
# [Boolean] whether the file is supplied as a pathname or string.
|
103
|
+
#
|
104
|
+
def is_path?
|
105
|
+
!!((@file.is_a?(String) || @file.is_a?(Pathname)) && !@file.blank?)
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# === Returns
|
110
|
+
#
|
111
|
+
# [Boolean] whether the file is valid and has a non-zero size
|
112
|
+
#
|
113
|
+
def empty?
|
114
|
+
@file.nil? || self.size.nil? || (self.size.zero? && !self.exists?)
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# === Returns
|
119
|
+
#
|
120
|
+
# [Boolean] Whether the file exists
|
121
|
+
#
|
122
|
+
def exists?
|
123
|
+
self.path.present? && File.exist?(self.path)
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Returns the contents of the file.
|
128
|
+
#
|
129
|
+
# === Returns
|
130
|
+
#
|
131
|
+
# [String] contents of the file
|
132
|
+
#
|
133
|
+
def read(*args)
|
134
|
+
if @content
|
135
|
+
if args.empty?
|
136
|
+
@content
|
137
|
+
else
|
138
|
+
length, outbuf = args
|
139
|
+
raise ArgumentError, 'outbuf argument not supported since the content is already loaded' if outbuf
|
140
|
+
|
141
|
+
@content[0, length]
|
142
|
+
end
|
143
|
+
elsif is_path?
|
144
|
+
File.open(@file, 'rb') { |file| file.read(*args) }
|
145
|
+
else
|
146
|
+
@file.try(:rewind)
|
147
|
+
@content = @file.read(*args)
|
148
|
+
@file.try(:close) unless @file.class.ancestors.include?(::StringIO) || @file.try(:closed?)
|
149
|
+
@content
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Moves the file to the given path
|
155
|
+
#
|
156
|
+
# === Parameters
|
157
|
+
#
|
158
|
+
# [new_path (String)] The path where the file should be moved.
|
159
|
+
# [permissions (Integer)] permissions to set on the file in its new location.
|
160
|
+
# [directory_permissions (Integer)] permissions to set on created directories.
|
161
|
+
#
|
162
|
+
def move_to(new_path, permissions=nil, directory_permissions=nil, keep_filename=false)
|
163
|
+
return if self.empty?
|
164
|
+
|
165
|
+
new_path = File.expand_path(new_path)
|
166
|
+
|
167
|
+
mkdir!(new_path, directory_permissions)
|
168
|
+
move!(new_path)
|
169
|
+
chmod!(new_path, permissions)
|
170
|
+
self.file = {tempfile: new_path, filename: keep_filename ? original_filename : nil, content_type: declared_content_type}
|
171
|
+
self
|
172
|
+
end
|
173
|
+
|
174
|
+
##
|
175
|
+
# Helper to move file to new path.
|
176
|
+
#
|
177
|
+
def move!(new_path)
|
178
|
+
if exists?
|
179
|
+
FileUtils.mv(path, new_path) unless File.identical?(new_path, path)
|
180
|
+
else
|
181
|
+
File.open(new_path, 'wb') { |f| f.write(read) }
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
##
|
186
|
+
# Creates a copy of this file and moves it to the given path. Returns the copy.
|
187
|
+
#
|
188
|
+
# === Parameters
|
189
|
+
#
|
190
|
+
# [new_path (String)] The path where the file should be copied to.
|
191
|
+
# [permissions (Integer)] permissions to set on the copy
|
192
|
+
# [directory_permissions (Integer)] permissions to set on created directories.
|
193
|
+
#
|
194
|
+
# === Returns
|
195
|
+
#
|
196
|
+
# @return [SalebotUploader::SanitizedFile] the location where the file will be stored.
|
197
|
+
#
|
198
|
+
def copy_to(new_path, permissions=nil, directory_permissions=nil)
|
199
|
+
return if self.empty?
|
200
|
+
|
201
|
+
new_path = File.expand_path(new_path)
|
202
|
+
|
203
|
+
mkdir!(new_path, directory_permissions)
|
204
|
+
copy!(new_path)
|
205
|
+
chmod!(new_path, permissions)
|
206
|
+
self.class.new({tempfile: new_path, content_type: declared_content_type})
|
207
|
+
end
|
208
|
+
|
209
|
+
##
|
210
|
+
# Helper to create copy of file in new path.
|
211
|
+
#
|
212
|
+
def copy!(new_path)
|
213
|
+
if exists?
|
214
|
+
FileUtils.cp(path, new_path) unless new_path == path
|
215
|
+
else
|
216
|
+
File.open(new_path, 'wb') { |f| f.write(read) }
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Removes the file from the filesystem.
|
222
|
+
#
|
223
|
+
def delete
|
224
|
+
FileUtils.rm(self.path) if exists?
|
225
|
+
end
|
226
|
+
|
227
|
+
##
|
228
|
+
# Returns a File object, or nil if it does not exist.
|
229
|
+
#
|
230
|
+
# === Returns
|
231
|
+
#
|
232
|
+
# [File] a File object representing the SanitizedFile
|
233
|
+
#
|
234
|
+
def to_file
|
235
|
+
return @file if @file.is_a?(File)
|
236
|
+
|
237
|
+
File.open(path, 'rb') if exists?
|
238
|
+
end
|
239
|
+
|
240
|
+
##
|
241
|
+
# Returns the content type of the file.
|
242
|
+
#
|
243
|
+
# === Returns
|
244
|
+
#
|
245
|
+
# [String] the content type of the file
|
246
|
+
#
|
247
|
+
def content_type
|
248
|
+
@content_type ||=
|
249
|
+
identified_content_type ||
|
250
|
+
declared_content_type ||
|
251
|
+
guessed_safe_content_type ||
|
252
|
+
Marcel::MimeType::BINARY
|
253
|
+
end
|
254
|
+
|
255
|
+
##
|
256
|
+
# Sets the content type of the file.
|
257
|
+
#
|
258
|
+
# === Returns
|
259
|
+
#
|
260
|
+
# [String] the content type of the file
|
261
|
+
#
|
262
|
+
def content_type=(type)
|
263
|
+
@content_type = type
|
264
|
+
end
|
265
|
+
|
266
|
+
##
|
267
|
+
# Used to sanitize the file name. Public to allow overriding for non-latin characters.
|
268
|
+
#
|
269
|
+
# === Returns
|
270
|
+
#
|
271
|
+
# [Regexp] the regexp for sanitizing the file name
|
272
|
+
#
|
273
|
+
def sanitize_regexp
|
274
|
+
SalebotUploader::SanitizedFile.sanitize_regexp
|
275
|
+
end
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def file=(file)
|
280
|
+
if file.is_a?(Hash)
|
281
|
+
@file = file['tempfile'] || file[:tempfile]
|
282
|
+
@original_filename = file['filename'] || file[:filename]
|
283
|
+
@declared_content_type = file['content_type'] || file[:content_type] || file['type'] || file[:type]
|
284
|
+
else
|
285
|
+
@file = file
|
286
|
+
@original_filename = nil
|
287
|
+
@declared_content_type = nil
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# create the directory if it doesn't exist
|
292
|
+
def mkdir!(path, directory_permissions)
|
293
|
+
options = {}
|
294
|
+
options[:mode] = directory_permissions if directory_permissions
|
295
|
+
FileUtils.mkdir_p(File.dirname(path), **options) unless File.exist?(File.dirname(path))
|
296
|
+
end
|
297
|
+
|
298
|
+
def chmod!(path, permissions)
|
299
|
+
File.chmod(permissions, path) if permissions
|
300
|
+
end
|
301
|
+
|
302
|
+
# Sanitize the filename, to prevent hacking
|
303
|
+
def sanitize(name)
|
304
|
+
name = name.scrub
|
305
|
+
name = name.tr('\\', '/') # work-around for IE
|
306
|
+
name = File.basename(name)
|
307
|
+
name = name.gsub(sanitize_regexp, '_')
|
308
|
+
name = "_#{name}" if name =~ /\A\.+\z/
|
309
|
+
name = 'unnamed' if name.size.zero?
|
310
|
+
name.mb_chars.to_s
|
311
|
+
end
|
312
|
+
|
313
|
+
def declared_content_type
|
314
|
+
@declared_content_type ||
|
315
|
+
if @file.respond_to?(:content_type) && @file.content_type
|
316
|
+
@file.content_type.to_s.chomp
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Guess content type from its file extension. Limit what to be returned to prevent spoofing.
|
321
|
+
def guessed_safe_content_type
|
322
|
+
return unless path
|
323
|
+
|
324
|
+
type = Marcel::Magic.by_path(original_filename).to_s
|
325
|
+
type if type.start_with?('text/') || type.start_with?('application/json')
|
326
|
+
end
|
327
|
+
|
328
|
+
def identified_content_type
|
329
|
+
with_io do |io|
|
330
|
+
mimetype_by_magic = Marcel::Magic.by_magic(io)
|
331
|
+
mimetype_by_path = Marcel::Magic.by_path(path)
|
332
|
+
|
333
|
+
return nil if mimetype_by_magic.nil?
|
334
|
+
|
335
|
+
if mimetype_by_path&.child_of?(mimetype_by_magic.type)
|
336
|
+
mimetype_by_path.type
|
337
|
+
else
|
338
|
+
mimetype_by_magic.type
|
339
|
+
end
|
340
|
+
end
|
341
|
+
rescue Errno::ENOENT
|
342
|
+
nil
|
343
|
+
end
|
344
|
+
|
345
|
+
def with_io(&block)
|
346
|
+
if file.is_a?(IO)
|
347
|
+
begin
|
348
|
+
yield file
|
349
|
+
ensure
|
350
|
+
file.try(:rewind)
|
351
|
+
end
|
352
|
+
elsif path
|
353
|
+
File.open(path, &block)
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end # SanitizedFile
|
357
|
+
end # SalebotUploader
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module SalebotUploader
|
2
|
+
module Storage
|
3
|
+
|
4
|
+
##
|
5
|
+
# This file serves mostly as a specification for Storage engines. There is no requirement
|
6
|
+
# that storage engines must be a subclass of this class.
|
7
|
+
#
|
8
|
+
class Abstract
|
9
|
+
|
10
|
+
attr_reader :uploader
|
11
|
+
|
12
|
+
def initialize(uploader)
|
13
|
+
@uploader = uploader
|
14
|
+
end
|
15
|
+
|
16
|
+
def identifier
|
17
|
+
uploader.deduplicated_filename
|
18
|
+
end
|
19
|
+
|
20
|
+
def store!(file); end
|
21
|
+
|
22
|
+
def retrieve!(identifier); end
|
23
|
+
|
24
|
+
def cache!(new_file)
|
25
|
+
raise NotImplementedError, "Need to implement #cache! if you want to use #{self.class.name} as a cache storage."
|
26
|
+
end
|
27
|
+
|
28
|
+
def retrieve_from_cache!(identifier)
|
29
|
+
raise NotImplementedError, "Need to implement #retrieve_from_cache! if you want to use #{self.class.name} as a cache storage."
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_dir!(path)
|
33
|
+
raise NotImplementedError, "Need to implement #delete_dir! if you want to use #{self.class.name} as a cache storage."
|
34
|
+
end
|
35
|
+
|
36
|
+
def clean_cache!(seconds)
|
37
|
+
raise NotImplementedError, "Need to implement #clean_cache! if you want to use #{self.class.name} as a cache storage."
|
38
|
+
end
|
39
|
+
end # Abstract
|
40
|
+
end # Storage
|
41
|
+
end # SalebotUploader
|