shrine 1.1.0 → 1.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.
- checksums.yaml +4 -4
- data/README.md +55 -55
- data/doc/changing_location.md +1 -1
- data/doc/direct_s3.md +9 -4
- data/doc/refile.md +1 -1
- data/doc/regenerating_versions.md +5 -5
- data/lib/shrine.rb +26 -18
- data/lib/shrine/plugins/activerecord.rb +11 -10
- data/lib/shrine/plugins/backgrounding.rb +36 -46
- data/lib/shrine/plugins/data_uri.rb +1 -1
- data/lib/shrine/plugins/direct_upload.rb +5 -5
- data/lib/shrine/plugins/migration_helpers.rb +9 -9
- data/lib/shrine/plugins/remove_attachment.rb +3 -2
- data/lib/shrine/plugins/sequel.rb +15 -14
- data/lib/shrine/plugins/store_dimensions.rb +2 -2
- data/lib/shrine/plugins/validation_helpers.rb +7 -7
- data/lib/shrine/plugins/versions.rb +2 -2
- data/lib/shrine/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 685289a1b672f363582842cdb2109c4197f9e735
|
4
|
+
data.tar.gz: 37a64e5704dcc6125074dc2333ecf570ba5eaede
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 15150fb3c6d90f397c6e35f714b4eeed02d9c30ee8497fc9763f689b7e7732612e96a896c9d912cf04161a564e5196e73ba20f87df8d489d1562ffe4381dcf7b
|
7
|
+
data.tar.gz: eb4f056cf14cbacb40ee272008c08623e72767965b808d376f4880b83088a0bd57a20282ac7eb05138aea6dc6c0eae9d3c75c16b8bb8faae940816ee1d079fdc
|
data/README.md
CHANGED
@@ -22,7 +22,7 @@ Shrine has been tested on MRI 2.1, MRI 2.2, JRuby and Rubinius.
|
|
22
22
|
|
23
23
|
## Basics
|
24
24
|
|
25
|
-
Here's a basic example showing how
|
25
|
+
Here's a basic example showing how file upload works in Shrine:
|
26
26
|
|
27
27
|
```rb
|
28
28
|
require "shrine"
|
@@ -32,13 +32,13 @@ Shrine.storages[:file_system] = Shrine::Storage::FileSystem.new("uploads")
|
|
32
32
|
|
33
33
|
uploader = Shrine.new(:file_system)
|
34
34
|
|
35
|
-
uploaded_file = uploader.upload(File.open("
|
35
|
+
uploaded_file = uploader.upload(File.open("movie.mp4"))
|
36
36
|
uploaded_file #=> #<Shrine::UploadedFile>
|
37
|
-
uploaded_file.url #=> "/uploads/9260ea09d8effd.
|
37
|
+
uploaded_file.url #=> "/uploads/9260ea09d8effd.mp4"
|
38
38
|
uploaded_file.data #=>
|
39
39
|
# {
|
40
40
|
# "storage" => "file_system",
|
41
|
-
# "id" => "9260ea09d8effd.
|
41
|
+
# "id" => "9260ea09d8effd.mp4",
|
42
42
|
# "metadata" => {...},
|
43
43
|
# }
|
44
44
|
```
|
@@ -59,24 +59,21 @@ to be an actual IO, it's enough that it responds to these 5 methods:
|
|
59
59
|
`#read(*args)`, `#size`, `#eof?`, `#rewind` and `#close`.
|
60
60
|
`ActionDispatch::Http::UploadedFile` is one such object.
|
61
61
|
|
62
|
-
The returned `Shrine::UploadedFile` represents the file
|
63
|
-
and we can do a lot with it:
|
62
|
+
The returned object is a [`Shrine::UploadedFile`], which represents the file
|
63
|
+
that was uploaded, and we can do a lot with it:
|
64
64
|
|
65
65
|
```rb
|
66
|
-
uploaded_file.url #=> "/uploads/938kjsdf932.
|
66
|
+
uploaded_file.url #=> "/uploads/938kjsdf932.mp4"
|
67
67
|
uploaded_file.read #=> "..."
|
68
68
|
uploaded_file.exists? #=> true
|
69
|
-
uploaded_file.download #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20151004-74201-1t2jacf>
|
69
|
+
uploaded_file.download #=> #<Tempfile:/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20151004-74201-1t2jacf.mp4>
|
70
70
|
uploaded_file.metadata #=> {...}
|
71
|
+
uploaded_file.delete
|
72
|
+
# ...
|
71
73
|
```
|
72
74
|
|
73
75
|
To read about the metadata that is stored with the uploaded file, see the
|
74
|
-
[metadata](#metadata) section.
|
75
|
-
it.
|
76
|
-
|
77
|
-
```rb
|
78
|
-
uploaded_file.delete
|
79
|
-
```
|
76
|
+
[metadata](#metadata) section.
|
80
77
|
|
81
78
|
## Attachment
|
82
79
|
|
@@ -101,7 +98,7 @@ should create an uploader specific to the type of files we're uploading:
|
|
101
98
|
|
102
99
|
```rb
|
103
100
|
class ImageUploader < Shrine
|
104
|
-
# logic for uploading
|
101
|
+
# your logic for uploading files
|
105
102
|
end
|
106
103
|
```
|
107
104
|
|
@@ -110,9 +107,7 @@ Now if we assume that we have a "User" model, and we want our users to have an
|
|
110
107
|
|
111
108
|
```rb
|
112
109
|
class User
|
113
|
-
|
114
|
-
|
115
|
-
include ImageUploader[:avatar]
|
110
|
+
include ImageUploader[:avatar] # requires "avatar_data" attribute
|
116
111
|
end
|
117
112
|
```
|
118
113
|
|
@@ -127,7 +122,7 @@ user.avatar_data #=> "{\"storage\":\"cache\",\"id\":\"9260ea09d8effd.jpg\",\"met
|
|
127
122
|
```
|
128
123
|
|
129
124
|
The attachment module has added `#avatar`, `#avatar=` and `#avatar_url`
|
130
|
-
methods to our User
|
125
|
+
methods to our User, using regular module inclusion.
|
131
126
|
|
132
127
|
```rb
|
133
128
|
Shrine[:avatar] #=> #<Shrine::Attachment(avatar)>
|
@@ -142,10 +137,9 @@ Shrine.attachment(:avatar)
|
|
142
137
|
Shrine::Attachment.new(:document)
|
143
138
|
```
|
144
139
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
`avatar.url` if the attachment is present, otherwise returns nil.
|
140
|
+
* `#avatar=` – caches the file and saves a JSON representation into `avatar_data`
|
141
|
+
* `#avatar` – returns a `Shrine::UploadedFile` based on the data from `avatar_data`
|
142
|
+
* `#avatar_url` – calls `avatar.url` if attachment is present, otherwise returns nil.
|
149
143
|
|
150
144
|
This is how you would typically create the form for a `@user`:
|
151
145
|
|
@@ -195,10 +189,18 @@ user.destroy
|
|
195
189
|
user.avatar.exists? #=> false
|
196
190
|
```
|
197
191
|
|
192
|
+
*NOTE: The record will first be saved with the cached attachment, and afterwards
|
193
|
+
(in an "after commit" hook) updated with the stored attachment. This is done so
|
194
|
+
that processing/storing isn't performed inside a database transaction. If you're
|
195
|
+
doing processing, there will be a bried period of time when the record will exist
|
196
|
+
with an unprocessed attachment, so you may need to account for that.*
|
197
|
+
|
198
198
|
## Direct uploads
|
199
199
|
|
200
200
|
Shrine comes with a `direct_upload` plugin which provides a [Roda] endpoint
|
201
|
-
that
|
201
|
+
that accepts file uploads. This allows you to asynchronously start caching the
|
202
|
+
file the moment the user selects it (e.g. using the [jQuery-File-Upload] JS
|
203
|
+
library), which gives a nice experience to the user.
|
202
204
|
|
203
205
|
```rb
|
204
206
|
Shrine.plugin :direct_upload # Provides a Roda endpoint
|
@@ -208,21 +210,18 @@ Rails.application.routes.draw do
|
|
208
210
|
mount ImageUploader::UploadEndpoint => "/attachments/images"
|
209
211
|
end
|
210
212
|
```
|
211
|
-
```
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
"mime_type": "image/jpeg"
|
220
|
-
}
|
221
|
-
}
|
213
|
+
```js
|
214
|
+
$('[type="file"]').fileupload({
|
215
|
+
url: '/attachments/images/cache/avatar',
|
216
|
+
paramName: 'file',
|
217
|
+
add: function(e, data) { /* Disable the submit button */ },
|
218
|
+
progress: function(e, data) { /* Add a nice progress bar */ },
|
219
|
+
done: function(e, data) { /* Fill in the hidden field with the result */ }
|
220
|
+
});
|
222
221
|
```
|
223
222
|
|
224
|
-
The plugin also provides a route that can be used for doing direct S3 uploads
|
225
|
-
|
223
|
+
The plugin also provides a route that can be used for doing direct S3 uploads.
|
224
|
+
See the documentation of the plugin for more details, as well as the [example
|
226
225
|
app] to see how easy it is to implement multiple uploads directly to S3.
|
227
226
|
|
228
227
|
## Processing
|
@@ -240,11 +239,13 @@ class ImageUploader < Shrine
|
|
240
239
|
end
|
241
240
|
```
|
242
241
|
|
243
|
-
The `io` is the file being uploaded, and `context` we'll leave for later.
|
242
|
+
The `io` is the file being uploaded, and `context` we'll leave for later. You
|
244
243
|
may be wondering why we need this conditional. Well, when an attachment is
|
245
244
|
assigned and saved, an "upload" actually happens two times. First the file is
|
246
245
|
"uploaded" to cache on assignment, and then the cached file is reuploaded to
|
247
|
-
store on save.
|
246
|
+
store on save. You could theoretically do processing in both phases, depending
|
247
|
+
on your preferences (although it's generally not recommended to process on
|
248
|
+
caching).
|
248
249
|
|
249
250
|
Ok, now how do we do the actual processing? Well, Shrine actually doesn't ship
|
250
251
|
with any file processing functionality, because that is a generic problem that
|
@@ -344,10 +345,9 @@ user.save # "store"
|
|
344
345
|
```
|
345
346
|
|
346
347
|
The `:name` is the name of the attachment, in this case "avatar". The `:record`
|
347
|
-
is the model instance, in this case instance of `User`.
|
348
|
-
|
349
|
-
|
350
|
-
"store", other plugins add more of them.
|
348
|
+
is the model instance, in this case instance of `User`. Lastly, the `:phase`;
|
349
|
+
by default the two main phases of attaching are "cache" and "store", but some
|
350
|
+
plugins add more of them, and there are different ones for deleting files.
|
351
351
|
|
352
352
|
Context is really useful for doing conditional processing and validation, since
|
353
353
|
we have access to the record and attachment name. In general the context is
|
@@ -403,11 +403,11 @@ end
|
|
403
403
|
|
404
404
|
### MIME type
|
405
405
|
|
406
|
-
By default, "mime_type" is inherited from `#content_type` of the uploaded file
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
406
|
+
By default, "mime_type" is inherited from `#content_type` of the uploaded file,
|
407
|
+
which holds the value of the "Content-Type" header added by the browser solely
|
408
|
+
based on the extension of the uploaded file. This means that by default
|
409
|
+
Shrine's "mime_type" is *not* guaranteed to hold the actual MIME type of the
|
410
|
+
file.
|
411
411
|
|
412
412
|
To help with that Shrine provides the `determine_mime_type` plugin, which by
|
413
413
|
default uses the UNIX [file] utility to determine the actual MIME type:
|
@@ -470,13 +470,13 @@ If you want to generate your own locations, simply override
|
|
470
470
|
```rb
|
471
471
|
class ImageUploader < Shrine
|
472
472
|
def generate_location(io, context)
|
473
|
-
"#{context[:record].class}/#{
|
473
|
+
"#{context[:record].class}/#{super}"
|
474
474
|
end
|
475
475
|
end
|
476
476
|
```
|
477
477
|
|
478
|
-
Note that
|
479
|
-
tracking won't be detected properly (you can use `Shrine#generate_uid`).
|
478
|
+
Note that there should always be a random component in the location, otherwise
|
479
|
+
dirty tracking won't be detected properly (you can use `Shrine#generate_uid`).
|
480
480
|
|
481
481
|
When using `Shrine` directly you can bypass `#generate_location` by passing in
|
482
482
|
`:location`
|
@@ -511,8 +511,8 @@ user.save
|
|
511
511
|
user.avatar.url #=> "https://my-bucket.s3-eu-west-1.amazonaws.com/0943sf8gfk13.jpg"
|
512
512
|
```
|
513
513
|
|
514
|
-
If you're using S3 for
|
515
|
-
reuploading the file by issuing an S3 COPY command instead.
|
514
|
+
If you're using S3 both for cache and store, saving the record will avoid
|
515
|
+
reuploading the file by issuing an S3 COPY command instead. Also, the
|
516
516
|
`versions` plugin takes advantage of S3's MULTI DELETE capabilities, so
|
517
517
|
versions are deleted with a single HTTP request.
|
518
518
|
|
@@ -583,7 +583,7 @@ libraries are:
|
|
583
583
|
Shrine comes with a small core which provides only the essential functionality,
|
584
584
|
and all additional features are available via plugins. This way you can choose
|
585
585
|
exactly how much Shrine does for you. Shrine itself [ships with over 35
|
586
|
-
plugins], most of
|
586
|
+
plugins], most of which I haven't managed to cover here.
|
587
587
|
|
588
588
|
The plugin system respects inheritance, so you can choose which plugins will
|
589
589
|
be applied to which uploaders:
|
@@ -607,7 +607,6 @@ system].
|
|
607
607
|
|
608
608
|
The gem is available as open source under the terms of the [MIT License].
|
609
609
|
|
610
|
-
[Contributor Covenant]: http://contributor-covenant.org
|
611
610
|
[image_processing]: https://github.com/janko-m/image_processing
|
612
611
|
[fastimage]: https://github.com/sdsykes/fastimage
|
613
612
|
[file]: http://linux.die.net/man/1/file
|
@@ -624,3 +623,4 @@ The gem is available as open source under the terms of the [MIT License].
|
|
624
623
|
[FileSystem]: http://shrinerb.com/rdoc/classes/Shrine/Storage/FileSystem.html
|
625
624
|
[S3]: http://shrinerb.com/rdoc/classes/Shrine/Storage/S3.html
|
626
625
|
[Plugins & Storages]: http://shrinerb.com#external
|
626
|
+
[`Shrine::UploadedFile`]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/Base/FileMethods.html
|
data/doc/changing_location.md
CHANGED
@@ -16,7 +16,7 @@ Or by overriding `#generate_location`:
|
|
16
16
|
```rb
|
17
17
|
class MyUploader < Shrine
|
18
18
|
def generate_location(io, context)
|
19
|
-
"#{context[:record].class}/#{context[:record.id
|
19
|
+
"#{context[:record].class}/#{context[:record].id}/#{io.original_filename}"
|
20
20
|
end
|
21
21
|
end
|
22
22
|
```
|
data/doc/direct_s3.md
CHANGED
@@ -1,13 +1,18 @@
|
|
1
1
|
# Direct Uploads to S3
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
Shrine gives you the ability to upload files directly to S3, which frees your
|
4
|
+
server from accepting file uploads. If on saving the record you need to do some
|
5
|
+
file processing, you can kick that into a background job using the
|
6
|
+
`backgrounding` plugin. If you're not doing any processing and your permanent
|
7
|
+
storage is also S3, saving the record will perform an S3 COPY request from
|
8
|
+
cache to store, without any downloading and uploading (which is both fast and
|
9
|
+
memory-efficient).
|
7
10
|
|
8
11
|
```rb
|
9
12
|
require "shrine/storage/s3"
|
10
13
|
|
14
|
+
s3_options = {access_key_id: "...", secret_access_key: "...", region: "..."}
|
15
|
+
|
11
16
|
Shrine.storages = {
|
12
17
|
cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
|
13
18
|
store: Shrine::Storage::S3.new(prefix: "store", **s3_options),
|
data/doc/refile.md
CHANGED
@@ -306,7 +306,7 @@ class ImageUploader < Shrine
|
|
306
306
|
|
307
307
|
Attacher.validate do
|
308
308
|
validate_extension_inclusion [/jpe?g/, "png"]
|
309
|
-
validate_mime_type_inclusion [
|
309
|
+
validate_mime_type_inclusion ["image/jpeg", "image/png"]
|
310
310
|
end
|
311
311
|
end
|
312
312
|
```
|
@@ -129,19 +129,19 @@ Shrine.plugin :migration_helpers # before the model is loaded
|
|
129
129
|
```
|
130
130
|
|
131
131
|
```rb
|
132
|
-
|
132
|
+
old_versions = []
|
133
133
|
|
134
134
|
User.paged_each do |user|
|
135
135
|
user.update_avatar do |avatar|
|
136
136
|
old_version = avatar.delete(:old_version)
|
137
|
-
|
137
|
+
old_versions << old_version if old_version
|
138
138
|
avatar
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
|
-
if
|
143
|
-
uploader =
|
144
|
-
uploader.delete(
|
142
|
+
if old_versions.any?
|
143
|
+
uploader = old_versions.first.uploader
|
144
|
+
uploader.delete(old_versions)
|
145
145
|
end
|
146
146
|
```
|
147
147
|
|
data/lib/shrine.rb
CHANGED
@@ -282,7 +282,7 @@ class Shrine
|
|
282
282
|
|
283
283
|
private
|
284
284
|
|
285
|
-
# Extracts the filename from the IO using
|
285
|
+
# Extracts the filename from the IO using some basic heuristics.
|
286
286
|
def extract_filename(io)
|
287
287
|
if io.respond_to?(:original_filename)
|
288
288
|
io.original_filename
|
@@ -291,7 +291,7 @@ class Shrine
|
|
291
291
|
end
|
292
292
|
end
|
293
293
|
|
294
|
-
# Extracts the MIME type from the IO using
|
294
|
+
# Extracts the MIME type from the IO using some basic heuristics.
|
295
295
|
def extract_mime_type(io)
|
296
296
|
if io.respond_to?(:mime_type)
|
297
297
|
io.mime_type
|
@@ -311,7 +311,7 @@ class Shrine
|
|
311
311
|
def _store(io, context)
|
312
312
|
_enforce_io(io)
|
313
313
|
context[:location] ||= get_location(io, context)
|
314
|
-
context[:metadata] ||=
|
314
|
+
context[:metadata] ||= get_metadata(io, context)
|
315
315
|
|
316
316
|
put(io, context)
|
317
317
|
|
@@ -335,6 +335,7 @@ class Shrine
|
|
335
335
|
# Does the actual uploading, calling `#upload` on the storage.
|
336
336
|
def copy(io, context)
|
337
337
|
storage.upload(io, context[:location], context[:metadata])
|
338
|
+
ensure
|
338
339
|
io.close rescue nil
|
339
340
|
end
|
340
341
|
|
@@ -354,6 +355,16 @@ class Shrine
|
|
354
355
|
generate_location(io, context)
|
355
356
|
end
|
356
357
|
|
358
|
+
# Copies the metadata over from an UploadedFile or calls
|
359
|
+
# #extract_metadata.
|
360
|
+
def get_metadata(io, context)
|
361
|
+
if io.is_a?(UploadedFile)
|
362
|
+
io.metadata.dup
|
363
|
+
else
|
364
|
+
extract_metadata(io, context)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
357
368
|
# Checks if the object is a valid IO by checking that it responds to
|
358
369
|
# `#read`, `#eof?`, `#rewind`, `#size` and `#close`, otherwise raises
|
359
370
|
# Shrine::InvalidFile.
|
@@ -512,16 +523,12 @@ class Shrine
|
|
512
523
|
promote(get) if promote?(get)
|
513
524
|
end
|
514
525
|
|
515
|
-
#
|
516
|
-
#
|
517
|
-
# file is deleted.
|
526
|
+
# Uploads the cached file to store, and updates the record with the
|
527
|
+
# stored file.
|
518
528
|
def promote(cached_file)
|
519
529
|
stored_file = store!(cached_file, phase: :store)
|
520
|
-
|
521
|
-
|
522
|
-
else
|
523
|
-
delete!(stored_file, phase: :stored)
|
524
|
-
end
|
530
|
+
(result = swap(stored_file)) or delete!(stored_file, phase: :stored)
|
531
|
+
result
|
525
532
|
end
|
526
533
|
|
527
534
|
# Deletes the attachment that was replaced, and is called after saving
|
@@ -549,7 +556,7 @@ class Shrine
|
|
549
556
|
end
|
550
557
|
end
|
551
558
|
|
552
|
-
# Runs the validations defined by `
|
559
|
+
# Runs the validations defined by `Attacher.validate`.
|
553
560
|
def validate
|
554
561
|
errors.clear
|
555
562
|
instance_exec(&validate_block) if validate_block && get
|
@@ -573,11 +580,17 @@ class Shrine
|
|
573
580
|
end
|
574
581
|
|
575
582
|
# Returns true if uploaded_file exists and is cached. If it's true,
|
576
|
-
#
|
583
|
+
# \#promote will be called.
|
577
584
|
def promote?(uploaded_file)
|
578
585
|
uploaded_file && cache.uploaded?(uploaded_file)
|
579
586
|
end
|
580
587
|
|
588
|
+
# Alias to #update, overriden in ORM plugins.
|
589
|
+
def swap(uploaded_file)
|
590
|
+
update(uploaded_file)
|
591
|
+
uploaded_file
|
592
|
+
end
|
593
|
+
|
581
594
|
# Sets and saves the uploaded file.
|
582
595
|
def update(uploaded_file)
|
583
596
|
_set(uploaded_file)
|
@@ -610,11 +623,6 @@ class Shrine
|
|
610
623
|
shrine_class.opts[:validate]
|
611
624
|
end
|
612
625
|
|
613
|
-
# Checks if the uploaded file matches the written one.
|
614
|
-
def changed?(uploaded_file)
|
615
|
-
get != uploaded_file
|
616
|
-
end
|
617
|
-
|
618
626
|
# It dumps the UploadedFile to JSON and writes the result to the column.
|
619
627
|
def _set(uploaded_file)
|
620
628
|
write(uploaded_file ? uploaded_file.to_json : nil)
|
@@ -66,27 +66,28 @@ class Shrine
|
|
66
66
|
module AttacherClassMethods
|
67
67
|
# Needed by the backgrounding plugin.
|
68
68
|
def find_record(record_class, record_id)
|
69
|
-
record_class.
|
69
|
+
record_class.where(id: record_id).first
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
73
|
module AttacherMethods
|
74
74
|
private
|
75
75
|
|
76
|
+
# Updates the current attachment with the new one, unless the current
|
77
|
+
# attachment has changed.
|
78
|
+
def swap(uploaded_file)
|
79
|
+
record.class.transaction do
|
80
|
+
break if record.send("#{name}_data") != record.reload.send("#{name}_data")
|
81
|
+
super
|
82
|
+
end
|
83
|
+
rescue ActiveRecord::RecordNotFound
|
84
|
+
end
|
85
|
+
|
76
86
|
# We save the record after updating, raising any validation errors.
|
77
87
|
def update(uploaded_file)
|
78
88
|
super
|
79
89
|
record.save!
|
80
90
|
end
|
81
|
-
|
82
|
-
# If we're in a transaction, then promoting is happening inline. If
|
83
|
-
# we're not, then this is happening in a background job. In that case
|
84
|
-
# when we're checking that the attachment changed during storing, we
|
85
|
-
# need to first reload the record to pick up new columns.
|
86
|
-
def changed?(uploaded_file)
|
87
|
-
record.reload
|
88
|
-
super
|
89
|
-
end
|
90
91
|
end
|
91
92
|
end
|
92
93
|
|
@@ -1,25 +1,18 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
|
-
# The
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# plugin :backgrounding
|
8
|
-
#
|
9
|
-
# ## Promoting
|
10
|
-
#
|
11
|
-
# If you're doing processing, or your `:store` is something other than
|
12
|
-
# Storage::FileSystem, it's recommended to put promoting (moving to store)
|
13
|
-
# into a background job. This plugin allows you to do that by calling
|
14
|
-
# `Shrine::Attacher.promote`:
|
3
|
+
# The backgrounding plugin enables you to remove processing/storing/deleting
|
4
|
+
# of files from record's lifecycle, and put them into background jobs.
|
5
|
+
# This is generally useful if you're doing processing and/or your store is
|
6
|
+
# something other than Storage::FileSystem.
|
15
7
|
#
|
8
|
+
# Shrine.plugin :backgrounding
|
16
9
|
# Shrine::Attacher.promote { |data| UploadJob.perform_async(data) }
|
10
|
+
# Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
17
11
|
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
# record.
|
12
|
+
# The `data` variable is a serializable hash containing all context needed
|
13
|
+
# for promotion/deletion. You then just need to declare `UploadJob` and
|
14
|
+
# `DeleteJob`, and call `Shrine::Attacher.promote`/`Shrine::Attacher.delete`
|
15
|
+
# with the data hash:
|
23
16
|
#
|
24
17
|
# class UploadJob
|
25
18
|
# include Sidekiq::Worker
|
@@ -29,22 +22,6 @@ class Shrine
|
|
29
22
|
# end
|
30
23
|
# end
|
31
24
|
#
|
32
|
-
# Shrine automatically handles all concurrency issues, such as canceling
|
33
|
-
# promoting if the attachment has changed in the meanwhile.
|
34
|
-
#
|
35
|
-
# ## Deleting
|
36
|
-
#
|
37
|
-
# If your `:store` is something other than Storage::FileSystem, it's
|
38
|
-
# recommended to put deleting files into a background job. This plugin
|
39
|
-
# allows you to do that by calling `Shrine::Attacher.delete`:
|
40
|
-
#
|
41
|
-
# Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
42
|
-
#
|
43
|
-
# When you call `Shrine::Attacher.delete` with a block, it will save the
|
44
|
-
# block and call it on every delete. Then in your background job you can
|
45
|
-
# again call `Shrine::Attacher.delete` with the data, and internally it
|
46
|
-
# will resolve all necessary objects, and delete the file.
|
47
|
-
#
|
48
25
|
# class DeleteJob
|
49
26
|
# include Sidekiq::Worker
|
50
27
|
#
|
@@ -53,20 +30,26 @@ class Shrine
|
|
53
30
|
# end
|
54
31
|
# end
|
55
32
|
#
|
56
|
-
#
|
33
|
+
# Internally these methods will resolve all necessary objects, do the
|
34
|
+
# promotion/deletion, and in case of promotion update the record with the
|
35
|
+
# stored attachment. Concurrency issues, like record being deleted or
|
36
|
+
# attachment being changed, are handled automatically.
|
57
37
|
#
|
58
38
|
# The examples above used Sidekiq, but obviously you can just as well use
|
59
|
-
# any other backgrounding library.
|
60
|
-
#
|
39
|
+
# any other backgrounding library. This setup will work globally for all
|
40
|
+
# uploaders.
|
61
41
|
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
42
|
+
# Both methods return the record (if it exists and the action didn't
|
43
|
+
# abort), so you can use it to do additional actions:
|
44
|
+
#
|
45
|
+
# def perform(data)
|
46
|
+
# record = Shrine::Attacher.promote(data)
|
47
|
+
# record.update(published: true) if record.is_a?(Post)
|
65
48
|
# end
|
66
49
|
#
|
67
|
-
# If you
|
68
|
-
#
|
69
|
-
#
|
50
|
+
# If you're generating versions, and you want to process some versions in
|
51
|
+
# the foreground before kicking off a background job, you can use the
|
52
|
+
# `recache` plugin.
|
70
53
|
module Backgrounding
|
71
54
|
module AttacherClassMethods
|
72
55
|
# If block is passed in, stores it to be called on promotion. Otherwise
|
@@ -77,13 +60,16 @@ class Shrine
|
|
77
60
|
else
|
78
61
|
record_class, record_id = data["record"]
|
79
62
|
record_class = Object.const_get(record_class)
|
80
|
-
record = find_record(record_class, record_id)
|
63
|
+
record = find_record(record_class, record_id) or return
|
81
64
|
|
82
65
|
name = data["attachment"]
|
83
66
|
attacher = record.send("#{name}_attacher")
|
84
67
|
cached_file = attacher.uploaded_file(data["uploaded_file"])
|
68
|
+
return if cached_file != record.send(name)
|
69
|
+
|
70
|
+
attacher.promote(cached_file) or return
|
85
71
|
|
86
|
-
|
72
|
+
record
|
87
73
|
end
|
88
74
|
end
|
89
75
|
|
@@ -103,12 +89,15 @@ class Shrine
|
|
103
89
|
context = {name: name.to_sym, record: record, phase: phase.to_sym}
|
104
90
|
|
105
91
|
attacher.store.delete(uploaded_file, context)
|
92
|
+
|
93
|
+
record
|
106
94
|
end
|
107
95
|
end
|
108
96
|
end
|
109
97
|
|
110
98
|
module AttacherMethods
|
111
|
-
# Calls the promoting block
|
99
|
+
# Calls the promoting block (if registered) with a serializable data
|
100
|
+
# hash.
|
112
101
|
def _promote
|
113
102
|
if background_promote = shrine_class.opts[:backgrounding_promote]
|
114
103
|
data = {
|
@@ -125,7 +114,8 @@ class Shrine
|
|
125
114
|
|
126
115
|
private
|
127
116
|
|
128
|
-
# Calls the deleting block
|
117
|
+
# Calls the deleting block (if registered) with a serializable data
|
118
|
+
# hash.
|
129
119
|
def delete!(uploaded_file, phase:)
|
130
120
|
if background_delete = shrine_class.opts[:backgrounding_delete]
|
131
121
|
data = {
|
@@ -19,7 +19,7 @@ class Shrine
|
|
19
19
|
# user.avatar.size #=> 43423
|
20
20
|
# user.avatar.original_filename #=> nil
|
21
21
|
#
|
22
|
-
# If the data URI wasn't correctly parsed, an error message will added to
|
22
|
+
# If the data URI wasn't correctly parsed, an error message will be added to
|
23
23
|
# the attachment column. You can change the default error message:
|
24
24
|
#
|
25
25
|
# plugin :data_uri, error_message: "data URI was invalid"
|
@@ -18,10 +18,10 @@ class Shrine
|
|
18
18
|
#
|
19
19
|
# You should always mount a new endpoint for each uploader that you want to
|
20
20
|
# enable direct uploads for. This now gives your Ruby application a `POST
|
21
|
-
# /attachments/images/:storage/:name` route, which accepts a
|
21
|
+
# /attachments/images/:storage/:name` route, which accepts a "file" query
|
22
22
|
# parameter, and returns the uploaded file in JSON format:
|
23
23
|
#
|
24
|
-
# # POST /attachments/images/cache/avatar
|
24
|
+
# # POST /attachments/images/cache/avatar (file upload)
|
25
25
|
# {
|
26
26
|
# "id": "43kewit94.jpg",
|
27
27
|
# "storage": "cache",
|
@@ -32,9 +32,9 @@ class Shrine
|
|
32
32
|
# }
|
33
33
|
# }
|
34
34
|
#
|
35
|
-
# Once you've uploaded the file, you need to assign
|
36
|
-
# attachment field in the form. There are many great JavaScript
|
37
|
-
# for file uploads, most popular being [jQuery-File-Upload].
|
35
|
+
# Once you've uploaded the file, you need to assign the result to the
|
36
|
+
# hidden attachment field in the form. There are many great JavaScript
|
37
|
+
# libraries for file uploads, most popular being [jQuery-File-Upload].
|
38
38
|
#
|
39
39
|
# ## Limiting filesize
|
40
40
|
#
|
@@ -12,17 +12,18 @@ class Shrine
|
|
12
12
|
# user.avatar_cache #=> #<Shrine @storage_key=:cache @storage=#<Shrine::Storage::FileSystem @directory=public/uploads>>
|
13
13
|
# user.avatar_store #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3:0x007fb8343397c8 @bucket=#<Aws::S3::Bucket name="foo">>>
|
14
14
|
#
|
15
|
-
# The model will also get `#update_avatar` method, which
|
16
|
-
#
|
17
|
-
#
|
15
|
+
# The model will also get `#update_avatar` method, which can be used when
|
16
|
+
# doing attachment migrations. It will update the record's attachment with
|
17
|
+
# the result of the passed in block.
|
18
18
|
#
|
19
19
|
# user.update_avatar do |avatar|
|
20
20
|
# user.avatar_store.upload(avatar) # saved to the record
|
21
21
|
# end
|
22
22
|
#
|
23
23
|
# This will get triggered _only_ if the attachment is not nil and is
|
24
|
-
# stored
|
25
|
-
#
|
24
|
+
# stored, and will get saved only if the current attachment hasn't changed
|
25
|
+
# while executing the block. The result can be anything that responds to
|
26
|
+
# `#to_json` and evaluates to uploaded files' data.
|
26
27
|
module MigrationHelpers
|
27
28
|
module AttachmentMethods
|
28
29
|
def initialize(name)
|
@@ -48,10 +49,9 @@ class Shrine
|
|
48
49
|
# Updates the attachment with the result of the block. It will get
|
49
50
|
# called only if the attachment exists and is stored.
|
50
51
|
def update_stored(&block)
|
51
|
-
|
52
|
-
|
53
|
-
new_attachment
|
54
|
-
update(new_attachment) unless changed?(attachment)
|
52
|
+
return if get.nil? || cache.uploaded?(get)
|
53
|
+
new_attachment = block.call(get)
|
54
|
+
swap(new_attachment)
|
55
55
|
end
|
56
56
|
end
|
57
57
|
end
|
@@ -16,7 +16,8 @@ class Shrine
|
|
16
16
|
# <% end %>
|
17
17
|
#
|
18
18
|
# Now when the checkbox is ticked and the form is submitted, the attached
|
19
|
-
# file will be removed.
|
19
|
+
# file will be removed. Note that the "remove_avatar" field needs to be
|
20
|
+
# declared somewhere after the hidden field.
|
20
21
|
module RemoveAttachment
|
21
22
|
module AttachmentMethods
|
22
23
|
def initialize(name)
|
@@ -38,7 +39,7 @@ class Shrine
|
|
38
39
|
# We remove the attachment if the value evaluates to true.
|
39
40
|
def remove=(value)
|
40
41
|
@remove = value
|
41
|
-
|
42
|
+
assign(nil) if remove?
|
42
43
|
end
|
43
44
|
|
44
45
|
def remove
|
@@ -14,10 +14,10 @@ class Shrine
|
|
14
14
|
# * `after_commit` -- Promotes the attachment, deletes replaced ones.
|
15
15
|
# * `after_destroy_commit` -- Deletes the attachment.
|
16
16
|
#
|
17
|
-
# Note that if your tests are wrapped in transactions,
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
17
|
+
# Note that if your tests are wrapped in transactions, for testing
|
18
|
+
# attachments you should set `Sequel::Model.use_transactions` to `false`,
|
19
|
+
# so that `after_commit` and `after_destroy_commit` callbacks get properly
|
20
|
+
# called.
|
21
21
|
#
|
22
22
|
# If you want to put some parts of this lifecycle into a background job, see
|
23
23
|
# the backgrounding plugin.
|
@@ -64,28 +64,29 @@ class Shrine
|
|
64
64
|
module AttacherClassMethods
|
65
65
|
# Needed by the backgrounding plugin.
|
66
66
|
def find_record(record_class, record_id)
|
67
|
-
record_class.with_pk
|
67
|
+
record_class.with_pk(record_id)
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
71
|
module AttacherMethods
|
72
72
|
private
|
73
73
|
|
74
|
+
# Updates the current attachment with the new one, unless the current
|
75
|
+
# attachment has changed.
|
76
|
+
def swap(uploaded_file)
|
77
|
+
record.db.transaction do
|
78
|
+
break if record.send("#{name}_data") != record.reload.send("#{name}_data")
|
79
|
+
super
|
80
|
+
end
|
81
|
+
rescue Sequel::Error
|
82
|
+
end
|
83
|
+
|
74
84
|
# We save the record after updating, raising any validation errors.
|
75
85
|
def update(uploaded_file)
|
76
86
|
super
|
77
87
|
record.save(raise_on_failure: true)
|
78
88
|
end
|
79
89
|
|
80
|
-
# If we're in a transaction, then promoting is happening inline. If
|
81
|
-
# we're not, then this is happening in a background job. In that case
|
82
|
-
# when we're checking that the attachment changed during storing, we
|
83
|
-
# need to first reload the record to pick up new columns.
|
84
|
-
def changed?(uploaded_file)
|
85
|
-
record.reload
|
86
|
-
super
|
87
|
-
end
|
88
|
-
|
89
90
|
# Support for Postgres JSON columns.
|
90
91
|
def read
|
91
92
|
value = super
|
@@ -68,11 +68,11 @@ class Shrine
|
|
68
68
|
|
69
69
|
module FileMethods
|
70
70
|
def width
|
71
|
-
metadata["width"]
|
71
|
+
Integer(metadata["width"]) if metadata["width"]
|
72
72
|
end
|
73
73
|
|
74
74
|
def height
|
75
|
-
metadata["height"]
|
75
|
+
Integer(metadata["height"]) if metadata["height"]
|
76
76
|
end
|
77
77
|
end
|
78
78
|
end
|
@@ -28,7 +28,7 @@ class Shrine
|
|
28
28
|
# If you would like to change the error message inline, you can pass the
|
29
29
|
# `:message` option to any validation method:
|
30
30
|
#
|
31
|
-
# validate_mime_type_inclusion [
|
31
|
+
# validate_mime_type_inclusion [/\Aimage/], message: "is not an image"
|
32
32
|
#
|
33
33
|
# For a complete list of all validation helpers, see AttacherMethods.
|
34
34
|
module ValidationHelpers
|
@@ -110,7 +110,7 @@ class Shrine
|
|
110
110
|
# Validates that the MIME type is in the `whitelist`. The whitelist is
|
111
111
|
# an array of strings or regexes.
|
112
112
|
#
|
113
|
-
# validate_mime_type_inclusion ["audio/mp3",
|
113
|
+
# validate_mime_type_inclusion ["audio/mp3", /\Avideo/]
|
114
114
|
def validate_mime_type_inclusion(whitelist, message: nil)
|
115
115
|
if whitelist.none? { |mime_type| regex(mime_type) =~ get.mime_type.to_s }
|
116
116
|
errors << error_message(:mime_type_inclusion, message, whitelist)
|
@@ -120,7 +120,7 @@ class Shrine
|
|
120
120
|
# Validates that the MIME type is not in the `blacklist`. The blacklist
|
121
121
|
# is an array of strings or regexes.
|
122
122
|
#
|
123
|
-
# validate_mime_type_exclusion ["image/gif",
|
123
|
+
# validate_mime_type_exclusion ["image/gif", /\Aaudio/]
|
124
124
|
def validate_mime_type_exclusion(blacklist, message: nil)
|
125
125
|
if blacklist.any? { |mime_type| regex(mime_type) =~ get.mime_type.to_s }
|
126
126
|
errors << error_message(:mime_type_exclusion, message, blacklist)
|
@@ -130,7 +130,7 @@ class Shrine
|
|
130
130
|
# Validates that the extension is in the `whitelist`. The whitelist
|
131
131
|
# is an array of strings or regexes.
|
132
132
|
#
|
133
|
-
# validate_extension_inclusion [
|
133
|
+
# validate_extension_inclusion [/\Ajpe?g\z/i]
|
134
134
|
def validate_extension_inclusion(whitelist, message: nil)
|
135
135
|
if whitelist.none? { |extension| regex(extension) =~ get.extension.to_s }
|
136
136
|
errors << error_message(:extension_inclusion, message, whitelist)
|
@@ -140,7 +140,7 @@ class Shrine
|
|
140
140
|
# Validates that the extension is not in the `blacklist`. The blacklist
|
141
141
|
# is an array of strings or regexes.
|
142
142
|
#
|
143
|
-
# validate_extension_exclusion ["mov",
|
143
|
+
# validate_extension_exclusion ["mov", /\Amp/i]
|
144
144
|
def validate_extension_exclusion(blacklist, message: nil)
|
145
145
|
if blacklist.any? { |extension| regex(extension) =~ get.extension.to_s }
|
146
146
|
errors << error_message(:extension_exclusion, message, blacklist)
|
@@ -150,8 +150,8 @@ class Shrine
|
|
150
150
|
private
|
151
151
|
|
152
152
|
# Converts a string to a regex.
|
153
|
-
def regex(
|
154
|
-
|
153
|
+
def regex(value)
|
154
|
+
value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value)}\z/
|
155
155
|
end
|
156
156
|
|
157
157
|
# Returns the direct message if given, otherwise uses the default error
|
@@ -171,7 +171,7 @@ class Shrine
|
|
171
171
|
if file = get[version]
|
172
172
|
file.url(**options)
|
173
173
|
else
|
174
|
-
default_url(options
|
174
|
+
default_url(**options, version: version)
|
175
175
|
end
|
176
176
|
else
|
177
177
|
raise Error, "must call #{name}_url with the name of the version"
|
@@ -180,7 +180,7 @@ class Shrine
|
|
180
180
|
if get || version.nil?
|
181
181
|
super(**options)
|
182
182
|
else
|
183
|
-
default_url(options
|
183
|
+
default_url(**options, version: version)
|
184
184
|
end
|
185
185
|
end
|
186
186
|
end
|
data/lib/shrine/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shrine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janko Marohnić
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-01-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: down
|