shrine 2.3.1 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

@@ -211,9 +211,20 @@ class Shrine
211
211
 
212
212
  def assign_cached(value)
213
213
  cached_file = uploaded_file(value)
214
- warn "Generating versions in the :cache action is deprecated and will be forbidden in Shrine 3." if cached_file.is_a?(Hash)
214
+ warn "Assigning cached hash of files is deprecated for security reasons and will be removed in Shrine 3." if cached_file.is_a?(Hash)
215
215
  super(cached_file)
216
216
  end
217
+
218
+ # Converts the Hash of UploadedFile objects into a Hash of data.
219
+ def convert_to_data(value)
220
+ if value.is_a?(Hash)
221
+ value.inject({}) do |hash, (name, uploaded_file)|
222
+ hash.merge!(name => super(uploaded_file))
223
+ end
224
+ else
225
+ super
226
+ end
227
+ end
217
228
  end
218
229
  end
219
230
 
@@ -25,11 +25,11 @@ class Shrine
25
25
  # ## Host
26
26
  #
27
27
  # It's generally a good idea to serve your files via a CDN, so an
28
- # additional `:host` option can be provided:
28
+ # additional `:host` option can be provided to `#url`:
29
29
  #
30
- # storage = Shrine::Storage::FileSystem.new("public",
31
- # prefix: "uploads", host: "http://abc123.cloudfront.net")
32
- # storage.url("image.jpg") #=> "http://abc123.cloudfront.net/uploads/image.jpg"
30
+ # storage = Shrine::Storage::FileSystem.new("public", prefix: "uploads")
31
+ # storage.url("image.jpg", host: "http://abc123.cloudfront.net")
32
+ # #=> "http://abc123.cloudfront.net/uploads/image.jpg"
33
33
  #
34
34
  # If you're not using a CDN, it's recommended that you still set `:host` to
35
35
  # your application's domain (at least in production).
@@ -37,8 +37,9 @@ class Shrine
37
37
  # The `:host` option can also be used wihout `:prefix`, and is
38
38
  # useful if you for example have files located on another server:
39
39
  #
40
- # storage = Shrine::Storage::FileSystem.new("files", host: "http://943.23.43.1")
41
- # storage.url("image.jpg") #=> "http://943.23.43.1/files/image.jpg"
40
+ # storage = Shrine::Storage::FileSystem.new("/opt/files")
41
+ # storage.url("image.jpg", host: "http://943.23.43.1")
42
+ # #=> "http://943.23.43.1/opt/files/image.jpg"
42
43
  #
43
44
  # ## Clearing cache
44
45
  #
@@ -80,10 +81,6 @@ class Shrine
80
81
  # : The directory relative to `directory` to which files will be stored,
81
82
  # and it is included in the URL.
82
83
  #
83
- # :host
84
- # : URLs will by default be relative if `:prefix` is set, and you
85
- # can use this option to set a CDN host (e.g. `//abc123.cloudfront.net`).
86
- #
87
84
  # :permissions
88
85
  # : The UNIX permissions applied to created files. Can be set to `nil`,
89
86
  # in which case the default permissions will be applied. Defaults to
@@ -99,6 +96,8 @@ class Shrine
99
96
  # deleted, but if it happens that it causes too much load on the
100
97
  # filesystem, you can set this option to `false`.
101
98
  def initialize(directory, prefix: nil, host: nil, clean: true, permissions: 0644, directory_permissions: 0755)
99
+ warn "The :host option to Shrine::Storage::FileSystem#initialize is deprecated and will be removed in Shrine 3. Pass :host to FileSystem#url instead, you can also use default_url_options plugin." if host
100
+
102
101
  if prefix
103
102
  @prefix = Pathname(relative(prefix))
104
103
  @directory = Pathname(directory).join(@prefix)
@@ -162,12 +161,15 @@ class Shrine
162
161
  def delete(id)
163
162
  path(id).delete
164
163
  clean(id) if clean?
164
+ rescue Errno::ENOENT
165
165
  end
166
166
 
167
- # If #prefix is present, returns the path relative to #directory,
168
- # with an optional #host in front. Otherwise returns the full path to the
169
- # file (also with an optional #host).
170
- def url(id, **options)
167
+ # If #prefix is not present, returns a path composed of #directory and
168
+ # the given `id`. If #prefix is present, it excludes the #directory part
169
+ # from the returned path (e.g. #directory can be set to "public" folder).
170
+ # Both cases accept a `:host` value which will be prefixed to the
171
+ # generated path.
172
+ def url(id, host: self.host, **options)
171
173
  path = (prefix ? relative_path(id) : path(id)).to_s
172
174
  host ? host + path : path
173
175
  end
@@ -3,6 +3,7 @@ require "shrine"
3
3
  require "forwardable"
4
4
  require "stringio"
5
5
  require "tempfile"
6
+ require "securerandom"
6
7
 
7
8
  class Shrine
8
9
  # Error which is thrown when Storage::Linter fails.
@@ -83,6 +84,11 @@ class Shrine
83
84
  def lint_delete(id)
84
85
  storage.delete(id)
85
86
  error :delete, "file still #exists? after deleting" if storage.exists?(id)
87
+ begin
88
+ storage.delete(SecureRandom.hex)
89
+ rescue => exception
90
+ error :delete, "shouldn't fail if the file doesn't exist, but raised #{exception.class}"
91
+ end
86
92
  end
87
93
 
88
94
  def lint_move(uploaded_file, id)
@@ -1,6 +1,7 @@
1
1
  require "aws-sdk"
2
2
  require "down"
3
3
  require "uri"
4
+ require "cgi/util"
4
5
 
5
6
  class Shrine
6
7
  module Storage
@@ -32,10 +33,7 @@ class Shrine
32
33
  # Sometimes you'll want to add additional upload options to all S3 uploads.
33
34
  # You can do that by passing the `:upload` option:
34
35
  #
35
- # Shrine::Storage::S3.new(
36
- # upload_options: {acl: "public-read", cache_control: "public, max-age=3600"},
37
- # **s3_options
38
- # )
36
+ # Shrine::Storage::S3.new(upload_options: {acl: "public-read"}, **s3_options)
39
37
  #
40
38
  # These options will be passed to aws-sdk's methods for [uploading],
41
39
  # [copying] and [presigning].
@@ -53,28 +51,40 @@ class Shrine
53
51
  # end
54
52
  # end
55
53
  #
56
- # Note that these aren't applied to presigns, since presigns are generated
57
- # using the storage directly.
54
+ # Note that, unlike the `:upload_options` storage option, the
55
+ # `upload_options` plugin won't forward the given options for generating
56
+ # presigns, since presigns are generated using the storage directly.
58
57
  #
59
58
  # ## URL options
60
59
  #
61
60
  # This storage supports various URL options that will be forwarded from
62
61
  # uploaded file.
63
62
  #
64
- # uploaded_file.url(public: true) # public URL without signed parameters
65
- # uploaded_file.url(download: true) # forced download URL
63
+ # s3.url(public: true) # public URL without signed parameters
64
+ # s3.url(download: true) # forced download URL
66
65
  #
67
66
  # All other options are forwarded to the [aws-sdk] gem:
68
67
  #
69
- # uploaded_file.url(expires_in: 15)
70
- # uploaded_file.urL(virtual_host: true)
68
+ # s3.url(expires_in: 15)
69
+ # s3.url(virtual_host: true)
71
70
  #
72
71
  # ## CDN
73
72
  #
74
73
  # If you're using a CDN with S3 like Amazon CloudFront, you can specify
75
- # the `:host` option to have all your URLs use the CDN host:
74
+ # the `:host` option to `#url`:
75
+ #
76
+ # s3 = Shrine::Storage::S3.new(**s3_options)
77
+ # s3.url("image.jpg", host: "http://abc123.cloudfront.net")
78
+ # #=> "http://abc123.cloudfront.net/image.jpg"
79
+ #
80
+ # ## Accelerate endpoint
81
+ #
82
+ # To use Amazon S3's [Transfer Acceleration] feature, you can change the
83
+ # `:endpoint` of the underlying client to the accelerate endpoint, and this
84
+ # will be applied both to regular and presigned uploads, as well as
85
+ # download URLs.
76
86
  #
77
- # Shrine::Storage::S3.new(host: "http://abc123.cloudfront.net", **s3_options)
87
+ # Shrine::Storage::S3.new(endpoint: "https://s3-accelerate.amazonaws.com")
78
88
  #
79
89
  # ## Presigns
80
90
  #
@@ -108,6 +118,7 @@ class Shrine
108
118
  # [copying]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#copy_from-instance_method
109
119
  # [presigning]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_post-instance_method
110
120
  # [aws-sdk]: https://github.com/aws/aws-sdk-ruby
121
+ # [Transfer Acceleration]: http://docs.aws.amazon.com/AmazonS3/latest/dev/transfer-acceleration.html
111
122
  class S3
112
123
  attr_reader :s3, :bucket, :prefix, :host, :upload_options
113
124
 
@@ -122,10 +133,6 @@ class Shrine
122
133
  # :prefix
123
134
  # : "Folder" name inside the bucket to store files into.
124
135
  #
125
- # :host
126
- # : This option is used for setting CDNs, e.g. it can be set to
127
- # `//abc123.cloudfront.net`.
128
- #
129
136
  # :upload_options
130
137
  # : Additional options that will be used for uploading files, they will
131
138
  # be passed to [`Aws::S3::Object#put`], [`Aws::S3::Object#copy_from`]
@@ -142,6 +149,8 @@ class Shrine
142
149
  # [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_post-instance_method
143
150
  # [`Aws::S3::Client#initialize`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Client.html#initialize-instance_method
144
151
  def initialize(bucket:, prefix: nil, host: nil, upload_options: {}, multipart_threshold: 15*1024*1024, **s3_options)
152
+ warn "The :host option to Shrine::Storage::S3#initialize is deprecated and will be removed in Shrine 3. Pass :host to S3#url instead, you can also use default_url_options plugin." if host
153
+
145
154
  @prefix = prefix
146
155
  @s3 = Aws::S3::Resource.new(**s3_options)
147
156
  @bucket = @s3.bucket(bucket)
@@ -156,10 +165,17 @@ class Shrine
156
165
  # It assigns the correct "Content-Type" taken from the MIME type, because
157
166
  # by default S3 sets everything to "application/octet-stream".
158
167
  def upload(io, id, shrine_metadata: {}, **upload_options)
159
- options = {content_type: shrine_metadata["mime_type"]}
168
+ content_type, filename = shrine_metadata.values_at("mime_type", "filename")
169
+
170
+ options = {}
171
+ options[:content_type] = content_type if content_type
172
+ options[:content_disposition] = "inline; filename=\"#{filename}\"" if filename
173
+
160
174
  options.update(@upload_options)
161
175
  options.update(upload_options)
162
176
 
177
+ options[:content_disposition] = encode_content_disposition(options[:content_disposition]) if options[:content_disposition]
178
+
163
179
  if copyable?(io)
164
180
  copy(io, id, **options)
165
181
  else
@@ -176,7 +192,7 @@ class Shrine
176
192
  tempfile.tap(&:open)
177
193
  end
178
194
 
179
- # Alias for #download.
195
+ # Returns a `Down::ChunkedIO` object representing the S3 object.
180
196
  def open(id)
181
197
  Down.open(url(id), ssl_ca_cert: Aws.config[:ssl_ca_bundle])
182
198
  end
@@ -207,6 +223,11 @@ class Shrine
207
223
  # : Creates an unsigned version of the URL (the permissions on the S3
208
224
  # bucket need to be modified to allow public URLs).
209
225
  #
226
+ # :host
227
+ # : This option replaces the host part of the returned URL, and is
228
+ # typically useful for setting CDN hosts (e.g.
229
+ # `http://abc123.cloudfront.net`)
230
+ #
210
231
  # :download
211
232
  # : If set to `true`, creates a "forced download" link, which means that
212
233
  # the browser will never display the file and always ask the user to
@@ -217,8 +238,9 @@ class Shrine
217
238
  #
218
239
  # [`Aws::S3::Object#presigned_url`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_url-instance_method
219
240
  # [`Aws::S3::Object#public_url`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#public_url-instance_method
220
- def url(id, download: nil, public: nil, **options)
221
- options[:response_content_disposition] = "attachment" if download
241
+ def url(id, download: nil, public: nil, host: self.host, **options)
242
+ options[:response_content_disposition] ||= "attachment" if download
243
+ options[:response_content_disposition] = encode_content_disposition(options[:response_content_disposition]) if options[:response_content_disposition]
222
244
 
223
245
  if public
224
246
  url = object(id).public_url(**options)
@@ -238,11 +260,7 @@ class Shrine
238
260
  # Deletes all files from the storage.
239
261
  def clear!
240
262
  objects = bucket.object_versions(prefix: prefix)
241
- if objects.respond_to?(:batch_delete!)
242
- objects.batch_delete!
243
- else
244
- objects.delete
245
- end
263
+ objects.respond_to?(:batch_delete!) ? objects.batch_delete! : objects.delete
246
264
  end
247
265
 
248
266
  # Returns a signature for direct uploads. Internally it calls
@@ -252,6 +270,8 @@ class Shrine
252
270
  # [`Aws::S3::Bucket#presigned_post`]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Bucket.html#presigned_post-instance_method
253
271
  def presign(id, **options)
254
272
  options = upload_options.merge(options)
273
+ options[:content_disposition] = encode_content_disposition(options[:content_disposition]) if options[:content_disposition]
274
+
255
275
  object(id).presigned_post(options)
256
276
  end
257
277
 
@@ -308,6 +328,12 @@ class Shrine
308
328
  def multipart?(io)
309
329
  io.size && io.size >= @multipart_threshold
310
330
  end
331
+
332
+ def encode_content_disposition(content_disposition)
333
+ content_disposition.sub(/(?<=filename=").+(?=")/) do |filename|
334
+ CGI.escape(filename).sub("+", " ")
335
+ end
336
+ end
311
337
  end
312
338
  end
313
339
  end
@@ -5,8 +5,8 @@ class Shrine
5
5
 
6
6
  module VERSION
7
7
  MAJOR = 2
8
- MINOR = 3
9
- TINY = 1
8
+ MINOR = 4
9
+ TINY = 0
10
10
  PRE = nil
11
11
 
12
12
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
@@ -26,7 +26,7 @@ direct uploads for fully asynchronous user experience.
26
26
  gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "shrine.gemspec", "doc/*.md"]
27
27
  gem.require_path = "lib"
28
28
 
29
- gem.add_dependency "down", ">= 2.3.5"
29
+ gem.add_dependency "down", ">= 2.3.6"
30
30
 
31
31
  gem.add_development_dependency "rake", "~> 11.1"
32
32
  gem.add_development_dependency "minitest", "~> 5.8"
@@ -35,7 +35,7 @@ direct uploads for fully asynchronous user experience.
35
35
  gem.add_development_dependency "webmock"
36
36
  gem.add_development_dependency "rack-test_app"
37
37
  gem.add_development_dependency "dotenv"
38
- gem.add_development_dependency "shrine-memory", ">= 0.2.1"
38
+ gem.add_development_dependency "shrine-memory", ">= 0.2.2"
39
39
 
40
40
  gem.add_development_dependency "roda"
41
41
  gem.add_development_dependency "rack", "~> 1.6.4"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shrine
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-09-01 00:00:00.000000000 Z
11
+ date: 2016-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 2.3.5
19
+ version: 2.3.6
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 2.3.5
26
+ version: 2.3.6
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -128,14 +128,14 @@ dependencies:
128
128
  requirements:
129
129
  - - ">="
130
130
  - !ruby/object:Gem::Version
131
- version: 0.2.1
131
+ version: 0.2.2
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - ">="
137
137
  - !ruby/object:Gem::Version
138
- version: 0.2.1
138
+ version: 0.2.2
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: roda
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -293,6 +293,7 @@ extra_rdoc_files: []
293
293
  files:
294
294
  - LICENSE.txt
295
295
  - README.md
296
+ - doc/attacher.md
296
297
  - doc/carrierwave.md
297
298
  - doc/changing_location.md
298
299
  - doc/creating_plugins.md
@@ -304,6 +305,7 @@ files:
304
305
  - doc/refile.md
305
306
  - doc/regenerating_versions.md
306
307
  - doc/securing_uploads.md
308
+ - doc/testing.md
307
309
  - lib/shrine.rb
308
310
  - lib/shrine/plugins/activerecord.rb
309
311
  - lib/shrine/plugins/add_metadata.rb
@@ -311,7 +313,6 @@ files:
311
313
  - lib/shrine/plugins/backgrounding.rb
312
314
  - lib/shrine/plugins/backup.rb
313
315
  - lib/shrine/plugins/cached_attachment_data.rb
314
- - lib/shrine/plugins/concatenation.rb
315
316
  - lib/shrine/plugins/copy.rb
316
317
  - lib/shrine/plugins/data_uri.rb
317
318
  - lib/shrine/plugins/default_storage.rb
@@ -1,73 +0,0 @@
1
- class Shrine
2
- module Plugins
3
- # The `concatenation` plugin allows you to assign to the attacher a
4
- # cached file which is composed of multiple uploaded parts. The plugin
5
- # will then call `#concat` on the storage, which is expected to
6
- # concatenate the given parts into a single file. The assigned
7
- # attachment will then be a complete cached file.
8
- #
9
- # plugin :concatenation
10
- #
11
- # The plugin expects to receive the cached file in the standard JSON
12
- # format, with an additional `"parts"` key which is the array of
13
- # uploaded parts:
14
- #
15
- # {
16
- # "id": "lsdg94l31.jpg",
17
- # "storage": "cache",
18
- # "parts": [
19
- # {"id": "aaa", "storage": "cache", "metadata": {}},
20
- # {"id": "bbb", "storage": "cache", "metadata": {}},
21
- # # ...
22
- # ],
23
- # "metadata": {
24
- # # ...
25
- # }
26
- # }
27
- #
28
- # The `"metadata"` field of individual parts should contain information
29
- # that your storage needs to perform concatenation, refer to the
30
- # documentation of your storage.
31
- #
32
- # You can also pass additional storage-specific concatenation options:
33
- #
34
- # plugin :concatenation, options: {use_accelerate_endpoint: true}
35
- #
36
- # plugin :concatenation, options: ->(io, context) do
37
- # {use_accelerate_endpoint: true} unless context[:record].guest?
38
- # end
39
- module Concatenation
40
- def self.configure(uploader, opts = {})
41
- uploader.opts[:concatenation_options] = opts.fetch(:options, uploader.opts.fetch(:options, {}))
42
- end
43
-
44
- module AttacherMethods
45
- def assign(value)
46
- if value.is_a?(String) && !value.empty?
47
- data = JSON.parse(value)
48
-
49
- if data.key?("parts")
50
- parts = data["parts"].map { |part_data| uploaded_file(part_data) }
51
- location = data["id"]
52
- metadata = data["metadata"]
53
-
54
- options = shrine_class.opts[:concatenation_options]
55
- options = options.call(uploaded_file(data), context) if options.respond_to?(:call)
56
- options ||= {}
57
-
58
- cache.storage.concat(parts, location, shrine_metadata: metadata, **options)
59
-
60
- data.delete("parts")
61
-
62
- assign(data.to_json)
63
- else
64
- super
65
- end
66
- end
67
- end
68
- end
69
- end
70
-
71
- register_plugin(:concatenation, Concatenation)
72
- end
73
- end