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.
- checksums.yaml +4 -4
- data/README.md +101 -149
- data/doc/carrierwave.md +12 -16
- data/doc/changing_location.md +50 -0
- data/doc/creating_plugins.md +2 -2
- data/doc/creating_storages.md +70 -9
- data/doc/direct_s3.md +132 -61
- data/doc/migrating_storage.md +12 -10
- data/doc/paperclip.md +12 -17
- data/doc/refile.md +338 -0
- data/doc/regenerating_versions.md +75 -11
- data/doc/securing_uploads.md +172 -0
- data/lib/shrine.rb +21 -16
- data/lib/shrine/plugins/activerecord.rb +2 -2
- data/lib/shrine/plugins/background_helpers.rb +2 -148
- data/lib/shrine/plugins/backgrounding.rb +148 -0
- data/lib/shrine/plugins/backup.rb +88 -0
- data/lib/shrine/plugins/data_uri.rb +25 -4
- data/lib/shrine/plugins/default_url.rb +37 -0
- data/lib/shrine/plugins/delete_uploaded.rb +40 -0
- data/lib/shrine/plugins/determine_mime_type.rb +4 -2
- data/lib/shrine/plugins/direct_upload.rb +107 -62
- data/lib/shrine/plugins/download_endpoint.rb +157 -0
- data/lib/shrine/plugins/hooks.rb +19 -5
- data/lib/shrine/plugins/keep_location.rb +43 -0
- data/lib/shrine/plugins/moving.rb +11 -10
- data/lib/shrine/plugins/parallelize.rb +1 -5
- data/lib/shrine/plugins/parsed_json.rb +7 -1
- data/lib/shrine/plugins/pretty_location.rb +6 -0
- data/lib/shrine/plugins/rack_file.rb +7 -1
- data/lib/shrine/plugins/remove_invalid.rb +22 -0
- data/lib/shrine/plugins/sequel.rb +2 -2
- data/lib/shrine/plugins/upload_options.rb +41 -0
- data/lib/shrine/plugins/versions.rb +9 -7
- data/lib/shrine/storage/file_system.rb +46 -30
- data/lib/shrine/storage/linter.rb +48 -25
- data/lib/shrine/storage/s3.rb +89 -22
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +3 -3
- 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/
|
15
|
+
# user.avatar_data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUA"
|
15
16
|
# user.avatar #=> #<Shrine::UploadedFile>
|
16
17
|
#
|
17
|
-
# user.avatar.mime_type #=> "image/
|
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
|
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[
|
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
|
12
|
-
#
|
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
|
9
|
-
#
|
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
|
-
#
|
17
|
-
# mount ImageUploader.direct_endpoint => "/attachments/images"
|
16
|
+
# mount ImageUploader::UploadEndpoint => "/attachments/images"
|
18
17
|
# end
|
19
18
|
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
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
|
-
#
|
37
|
-
#
|
38
|
-
# popular
|
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
|
-
# ##
|
39
|
+
# ## Limiting filesize
|
41
40
|
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
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://
|
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
|
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
|
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
|
-
#
|
87
|
+
# content_type: request.params["content_type"], # use "content_type" query parameter
|
79
88
|
# }
|
80
89
|
# end
|
81
90
|
#
|
82
|
-
#
|
83
|
-
#
|
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
|
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
|
-
#
|
129
|
-
def
|
130
|
-
|
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
|
-
|
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
|
-
#
|
136
|
-
def
|
137
|
-
|
138
|
-
|
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
|
-
|
156
|
-
|
157
|
-
|
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
|
-
|
161
|
-
|
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
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
-
|
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
|
|