shrine 2.1.1 → 2.2.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.

@@ -1,7 +1,5 @@
1
1
  require "roda"
2
-
3
2
  require "json"
4
- require "securerandom"
5
3
 
6
4
  class Shrine
7
5
  module Plugins
@@ -10,18 +8,33 @@ class Shrine
10
8
  #
11
9
  # plugin :direct_upload
12
10
  #
13
- # This is how you could mount the endpoint in a Rails application:
11
+ # The Roda endpoint provides two routes:
12
+ #
13
+ # * `POST /:storage/upload`
14
+ # * `GET /:storage/presign`
15
+ #
16
+ # This first route is for doing direct uploads to your app, the received
17
+ # file will be uploaded the underlying storage. The second route is for
18
+ # doing direct uploads to a 3rd-party service, it will return the URL where
19
+ # the file can be uploaded to, along with the necessary request parameters.
20
+ #
21
+ # This is how you can mount the endpoint in a Rails application:
14
22
  #
15
23
  # Rails.application.routes.draw do
16
- # mount ImageUploader::UploadEndpoint => "/attachments/images"
24
+ # mount ImageUploader::UploadEndpoint => "/images"
17
25
  # end
18
26
  #
19
- # You should always mount a new endpoint for each uploader that you want to
20
- # enable direct uploads for. This now gives your Ruby application a `POST
21
- # /attachments/images/:storage/upload` route, which accepts a "file" query
22
- # parameter, and returns the uploaded file in JSON format:
27
+ # Now your application will get `POST /images/cache/upload` and `GET
28
+ # /images/cache/presign` routes. Whether you upload files to your app or to
29
+ # to a 3rd-party service, you'll probably want to use a JavaScript file
30
+ # upload library like [jQuery-File-Upload] or [Dropzone].
31
+ #
32
+ # ## Uploads
23
33
  #
24
- # # POST /attachments/images/cache/upload (file upload)
34
+ # The upload route accepts a "file" query parameter, and returns the
35
+ # uploaded file in JSON format:
36
+ #
37
+ # # POST /images/cache/upload
25
38
  # {
26
39
  # "id": "43kewit94.jpg",
27
40
  # "storage": "cache",
@@ -32,14 +45,13 @@ class Shrine
32
45
  # }
33
46
  # }
34
47
  #
48
+ # Once you've uploaded the file, you can assign the result to the hidden
49
+ # attachment field in the form, or immediately send it to the server.
50
+ #
35
51
  # Note that the endpoint uploads the file standalone, without any knowledge
36
52
  # of the record, so `context[:record]` and `context[:name]` will be nil.
37
53
  #
38
- # Once you've uploaded the file, you need to assign the result to the
39
- # hidden attachment field in the form. There are many great JavaScript
40
- # libraries for file uploads, most popular being [jQuery-File-Upload].
41
- #
42
- # ## Limiting filesize
54
+ # ### Limiting filesize
43
55
  #
44
56
  # It's good idea to limit the maximum filesize of uploaded files, if you
45
57
  # set the `:max_size` option, files which are too big will get
@@ -47,22 +59,15 @@ class Shrine
47
59
  #
48
60
  # plugin :direct_upload, max_size: 5*1024*1024 # 5 MB
49
61
  #
50
- # Note that this option doesn't affect presigned uploads, but there you can
51
- # limit the filesize with storage options.
52
- #
53
- # ## Presigning
54
- #
55
- # An alternative to the direct endpoint is uploading directly to the
56
- # underlying storage (currently only supported by Amazon S3). These uploads
57
- # usually require extra information from the server, you can enable that
58
- # route by passing `presign: true`:
62
+ # Note that this option doesn't affect presigned uploads, there you can
63
+ # apply filesize limit when generating a presign.
59
64
  #
60
- # plugin :direct_upload, presign: true
65
+ # ## Presigns
61
66
  #
62
- # This will add `GET /:storage/presign`, and disable the default `POST
63
- # /:storage/:name` (for security reasons) The response for that request
64
- # looks something like this:
67
+ # The presign route returns the URL to the 3rd-party service to which you
68
+ # can upload the file, along with the necessary query parameters.
65
69
  #
70
+ # # GET /images/cache/presign
66
71
  # {
67
72
  # "url" => "https://my-bucket.s3-eu-west-1.amazonaws.com",
68
73
  # "fields" => {
@@ -75,39 +80,40 @@ class Shrine
75
80
  # }
76
81
  # }
77
82
  #
78
- # The `url` is where the file needs to be uploaded to, and `fields` is
79
- # additional data that needs to be send on the upload. The `fields.key`
80
- # attribute is the location where the file will be uploaded to, it is
81
- # generated randomly without an extension, but you can add it:
83
+ # If you want that the generated location includes a file extension, you
84
+ # can specify the `extension` query parameter: `GET
85
+ # /:storage/presign?extension=.png`.
82
86
  #
83
- # GET /cache/presign?extension=.png
87
+ # You can also completely change how the key is generated, with
88
+ # `:presign_location`:
84
89
  #
85
- # You can change how the key is generated with `:presign_location`:
90
+ # plugin :direct_upload, presign_location: ->(request) { "${filename}" }
86
91
  #
87
- # plugin :direct_upload, presign: true, presign_location: ->(request) { "${filename}" }
92
+ # This presign route internally calls `#presign` on the storage, which also
93
+ # accepts some service-specific options. You can generate these additional
94
+ # options per-request with `:presign_options`:
88
95
  #
89
- # If you want additional options to be passed to Storage::S3#presign, you
90
- # can pass `:presign_options` with a hash or a block (which gets yielded
91
- # Roda's request object):
96
+ # plugin :direct_upload, presign_options: {acl: "public-read"}
92
97
  #
93
- # plugin :direct_upload, presign: true, presign_options: {acl: "public-read"}
94
- #
95
- # plugin :direct_upload, presign: true, presign_options: ->(request) do
98
+ # plugin :direct_upload, presign_options: ->(request) do
96
99
  # options = {}
97
100
  # options[:content_length_range] = 0..(5*1024*1024) # limit the filesize to 5 MB
98
101
  # options[:content_type] = request.params["content_type"] # use "content_type" query parameter
99
102
  # options
100
103
  # end
101
104
  #
105
+ # Both `:presign_location` and `:presign_options` in their block versions
106
+ # are yielded an instance of [`Roda::Request`].
107
+ #
102
108
  # See the [Direct Uploads to S3] guide for further instructions on how to
103
109
  # hook the presigned uploads to a form.
104
110
  #
105
111
  # ### Testing presigns
106
112
  #
107
- # If you want to test presigned uploads, but don't want to pay the
108
- # performance cost of using Amazon S3 storage in tests, you can simply swap
109
- # out S3 with a storage like FileSystem. The presigns will still get
110
- # generated, but will simply point to this endpoint's upload route.
113
+ # If you want to test presigned uploads, but don't want to use Amazon S3 in
114
+ # tests for performance reasons, you can simply swap out S3 with a storage
115
+ # like FileSystem. The presigns will still get generated, but will simply
116
+ # point to this endpoint's upload route instead.
111
117
  #
112
118
  # ## Allowed storages
113
119
  #
@@ -117,18 +123,6 @@ class Shrine
117
123
  #
118
124
  # plugin :direct_upload, allowed_storages: [:cache, :store]
119
125
  #
120
- # ## Authentication
121
- #
122
- # If you want to authenticate the endpoint, you should be able to do it
123
- # easily if your web framework has a good enough router. For example, in
124
- # Rails you could add a `constraints` directive:
125
- #
126
- # Rails.application.routes.draw do
127
- # constraints(->(r){r.env["warden"].authenticate!}) do
128
- # mount ImageUploader::UploadEndpoint => "/attachments/images"
129
- # end
130
- # end
131
- #
132
126
  # ## Customizing endpoint
133
127
  #
134
128
  # Since the endpoint is a [Roda] app, it can be easily customized via
@@ -150,6 +144,7 @@ class Shrine
150
144
  #
151
145
  # [Roda]: https://github.com/jeremyevans/roda
152
146
  # [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
147
+ # [Dropzone]: https://github.com/enyo/dropzone
153
148
  # [supports]: https://github.com/blueimp/jQuery-File-Upload/wiki/Options#progress
154
149
  # ["accept" attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept
155
150
  # [`Roda::RodaRequest`]: http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Base/RequestMethods.html
@@ -161,7 +156,6 @@ class Shrine
161
156
 
162
157
  def self.configure(uploader, opts = {})
163
158
  uploader.opts[:direct_upload_allowed_storages] = opts.fetch(:allowed_storages, uploader.opts.fetch(:direct_upload_allowed_storages, [:cache]))
164
- uploader.opts[:direct_upload_presign] = opts.fetch(:presign, uploader.opts[:direct_upload_presign])
165
159
  uploader.opts[:direct_upload_presign_options] = opts.fetch(:presign_options, uploader.opts.fetch(:direct_upload_presign_options, {}))
166
160
  uploader.opts[:direct_upload_presign_location] = opts.fetch(:presign_location, uploader.opts[:direct_upload_presign_location])
167
161
  uploader.opts[:direct_upload_max_size] = opts.fetch(:max_size, uploader.opts[:direct_upload_max_size])
@@ -201,7 +195,7 @@ class Shrine
201
195
  uploaded_file = upload(file, context)
202
196
 
203
197
  json uploaded_file
204
- end unless presign? && presign_storage?
198
+ end
205
199
 
206
200
  r.get "presign" do
207
201
  location = get_presign_location
@@ -210,7 +204,7 @@ class Shrine
210
204
  presign_data = generate_presign(location, options)
211
205
 
212
206
  json presign_data
213
- end if presign?
207
+ end
214
208
  end
215
209
  end
216
210
 
@@ -226,14 +220,14 @@ class Shrine
226
220
 
227
221
  # Retrieves the context for the upload.
228
222
  def get_context(name)
229
- context = {phase: :cache}
223
+ context = {action: :cache, phase: :cache}
230
224
 
231
225
  if name != "upload"
232
226
  warn "The \"POST /:storage/:name\" route of the direct_upload Shrine plugin is deprecated, and it will be removed in Shrine 3. Use \"POST /:storage/upload\" instead."
233
227
  context[:name] = name
234
228
  end
235
229
 
236
- if presign? && !presign_storage?
230
+ unless presign_storage?
237
231
  context[:location] = request.params["key"]
238
232
  end
239
233
 
@@ -339,10 +333,6 @@ class Shrine
339
333
  shrine_class.opts[:direct_upload_allowed_storages]
340
334
  end
341
335
 
342
- def presign?
343
- shrine_class.opts[:direct_upload_presign]
344
- end
345
-
346
336
  def presign_options
347
337
  shrine_class.opts[:direct_upload_presign_options]
348
338
  end
@@ -25,7 +25,8 @@ class Shrine
25
25
  # user.avatar.url #=> "/attachments/store/sdg0lsf8.jpg"
26
26
  #
27
27
  # :storages
28
- # : An array of storage keys which the download endpoint should be used for.
28
+ # : An array of storage keys which the download endpoint should be applied
29
+ # on.
29
30
  #
30
31
  # :prefix
31
32
  # : The location where the download endpoint was mounted. If it was
@@ -110,17 +111,20 @@ class Shrine
110
111
  io = get_stream_io(id)
111
112
  response["Content-Length"] = io.size.to_s if io.size
112
113
 
113
- chunks = Enumerator.new do |y|
114
+ body = Enumerator.new do |y|
114
115
  if io.respond_to?(:each_chunk)
115
116
  io.each_chunk { |chunk| y.yield(chunk) }
116
117
  else
117
118
  y.yield io.read(16*1024, buffer ||= "") until io.eof?
118
119
  end
120
+ end
121
+
122
+ proxy = Rack::BodyProxy.new(body) do
119
123
  io.close
120
124
  io.delete if io.class.name == "Tempfile"
121
125
  end
122
126
 
123
- r.halt response.finish_with_body(chunks)
127
+ r.halt response.finish_with_body(proxy)
124
128
  end
125
129
  end
126
130
  end
@@ -102,7 +102,7 @@ class Shrine
102
102
 
103
103
  _log(
104
104
  action: action,
105
- phase: context[:phase],
105
+ phase: context[:action],
106
106
  uploader: self.class,
107
107
  attachment: context[:name],
108
108
  record_class: (context[:record].class if context[:record]),
@@ -1,14 +1,15 @@
1
1
  class Shrine
2
2
  module Plugins
3
- # The moving plugin makes so that when files are supposed to be uploaded,
4
- # they are moved instead. For example, on FileSystem moving is
5
- # instantaneous regardless of the filesize, so it's suitable for speeding
6
- # up uploads for larger files.
3
+ # The moving plugin will *move* files to storages instead of copying them,
4
+ # when the storage supports it. For FileSystem this will issue a `mv`
5
+ # command, which is instantaneous regardless of the filesize, so in that
6
+ # case loading this plugin can significantly speed up the attachment
7
+ # process.
7
8
  #
8
9
  # plugin :moving
9
10
  #
10
11
  # By default files will be moved whenever the storage supports it. If you
11
- # want moving to happen only for certain storages, you can set `storages`:
12
+ # want moving to happen only for certain storages, you can set `:storages`:
12
13
  #
13
14
  # plugin :moving, storages: [:cache]
14
15
  module Moving
@@ -0,0 +1,50 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The process plugin allows you to declaratively define
4
+ # file processing for specified actions, allowing you to transform
5
+ #
6
+ # def process(io, context)
7
+ # if context[:action] == :store
8
+ # # ...
9
+ # end
10
+ # end
11
+ #
12
+ # into
13
+ #
14
+ # process(:store) do |io, context|
15
+ # # ...
16
+ # end
17
+ #
18
+ # The declarations are additive and inherited, so for the same action you
19
+ # can declare multiple blocks, and they will be performed in the same order,
20
+ # where output from previous will be input to next. You can return `nil`
21
+ # in any block to signal that no processing was performed and that the
22
+ # original file should be used.
23
+ module Processing
24
+ def self.configure(uploader)
25
+ uploader.opts[:processing] = {}
26
+ end
27
+
28
+ module ClassMethods
29
+ def process(action, &block)
30
+ opts[:processing][action] ||= []
31
+ opts[:processing][action] << block
32
+ end
33
+ end
34
+
35
+ module InstanceMethods
36
+ def process(io, context = {})
37
+ pipeline = opts[:processing][context[:action]] || []
38
+
39
+ result = pipeline.inject(io) do |input, processing|
40
+ instance_exec(input, context, &processing) || input
41
+ end
42
+
43
+ result unless result == io
44
+ end
45
+ end
46
+ end
47
+
48
+ register_plugin(:processing, Processing)
49
+ end
50
+ end
@@ -20,6 +20,9 @@ class Shrine
20
20
  # and this is what is passed to `Shrine#upload`.
21
21
  #
22
22
  # plugin :rack_file
23
+ #
24
+ # Note that this plugin is not needed in Rails applications, because Rails
25
+ # already wraps Rack uploaded files in `ActionDispatch::Http::UploadedFile`.
23
26
  module RackFile
24
27
  module AttacherMethods
25
28
  # Checks whether a file is a Rack file hash, and in that case wraps the
@@ -6,25 +6,21 @@ class Shrine
6
6
  # the user immediately sees them) and other versions you want to generate
7
7
  # in the promotion phase in a background job.
8
8
  #
9
- # The phase will be set to `:recache`:
9
+ # plugin :recache
10
+ # plugin :processing
10
11
  #
11
- # class ImageUploader
12
- # plugin :recache
12
+ # process(:recache) do |io, context|
13
+ # # perform cheap processing
14
+ # end
13
15
  #
14
- # def process(io, context)
15
- # case context[:phase]
16
- # when :recache
17
- # # generate cheap versions
18
- # when :store
19
- # # generate more expensive versions
20
- # end
21
- # end
16
+ # process(:store) do |io, context|
17
+ # # perform more expensive processing
22
18
  # end
23
19
  module Recache
24
20
  module AttacherMethods
25
21
  def save
26
22
  if get && cache.uploaded?(get)
27
- _set cache!(get, phase: :recache)
23
+ _set cache!(get, action: :recache)
28
24
  end
29
25
  super
30
26
  end
@@ -10,7 +10,7 @@ class Shrine
10
10
  super
11
11
  ensure
12
12
  if errors.any? && cache.uploaded?(get)
13
- _delete(get, phase: :validate)
13
+ _delete(get, action: :validate)
14
14
  _set(nil)
15
15
  end
16
16
  end
@@ -12,10 +12,8 @@ class Shrine
12
12
 
13
13
  def assign_cached(cached_file)
14
14
  uploaded_file(cached_file) do |file|
15
- file.to_io # open
16
- real_metadata = cache.extract_metadata(file, context)
15
+ real_metadata = file.open { cache.extract_metadata(file, context) }
17
16
  file.metadata.update(real_metadata)
18
- file.close
19
17
  end
20
18
 
21
19
  super(cached_file)
@@ -41,6 +41,11 @@ class Shrine
41
41
  # end
42
42
  # end
43
43
  #
44
+ # If you don't want callbacks (e.g. you want to use the attacher object
45
+ # directly), you can turn them off:
46
+ #
47
+ # plugin :sequel, callbacks: false
48
+ #
44
49
  # ## Validations
45
50
  #
46
51
  # Additionally, any Shrine validation errors will added to Sequel's
@@ -51,36 +56,52 @@ class Shrine
51
56
  # include ImageUploader[:avatar]
52
57
  # validates_presence_of :avatar
53
58
  # end
59
+ #
60
+ # If you're doing validation separately from your models, you can turn off
61
+ # validations for your models:
62
+ #
63
+ # plugin :sequel, validations: false
54
64
  module Sequel
65
+ def self.configure(uploader, opts = {})
66
+ uploader.opts[:sequel_callbacks] = opts.fetch(:callbacks, uploader.opts.fetch(:sequel_callbacks, true))
67
+ uploader.opts[:sequel_validations] = opts.fetch(:validations, uploader.opts.fetch(:sequel_validations, true))
68
+ end
69
+
55
70
  module AttachmentMethods
56
71
  def included(model)
57
72
  super
58
73
 
59
74
  return unless model < ::Sequel::Model
60
75
 
61
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
62
- def validate
63
- super
64
- #{@name}_attacher.errors.each do |message|
65
- errors.add(:#{@name}, message)
76
+ if shrine_class.opts[:sequel_validations]
77
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
78
+ def validate
79
+ super
80
+ #{@name}_attacher.errors.each do |message|
81
+ errors.add(:#{@name}, message)
82
+ end
66
83
  end
67
- end
84
+ RUBY
85
+ end
68
86
 
69
- def before_save
70
- super
71
- #{@name}_attacher.save if #{@name}_attacher.attached?
72
- end
87
+ if shrine_class.opts[:sequel_callbacks]
88
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
89
+ def before_save
90
+ super
91
+ #{@name}_attacher.save if #{@name}_attacher.attached?
92
+ end
73
93
 
74
- def after_commit
75
- super
76
- #{@name}_attacher.finalize if #{@name}_attacher.attached?
77
- end
94
+ def after_commit
95
+ super
96
+ #{@name}_attacher.finalize if #{@name}_attacher.attached?
97
+ end
78
98
 
79
- def after_destroy_commit
80
- super
81
- #{@name}_attacher.destroy
82
- end
83
- RUBY
99
+ def after_destroy_commit
100
+ super
101
+ #{@name}_attacher.destroy
102
+ end
103
+ RUBY
104
+ end
84
105
  end
85
106
  end
86
107