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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +663 -0
- data/doc/creating_plugins.md +100 -0
- data/doc/creating_storages.md +108 -0
- data/doc/direct_s3.md +97 -0
- data/doc/migrating_storage.md +79 -0
- data/doc/regenerating_versions.md +38 -0
- data/lib/shrine.rb +806 -0
- data/lib/shrine/plugins/activerecord.rb +89 -0
- data/lib/shrine/plugins/background_helpers.rb +148 -0
- data/lib/shrine/plugins/cached_attachment_data.rb +47 -0
- data/lib/shrine/plugins/data_uri.rb +93 -0
- data/lib/shrine/plugins/default_storage.rb +39 -0
- data/lib/shrine/plugins/delete_invalid.rb +25 -0
- data/lib/shrine/plugins/determine_mime_type.rb +119 -0
- data/lib/shrine/plugins/direct_upload.rb +274 -0
- data/lib/shrine/plugins/dynamic_storage.rb +57 -0
- data/lib/shrine/plugins/hooks.rb +123 -0
- data/lib/shrine/plugins/included.rb +48 -0
- data/lib/shrine/plugins/keep_files.rb +54 -0
- data/lib/shrine/plugins/logging.rb +158 -0
- data/lib/shrine/plugins/migration_helpers.rb +61 -0
- data/lib/shrine/plugins/moving.rb +75 -0
- data/lib/shrine/plugins/multi_delete.rb +47 -0
- data/lib/shrine/plugins/parallelize.rb +62 -0
- data/lib/shrine/plugins/pretty_location.rb +32 -0
- data/lib/shrine/plugins/recache.rb +36 -0
- data/lib/shrine/plugins/remote_url.rb +127 -0
- data/lib/shrine/plugins/remove_attachment.rb +59 -0
- data/lib/shrine/plugins/restore_cached.rb +36 -0
- data/lib/shrine/plugins/sequel.rb +94 -0
- data/lib/shrine/plugins/store_dimensions.rb +82 -0
- data/lib/shrine/plugins/validation_helpers.rb +168 -0
- data/lib/shrine/plugins/versions.rb +177 -0
- data/lib/shrine/storage/file_system.rb +165 -0
- data/lib/shrine/storage/linter.rb +94 -0
- data/lib/shrine/storage/s3.rb +118 -0
- data/lib/shrine/version.rb +14 -0
- data/shrine.gemspec +46 -0
- metadata +364 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 21c82440698540e29ee787915b4bbdf9c3289f72
|
4
|
+
data.tar.gz: 70e3d33cd286ed3911df5d130b3f72f67c16e571
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 349f0b0bfd9c3f96e752a99e1e126c15adac5071130a0804407bd6990d76a831cb68ea86f8dd72888cf04891b4623cf61d0bd4ec5ce5eaf9e1401cea1c7c4c72
|
7
|
+
data.tar.gz: 2ba7dca90b8e1730a8fa2d303ad2750bd2d96c0d5cf52770486c912b85c63ff0c04ec4c60c48ad780a97ff5711b1da5a24d7ad9e28e381decd24b546c66ebbbd
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Janko Marohnić
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,663 @@
|
|
1
|
+
# Shrine
|
2
|
+
|
3
|
+
Shrine is a toolkit for file uploads in Ruby applications.
|
4
|
+
|
5
|
+
## Resources
|
6
|
+
|
7
|
+
* Documentation: [shrinerb.com](http://shrinerb.com)
|
8
|
+
* Source: [github.com/janko-m/shrine](https://github.com/janko-m/shrine)
|
9
|
+
* Bugs: [github.com/janko-m/shrine/issues](https://github.com/janko-m/shrine/issues)
|
10
|
+
* Discussion: [groups.google.com/group/ruby-shrine](https://groups.google.com/forum/#!forum/ruby-shrine)
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
```rb
|
15
|
+
gem "shrine"
|
16
|
+
```
|
17
|
+
|
18
|
+
Shrine has been tested on MRI 2.1, MRI 2.2 and JRuby.
|
19
|
+
|
20
|
+
## Basics
|
21
|
+
|
22
|
+
Here's a basic example showing how the file upload works:
|
23
|
+
|
24
|
+
```rb
|
25
|
+
require "shrine"
|
26
|
+
require "shrine/storage/file_system"
|
27
|
+
|
28
|
+
Shrine.storages[:file_system] = Shrine::Storage::FileSystem.new("uploads")
|
29
|
+
|
30
|
+
uploader = Shrine.new(:file_system)
|
31
|
+
|
32
|
+
uploaded_file = uploader.upload(File.open("avatar.jpg"))
|
33
|
+
uploaded_file #=> #<Shrine::UploadedFile>
|
34
|
+
uploaded_file.url #=> "uploads/9260ea09d8effd.jpg"
|
35
|
+
uploaded_file.data #=>
|
36
|
+
# {
|
37
|
+
# "storage" => "file_system",
|
38
|
+
# "id" => "9260ea09d8effd.jpg",
|
39
|
+
# "metadata" => {...},
|
40
|
+
# }
|
41
|
+
```
|
42
|
+
|
43
|
+
First we add the storage we want to use to Shrine's registry. Storages are
|
44
|
+
simple Ruby classes which perform the actual uploads. We instantiate a `Shrine`
|
45
|
+
with the storage name, and when we call `Shrine#upload` the following happens:
|
46
|
+
|
47
|
+
* a unique location is generated for the file
|
48
|
+
* metadata is extracted from the file
|
49
|
+
* the underlying storage is called to store the file
|
50
|
+
* a `Shrine::UploadedFile` is returned with these data
|
51
|
+
|
52
|
+
The argument to `Shrine#upload` needs to be an IO-like object. So, `File`,
|
53
|
+
`Tempfile` and `StringIO` are all valid arguments. But the object doesn't have
|
54
|
+
to be an actual IO, it's enough that it responds to these 5 methods:
|
55
|
+
`#read(*args)`, `#size`, `#eof?`, `#rewind` and `#close`.
|
56
|
+
`ActionDispatch::Http::UploadedFile` is one such object.
|
57
|
+
|
58
|
+
Now that we've uploaded the file to the underlying storage, we can download it:
|
59
|
+
|
60
|
+
```rb
|
61
|
+
file = uploaded_file.download
|
62
|
+
file #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20151004-74201-1t2jacf>
|
63
|
+
```
|
64
|
+
|
65
|
+
When we're done, we can delete the file:
|
66
|
+
|
67
|
+
```rb
|
68
|
+
uploader.delete(uploaded_file)
|
69
|
+
uploaded_file.exists? #=> false
|
70
|
+
```
|
71
|
+
|
72
|
+
## Attachment
|
73
|
+
|
74
|
+
In web applications, instead of managing files directly, we want to treat them
|
75
|
+
as "attachments" to models and to tie them to the lifecycle of records. Shrine
|
76
|
+
does this by generating and including "attachment" modules.
|
77
|
+
|
78
|
+
Firstly we need to assign the special `:cache` and `:store` storages:
|
79
|
+
|
80
|
+
```rb
|
81
|
+
Shrine.storages = {
|
82
|
+
cache: Shrine::Storage::FileSystem.new(Dir.tmpdir),
|
83
|
+
store: Shrine::Storage::FileSystem.new("public", subdirectory: "uploads"),
|
84
|
+
}
|
85
|
+
```
|
86
|
+
|
87
|
+
Next we should create an uploader specific to the type of files we're
|
88
|
+
uploading:
|
89
|
+
|
90
|
+
```rb
|
91
|
+
class ImageUploader < Shrine
|
92
|
+
# here goes your uploading logic
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
Now if we assume that we have a "User" model, and we want our users to have an
|
97
|
+
"avatar", we can generate and include an "attachment" module:
|
98
|
+
|
99
|
+
```rb
|
100
|
+
class User
|
101
|
+
attr_accessor :avatar_data
|
102
|
+
|
103
|
+
include ImageUploader[:avatar]
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
Now our model has gained special methods for attaching avatars:
|
108
|
+
|
109
|
+
```rb
|
110
|
+
user = User.new
|
111
|
+
user.avatar = File.open("avatar.jpg") # uploads the file to `:cache`
|
112
|
+
user.avatar #=> #<Shrine::UploadedFile>
|
113
|
+
user.avatar_url #=> "/uploads/9260ea09d8effd.jpg"
|
114
|
+
user.avatar_data #=>
|
115
|
+
# {
|
116
|
+
# "storage" => "cache",
|
117
|
+
# "id" => "9260ea09d8effd.jpg",
|
118
|
+
# "metadata" => {...},
|
119
|
+
# }
|
120
|
+
```
|
121
|
+
|
122
|
+
The attachment module has added `#avatar`, `#avatar=` and `#avatar_url`
|
123
|
+
methods to our User. This is what's happening:
|
124
|
+
|
125
|
+
```rb
|
126
|
+
Shrine[:avatar] #=> #<Shrine::Attachment(avatar)>
|
127
|
+
Shrine[:avatar].class #=> Module
|
128
|
+
Shrine[:avatar].instance_methods #=> [:avatar=, :avatar, :avatar_url, ...]
|
129
|
+
|
130
|
+
Shrine[:document] #=> #<Shrine::Attachment(document)>
|
131
|
+
Shrine[:document].instance_methods #=> [:document=, :document, :document_url, ...]
|
132
|
+
|
133
|
+
# If you prefer to be more explicit, you can use the expanded forms
|
134
|
+
Shrine.attachment(:avatar)
|
135
|
+
Shrine::Attachment.new(:document)
|
136
|
+
```
|
137
|
+
|
138
|
+
The setter (`#avatar=`) caches the assigned file and writes it to the "data"
|
139
|
+
column (`avatar_data`). The getter (`#avatar`) reads the "data" column and
|
140
|
+
returns a `Shrine::UploadedFile`. The url method (`#avatar_url`) calls
|
141
|
+
`avatar.url` if the attachment is present, otherwise returns nil.
|
142
|
+
|
143
|
+
### ORM
|
144
|
+
|
145
|
+
Your models probably won't be POROs, so Shrine ships with plugins for
|
146
|
+
Sequel and ActiveRecord ORMs. Shrine uses the "\<attachment\>\_data" column
|
147
|
+
for storing attachments, so you'll need to add it in a migration:
|
148
|
+
|
149
|
+
```rb
|
150
|
+
add_column :users, :avatar_data, :text
|
151
|
+
```
|
152
|
+
```rb
|
153
|
+
Shrine.plugin :sequel
|
154
|
+
```
|
155
|
+
```rb
|
156
|
+
class User < Sequel::Model
|
157
|
+
include ImageUploader[:avatar]
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
In addition to getters and setters, the ORM plugins add the appropriate
|
162
|
+
callbacks:
|
163
|
+
|
164
|
+
```rb
|
165
|
+
user.avatar = File.open("avatar.jpg")
|
166
|
+
user.avatar.storage_key #=> "cache"
|
167
|
+
user.save
|
168
|
+
user.avatar.storage_key #=> "store"
|
169
|
+
user.destroy
|
170
|
+
user.avatar.exists? #=> false
|
171
|
+
```
|
172
|
+
|
173
|
+
This is how you would typically create the form for a `@user`:
|
174
|
+
|
175
|
+
```erb
|
176
|
+
<form action="/users" method="post" enctype="multipart/form-data">
|
177
|
+
<input name="user[avatar]" type="hidden" value="<%= @user.avatar_data %>">
|
178
|
+
<input name="user[avatar]" type="file">
|
179
|
+
</form>
|
180
|
+
```
|
181
|
+
|
182
|
+
The "file" field is for file upload, while the "hidden" field is to make the
|
183
|
+
file persist in case of validation errors, and for direct uploads.
|
184
|
+
|
185
|
+
## Direct uploads
|
186
|
+
|
187
|
+
Shrine comes with a `direct_upload` plugin which provides an endpoint
|
188
|
+
(implemented in [Roda]) that can be used for AJAX uploads.
|
189
|
+
|
190
|
+
```rb
|
191
|
+
Shrine.plugin :direct_upload # Exposes a Roda endpoint
|
192
|
+
```
|
193
|
+
```rb
|
194
|
+
Rails.application.routes.draw do
|
195
|
+
# adds `POST /attachments/images/:storage/:name`
|
196
|
+
mount ImageUploader.direct_endpoint => "/attachments/images"
|
197
|
+
end
|
198
|
+
```
|
199
|
+
```sh
|
200
|
+
$ curl -F "file=@/path/to/avatar.jpg" localhost:3000/attachments/images/cache/avatar
|
201
|
+
# {"id":"43kewit94.jpg","storage":"cache","metadata":{...}}
|
202
|
+
```
|
203
|
+
|
204
|
+
There are many great JavaScript libraries for AJAX file uploads, for example
|
205
|
+
this is how we could hook up [jQuery-File-Upload] to our endpoint:
|
206
|
+
|
207
|
+
```js
|
208
|
+
$('[type="file"]').fileupload({
|
209
|
+
url: '/attachments/images/cache/avatar',
|
210
|
+
paramName: 'file',
|
211
|
+
done: function(e, data) { $(this).prev().value(data.result) }
|
212
|
+
});
|
213
|
+
```
|
214
|
+
|
215
|
+
This plugin also provides a route for direct S3 uploads. See the [example app]
|
216
|
+
for how you can do multiple uploads directly to S3.
|
217
|
+
|
218
|
+
## Processing
|
219
|
+
|
220
|
+
Whenever a file is uploaded, `Shrine#process` is called, and this is where
|
221
|
+
you're expected to define your processing.
|
222
|
+
|
223
|
+
```rb
|
224
|
+
class ImageUploader < Shrine
|
225
|
+
def process(io, context)
|
226
|
+
if context[:phase] == :store
|
227
|
+
# processing...
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
```
|
232
|
+
|
233
|
+
The `io` is the file being uploaded, and `context` we'll leave for later. You
|
234
|
+
may be wondering why we need this conditional. Well, when an attachment is
|
235
|
+
assigned and saved, an "upload" actually happens two times. First the file is
|
236
|
+
"uploaded" to `:cache` on assignment, and then the cached file is reuploaded to
|
237
|
+
`:store` on save.
|
238
|
+
|
239
|
+
Ok, now how do we do the actual processing? Well, Shrine actually doesn't ship
|
240
|
+
with any image processing functionality, because that is a generic problem that
|
241
|
+
belongs in a separate gem. So, I created the [image_processing] gem which you
|
242
|
+
can use with Shrine:
|
243
|
+
|
244
|
+
```rb
|
245
|
+
require "image_processing/mini_magick"
|
246
|
+
|
247
|
+
class ImageUploader < Shrine
|
248
|
+
include ImageProcessing::MiniMagick
|
249
|
+
|
250
|
+
def process(io, context)
|
251
|
+
if context[:phase] == :store
|
252
|
+
process_to_limit!(io.download, 700, 700)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
```
|
257
|
+
|
258
|
+
Notice that we needed to call `io.download`. This is because the original file
|
259
|
+
was already stored to `:cache`, and now this cached file is being uploaded to
|
260
|
+
`:store`. The cached file is an instance of `Shrine::UploadedFile`, but for
|
261
|
+
processing we need to work with actual files, so we first need to download it.
|
262
|
+
|
263
|
+
In general, processing works in a way that if `#process` returns a file, Shrine
|
264
|
+
continues storing that file, otherwise if nil is returned, Shrine continues
|
265
|
+
storing the original file.
|
266
|
+
|
267
|
+
### Versions
|
268
|
+
|
269
|
+
Often you'll want to store various thumbnails alongside your original image.
|
270
|
+
For that you just need to load the `versions` plugin, and now in `#process`
|
271
|
+
you can return a Hash of versions:
|
272
|
+
|
273
|
+
```rb
|
274
|
+
require "image_processing/mini_magick"
|
275
|
+
|
276
|
+
class ImageUploader < Shrine
|
277
|
+
include ImageProcessing::MiniMagick
|
278
|
+
plugin :versions, names: [:large, :medium, :small]
|
279
|
+
|
280
|
+
def process(io, context)
|
281
|
+
if context[:phase] == :store
|
282
|
+
size_700 = process_to_limit!(io.download, 700, 700)
|
283
|
+
size_500 = process_to_limit!(size_700, 500, 500)
|
284
|
+
size_300 = process_to_limit!(size_500, 300, 300)
|
285
|
+
|
286
|
+
{large: size_700, medium: size_500, small: size_300}
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
```
|
291
|
+
|
292
|
+
As you see, instead of a complex class-level DSL, Shrine provides a very simple
|
293
|
+
instance-level interface where you're in complete control over processing. The
|
294
|
+
processed files are Ruby Tempfiles and they should eventually get deleted by
|
295
|
+
themselves, but you can also use the `moving` plugin to delete them immediately
|
296
|
+
after upload.
|
297
|
+
|
298
|
+
Now when you access the stored attachment, a Hash of versions will be returned
|
299
|
+
instead:
|
300
|
+
|
301
|
+
```rb
|
302
|
+
user.avatar #=>
|
303
|
+
# {
|
304
|
+
# large: #<Shrine::UploadedFile>,
|
305
|
+
# medium: #<Shrine::UploadedFile>,
|
306
|
+
# small: #<Shrine::UploadedFile>,
|
307
|
+
# }
|
308
|
+
user.avatar.class #=> Hash
|
309
|
+
|
310
|
+
# With the store_dimensions plugin
|
311
|
+
user.avatar[:large].width #=> 700
|
312
|
+
user.avatar[:medium].width #=> 500
|
313
|
+
user.avatar[:small].width #=> 300
|
314
|
+
|
315
|
+
# The plugin expands this method to accept version names.
|
316
|
+
user.avatar_url(:large) #=> "..."
|
317
|
+
```
|
318
|
+
|
319
|
+
## Context
|
320
|
+
|
321
|
+
You may have noticed the `context` variable as the second argument to
|
322
|
+
`Shrine#process`. This variable contains information about the context in
|
323
|
+
which the file is uploaded.
|
324
|
+
|
325
|
+
```rb
|
326
|
+
class ImageUploader < Shrine
|
327
|
+
def process(io, context)
|
328
|
+
puts context
|
329
|
+
end
|
330
|
+
end
|
331
|
+
```
|
332
|
+
```rb
|
333
|
+
user = User.new
|
334
|
+
user.avatar = File.open("avatar.jpg") # "cache"
|
335
|
+
user.save # "store"
|
336
|
+
```
|
337
|
+
```
|
338
|
+
{:name=>:avatar, :record=>#<User:0x007fe1627f1138>, :phase=>:cache}
|
339
|
+
{:name=>:avatar, :record=>#<User:0x007fe1627f1138>, :phase=>:store}
|
340
|
+
```
|
341
|
+
|
342
|
+
The `:name` is the name of the attachment, in this case "avatar". The `:record`
|
343
|
+
is the model instance, in this case instance of `User`. As for `:phase`, in web
|
344
|
+
applications a file upload isn't an event that happens at once, it's a process
|
345
|
+
that happens in *phases*. By default there are only 2 phases, "cache" and
|
346
|
+
"store", other plugins add more of them.
|
347
|
+
|
348
|
+
Context is really useful for doing conditional processing and validation, since
|
349
|
+
we have access to the record and attachment name. In general the context is
|
350
|
+
used deeply in Shrine for various purposes.
|
351
|
+
|
352
|
+
## Validations
|
353
|
+
|
354
|
+
Validations are registered by calling `Shrine::Attacher.validate`, and are best
|
355
|
+
done with the `validation_helpers` plugin:
|
356
|
+
|
357
|
+
```rb
|
358
|
+
class ImageUploader < Shrine
|
359
|
+
plugin :validation_helpers
|
360
|
+
|
361
|
+
Attacher.validate do
|
362
|
+
# Evaluated inside an instance of Shrine::Attacher.
|
363
|
+
if record.guest?
|
364
|
+
validate_max_size 2*1024*1024, message: "is too large (max is 2 MB)"
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
368
|
+
```
|
369
|
+
|
370
|
+
```rb
|
371
|
+
user = User.new
|
372
|
+
user.avatar = File.open("big_image.jpg")
|
373
|
+
user.valid? #=> false
|
374
|
+
user.errors.to_hash #=> {avatar: ["is too large (max is 2 MB)"]}
|
375
|
+
```
|
376
|
+
|
377
|
+
## Metadata
|
378
|
+
|
379
|
+
By default Shrine extracts and stores general file metadata:
|
380
|
+
|
381
|
+
```rb
|
382
|
+
class UsersController < ApplicationController
|
383
|
+
def create
|
384
|
+
user = User.create(params[:user])
|
385
|
+
user.avatar.metadata #=>
|
386
|
+
# {
|
387
|
+
# "filename" => "my_avatar.jpg",
|
388
|
+
# "mime_type" => "image/jpeg",
|
389
|
+
# "size" => 345993,
|
390
|
+
# }
|
391
|
+
|
392
|
+
user.avatar.original_filename #=> "my_avatar.jpg"
|
393
|
+
user.avatar.mime_type #=> "image/jpeg"
|
394
|
+
user.avatar.size #=> 345993
|
395
|
+
end
|
396
|
+
end
|
397
|
+
```
|
398
|
+
|
399
|
+
### MIME type
|
400
|
+
|
401
|
+
By default, "mime_type" is inherited from `#content_type` of the uploaded file.
|
402
|
+
In case of Rails, this value is set from the `Content-Type` header, which the
|
403
|
+
browser sets solely based on the extension of the uploaded file. This means
|
404
|
+
that by default Shrine's "mime_type" is *not* guaranteed to hold the actual
|
405
|
+
MIME type of the file.
|
406
|
+
|
407
|
+
To help with that Shrine provides the `extract_mime_type` plugin, which by
|
408
|
+
deafult uses the UNIX [file] utility to determine the actual MIME type:
|
409
|
+
|
410
|
+
```rb
|
411
|
+
Shrine.plugin :extract_mime_type
|
412
|
+
```
|
413
|
+
```rb
|
414
|
+
user = User.create(avatar: File.open("image.mp4")) # image with a .mp4 extension
|
415
|
+
user.avatar.mime_type #=> "image/png"
|
416
|
+
```
|
417
|
+
|
418
|
+
### Dimensions
|
419
|
+
|
420
|
+
Shrine ships with the `store_dimensions` plugin which extracts dimensions
|
421
|
+
using the [fastimage] gem.
|
422
|
+
|
423
|
+
```rb
|
424
|
+
ImageUploader.plugin :store_dimensions
|
425
|
+
```
|
426
|
+
```rb
|
427
|
+
user = User.create(avatar: File.open("image.jpg"))
|
428
|
+
user.avatar.width #=> 400
|
429
|
+
user.avatar.height #=> 500
|
430
|
+
```
|
431
|
+
|
432
|
+
The fastimage gem has built-in protection against [image bombs].
|
433
|
+
|
434
|
+
### Custom metadata
|
435
|
+
|
436
|
+
You can also extract and store custom metadata, by overriding
|
437
|
+
`Shrine#extract_metadata`:
|
438
|
+
|
439
|
+
```rb
|
440
|
+
class ImageUploader < Shrine
|
441
|
+
def extract_metadata(io, context)
|
442
|
+
metadata = super
|
443
|
+
metadata["custom"] = extract_custom(io)
|
444
|
+
metadata
|
445
|
+
end
|
446
|
+
end
|
447
|
+
```
|
448
|
+
|
449
|
+
## Default URL
|
450
|
+
|
451
|
+
When attachment is missing, `user.avatar_url` by default returns nil. This
|
452
|
+
because it internally calls `Shrine#default_url`, which returns nil unless
|
453
|
+
overriden. For custom default URLs simply override the method:
|
454
|
+
|
455
|
+
```rb
|
456
|
+
class ImageUploader < Shrine
|
457
|
+
def default_url(context)
|
458
|
+
"/images/fallback/#{context[:name]}.png"
|
459
|
+
end
|
460
|
+
end
|
461
|
+
```
|
462
|
+
|
463
|
+
## Locations
|
464
|
+
|
465
|
+
By default files will all be put in the same folder. If you want that each
|
466
|
+
record has its own directory, you can use the `pretty_location` plugin:
|
467
|
+
|
468
|
+
```rb
|
469
|
+
Shrine.plugin :pretty_location
|
470
|
+
```
|
471
|
+
```rb
|
472
|
+
user = User.create(avatar: File.open("avatar.jpg"))
|
473
|
+
user.avatar.id #=> "user/34/avatar/34krtreds2df.jpg"
|
474
|
+
```
|
475
|
+
|
476
|
+
If you want to generate your own locations, simply override
|
477
|
+
`Shrine#generate_location`:
|
478
|
+
|
479
|
+
```rb
|
480
|
+
class ImageUploader < Shrine
|
481
|
+
def generate_location(io, context)
|
482
|
+
# your custom logic
|
483
|
+
end
|
484
|
+
end
|
485
|
+
```
|
486
|
+
|
487
|
+
Note that in this case should be careful to make the locations unique,
|
488
|
+
otherwise dirty tracking won't be detected properly (you can use
|
489
|
+
`Shrine#generate_uid`).
|
490
|
+
|
491
|
+
When using `Shrine` directly you can bypass `#generate_location` by passing in
|
492
|
+
`:location`
|
493
|
+
|
494
|
+
```rb
|
495
|
+
file = File.open("avatar.jpg")
|
496
|
+
Shrine.new(:store).upload(file, location: "a/specific/location.jpg")
|
497
|
+
```
|
498
|
+
|
499
|
+
## Amazon S3
|
500
|
+
|
501
|
+
So far in the examples we've only used the FileSystem storage. However, Shrine
|
502
|
+
also ships with S3 storage (which internally uses the [aws-sdk] gem).
|
503
|
+
|
504
|
+
```
|
505
|
+
gem "aws-sdk", "~> 2.1"
|
506
|
+
```
|
507
|
+
|
508
|
+
It's typically good to use FileSystem for `:cache`, and S3 for `:store`:
|
509
|
+
|
510
|
+
```rb
|
511
|
+
require "shrine"
|
512
|
+
require "shrine/storage/file_system"
|
513
|
+
require "shrine/storage/s3"
|
514
|
+
|
515
|
+
s3_options = {
|
516
|
+
access_key_id: "<ACCESS_KEY_ID>", # "xyz"
|
517
|
+
secret_access_key: "<SECRET_ACCESS_KEY>", # "abc"
|
518
|
+
region: "<REGION>", # "eu-west-1"
|
519
|
+
bucket: "<BUCKET>", # "my-app"
|
520
|
+
}
|
521
|
+
|
522
|
+
Shrine.storages = {
|
523
|
+
cache: Shrine::Storage::FileSystem.new("public", subdirectory: "uploads"),
|
524
|
+
store: Shrine::Storage::S3.new(s3_options),
|
525
|
+
}
|
526
|
+
```
|
527
|
+
|
528
|
+
```rb
|
529
|
+
user = User.new(avatar: File.open(:avatar))
|
530
|
+
user.avatar.url #=> "/uploads/j4k343ui12ls9.jpg"
|
531
|
+
user.save
|
532
|
+
user.avatar.url #=> "https://s3-sa-east-1.amazonaws.com/my-bucket/0943sf8gfk13.jpg"
|
533
|
+
```
|
534
|
+
|
535
|
+
If you're using S3 for both `:cache` and `:store`, saving the record will
|
536
|
+
execute an S3 COPY command if possible, which avoids reuploading the file.
|
537
|
+
Also, the `versions` plugin takes advantage of S3's MULTI DELETE capabilities,
|
538
|
+
so versions are deleted with a single HTTP request.
|
539
|
+
|
540
|
+
## Background jobs
|
541
|
+
|
542
|
+
Unlike other uploading libraries, Shrine embraces that putting phases of file
|
543
|
+
upload into background jobs is essential for scaling and good user experience,
|
544
|
+
so it ships with `background_helpers` plugin which makes backgrounding really
|
545
|
+
easy:
|
546
|
+
|
547
|
+
```rb
|
548
|
+
Shrine.plugin :background_helpers
|
549
|
+
Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
|
550
|
+
Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
551
|
+
```
|
552
|
+
```rb
|
553
|
+
class UploadJob
|
554
|
+
include Sidekiq::Worker
|
555
|
+
def perform(data)
|
556
|
+
Shrine::Attacher.promote(data)
|
557
|
+
end
|
558
|
+
end
|
559
|
+
```
|
560
|
+
```rb
|
561
|
+
class DeleteJob
|
562
|
+
include Sidekiq::Worker
|
563
|
+
def perform(data)
|
564
|
+
Shrine::Attacher.delete(data)
|
565
|
+
end
|
566
|
+
end
|
567
|
+
```
|
568
|
+
|
569
|
+
The above puts all promoting (moving to store) and deleting of files into a
|
570
|
+
background Sidekiq job. Obviously instead of Sidekiq you can just as well use
|
571
|
+
any other backgrounding library.
|
572
|
+
|
573
|
+
### Seamless user experience
|
574
|
+
|
575
|
+
In combination with direct upload for caching, this provides a completely
|
576
|
+
seamless user experience. First the user ansynchronosuly caches the file and
|
577
|
+
hopefully sees a nice progress bar. After this is finishes and user submits the
|
578
|
+
form, promoting will be kicked off into a background job, and the record will
|
579
|
+
be saved with the cached image. If your cache is public (e.g. in the "public"
|
580
|
+
folder), the end user will immediately see their uploaded file, because the URL
|
581
|
+
will point to the cached version.
|
582
|
+
|
583
|
+
In the meanwhile, what `#promote` does is it uploads the cached file `:store`,
|
584
|
+
and writes the stored file to the column. When the record gets saved, the URL
|
585
|
+
will switch from filesystem to S3, but the user won't even notice that
|
586
|
+
something happened, because they will still see the same image.
|
587
|
+
|
588
|
+
### Generality
|
589
|
+
|
590
|
+
This solution is completely agnostic about what kind of attachment it is
|
591
|
+
uploading/deleting, and for which model. This means that all attachments can
|
592
|
+
use this same worker. Also, there is no need for any extra columns.
|
593
|
+
|
594
|
+
### Safety
|
595
|
+
|
596
|
+
It is possible that the user changes their mind and reuploads a new file before
|
597
|
+
the background job finished promoting. With a naive implementation, this means
|
598
|
+
that after uploading a new file, there can happen a brief moment where the user
|
599
|
+
sees the old file again, which can be upsetting.
|
600
|
+
|
601
|
+
Shrine handles this gracefully. After `#promote` uploads the cached file to
|
602
|
+
`:store`, it checks if the cached file still matches the file in the record
|
603
|
+
column. If the files are different, that means the user uploaded a new
|
604
|
+
attachment, and Shrine won't do the replacement. Additionally, this job is
|
605
|
+
idempotent, meaning it can be safely repeated in case of failure.
|
606
|
+
|
607
|
+
## Clearing cache
|
608
|
+
|
609
|
+
Your `:cache` storage will grow over time, so you'll want to periodically clean
|
610
|
+
it. If you're using FileSystem as your `:cache`, you can put this in a
|
611
|
+
scheduled job:
|
612
|
+
|
613
|
+
```rb
|
614
|
+
file_system = Shrine.storages[:cache]
|
615
|
+
file_system.clear!(older_than: 1.week.ago) # adjust the time
|
616
|
+
```
|
617
|
+
|
618
|
+
If your `:cache` is S3, Amazon provides settings for automatic cache clearing,
|
619
|
+
see [this article](http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html).
|
620
|
+
|
621
|
+
## Plugins
|
622
|
+
|
623
|
+
Shrine comes with a small core which provides only the essential functionality.
|
624
|
+
However, it comes with a lot of additional features which can be loaded via
|
625
|
+
plugins. This way you can choose exactly how much Shrine does for you. Shrine
|
626
|
+
itself [ships with over 25 plugins], most of them I haven't managed to cover
|
627
|
+
here.
|
628
|
+
|
629
|
+
The plugin system respects inheritance, so you can choose which plugins will
|
630
|
+
be applied to which uploaders:
|
631
|
+
|
632
|
+
```rb
|
633
|
+
Shrine.plugin :logging # enables logging for all uploaders
|
634
|
+
|
635
|
+
class ImageUploader < Shrine
|
636
|
+
plugin :store_dimensions # stores dimensions only for this uploader
|
637
|
+
end
|
638
|
+
```
|
639
|
+
|
640
|
+
## Inspiration
|
641
|
+
|
642
|
+
Shrine was heavily inspired by [Refile] and [Roda]. From Refile it borrows the
|
643
|
+
idea of "backends" (here named "storages"), attachment interface, and direct
|
644
|
+
uploads. From Roda it borrows the implementation of an extensible [plugin
|
645
|
+
system].
|
646
|
+
|
647
|
+
## License
|
648
|
+
|
649
|
+
The gem is available as open source under the terms of the [MIT License].
|
650
|
+
|
651
|
+
[Contributor Covenant]: http://contributor-covenant.org
|
652
|
+
[image_processing]: https://github.com/janko-m/image_processing
|
653
|
+
[fastimage]: https://github.com/sdsykes/fastimage
|
654
|
+
[file]: http://linux.die.net/man/1/file
|
655
|
+
[image bombs]: https://www.bamsoftware.com/hacks/deflate.html
|
656
|
+
[aws-sdk]: https://github.com/aws/aws-sdk-ruby
|
657
|
+
[jQuery-File-Upload]: https://github.com/blueimp/jQuery-File-Upload
|
658
|
+
[Roda]: https://github.com/jeremyevans/roda
|
659
|
+
[Refile]: https://github.com/refile/refile
|
660
|
+
[plugin system]: http://twin.github.io/the-plugin-system-of-sequel-and-roda/
|
661
|
+
[MIT License]: http://opensource.org/licenses/MIT
|
662
|
+
[example app]: https://github.com/janko-m/shrine-example
|
663
|
+
[ships with over 25 plugins]: http://shrinerb.com#plugins
|