shrine 2.11.0 → 2.12.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/CHANGELOG.md +28 -0
- data/README.md +25 -22
- data/doc/advantages.md +22 -10
- data/doc/carrierwave.md +13 -10
- data/doc/creating_storages.md +1 -1
- data/doc/direct_s3.md +6 -6
- data/doc/multiple_files.md +195 -51
- data/doc/paperclip.md +11 -8
- data/doc/processing.md +36 -29
- data/doc/refile.md +8 -7
- data/lib/shrine.rb +7 -7
- data/lib/shrine/plugins/data_uri.rb +1 -1
- data/lib/shrine/plugins/determine_mime_type.rb +3 -2
- data/lib/shrine/plugins/download_endpoint.rb +86 -37
- data/lib/shrine/plugins/dynamic_storage.rb +5 -1
- data/lib/shrine/plugins/parallelize.rb +0 -1
- data/lib/shrine/plugins/presign_endpoint.rb +1 -6
- data/lib/shrine/plugins/processing.rb +5 -9
- data/lib/shrine/plugins/rack_response.rb +1 -1
- data/lib/shrine/plugins/remote_url.rb +24 -10
- data/lib/shrine/plugins/signature.rb +1 -2
- data/lib/shrine/plugins/validation_helpers.rb +15 -2
- data/lib/shrine/plugins/versions.rb +18 -22
- data/lib/shrine/storage/file_system.rb +4 -3
- data/lib/shrine/storage/s3.rb +20 -12
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +8 -2
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4876f91bcf2a7fc1c06f6eba68fda62e450a6363f0948387e91f48988a0b295
|
4
|
+
data.tar.gz: 514cd9b743ae1d574ba60f16d842a9c3dd14190b56091bd6df1f45d244c9ad7e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6132a5c6c94a7913b1093af118870ac3b9e7e7a35fffabc6e2aec7d58884cad5b3020a7500d755907b7e456e73ce4ba618443bd3a3013ffb0e9ed57149f500f3
|
7
|
+
data.tar.gz: 2e788af689a199e722eec66caedf6eb263fe6f2854fa039cc3c55b20d937faf18ee0bd758cb1e1b9ecb774a09c8e9f7dbe366ea143f4992129e500cc9f10a9ba
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,31 @@
|
|
1
|
+
## 2.12.0 (2018-08-22)
|
2
|
+
|
3
|
+
* Ignore nil values when assigning files from a remote URL (@janko-m)
|
4
|
+
|
5
|
+
* Ignore nil values when assigning files from a data URI (@GeekOnCoffee)
|
6
|
+
|
7
|
+
* Raise `Shrine::Error` when child process failed to be spawned in `:file` MIME type analyzer (@hmistry)
|
8
|
+
|
9
|
+
* Use the appropriate unit in error messages of filesize validators in `validation_helpers` plugin (@hmistry)
|
10
|
+
|
11
|
+
* Fix subclassing not inheriting storage resolvers from superclass in `dynamic_storage` plugin (@janko-m)
|
12
|
+
|
13
|
+
* Un-deprecate assigning cached versions (@janko-m)
|
14
|
+
|
15
|
+
* Add `Attacher#assign_remote_url` which allows dynamically passing downloader options (@janko-m)
|
16
|
+
|
17
|
+
* Deprecate `:storages` option in `download_endpoint` plugin in favour of `UploadedFile#download_url` (@janko-m)
|
18
|
+
|
19
|
+
* Add `:redirect` option to `download_endpoint` plugin for redirecting to the uploaded file (@janko-m)
|
20
|
+
|
21
|
+
* Fix encoding issues when uploading IO object with unknown size to S3 (@janko-m)
|
22
|
+
|
23
|
+
* Accept additional `File.open` arguments in `FileSystem#open` (@janko-m)
|
24
|
+
|
25
|
+
* Add `:rewindable` option to `S3#open` for disabling caching of read content to disk (@janko-m)
|
26
|
+
|
27
|
+
* Make `UploadedFile#open` always open a new IO object and close the previous one (@janko-m)
|
28
|
+
|
1
29
|
## 2.11.0 (2018-04-28)
|
2
30
|
|
3
31
|
* Add `Shrine.with_file` for temporarily converting an IO-like object into a file (@janko-m)
|
data/README.md
CHANGED
@@ -9,7 +9,7 @@ Shrine is a toolkit for file attachments in Ruby applications. Some highlights:
|
|
9
9
|
* **Flexible processing** – generate thumbnails with [ImageMagick] or [libvips] using the [ImageProcessing][image_processing] gem
|
10
10
|
* **Metadata validation** – [validate files][validation_helpers plugin] based on [extracted metadata][Extracting Metadata]
|
11
11
|
* **Direct uploads** – upload asynchronously [to your app][upload_endpoint plugin] or [to the cloud][presign_endpoint plugin] using [Uppy]
|
12
|
-
* **Resumable uploads** – make large file uploads [resumable][tus] by pointing [Uppy][uppy tus
|
12
|
+
* **Resumable uploads** – make large file uploads [resumable][tus] by pointing [Uppy][uppy tus] to a [resumable endpoint][tus-ruby-server]
|
13
13
|
* **Background jobs** – built-in support for [background processing][backgrounding plugin] that supports [any backgrounding library][backgrounding libraries]
|
14
14
|
|
15
15
|
If you're curious how it compares to other file attachment libraries, see the [Advantages of Shrine].
|
@@ -17,6 +17,7 @@ If you're curious how it compares to other file attachment libraries, see the [A
|
|
17
17
|
## Resources
|
18
18
|
|
19
19
|
- Documentation: [shrinerb.com](https://shrinerb.com)
|
20
|
+
- Demo code: [Roda][roda demo] / [Rails][rails demo]
|
20
21
|
- Source: [github.com/shrinerb/shrine](https://github.com/shrinerb/shrine)
|
21
22
|
- Bugs: [github.com/shrinerb/shrine/issues](https://github.com/shrinerb/shrine/issues)
|
22
23
|
- Help & Discussion: [groups.google.com/group/ruby-shrine](https://groups.google.com/forum/#!forum/ruby-shrine)
|
@@ -81,7 +82,7 @@ uploads][direct S3 uploads guide].
|
|
81
82
|
|
82
83
|
```rb
|
83
84
|
# with Forme:
|
84
|
-
|
85
|
+
form @photo, action: "/photos", enctype: "multipart/form-data" do |f|
|
85
86
|
f.input :image, type: :hidden, value: @photo.cached_image_data
|
86
87
|
f.input :image, type: :file
|
87
88
|
f.button "Create"
|
@@ -140,10 +141,10 @@ gem "aws-sdk-s3", "~> 1.2" # for AWS S3 storage
|
|
140
141
|
require "shrine/storage/s3"
|
141
142
|
|
142
143
|
s3_options = {
|
144
|
+
bucket: "my-bucket", # required
|
143
145
|
access_key_id: "abc",
|
144
146
|
secret_access_key: "xyz",
|
145
147
|
region: "my-region",
|
146
|
-
bucket: "my-bucket",
|
147
148
|
}
|
148
149
|
|
149
150
|
Shrine.storages = {
|
@@ -503,16 +504,17 @@ class ImageUploader < Shrine
|
|
503
504
|
plugin :delete_raw # delete processed files after uploading
|
504
505
|
|
505
506
|
process(:store) do |io, context|
|
506
|
-
|
507
|
-
pipeline = ImageProcessing::MiniMagick.source(original)
|
507
|
+
versions = { original: io } # retain original
|
508
508
|
|
509
|
-
|
510
|
-
|
511
|
-
size_300 = pipeline.resize_to_limit!(300, 300)
|
509
|
+
io.download do |original|
|
510
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
512
511
|
|
513
|
-
|
512
|
+
versions[:large] = pipeline.resize_to_limit!(800, 800)
|
513
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
514
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
515
|
+
end
|
514
516
|
|
515
|
-
|
517
|
+
versions # return the hash of processed files
|
516
518
|
end
|
517
519
|
end
|
518
520
|
```
|
@@ -696,9 +698,10 @@ demo app which implements multiple uploads directly to S3.
|
|
696
698
|
### Resumable uploads
|
697
699
|
|
698
700
|
When your app is dealing with large uploads (e.g. videos), keep in mind that it
|
699
|
-
can be challening for your users to upload these large files to your app
|
700
|
-
|
701
|
-
during uploading,
|
701
|
+
can be challening for your users to upload these large files to your app. Many
|
702
|
+
users might not have a great internet connection, and if it happens to break at
|
703
|
+
any point during uploading, they would need to restart the upload from the
|
704
|
+
beginning.
|
702
705
|
|
703
706
|
Luckily, there is a solution for this. **[Tus.io][tus]** is an open protocol
|
704
707
|
for resumable file uploads, which enables the client and the server to achieve
|
@@ -706,11 +709,11 @@ reliable file uploads even on unstable connections, by enabling the upload to
|
|
706
709
|
be resumed in case of interruptions, even after the browser was closed or the
|
707
710
|
device was shut down.
|
708
711
|
|
709
|
-
On the client side you can use [Uppy][uppy tus
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
demo]
|
712
|
+
On the client side you can use [Uppy][uppy tus] with [tus-js-client], have it
|
713
|
+
upload files to a [tus-ruby-server], and finally attach the uploaded files with
|
714
|
+
the help of [shrine-tus]. See [this walkthrough][resumable uploads walkthrough]
|
715
|
+
that adds resumable uploads from scratch, as well as the [Roda demo][resumable
|
716
|
+
demo] for a complete example.
|
714
717
|
|
715
718
|
## Backgrounding
|
716
719
|
|
@@ -872,7 +875,7 @@ The gem is available as open source under the terms of the [MIT License].
|
|
872
875
|
[presign_endpoint plugin]: https://shrinerb.com/rdoc/classes/Shrine/Plugins/PresignEndpoint.html
|
873
876
|
[Uppy]: https://uppy.io
|
874
877
|
[tus]: https://tus.io
|
875
|
-
[uppy tus
|
878
|
+
[uppy tus]: https://uppy.io/docs/tus/
|
876
879
|
[tus-ruby-server]: https://github.com/janko-m/tus-ruby-server
|
877
880
|
[backgrounding plugin]: https://shrinerb.com/rdoc/classes/Shrine/Plugins/Backgrounding.html
|
878
881
|
[Advantages of Shrine]: https://shrinerb.com/rdoc/files/doc/advantages_md.html
|
@@ -885,14 +888,14 @@ The gem is available as open source under the terms of the [MIT License].
|
|
885
888
|
[Extracting Metadata]: https://shrinerb.com/rdoc/files/doc/metadata_md.html
|
886
889
|
[File Processing]: https://shrinerb.com/rdoc/files/doc/processing_md.html
|
887
890
|
[File Validation]: https://shrinerb.com/rdoc/files/doc/validation_md.html
|
888
|
-
[direct uploads walkthrough]: https://
|
889
|
-
[direct S3 uploads walkthrough]: https://
|
891
|
+
[direct uploads walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-App-Uploads
|
892
|
+
[direct S3 uploads walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-S3-Uploads<Paste>
|
890
893
|
[direct S3 uploads guide]: https://shrinerb.com/rdoc/files/doc/direct_s3_md.html
|
891
894
|
[roda demo]: https://github.com/shrinerb/shrine/tree/master/demo
|
892
895
|
[rails demo]: https://github.com/erikdahlstrand/shrine-rails-example
|
893
896
|
[tus-js-client]: https://github.com/tus/tus-js-client
|
894
897
|
[shrine-tus]: https://github.com/shrinerb/shrine-tus
|
895
|
-
[resumable uploads walkthrough]: https://
|
898
|
+
[resumable uploads walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Resumable-Uploads
|
896
899
|
[resumable demo]: https://github.com/shrinerb/shrine-tus-demo
|
897
900
|
[backgrounding libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-libraries
|
898
901
|
[S3 lifecycle Console]: http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html
|
data/doc/advantages.md
CHANGED
@@ -185,18 +185,23 @@ end
|
|
185
185
|
### On-the-fly processing
|
186
186
|
|
187
187
|
Shrine is primarily designed for processing files on upload, since that's
|
188
|
-
applicable to all types of files,
|
189
|
-
[Refile], and [Active Storage]
|
188
|
+
applicable to all types of files, though on-the-fly processing that [Dragonfly],
|
189
|
+
[Refile], and [Active Storage] can make managing image thumbnails a lot easier.
|
190
190
|
|
191
191
|
However, there are many specialized solutions that provide on-the-fly
|
192
192
|
processing functionality, both open source and commercial, and it's fairly easy
|
193
193
|
to apply them to files uploaded by Shrine.
|
194
194
|
|
195
195
|
```rb
|
196
|
-
|
197
|
-
.
|
198
|
-
|
199
|
-
|
196
|
+
def thumbnail_url(uploaded_file, dimensions)
|
197
|
+
Dragonfly.app
|
198
|
+
.fetch(uploaded_file.url)
|
199
|
+
.thumb(dimensions)
|
200
|
+
.url
|
201
|
+
end
|
202
|
+
```
|
203
|
+
```rb
|
204
|
+
thumbnail_url(photo.image, "500x400") #=> "/attachments/W1siZnUiLCJodHRwOi8vd3d3LnB1YmxpY2RvbWFpbn..."
|
200
205
|
```
|
201
206
|
|
202
207
|
## Metadata
|
@@ -265,10 +270,13 @@ to upload them to your app, especially on flaky internet connections. With
|
|
265
270
|
a simple HTTP request, should there be any interruption during the execution,
|
266
271
|
the whole upload needs to be retried from the beginning.
|
267
272
|
|
268
|
-
To fix this problem,
|
269
|
-
resumable uploads – **[tus]**.
|
270
|
-
|
271
|
-
|
273
|
+
To fix this problem, [Transloadit] company has created an open HTTP-based
|
274
|
+
protocol for resumable uploads – **[tus]**. To use it, you can choose from
|
275
|
+
numerous client and server [implementations][tus implementations] of the
|
276
|
+
protocol. In this case you would typically have a [JavaScript
|
277
|
+
client][tus-js-client] (via [Uppy][uppy tus]) upload to a [Ruby
|
278
|
+
server][tus-ruby-server], and then attach uploaded files using the handy
|
279
|
+
[Shrine integration][shrine-tus].
|
272
280
|
|
273
281
|
## Security
|
274
282
|
|
@@ -340,7 +348,11 @@ limit.
|
|
340
348
|
[background job]: http://shrinerb.com/rdoc/classes/Shrine/Plugins/Backgrounding.html
|
341
349
|
[backgrounding libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-libraries
|
342
350
|
[Down streaming]: https://github.com/janko-m/down#streaming
|
351
|
+
[Transloadit]: https://transloadit.com
|
343
352
|
[tus]: https://tus.io
|
353
|
+
[tus implementations]: https://tus.io/implementations.html
|
354
|
+
[tus-js-client]: https://github.com/tus/tus-js-client
|
355
|
+
[uppy tus]: https://uppy.io/docs/tus/
|
344
356
|
[tus-ruby-server]: https://github.com/janko-m/tus-ruby-server
|
345
357
|
[shrine-tus]: https://github.com/shrinerb/shrine-tus
|
346
358
|
[ImageTragick]: https://imagetragick.com
|
data/doc/carrierwave.md
CHANGED
@@ -19,15 +19,17 @@ CarrierWave.configure do |config|
|
|
19
19
|
provider: "AWS",
|
20
20
|
aws_access_key_id: "abc",
|
21
21
|
aws_secret_access_key: "xyz",
|
22
|
+
region: "eu-west-1",
|
22
23
|
}
|
23
24
|
config.fog_directory = "my-bucket"
|
24
25
|
end
|
25
26
|
```
|
26
27
|
```rb
|
27
28
|
Shrine.storages[:store] = Shrine::Storage::S3.new(
|
28
|
-
bucket:
|
29
|
-
|
30
|
-
|
29
|
+
bucket: "my-bucket",
|
30
|
+
access_key_id: "abc",
|
31
|
+
secret_access_key: "xyz",
|
32
|
+
region: "eu-west-1",
|
31
33
|
)
|
32
34
|
```
|
33
35
|
|
@@ -97,16 +99,17 @@ class ImageUploader < Shrine
|
|
97
99
|
plugin :versions
|
98
100
|
|
99
101
|
process(:store) do |io, context|
|
100
|
-
|
101
|
-
pipeline = ImageProcessing::MiniMagick.source(original)
|
102
|
+
versions = {}
|
102
103
|
|
103
|
-
|
104
|
-
|
105
|
-
size_300 = pipeline.resize_to_limit!(300, 300)
|
104
|
+
io.download do |original|
|
105
|
+
pipeline = ImageProcessing::MiniMagick.source(original)
|
106
106
|
|
107
|
-
|
107
|
+
versions[:original] = pipeline.resize_to_limit!(800, 800)
|
108
|
+
versions[:medium] = pipeline.resize_to_limit!(500, 500)
|
109
|
+
versions[:small] = pipeline.resize_to_limit!(300, 300)
|
110
|
+
end
|
108
111
|
|
109
|
-
|
112
|
+
versions # return the hash of processed files
|
110
113
|
end
|
111
114
|
end
|
112
115
|
```
|
data/doc/creating_storages.md
CHANGED
@@ -52,7 +52,7 @@ end
|
|
52
52
|
```
|
53
53
|
|
54
54
|
Unless you're already using a Ruby SDK, it's recommended to use [HTTP.rb] for
|
55
|
-
uploading. It accepts any IO object that
|
55
|
+
uploading. It accepts any IO object that implements `IO#read` (not just file
|
56
56
|
objects), and it streams the request body directly to the TCP socket, both for
|
57
57
|
raw and multipart uploads, making it suitable for large uploads.
|
58
58
|
|
data/doc/direct_s3.md
CHANGED
@@ -35,9 +35,9 @@ gem "aws-sdk-s3", "~> 1.2"
|
|
35
35
|
require "shrine/storage/s3"
|
36
36
|
|
37
37
|
s3_options = {
|
38
|
+
bucket: "<YOUR BUCKET>", # required
|
38
39
|
access_key_id: "<YOUR KEY>",
|
39
40
|
secret_access_key: "<YOUR SECRET>",
|
40
|
-
bucket: "<YOUR BUCKET>",
|
41
41
|
region: "<REGION>",
|
42
42
|
}
|
43
43
|
|
@@ -171,12 +171,12 @@ presigned_data = Shrine.storages[:cache].presign(
|
|
171
171
|
success_action_redirect: new_album_url
|
172
172
|
)
|
173
173
|
|
174
|
-
|
174
|
+
form action: presigned_data[:url], method: "post", enctype: "multipart/form-data" do |f|
|
175
175
|
presigned_data[:fields].each do |name, value|
|
176
176
|
f.input :hidden, name: name, value: value
|
177
177
|
end
|
178
178
|
f.input :file, name: "file"
|
179
|
-
f.
|
179
|
+
f.button "Submit"
|
180
180
|
end
|
181
181
|
```
|
182
182
|
|
@@ -206,7 +206,7 @@ cached_file = {
|
|
206
206
|
metadata: {},
|
207
207
|
}
|
208
208
|
|
209
|
-
|
209
|
+
form @album, action: "/albums" do |f|
|
210
210
|
f.input :image, type: :hidden, value: cached_file.to_json
|
211
211
|
f.button "Save"
|
212
212
|
end
|
@@ -362,8 +362,8 @@ setup] guide.
|
|
362
362
|
|
363
363
|
[`Shrine::Storage::S3#presign`]: https://shrinerb.com/rdoc/classes/Shrine/Storage/S3.html#method-i-presign
|
364
364
|
[`Aws::S3::PresignedPost`]: http://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/S3/Bucket.html#presigned_post-instance_method
|
365
|
-
[direct S3 upload walkthrough]: https://
|
366
|
-
[checksum walkthrough]: https://
|
365
|
+
[direct S3 upload walkthrough]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-S3-Uploads
|
366
|
+
[checksum walkthrough]: https://github.com/shrinerb/shrine/wiki/Using-Checksums-in-Direct-Uploads
|
367
367
|
[roda demo]: https://github.com/shrinerb/shrine/tree/master/demo
|
368
368
|
[rails demo]: https://github.com/erikdahlstrand/shrine-rails-example
|
369
369
|
[Uppy]: https://uppy.io
|
data/doc/multiple_files.md
CHANGED
@@ -1,52 +1,78 @@
|
|
1
1
|
# Multiple Files
|
2
2
|
|
3
|
-
There are
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
3
|
+
There are times when you want to allow users to attach multiple files to a
|
4
|
+
single resource, like an album having many photos or a playlist having many
|
5
|
+
songs. Some file attachment libraries provide a special interface for multiple
|
6
|
+
attachments, but Shrine doesn't have one, because it's more flexible to use
|
7
|
+
the "nested attributes" feature of your ORM directly to implement this.
|
8
|
+
|
9
|
+
The basic idea is to create a separate table that will have a many-to-one
|
10
|
+
relationship with the main table, and files will be attached on the records in
|
11
|
+
the new table. That way each record from the main table can implicitly have
|
11
12
|
multiple attachments through the associated records.
|
12
13
|
|
13
14
|
```
|
14
|
-
|
15
|
+
album1
|
15
16
|
photo1
|
16
17
|
- attachment1
|
17
18
|
photo2
|
18
19
|
- attachment2
|
19
20
|
photo3
|
20
21
|
- attachment3
|
22
|
+
|
23
|
+
album2
|
24
|
+
photo4
|
25
|
+
- attachment4
|
26
|
+
photo5
|
27
|
+
- attachment5
|
28
|
+
|
29
|
+
...
|
30
|
+
```
|
31
|
+
|
32
|
+
To illustrate, this code will create an album with three photos using nested
|
33
|
+
attributes:
|
34
|
+
|
35
|
+
```rb
|
36
|
+
Album.create(
|
37
|
+
title: "My Album",
|
38
|
+
photos_attributes: [
|
39
|
+
{ image: File.open("image1.jpg", "rb") },
|
40
|
+
{ image: File.open("image2.jpg", "rb") },
|
41
|
+
{ image: File.open("image3.jpg", "rb") },
|
42
|
+
]
|
43
|
+
)
|
21
44
|
```
|
22
45
|
|
23
|
-
This design gives you
|
46
|
+
This design gives you the greatest flexibility, allowing you to support:
|
24
47
|
|
25
48
|
* adding new attachments
|
26
49
|
* updating existing attachments
|
27
50
|
* removing existing attachments
|
28
|
-
* sorting attachments
|
29
|
-
* having additional fields on attachments (captions, votes, number of downloads etc.)
|
51
|
+
* sorting attachments (via a separate position column)
|
52
|
+
* having additional fields on attachments (e.g. captions, votes, number of downloads etc.)
|
53
|
+
* expanding this to be "many-to-many" relation (e.g. create different playlists from a list of songs, etc)
|
30
54
|
* ...
|
31
55
|
|
32
|
-
|
33
|
-
via nested attributes, which you would in general use for any dynamic
|
34
|
-
"one-to-many" association. The examples will be using Sequel, but with
|
35
|
-
ActiveRecord it's very similar, here are the docs:
|
56
|
+
## How to Implement
|
36
57
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
For simplicity, for the rest of this guide we will assume that we have "albums"
|
41
|
-
that can have multiple "photos".
|
58
|
+
For the rest of this guide, we will use the example where we have "albums" that
|
59
|
+
can have multiple "photos" in it. The main table is the albums table and the
|
60
|
+
files (or attachments) table will be the photos table.
|
42
61
|
|
43
|
-
|
62
|
+
### 1. Create the main resource and attachment table
|
44
63
|
|
45
|
-
Let's create a table for
|
64
|
+
Let's create a table for the main resource and attachments, and add a foreign
|
65
|
+
key in the attachment table for the main table:
|
46
66
|
|
47
67
|
```rb
|
68
|
+
# Sequel
|
48
69
|
Sequel.migration do
|
49
70
|
change do
|
71
|
+
create_table :albums do
|
72
|
+
primary_key :id
|
73
|
+
column :title, :text
|
74
|
+
end
|
75
|
+
|
50
76
|
create_table :photos do
|
51
77
|
primary_key :id
|
52
78
|
foreign_key :album_id, :albums
|
@@ -54,63 +80,181 @@ Sequel.migration do
|
|
54
80
|
end
|
55
81
|
end
|
56
82
|
end
|
83
|
+
|
84
|
+
# Active Record
|
85
|
+
class CreateAlbumsAndPhotos < ActiveRecord::Migration
|
86
|
+
def change
|
87
|
+
create_table :albums do |t|
|
88
|
+
t.string :title
|
89
|
+
t.timestamps
|
90
|
+
end
|
91
|
+
|
92
|
+
create_table :photos do |t|
|
93
|
+
t.references :album, foreign_key: true
|
94
|
+
t.text :image_data
|
95
|
+
t.timestamps
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
57
99
|
```
|
58
100
|
|
59
|
-
In
|
101
|
+
In the Photo model, create a Shrine attachment attribute named `image`
|
102
|
+
(`:image` matches the `_data` column prefix above):
|
60
103
|
|
61
104
|
```rb
|
105
|
+
# Sequel
|
62
106
|
class Photo < Sequel::Model
|
63
107
|
include ImageUploader::Attachment.new(:image)
|
64
108
|
end
|
109
|
+
|
110
|
+
# Active Record
|
111
|
+
class Photo < ActiveRecord::Base
|
112
|
+
include ImageUploader::Attachment.new(:image)
|
113
|
+
end
|
65
114
|
```
|
66
115
|
|
67
|
-
|
116
|
+
### 2. Declare nested attributes
|
68
117
|
|
69
|
-
|
70
|
-
|
118
|
+
Using nested attributes is the easiest way to implement any dynamic
|
119
|
+
"one-to-many" association. In the Album model we'll declare a one-to-many
|
120
|
+
relationship to the photos table, and allow it to directly accept attributes
|
121
|
+
for the associated photo records by enabling nested attributes:
|
71
122
|
|
72
123
|
```rb
|
124
|
+
# Sequel
|
73
125
|
class Album < Sequel::Model
|
74
126
|
one_to_many :photos
|
127
|
+
plugin :association_dependencies, photos: :destroy # destroy photos when album is destroyed
|
128
|
+
|
129
|
+
plugin :nested_attributes
|
130
|
+
nested_attributes :photos, destroy: true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Active Record
|
134
|
+
class Album < ActiveRecord::Base
|
135
|
+
has_many :photos, dependent: :destroy
|
136
|
+
accepts_nested_attributes_for :photos, allow_destroy: true
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
Documentation on nested attributes:
|
141
|
+
|
142
|
+
* [`Sequel::Model.nested_attributes`]
|
143
|
+
* [`ActiveRecord::Base.accepts_nested_attributes_for`]
|
144
|
+
|
145
|
+
### 3. Create the View
|
75
146
|
|
76
|
-
|
77
|
-
|
147
|
+
Create a form like you normally do to create the album. To this form we'll add
|
148
|
+
a file field for selecting photos, which will have `multiple` attribute to
|
149
|
+
allow the user to select multiple files. We'll also display nested fields for
|
150
|
+
already created photos, so that the same form can be used for updating the
|
151
|
+
album/photos as well (they will be submitted under the
|
152
|
+
`album[photos_attributes]` parameter).
|
153
|
+
|
154
|
+
```rb
|
155
|
+
# with Forme:
|
156
|
+
form @album, action: "/photos", enctype: "multipart/form-data" do |f|
|
157
|
+
f.input :title
|
158
|
+
f.subform :photos do # adds new `album[photos_attributes]` parameter
|
159
|
+
f.input :image, type: :hidden, value: f.obj.cached_image_data
|
160
|
+
f.input :image, type: :file
|
161
|
+
f.input :_delete, type: :checkbox unless f.obj.new?
|
162
|
+
end
|
163
|
+
f.input "files[]", type: :file, attr: { multiple: true }, obj: nil
|
164
|
+
f.button "Create"
|
165
|
+
end
|
166
|
+
|
167
|
+
# with Rails form builder:
|
168
|
+
form_for @album do |f|
|
169
|
+
f.text_field :title
|
170
|
+
f.fields_for :photos do |p| # adds new `album[photos_attributes]` parameter
|
171
|
+
p.hidden_field :image, value: p.object.cached_image_data
|
172
|
+
p.file_field :image
|
173
|
+
p.check_box :_destroy unless p.object.new_record?
|
174
|
+
end
|
175
|
+
file_field_tag "files[]", multiple: true
|
176
|
+
f.submit "Create"
|
78
177
|
end
|
79
178
|
```
|
80
179
|
|
81
|
-
|
180
|
+
In your controller you should still be able to assign all the attributes to the
|
181
|
+
album, just remember to whitelist the new parameter for the nested attributes,
|
182
|
+
in this case `photos_attributes`.
|
82
183
|
|
83
|
-
|
84
|
-
|
184
|
+
### 4. Direct Upload
|
185
|
+
|
186
|
+
On the client side, you can asynchronously upload each of the files to a direct
|
187
|
+
upload endpoint as soon as they're selected. There are two methods of
|
188
|
+
implementing direct uploads: upload to your app using the [`upload_endpoint`]
|
189
|
+
plugin or upload to directly to storage like S3 using the [`presign_endpoint`]
|
190
|
+
plugin. It's recommended to use [Uppy] to handle the uploads on the client side.
|
191
|
+
|
192
|
+
Once files are uploaded asynchronously, you can dynamically insert photo
|
193
|
+
attachment fields for the `image` attribute to the form filled with uploaded
|
194
|
+
file data, so that the corresponding photo records are automatically created
|
195
|
+
after form submission. The hidden attachment fields should contain uploaded
|
196
|
+
file data in JSON format, just like when doing single direct uploads. The
|
197
|
+
attachment field names should be namespaced according to the convention that
|
198
|
+
the nested attributes feature expects. In this case it should be
|
199
|
+
`album[photos_attributes][<idx>]`, where `<idx>` is any incrementing integer
|
200
|
+
(e.g. you can use the current UNIX timestamp).
|
85
201
|
|
86
202
|
```rb
|
87
|
-
|
88
|
-
|
203
|
+
# naming format in which photos fields should be generated and submitted
|
204
|
+
album[photos_attributes][11111][image] = '{"id":"38k25.jpg","storage":"cache","metadata":{...}}'
|
205
|
+
album[photos_attributes][29323][image] = '{"id":"sg0fg.jpg","storage":"cache","metadata":{...}}'
|
206
|
+
album[photos_attributes][34820][image] = '{"id":"041jd.jpg","storage":"cache","metadata":{...}}'
|
207
|
+
# ...
|
89
208
|
```
|
90
209
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
210
|
+
See the walkthroughs for setting up simple [direct app uploads] and
|
211
|
+
[direct S3 uploads], and the [Roda][roda demo] or [Rails][rails demo] demo app
|
212
|
+
which implements multiple file uploads.
|
213
|
+
|
214
|
+
### 5. Adding Validations
|
95
215
|
|
96
|
-
|
97
|
-
|
216
|
+
You can add file validations to the `Photo` model using the
|
217
|
+
`validation_helpers` plugin. You just need to make sure that your ORM is
|
218
|
+
configured to automatically validate associated records.
|
98
219
|
|
99
220
|
```rb
|
100
|
-
|
101
|
-
|
102
|
-
|
221
|
+
class ImageUploader < Shrine
|
222
|
+
plugin :validation_helpers
|
223
|
+
plugin :determine_mime_type
|
224
|
+
|
225
|
+
Attacher.validate do
|
226
|
+
validate_max_size 10*1024*1024
|
227
|
+
validate_mime_type_inclusion %w[image/jpeg image/png]
|
228
|
+
end
|
229
|
+
end
|
230
|
+
```
|
231
|
+
```rb
|
232
|
+
# Sequel
|
233
|
+
class Album < Sequel::Model
|
234
|
+
# ... (nested_attributes already enables validating associated photos) ...
|
235
|
+
end
|
236
|
+
|
237
|
+
# ActiveRecord
|
238
|
+
class Album < ActiveRecord::Base
|
239
|
+
# ...
|
240
|
+
validates_associated :photos
|
241
|
+
end
|
103
242
|
```
|
104
243
|
|
105
|
-
|
106
|
-
create the associated records, and assign the Shrine attachments.
|
244
|
+
### 6. Conclusion
|
107
245
|
|
108
|
-
Now
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
246
|
+
Now we have a simple interface for accepting multiple attachments, which
|
247
|
+
internally uses nested attributes to create multiple associated records, each
|
248
|
+
with a single attachment. After creation you can also add new attachments, or
|
249
|
+
update and delete existing ones, which the nested attributes feature gives you
|
250
|
+
for free.
|
113
251
|
|
114
252
|
[`Sequel::Model.nested_attributes`]: http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/NestedAttributes.html
|
115
253
|
[`ActiveRecord::Base.accepts_nested_attributes_for`]: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
|
116
|
-
[
|
254
|
+
[`upload_endpoint`]: https://shrinerb.com/rdoc/classes/Shrine/Plugins/UploadEndpoint.html
|
255
|
+
[`presign_endpoint`]: https://shrinerb.com/rdoc/classes/Shrine/Plugins/PresignEndpoint.html
|
256
|
+
[Uppy]: https://uppy.io
|
257
|
+
[direct app uploads]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-App-Uploads
|
258
|
+
[direct S3 uploads]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-S3-Uploads
|
259
|
+
[roda demo]: https://github.com/shrinerb/shrine/tree/master/demo
|
260
|
+
[rails demo]: https://github.com/erikdahlstrand/shrine-rails-example
|