shrine 1.0.0 → 1.1.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.

Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -149
  3. data/doc/carrierwave.md +12 -16
  4. data/doc/changing_location.md +50 -0
  5. data/doc/creating_plugins.md +2 -2
  6. data/doc/creating_storages.md +70 -9
  7. data/doc/direct_s3.md +132 -61
  8. data/doc/migrating_storage.md +12 -10
  9. data/doc/paperclip.md +12 -17
  10. data/doc/refile.md +338 -0
  11. data/doc/regenerating_versions.md +75 -11
  12. data/doc/securing_uploads.md +172 -0
  13. data/lib/shrine.rb +21 -16
  14. data/lib/shrine/plugins/activerecord.rb +2 -2
  15. data/lib/shrine/plugins/background_helpers.rb +2 -148
  16. data/lib/shrine/plugins/backgrounding.rb +148 -0
  17. data/lib/shrine/plugins/backup.rb +88 -0
  18. data/lib/shrine/plugins/data_uri.rb +25 -4
  19. data/lib/shrine/plugins/default_url.rb +37 -0
  20. data/lib/shrine/plugins/delete_uploaded.rb +40 -0
  21. data/lib/shrine/plugins/determine_mime_type.rb +4 -2
  22. data/lib/shrine/plugins/direct_upload.rb +107 -62
  23. data/lib/shrine/plugins/download_endpoint.rb +157 -0
  24. data/lib/shrine/plugins/hooks.rb +19 -5
  25. data/lib/shrine/plugins/keep_location.rb +43 -0
  26. data/lib/shrine/plugins/moving.rb +11 -10
  27. data/lib/shrine/plugins/parallelize.rb +1 -5
  28. data/lib/shrine/plugins/parsed_json.rb +7 -1
  29. data/lib/shrine/plugins/pretty_location.rb +6 -0
  30. data/lib/shrine/plugins/rack_file.rb +7 -1
  31. data/lib/shrine/plugins/remove_invalid.rb +22 -0
  32. data/lib/shrine/plugins/sequel.rb +2 -2
  33. data/lib/shrine/plugins/upload_options.rb +41 -0
  34. data/lib/shrine/plugins/versions.rb +9 -7
  35. data/lib/shrine/storage/file_system.rb +46 -30
  36. data/lib/shrine/storage/linter.rb +48 -25
  37. data/lib/shrine/storage/s3.rb +89 -22
  38. data/lib/shrine/version.rb +1 -1
  39. data/shrine.gemspec +3 -3
  40. metadata +16 -5
@@ -0,0 +1,88 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The backup plugin allows you to automatically backup up stored files to
4
+ # an additional storage.
5
+ #
6
+ # storages[:backup_store] = Shrine::Storage::S3.new(options)
7
+ # plugin :backup, storage: :backup_store
8
+ #
9
+ # After the cached file is promoted to store, it will be reuploaded from
10
+ # store to the provided "backup" storage.
11
+ #
12
+ # user.update(avatar: file) # uploaded both to :store and :backup_store
13
+ #
14
+ # By default whenever stored files are deleted backed up files are deleted
15
+ # as well, but you can keep files on the "backup" storage by passing
16
+ # `delete: false`:
17
+ #
18
+ # plugin :backup, storage: :backup_store, delete: false
19
+ #
20
+ # Note that when adding this plugin with already existing stored files,
21
+ # Shrine won't know whether a stored file is backed up or not, so
22
+ # attempting to delete the backup could result in an error. To avoid that
23
+ # you can set `delete: false` until you manually back up the existing
24
+ # stored files.
25
+ module Backup
26
+ def self.configure(uploader, storage:, delete: true)
27
+ uploader.opts[:backup_storage] = storage
28
+ uploader.opts[:backup_delete] = delete
29
+ end
30
+
31
+ module AttacherMethods
32
+ def backup_file(uploaded_file)
33
+ uploaded_file(uploaded_file) { |f| f.data["storage"] = backup_storage.to_s }
34
+ uploaded_file(uploaded_file.to_json)
35
+ end
36
+
37
+ private
38
+
39
+ # Back up the stored file and return it.
40
+ def store!(io, phase:)
41
+ super.tap { |stored_file| store_backup!(stored_file) }
42
+ end
43
+
44
+ # Delete the backed up file unless `:delete` was set to false.
45
+ def delete!(uploaded_file, phase:)
46
+ super.tap { |deleted_file| delete_backup!(deleted_file) if backup_delete? }
47
+ end
48
+
49
+ # Upload the stored file to the backup storage.
50
+ def store_backup!(stored_file)
51
+ backup_store.upload(stored_file, context.merge(phase: :backup))
52
+ end
53
+
54
+ # Deleted the stored file from the backup storage.
55
+ def delete_backup!(deleted_file)
56
+ backup_store.delete(backup_file(deleted_file), context.merge(phase: :backup))
57
+ end
58
+
59
+ def backup_store
60
+ @backup_store ||= shrine_class.new(backup_storage)
61
+ end
62
+
63
+ def backup_storage
64
+ shrine_class.opts[:backup_storage]
65
+ end
66
+
67
+ def backup_delete?
68
+ shrine_class.opts[:backup_delete]
69
+ end
70
+ end
71
+
72
+ module InstanceMethods
73
+ private
74
+
75
+ # We preserve the location when uploading from store to backup.
76
+ def get_location(io, context)
77
+ if context[:phase] == :backup
78
+ io.id
79
+ else
80
+ super
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ register_plugin(:backup, Backup)
87
+ end
88
+ end
@@ -1,4 +1,5 @@
1
1
  require "base64"
2
+ require "stringio"
2
3
 
3
4
  class Shrine
4
5
  module Plugins
@@ -11,10 +12,10 @@ class Shrine
11
12
  # `#avatar_data_uri` and `#avatar_data_uri=` methods to your model.
12
13
  #
13
14
  # user.avatar #=> nil
14
- # user.avatar_data_uri = "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
15
+ # user.avatar_data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
15
16
  # user.avatar #=> #<Shrine::UploadedFile>
16
17
  #
17
- # user.avatar.mime_type #=> "image/jpeg"
18
+ # user.avatar.mime_type #=> "image/png"
18
19
  # user.avatar.size #=> 43423
19
20
  # user.avatar.original_filename #=> nil
20
21
  #
@@ -24,12 +25,18 @@ class Shrine
24
25
  # plugin :data_uri, error_message: "data URI was invalid"
25
26
  # plugin :data_uri, error_message: ->(uri) { I18n.t("errors.data_uri_invalid") }
26
27
  #
28
+ # This plugin also adds a `UploadedFile#data_uri` method (and `#base64`),
29
+ # which returns a base64-encoded data URI of any UploadedFile:
30
+ #
31
+ # user.avatar.data_uri #=> "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
32
+ # user.avatar.base64 #=> "iVBORw0KGgoAAAANSUhEUgAAAAUA"
33
+ #
27
34
  # [data URIs]: https://tools.ietf.org/html/rfc2397
28
35
  # [HTML5 Canvas]: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API
29
36
  module DataUri
30
37
  DEFAULT_ERROR_MESSAGE = "data URI was invalid"
31
38
  DEFAULT_CONTENT_TYPE = "text/plain"
32
- DATA_URI_REGEXP = /\Adata:([-\w]+\/[-\w\+\.]+)?;base64,(.*)/m
39
+ DATA_URI_REGEXP = /\Adata:([-\w.+]+\/[-\w.+]+)?(;base64)?,(.*)\z/m
33
40
 
34
41
  def self.configure(uploader, error_message: DEFAULT_ERROR_MESSAGE)
35
42
  uploader.opts[:data_uri_error_message] = error_message
@@ -61,7 +68,7 @@ class Shrine
61
68
 
62
69
  if match = uri.match(DATA_URI_REGEXP)
63
70
  content_type = match[1] || DEFAULT_CONTENT_TYPE
64
- content = Base64.decode64(match[2])
71
+ content = match[2] ? Base64.decode64(match[3]) : match[3]
65
72
 
66
73
  assign DataFile.new(content, content_type: content_type)
67
74
  else
@@ -78,6 +85,20 @@ class Shrine
78
85
  end
79
86
  end
80
87
 
88
+ module FileMethods
89
+ # Returns the data URI representation of the file.
90
+ def data_uri
91
+ @data_uri ||= "data:#{mime_type || "text/plain"};base64,#{base64}"
92
+ end
93
+
94
+ # Returns contents of the file base64-encoded.
95
+ def base64
96
+ content = storage.read(id)
97
+ base64 = Base64.encode64(content)
98
+ base64.chomp
99
+ end
100
+ end
101
+
81
102
  class DataFile < StringIO
82
103
  attr_reader :content_type
83
104
 
@@ -0,0 +1,37 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The default_url plugin allows setting the URL which will be returned when
4
+ # the attachment is missing.
5
+ #
6
+ # plugin :default_url do |context|
7
+ # "/#{context[:name]}/missing.jpg"
8
+ # end
9
+ #
10
+ # The default URL gets triggered when calling `<attachment>_url` on the
11
+ # model:
12
+ #
13
+ # user.avatar #=> nil
14
+ # user.avatar_url # "/avatar/missing.jpg"
15
+ #
16
+ # Any additional URL options will be present in the `context` hash.
17
+ module DefaultUrl
18
+ def self.configure(uploader, &block)
19
+ uploader.opts[:default_url] = block
20
+ end
21
+
22
+ module AttacherMethods
23
+ private
24
+
25
+ def default_url(**options)
26
+ default_url_block.call(options.merge(context))
27
+ end
28
+
29
+ def default_url_block
30
+ shrine_class.opts[:default_url]
31
+ end
32
+ end
33
+ end
34
+
35
+ register_plugin(:default_url, DefaultUrl)
36
+ end
37
+ end
@@ -0,0 +1,40 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The delete_uploaded plugin will automatically delete files after they
4
+ # have been uploaded. This is especially useful when doing processing, to
5
+ # ensure that temporary files have been deleted after upload. One exception
6
+ # are `Shrine::UploadedFile` files, they won't get deleted for stability
7
+ # reasons.
8
+ #
9
+ # plugin :delete_uploaded
10
+ #
11
+ # By default this behaviour will be applied to all storages, but you can
12
+ # limit this only to specified storages:
13
+ #
14
+ # plugin :delete_uploaded, storages: [:store]
15
+ module DeleteUploaded
16
+ def self.configure(uploader, storages: :all)
17
+ uploader.opts[:delete_uploaded_storages] = storages
18
+ end
19
+
20
+ module InstanceMethods
21
+ private
22
+
23
+ # Deletes the uploaded file unless it's an UploadedFile.
24
+ def copy(io, context)
25
+ super
26
+ if io.respond_to?(:delete) && !io.is_a?(UploadedFile)
27
+ io.delete if delete_uploaded?(io)
28
+ end
29
+ end
30
+
31
+ def delete_uploaded?(io)
32
+ opts[:delete_uploaded_storages] == :all ||
33
+ opts[:delete_uploaded_storages].include?(storage_key)
34
+ end
35
+ end
36
+ end
37
+
38
+ register_plugin(:delete_uploaded, DeleteUploaded)
39
+ end
40
+ end
@@ -8,8 +8,9 @@ class Shrine
8
8
  # The plugin accepts the following analyzers:
9
9
  #
10
10
  # :file
11
- # : (Default). Uses the UNIX [file] utility to determine the MIME type
12
- # from file contents.
11
+ # : (Default). Uses the [file] utility to determine the MIME type from file
12
+ # contents. It is installed by default on most operating systems, but the
13
+ # [Windows equivalent] you need to install separately.
13
14
  #
14
15
  # :filemagic
15
16
  # : Uses the [ruby-filemagic] gem to determine the MIME type from file
@@ -42,6 +43,7 @@ class Shrine
42
43
  # end
43
44
  #
44
45
  # [file]: http://linux.die.net/man/1/file
46
+ # [Windows equivalent]: http://gnuwin32.sourceforge.net/packages/file.htm
45
47
  # [ruby-filemagic]: https://github.com/blackwinter/ruby-filemagic
46
48
  # [mimemagic]: https://github.com/minad/mimemagic
47
49
  # [mime-types]: https://github.com/mime-types/ruby-mime-types
@@ -5,22 +5,21 @@ require "securerandom"
5
5
 
6
6
  class Shrine
7
7
  module Plugins
8
- # The direct_upload plugin provides a Rack endpoint (implemented in [Roda])
9
- # which you can use to implement AJAX uploads.
8
+ # The direct_upload plugin provides a [Roda] endpoint which can be used for
9
+ # uploading individual files asynchronously.
10
10
  #
11
11
  # plugin :direct_upload
12
12
  #
13
13
  # This is how you could mount the endpoint in a Rails application:
14
14
  #
15
15
  # Rails.application.routes.draw do
16
- # # adds `POST /attachments/images/:storage/:name`
17
- # mount ImageUploader.direct_endpoint => "/attachments/images"
16
+ # mount ImageUploader::UploadEndpoint => "/attachments/images"
18
17
  # end
19
18
  #
20
- # Note that you should mount a separate endpoint for each uploader that you
21
- # want to use it with. This now gives your Ruby application a
22
- # `POST /attachments/images/:storage/:name` route, which accepts a `file`
23
- # query parameter, and returns the uploaded file in JSON format:
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/:name` route, which accepts a `file` query
22
+ # parameter, and returns the uploaded file in JSON format:
24
23
  #
25
24
  # # POST /attachments/images/cache/avatar
26
25
  # {
@@ -33,16 +32,26 @@ class Shrine
33
32
  # }
34
33
  # }
35
34
  #
36
- # There are many great JavaScript libraries for AJAX file uploads which can
37
- # be hooked up to this endpoint, [jQuery-File-Upload] being the most
38
- # popular one.
35
+ # Once you've uploaded the file, you need to assign this JSON to the hidden
36
+ # attachment field in the form. There are many great JavaScript libraries
37
+ # for file uploads, most popular being [jQuery-File-Upload].
39
38
  #
40
- # ## Presigned
39
+ # ## Limiting filesize
41
40
  #
42
- # An alternative to the direct endpoint is doing direct uploads to the
43
- # underlying storage. These uploads usually requires extra information
44
- # from the server, and this plugin can provide an endpoint to it, which
45
- # you can enable by passing in `presign: true`:
41
+ # It's good idea to limit the maximum filesize of uploaded files, if you
42
+ # set the `:max_size` option, files which are too big will get
43
+ # automatically deleted and 413 status will be returned:
44
+ #
45
+ # plugin :direct_upload, max_size: 5*1024*1024 # 5 MB
46
+ #
47
+ # Note that this option doesn't affect presigned uploads, but there you can
48
+ # limit the filesize with storage options.
49
+ #
50
+ # ## Presigning
51
+ #
52
+ # An alternative to the direct endpoint is uploading directly to the
53
+ # underlying storage (S3). These uploads usually require extra information
54
+ # from the server, you can enable that route by passing `presign: true`:
46
55
  #
47
56
  # plugin :direct_upload, presign: true
48
57
  #
@@ -51,7 +60,7 @@ class Shrine
51
60
  # request looks something like this:
52
61
  #
53
62
  # {
54
- # "url" => "https://shrine-testing.s3-eu-west-1.amazonaws.com",
63
+ # "url" => "https://my-bucket.s3-eu-west-1.amazonaws.com",
55
64
  # "fields" => {
56
65
  # "key" => "b7d575850ba61b44c8a9ff889dfdb14d88cdc25f8dd121004c8",
57
66
  # "policy" => "eyJleHBpcmF0aW9uIjoiMjAxNS0QwMToxMToyOVoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJzaHJpbmUtdGVzdGluZyJ9LHsia2V5IjoiYjdkNTc1ODUwYmE2MWI0NGU3Y2M4YTliZmY4OGU5ZGZkYjE2NTQ0ZDk4OGNkYzI1ZjhkZDEyMTAwNGM4In0seyJ4LWFtei1jcmVkZW50aWFsIjoiQUtJQUlKRjU1VE1aWlk0NVVUNlEvMjAxNTEwMjQvZXUtd2VzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsieC1hbXotYWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsieC1hbXotZGF0ZSI6IjIwMTUxMDI0VDAwMTEyOVoifV19",
@@ -65,29 +74,22 @@ class Shrine
65
74
  # The `url` is where the file needs to be uploaded to, and `fields` is
66
75
  # additional data that needs to be send on the upload. The `fields.key`
67
76
  # attribute is the location where the file will be uploaded to, it is
68
- # generated randomly, but you can add an extension to it:
77
+ # generated randomly without an extension, but you can add it:
69
78
  #
70
79
  # GET /cache/presign?extension=.png
71
80
  #
72
81
  # If you want additional options to be passed to Storage::S3#presign, you
73
- # can pass a block to `:presign` and return additional options:
82
+ # can pass a block to `:presign`, and it will yield Roda's request object:
74
83
  #
75
84
  # plugin :direct_upload, presign: ->(request) do
76
85
  # {
77
86
  # content_length_range: 0..(5*1024*1024), # limit the filesize to 5 MB
78
- # success_action_redirect: "http://example.com/webhook",
87
+ # content_type: request.params["content_type"], # use "content_type" query parameter
79
88
  # }
80
89
  # end
81
90
  #
82
- # The yielded object is an instance of [`Roda::RodaRequest`] (a subclass of
83
- # `Rack::Request`), which allows you to pass different options depending on
84
- # the request. For example, you could accept a `content_type` query
85
- # parameter and add it to options:
86
- #
87
- # plugin :direct_upload, presign: ->(request) do
88
- # {content_type: request.params["content_type"]} if request.params["content_type"]
89
- # end
90
- # # Now you can do `GET /cache/presign?content_type=image/jpeg`
91
+ # See the [Direct Uploads to S3] guide for further instructions on how to
92
+ # hook this up in a form.
91
93
  #
92
94
  # ## Allowed storages
93
95
  #
@@ -105,76 +107,99 @@ class Shrine
105
107
  #
106
108
  # Rails.application.routes.draw do
107
109
  # constraints(->(r){r.env["warden"].authenticate!}) do
108
- # mount ImageUploader.direct_endpoint => "/attachments/images"
110
+ # mount ImageUploader::UploadEndpoint => "/attachments/images"
109
111
  # end
110
112
  # end
111
113
  #
114
+ # ## Customizing endpoint
115
+ #
116
+ # Since the endpoint is a [Roda] app, it can be easily customized via
117
+ # plugins:
118
+ #
119
+ # class MyUploader
120
+ # class UploadEndpoint
121
+ # plugin :hooks
122
+ #
123
+ # after do |response|
124
+ # # ...
125
+ # end
126
+ # end
127
+ # end
128
+ #
129
+ # Upon subclassing uploader the upload endpoint is also subclassed. You can
130
+ # also call the plugin again in an uploader subclass to change its
131
+ # configuration.
132
+ #
112
133
  # [Roda]: https://github.com/jeremyevans/roda
113
134
  # [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
114
135
  # [supports]: https://github.com/blueimp/jQuery-File-Upload/wiki/Options#progress
115
136
  # ["accept" attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept
116
137
  # [`Roda::RodaRequest`]: http://roda.jeremyevans.net/rdoc/classes/Roda/RodaPlugins/Base/RequestMethods.html
138
+ # [Direct Uploads to S3]: http://shrinerb.com/rdoc/files/doc/direct_s3_md.html
117
139
  module DirectUpload
118
140
  def self.load_dependencies(uploader, *)
119
141
  uploader.plugin :rack_file
120
142
  end
121
143
 
122
- def self.configure(uploader, allowed_storages: [:cache], presign: nil, **)
144
+ def self.configure(uploader, allowed_storages: [:cache], presign: nil, max_size: nil)
123
145
  uploader.opts[:direct_upload_allowed_storages] = allowed_storages
124
146
  uploader.opts[:direct_upload_presign] = presign
147
+ uploader.opts[:direct_upload_max_size] = max_size
148
+
149
+ uploader.assign_upload_endpoint(App) unless uploader.const_defined?(:UploadEndpoint)
125
150
  end
126
151
 
127
152
  module ClassMethods
128
- # Return the cached Roda endpoint.
129
- def direct_endpoint
130
- @direct_endpoint ||= build_direct_endpoint
153
+ # Assigns the subclass a copy of the upload endpoint class.
154
+ def inherited(subclass)
155
+ super
156
+ subclass.assign_upload_endpoint(self::UploadEndpoint)
131
157
  end
132
158
 
133
- private
159
+ # Assigns the subclassed endpoint as the `UploadEndpoint` constant.
160
+ def assign_upload_endpoint(klass)
161
+ endpoint_class = Class.new(klass)
162
+ endpoint_class.opts[:shrine_class] = self
163
+ const_set(:UploadEndpoint, endpoint_class)
164
+ end
134
165
 
135
- # Builds the endpoint and assigns it the current Shrine class.
136
- def build_direct_endpoint
137
- app = Class.new(App)
138
- app.opts[:shrine_class] = self
139
- app.app
166
+ # Returns the Roda direct upload endpoint.
167
+ def direct_endpoint
168
+ warn "#{self}.direct_endpoint is deprecated and will be removed in Shrine 2, you should use #{self}::UploadEndpoint instead."
169
+ self::UploadEndpoint
140
170
  end
141
171
  end
142
172
 
173
+ # Routes incoming requests. It first asserts that the storage is existent
174
+ # and allowed, then the filesize isn't too large. Afterwards it proceeds
175
+ # with the file upload and returns the uploaded file as JSON.
143
176
  class App < Roda
144
177
  plugin :default_headers, "Content-Type"=>"application/json"
145
- plugin :halt
146
178
 
147
- # Routes incoming requests. We first check if the storage is allowed,
148
- # then proceed further with the upload, returning the uploaded file
149
- # as JSON.
150
179
  route do |r|
151
180
  r.on ":storage" do |storage_key|
152
181
  allow_storage!(storage_key)
153
182
  @uploader = shrine_class.new(storage_key.to_sym)
154
183
 
155
- unless presign
156
- r.post ":name" do |name|
157
- file = get_file
158
- context = {name: name, phase: :cache}
184
+ r.post ":name" do |name|
185
+ file = get_file
186
+ context = {name: name, phase: :cache}
159
187
 
160
- json @uploader.upload(file, context)
161
- end
162
- else
163
- r.get "presign" do
164
- location = SecureRandom.hex(30) + r.params["extension"].to_s
165
- options = presign.call(r) if presign.respond_to?(:call)
188
+ json @uploader.upload(file, context)
189
+ end unless presign
166
190
 
167
- signature = @uploader.storage.presign(location, options || {})
191
+ r.get "presign" do
192
+ location = SecureRandom.hex(30) + request.params["extension"].to_s
193
+ options = presign.call(request) if presign.respond_to?(:call)
168
194
 
169
- json Hash[url: signature.url, fields: signature.fields]
170
- end
171
- end
195
+ signature = @uploader.storage.presign(location, options || {})
196
+
197
+ json Hash[url: signature.url, fields: signature.fields]
198
+ end if presign
172
199
  end
173
200
  end
174
201
 
175
- def json(object)
176
- object.to_json
177
- end
202
+ private
178
203
 
179
204
  # Halts the request if storage is not allowed.
180
205
  def allow_storage!(storage)
@@ -188,10 +213,20 @@ class Shrine
188
213
  def get_file
189
214
  file = require_param!("file")
190
215
  error! 400, "The \"file\" query parameter is not a file." if !(file.is_a?(Hash) && file.key?(:tempfile))
216
+ check_filesize!(file[:tempfile]) if max_size
191
217
 
192
218
  RackFile::UploadedFile.new(file)
193
219
  end
194
220
 
221
+ # If the file is too big, deletes the file and halts the request.
222
+ def check_filesize!(file)
223
+ if file.size > max_size
224
+ file.delete
225
+ megabytes = max_size.to_f / 1024 / 1024
226
+ error! 413, "The file is too big (maximum size is #{megabytes} MB)."
227
+ end
228
+ end
229
+
195
230
  # Loudly requires the param.
196
231
  def require_param!(name)
197
232
  request.params.fetch(name)
@@ -201,7 +236,13 @@ class Shrine
201
236
 
202
237
  # Halts the request with the error message.
203
238
  def error!(status, message)
204
- request.halt status, {error: message}.to_json
239
+ response.status = status
240
+ response.write({error: message}.to_json)
241
+ request.halt
242
+ end
243
+
244
+ def json(object)
245
+ object.to_json
205
246
  end
206
247
 
207
248
  def shrine_class
@@ -215,6 +256,10 @@ class Shrine
215
256
  def presign
216
257
  shrine_class.opts[:direct_upload_presign]
217
258
  end
259
+
260
+ def max_size
261
+ shrine_class.opts[:direct_upload_max_size]
262
+ end
218
263
  end
219
264
  end
220
265