shrine 0.9.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +663 -0
  4. data/doc/creating_plugins.md +100 -0
  5. data/doc/creating_storages.md +108 -0
  6. data/doc/direct_s3.md +97 -0
  7. data/doc/migrating_storage.md +79 -0
  8. data/doc/regenerating_versions.md +38 -0
  9. data/lib/shrine.rb +806 -0
  10. data/lib/shrine/plugins/activerecord.rb +89 -0
  11. data/lib/shrine/plugins/background_helpers.rb +148 -0
  12. data/lib/shrine/plugins/cached_attachment_data.rb +47 -0
  13. data/lib/shrine/plugins/data_uri.rb +93 -0
  14. data/lib/shrine/plugins/default_storage.rb +39 -0
  15. data/lib/shrine/plugins/delete_invalid.rb +25 -0
  16. data/lib/shrine/plugins/determine_mime_type.rb +119 -0
  17. data/lib/shrine/plugins/direct_upload.rb +274 -0
  18. data/lib/shrine/plugins/dynamic_storage.rb +57 -0
  19. data/lib/shrine/plugins/hooks.rb +123 -0
  20. data/lib/shrine/plugins/included.rb +48 -0
  21. data/lib/shrine/plugins/keep_files.rb +54 -0
  22. data/lib/shrine/plugins/logging.rb +158 -0
  23. data/lib/shrine/plugins/migration_helpers.rb +61 -0
  24. data/lib/shrine/plugins/moving.rb +75 -0
  25. data/lib/shrine/plugins/multi_delete.rb +47 -0
  26. data/lib/shrine/plugins/parallelize.rb +62 -0
  27. data/lib/shrine/plugins/pretty_location.rb +32 -0
  28. data/lib/shrine/plugins/recache.rb +36 -0
  29. data/lib/shrine/plugins/remote_url.rb +127 -0
  30. data/lib/shrine/plugins/remove_attachment.rb +59 -0
  31. data/lib/shrine/plugins/restore_cached.rb +36 -0
  32. data/lib/shrine/plugins/sequel.rb +94 -0
  33. data/lib/shrine/plugins/store_dimensions.rb +82 -0
  34. data/lib/shrine/plugins/validation_helpers.rb +168 -0
  35. data/lib/shrine/plugins/versions.rb +177 -0
  36. data/lib/shrine/storage/file_system.rb +165 -0
  37. data/lib/shrine/storage/linter.rb +94 -0
  38. data/lib/shrine/storage/s3.rb +118 -0
  39. data/lib/shrine/version.rb +14 -0
  40. data/shrine.gemspec +46 -0
  41. metadata +364 -0
@@ -0,0 +1,274 @@
1
+ require "roda"
2
+
3
+ require "json"
4
+ require "forwardable"
5
+ require "securerandom"
6
+
7
+ class Shrine
8
+ module Plugins
9
+ # The direct_upload plugin provides a Rack endpoint (implemented in [Roda])
10
+ # which you can use to implement AJAX uploads.
11
+ #
12
+ # plugin :direct_upload, max_size: 20*1024*1024
13
+ #
14
+ # This is how you could mount the endpoint in a Rails application:
15
+ #
16
+ # Rails.application.routes.draw do
17
+ # # adds `POST /attachments/images/:storage/:name`
18
+ # mount ImageUploader.direct_endpoint => "/attachments/images"
19
+ # end
20
+ #
21
+ # Note that you should mount a separate endpoint for each uploader that you
22
+ # want to use it with. This now gives your Ruby application a
23
+ # `POST /attachments/images/:storage/:name` route, which accepts a "file"
24
+ # query parameter:
25
+ #
26
+ # $ curl -F "file=@/path/to/avatar.jpg" localhost:3000/attachments/images/cache/avatar
27
+ # # {"id":"43kewit94.jpg","storage":"cache","metadata":{...}}
28
+ #
29
+ # The endpoint returns all responses in JSON format. There are many great
30
+ # JavaScript libraries for AJAX file uploads, so for example if we have
31
+ # this form:
32
+ #
33
+ # <%= form_for @user do |f| %>
34
+ # <%= f.hidden_field :avatar, value: @user.avatar_data %>
35
+ # <%= f.file_field :avatar %>
36
+ # <% end %>
37
+ #
38
+ # this is how we could hook up [jQuery-File-Upload] to our direct upload
39
+ # endpoint:
40
+ #
41
+ # $('[type="file"]').fileupload({
42
+ # url: '/attachments/images/cache/avatar',
43
+ # paramName: 'file',
44
+ # done: function(e, data) { $(this).prev().value(data.result) }
45
+ # });
46
+ #
47
+ # Now whenever a file gets chosen, the upload will automatically start in
48
+ # the background. It's typically good to show a progress bar to the user,
49
+ # which jQuery-File-Upload [supports]. After the upload has finished, the
50
+ # uploaded file JSON is written to the hidden field, and will be sent on
51
+ # form submit.
52
+ #
53
+ # ## Presigned
54
+ #
55
+ # An alternative to the direct endpoint is doing direct uploads to the
56
+ # underlying storage. These uploads usually requires extra information
57
+ # from the server, and this plugin can provide an endpoint to it, which
58
+ # you can enable by passing in `presign: true`:
59
+ #
60
+ # plugin :direct_upload, presign: true
61
+ #
62
+ # This will disable the default `POST /:storage/:name` route (for security
63
+ # reasons), and enable `GET /:storage/presign`. The response for that
64
+ # request looks something like this:
65
+ #
66
+ # {
67
+ # "url" => "https://shrine-testing.s3-eu-west-1.amazonaws.com",
68
+ # "fields" => {
69
+ # "key" => "b7d575850ba61b44c8a9ff889dfdb14d88cdc25f8dd121004c8",
70
+ # "policy" => "eyJleHBpcmF0aW9uIjoiMjAxNS0QwMToxMToyOVoiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJzaHJpbmUtdGVzdGluZyJ9LHsia2V5IjoiYjdkNTc1ODUwYmE2MWI0NGU3Y2M4YTliZmY4OGU5ZGZkYjE2NTQ0ZDk4OGNkYzI1ZjhkZDEyMTAwNGM4In0seyJ4LWFtei1jcmVkZW50aWFsIjoiQUtJQUlKRjU1VE1aWlk0NVVUNlEvMjAxNTEwMjQvZXUtd2VzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LHsieC1hbXotYWxnb3JpdGhtIjoiQVdTNC1ITUFDLVNIQTI1NiJ9LHsieC1hbXotZGF0ZSI6IjIwMTUxMDI0VDAwMTEyOVoifV19",
71
+ # "x-amz-credential" => "AKIAIJF55TMZYT6Q/20151024/eu-west-1/s3/aws4_request",
72
+ # "x-amz-algorithm" => "AWS4-HMAC-SHA256",
73
+ # "x-amz-date" => "20151024T001129Z",
74
+ # "x-amz-signature" => "c1eb634f83f96b69bd675f535b3ff15ae184b102fcba51e4db5f4959b4ae26f4"
75
+ # }
76
+ # }
77
+ #
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. `GET /:storage/presign` accepts additional parameters:
82
+ #
83
+ # :extension
84
+ # : The extension of the file being uploaded (e.g ".jpg").
85
+ #
86
+ # :content_type
87
+ # : Sets the Content-Type header for the uploaded file on S3.
88
+ #
89
+ # Example:
90
+ #
91
+ # GET /cache/presign?content_type=image/jpeg
92
+ # GET /cache/presign?extension=.png
93
+ #
94
+ # ## Constraints
95
+ #
96
+ # Note that the direct upload doesn't run validations, they are only run
97
+ # when attached to the record. If you want to limit the MIME type of files,
98
+ # you could add an ["accept" attribute] to your file field. You could also
99
+ # add client side validations for the maximum file size.
100
+ #
101
+ # It's encouraged that you set the `:max_size` option for the endpoint.
102
+ # Once set, when a file that is too big is uploaded, the endpoint will
103
+ # automatically delete the file and return a 413 response. However, if for
104
+ # whatever reason you don't want to impose a limit on filesize, you can set
105
+ # the option to nil:
106
+ #
107
+ # plugin :direct_upload, max_size: nil
108
+ #
109
+ # ## Allowed storages
110
+ #
111
+ # While Shrine only accepts cached attachments on form submits (for security
112
+ # reasons), you can use this endpoint to upload files to any storage, just
113
+ # add it do allowed storages:
114
+ #
115
+ # plugin :direct_upload, allowed_storages: [:cache, :store]
116
+ #
117
+ # ## Authentication
118
+ #
119
+ # If you want to authenticate the endpoint, you should be able to do it
120
+ # easily if your web framework has a good enough router. For example, in
121
+ # Rails you could add a `constraints` directive:
122
+ #
123
+ # Rails.application.routes.draw do
124
+ # constraints(->(r){r.env["warden"].authenticate!}) do
125
+ # mount ImageUploader.direct_endpoint => "/attachments/images"
126
+ # end
127
+ # end
128
+ #
129
+ # [Roda]: https://github.com/jeremyevans/roda
130
+ # [jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
131
+ # [supports]: https://github.com/blueimp/jQuery-File-Upload/wiki/Options#progress
132
+ # ["accept" attribute]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-accept
133
+ module DirectUpload
134
+ def self.configure(uploader, allowed_storages: [:cache], max_size:, presign: nil)
135
+ uploader.opts[:direct_upload_allowed_storages] = allowed_storages
136
+ uploader.opts[:direct_upload_max_size] = max_size
137
+ uploader.opts[:direct_upload_presign] = presign
138
+ end
139
+
140
+ module ClassMethods
141
+ # Return the cached Roda endpoint.
142
+ def direct_endpoint
143
+ @direct_endpoint ||= build_direct_endpoint
144
+ end
145
+
146
+ private
147
+
148
+ # Builds the endpoint and assigns it the current Shrine class.
149
+ def build_direct_endpoint
150
+ app = Class.new(App)
151
+ app.opts[:shrine_class] = self
152
+ app.app
153
+ end
154
+ end
155
+
156
+ class App < Roda
157
+ plugin :default_headers, "Content-Type"=>"application/json"
158
+ plugin :halt
159
+
160
+ # Routes incoming requests. We first check if the storage is allowed,
161
+ # then proceed further with the upload, returning the uploaded file
162
+ # as JSON.
163
+ route do |r|
164
+ r.on ":storage" do |storage_key|
165
+ allow_storage!(storage_key)
166
+ @uploader = shrine_class.new(storage_key.to_sym)
167
+
168
+ r.post ":name" do |name|
169
+ file = get_file
170
+ context = {name: name, phase: :cache}
171
+
172
+ json @uploader.upload(file, context)
173
+ end unless presign?
174
+
175
+ r.get "presign" do
176
+ location = SecureRandom.hex(30).to_s + r.params["extension"].to_s
177
+ options = {}
178
+ options[:content_length_range] = 0..max_size if max_size
179
+ options[:content_type] = r.params["content_type"] if r.params["content_type"]
180
+
181
+ signature = @uploader.storage.presign(location, options)
182
+
183
+ json Hash[url: signature.url, fields: signature.fields]
184
+ end if presign?
185
+ end
186
+ end
187
+
188
+ def json(object)
189
+ object.to_json
190
+ end
191
+
192
+ # Halts the request if storage is not allowed.
193
+ def allow_storage!(storage)
194
+ if !allowed_storages.map(&:to_s).include?(storage)
195
+ error! 403, "Storage #{storage.inspect} is not allowed."
196
+ end
197
+ end
198
+
199
+ # Returns the Rack file wrapped in an IO-like object. If "file" is
200
+ # missing or is too big, the request is halted.
201
+ def get_file
202
+ file = require_param!("file")
203
+ error! 400, "The \"file\" query parameter is not a file." if !(file.is_a?(Hash) && file.key?(:tempfile))
204
+ check_filesize!(file[:tempfile]) if max_size
205
+
206
+ RackFile.new(file)
207
+ end
208
+
209
+ # If the file is too big, deletes the file and halts the request.
210
+ def check_filesize!(file)
211
+ if file.size > max_size
212
+ file.delete
213
+ megabytes = max_size.to_f / 1024 / 1024
214
+ error! 413, "The file is too big (maximum size is #{megabytes} MB)."
215
+ end
216
+ end
217
+
218
+ # Loudly requires the param.
219
+ def require_param!(name)
220
+ request.params.fetch(name)
221
+ rescue KeyError
222
+ error! 400, "Missing query parameter: #{name.inspect}"
223
+ end
224
+
225
+ # Halts the request with the error message.
226
+ def error!(status, message)
227
+ request.halt status, {error: message}.to_json
228
+ end
229
+
230
+ def shrine_class
231
+ opts[:shrine_class]
232
+ end
233
+
234
+ def allowed_storages
235
+ shrine_class.opts[:direct_upload_allowed_storages]
236
+ end
237
+
238
+ def max_size
239
+ shrine_class.opts[:direct_upload_max_size]
240
+ end
241
+
242
+ def presign?
243
+ shrine_class.opts[:direct_upload_presign]
244
+ end
245
+ end
246
+
247
+ # This is used to wrap the Rack hash into an IO-like object which Shrine
248
+ # can upload.
249
+ class RackFile
250
+ attr_reader :original_filename, :content_type
251
+ attr_accessor :tempfile
252
+
253
+ def initialize(tempfile:, filename: nil, type: nil, **)
254
+ @tempfile = tempfile
255
+ @original_filename = filename
256
+ @content_type = type
257
+ end
258
+
259
+ def path
260
+ @tempfile.path
261
+ end
262
+
263
+ def to_io
264
+ @tempfile
265
+ end
266
+
267
+ extend Forwardable
268
+ delegate Shrine::IO_METHODS.keys => :@tempfile
269
+ end
270
+ end
271
+
272
+ register_plugin(:direct_upload, DirectUpload)
273
+ end
274
+ end
@@ -0,0 +1,57 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The dynamic_storage plugin allows you to register a storage using a
4
+ # regex, and evaluate the storage class dynamically depending on the regex.
5
+ #
6
+ # Example:
7
+ #
8
+ # plugin :dynamic_storage
9
+ #
10
+ # storage /store_(\w+)/ do |match|
11
+ # Shrine::Storages::S3.new(bucket: match[1])
12
+ # end
13
+ #
14
+ # The above example uses S3 storage where the bucket name depends on the
15
+ # storage name suffix. For example, `:store_foo` will use S3 storage which
16
+ # saves files to the bucket "foo". The block is yielded an instance of
17
+ # `MatchData`.
18
+ #
19
+ # This can be useful in combination with the default_storage plugin.
20
+ module DynamicStorage
21
+ module ClassMethods
22
+ def dynamic_storages
23
+ @dynamic_storages ||= {}
24
+ end
25
+
26
+ def storage(regex, &block)
27
+ dynamic_storages[regex] = block
28
+ end
29
+
30
+ def find_storage(name)
31
+ resolve_dynamic_storage(name) or super
32
+ end
33
+
34
+ private
35
+
36
+ def resolve_dynamic_storage(name)
37
+ dynamic_storage_cache.fetch(name) do
38
+ dynamic_storages.each do |regex, block|
39
+ if match = name.to_s.match(regex)
40
+ dynamic_storage_cache[name] = block.call(match)
41
+ break
42
+ end
43
+ end
44
+
45
+ dynamic_storage_cache[name]
46
+ end
47
+ end
48
+
49
+ def dynamic_storage_cache
50
+ @dynamic_storage_cache ||= {}
51
+ end
52
+ end
53
+ end
54
+
55
+ register_plugin(:dynamic_storage, DynamicStorage)
56
+ end
57
+ end
@@ -0,0 +1,123 @@
1
+ class Shrine
2
+ module Plugins
3
+ # The hooks plugin allows you to trigger some code around
4
+ # processing/storing/deleting of each file.
5
+ #
6
+ # plugin :hooks
7
+ #
8
+ # Shrine uses instance methods for hooks. To define a hook for an uploader,
9
+ # you just add an instance method to the uploader:
10
+ #
11
+ # class ImageUploader < Shrine
12
+ # def around_process(io, context)
13
+ # super
14
+ # rescue
15
+ # ExceptionNotifier.processing_failed(io, context)
16
+ # end
17
+ # end
18
+ #
19
+ # Each hook will be called with 2 arguments, `io` and `context`. It's
20
+ # generally good to always call super when overriding a hook, especially if
21
+ # you're using inheritance with your uploaders.
22
+ #
23
+ # Shrine calls hooks in the following order when uploading a file:
24
+ #
25
+ # * `around_process`
26
+ # * `before_process`
27
+ # * PROCESS
28
+ # * `after_process`
29
+ # * `around_store`
30
+ # * `before_store`
31
+ # * STORE
32
+ # * `after_store`
33
+ #
34
+ # Shrine calls hooks in the following order when deleting a file:
35
+ #
36
+ # * `around_delete`
37
+ # * `before_delete`
38
+ # * DELETE
39
+ # * `after_delete`
40
+ #
41
+ # It may be useful to know that you can realize some form of communication
42
+ # between the hooks; whatever you save to the `context` hash will be
43
+ # forwarded further down:
44
+ #
45
+ # class ImageUploader < Shrine
46
+ # def before_process(io, context)
47
+ # context[:_foo] = "bar"
48
+ # super
49
+ # end
50
+ #
51
+ # def before_store(io, context)
52
+ # context[:_foo] #=> "bar"
53
+ # super
54
+ # end
55
+ # end
56
+ #
57
+ # In that case you should always somehow mark this key as private (for
58
+ # example with an underscore) so that it doesn't clash with any
59
+ # existing keys.
60
+ module Hooks
61
+ module InstanceMethods
62
+ def processed(io, context)
63
+ result = nil
64
+ around_process(io, context) { result = super }
65
+ result
66
+ end
67
+ private :processed
68
+
69
+ def around_process(*args)
70
+ before_process(*args)
71
+ yield
72
+ after_process(*args)
73
+ end
74
+
75
+ def before_process(*)
76
+ end
77
+
78
+ def after_process(*)
79
+ end
80
+
81
+
82
+ def store(io, context = {})
83
+ result = nil
84
+ around_store(io, context) { result = super }
85
+ result
86
+ end
87
+
88
+ def around_store(*args)
89
+ before_store(*args)
90
+ yield
91
+ after_store(*args)
92
+ end
93
+
94
+ def before_store(*)
95
+ end
96
+
97
+ def after_store(*)
98
+ end
99
+
100
+
101
+ def delete(io, context = {})
102
+ result = nil
103
+ around_delete(io, context) { result = super }
104
+ result
105
+ end
106
+
107
+ def around_delete(*args)
108
+ before_delete(*args)
109
+ yield
110
+ after_delete(*args)
111
+ end
112
+
113
+ def before_delete(*)
114
+ end
115
+
116
+ def after_delete(*)
117
+ end
118
+ end
119
+ end
120
+
121
+ register_plugin(:hooks, Hooks)
122
+ end
123
+ end