shrine 2.19.3 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +523 -41
- data/LICENSE.txt +1 -1
- data/README.md +83 -979
- data/doc/advantages.md +231 -204
- data/doc/attacher.md +304 -153
- data/doc/carrierwave.md +297 -226
- data/doc/changing_derivatives.md +308 -0
- data/doc/changing_location.md +103 -21
- data/doc/changing_storage.md +110 -0
- data/doc/creating_persistence_plugins.md +132 -0
- data/doc/creating_plugins.md +43 -23
- data/doc/creating_storages.md +19 -5
- data/doc/design.md +147 -97
- data/doc/direct_s3.md +38 -28
- data/doc/external/articles.md +63 -0
- data/doc/external/extensions.md +53 -0
- data/doc/external/misc.md +32 -0
- data/doc/getting_started.md +1156 -0
- data/doc/metadata.md +190 -109
- data/doc/multiple_files.md +93 -30
- data/doc/paperclip.md +384 -262
- data/doc/plugins/activerecord.md +177 -46
- data/doc/plugins/add_metadata.md +139 -38
- data/doc/plugins/atomic_helpers.md +217 -0
- data/doc/plugins/backgrounding.md +156 -98
- data/doc/plugins/cached_attachment_data.md +7 -5
- data/doc/plugins/column.md +121 -0
- data/doc/plugins/data_uri.md +23 -22
- data/doc/plugins/default_storage.md +36 -10
- data/doc/plugins/default_url.md +30 -13
- data/doc/plugins/delete_raw.md +4 -2
- data/doc/plugins/derivation_endpoint.md +186 -101
- data/doc/plugins/derivatives.md +839 -0
- data/doc/plugins/determine_mime_type.md +4 -2
- data/doc/plugins/download_endpoint.md +64 -8
- data/doc/plugins/dynamic_storage.md +5 -3
- data/doc/plugins/entity.md +263 -0
- data/doc/plugins/form_assign.md +55 -0
- data/doc/plugins/included.md +31 -8
- data/doc/plugins/infer_extension.md +21 -10
- data/doc/plugins/instrumentation.md +38 -16
- data/doc/plugins/keep_files.md +16 -17
- data/doc/plugins/metadata_attributes.md +42 -13
- data/doc/plugins/mirroring.md +118 -0
- data/doc/plugins/model.md +210 -0
- data/doc/plugins/module_include.md +4 -2
- data/doc/plugins/multi_cache.md +24 -0
- data/doc/plugins/persistence.md +101 -0
- data/doc/plugins/presign_endpoint.md +9 -4
- data/doc/plugins/pretty_location.md +16 -3
- data/doc/plugins/processing.md +4 -2
- data/doc/plugins/rack_file.md +8 -2
- data/doc/plugins/rack_response.md +6 -2
- data/doc/plugins/recache.md +4 -2
- data/doc/plugins/refresh_metadata.md +49 -9
- data/doc/plugins/remote_url.md +84 -47
- data/doc/plugins/remove_attachment.md +27 -6
- data/doc/plugins/remove_invalid.md +21 -6
- data/doc/plugins/restore_cached_data.md +11 -3
- data/doc/plugins/sequel.md +159 -35
- data/doc/plugins/signature.md +16 -5
- data/doc/plugins/store_dimensions.md +14 -2
- data/doc/plugins/tempfile.md +4 -2
- data/doc/plugins/type_predicates.md +96 -0
- data/doc/plugins/upload_endpoint.md +13 -13
- data/doc/plugins/upload_options.md +6 -4
- data/doc/plugins/{default_url_options.md → url_options.md} +9 -7
- data/doc/plugins/validation.md +97 -0
- data/doc/plugins/validation_helpers.md +16 -13
- data/doc/plugins/versions.md +15 -19
- data/doc/processing.md +438 -221
- data/doc/refile.md +188 -170
- data/doc/release_notes/1.0.0.md +4 -0
- data/doc/release_notes/1.1.0.md +6 -2
- data/doc/release_notes/1.2.0.md +4 -0
- data/doc/release_notes/1.3.0.md +4 -0
- data/doc/release_notes/1.4.0.md +4 -0
- data/doc/release_notes/1.4.1.md +4 -0
- data/doc/release_notes/1.4.2.md +4 -0
- data/doc/release_notes/2.0.0.md +4 -0
- data/doc/release_notes/2.0.1.md +4 -0
- data/doc/release_notes/2.1.0.md +5 -1
- data/doc/release_notes/2.1.1.md +4 -0
- data/doc/release_notes/2.10.0.md +4 -0
- data/doc/release_notes/2.10.1.md +4 -0
- data/doc/release_notes/2.11.0.md +4 -0
- data/doc/release_notes/2.12.0.md +4 -0
- data/doc/release_notes/2.13.0.md +4 -0
- data/doc/release_notes/2.14.0.md +5 -1
- data/doc/release_notes/2.15.0.md +11 -7
- data/doc/release_notes/2.16.0.md +4 -0
- data/doc/release_notes/2.17.0.md +4 -0
- data/doc/release_notes/2.18.0.md +4 -0
- data/doc/release_notes/2.19.0.md +6 -3
- data/doc/release_notes/2.2.0.md +4 -0
- data/doc/release_notes/2.3.0.md +4 -0
- data/doc/release_notes/2.3.1.md +4 -0
- data/doc/release_notes/2.4.0.md +4 -0
- data/doc/release_notes/2.4.1.md +4 -0
- data/doc/release_notes/2.5.0.md +4 -0
- data/doc/release_notes/2.6.0.md +4 -0
- data/doc/release_notes/2.6.1.md +4 -0
- data/doc/release_notes/2.7.0.md +4 -0
- data/doc/release_notes/2.8.0.md +4 -0
- data/doc/release_notes/2.9.0.md +4 -0
- data/doc/release_notes/3.0.0.md +981 -0
- data/doc/release_notes/3.0.1.md +22 -0
- data/doc/release_notes/3.1.0.md +73 -0
- data/doc/release_notes/3.2.0.md +96 -0
- data/doc/release_notes/3.2.1.md +31 -0
- data/doc/release_notes/3.2.2.md +14 -0
- data/doc/release_notes/3.3.0.md +105 -0
- data/doc/release_notes/3.4.0.md +35 -0
- data/doc/release_notes/3.5.0.md +63 -0
- data/doc/release_notes/3.6.0.md +23 -0
- data/doc/retrieving_uploads.md +5 -2
- data/doc/securing_uploads.md +60 -37
- data/doc/storage/file_system.md +20 -3
- data/doc/storage/memory.md +19 -0
- data/doc/storage/s3.md +122 -78
- data/doc/testing.md +141 -133
- data/doc/upgrading_to_3.md +708 -0
- data/doc/validation.md +54 -90
- data/lib/shrine/attacher.rb +292 -169
- data/lib/shrine/attachment.rb +13 -46
- data/lib/shrine/plugins/_persistence.rb +93 -0
- data/lib/shrine/plugins/activerecord.rb +77 -34
- data/lib/shrine/plugins/add_metadata.rb +25 -17
- data/lib/shrine/plugins/atomic_helpers.rb +119 -0
- data/lib/shrine/plugins/backgrounding.rb +77 -113
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -15
- data/lib/shrine/plugins/column.rb +102 -0
- data/lib/shrine/plugins/data_uri.rb +38 -36
- data/lib/shrine/plugins/default_storage.rb +45 -15
- data/lib/shrine/plugins/default_url.rb +12 -24
- data/lib/shrine/plugins/default_url_options.rb +3 -30
- data/lib/shrine/plugins/delete_raw.rb +10 -16
- data/lib/shrine/plugins/derivation_endpoint.rb +130 -171
- data/lib/shrine/plugins/derivatives.rb +645 -0
- data/lib/shrine/plugins/determine_mime_type.rb +9 -21
- data/lib/shrine/plugins/download_endpoint.rb +118 -133
- data/lib/shrine/plugins/dynamic_storage.rb +5 -11
- data/lib/shrine/plugins/entity.rb +158 -0
- data/lib/shrine/plugins/form_assign.rb +108 -0
- data/lib/shrine/plugins/included.rb +6 -6
- data/lib/shrine/plugins/infer_extension.rb +17 -20
- data/lib/shrine/plugins/instrumentation.rb +59 -43
- data/lib/shrine/plugins/keep_files.rb +3 -15
- data/lib/shrine/plugins/metadata_attributes.rb +28 -19
- data/lib/shrine/plugins/mirroring.rb +142 -0
- data/lib/shrine/plugins/model.rb +160 -0
- data/lib/shrine/plugins/module_include.rb +3 -3
- data/lib/shrine/plugins/multi_cache.rb +27 -0
- data/lib/shrine/plugins/presign_endpoint.rb +27 -28
- data/lib/shrine/plugins/pretty_location.rb +15 -9
- data/lib/shrine/plugins/processing.rb +22 -9
- data/lib/shrine/plugins/rack_file.rb +2 -42
- data/lib/shrine/plugins/rack_response.rb +21 -10
- data/lib/shrine/plugins/recache.rb +6 -5
- data/lib/shrine/plugins/refresh_metadata.rb +13 -11
- data/lib/shrine/plugins/remote_url.rb +49 -49
- data/lib/shrine/plugins/remove_attachment.rb +12 -6
- data/lib/shrine/plugins/remove_invalid.rb +19 -8
- data/lib/shrine/plugins/restore_cached_data.rb +13 -7
- data/lib/shrine/plugins/sequel.rb +86 -36
- data/lib/shrine/plugins/signature.rb +10 -16
- data/lib/shrine/plugins/store_dimensions.rb +35 -40
- data/lib/shrine/plugins/tempfile.rb +1 -3
- data/lib/shrine/plugins/type_predicates.rb +113 -0
- data/lib/shrine/plugins/upload_endpoint.rb +28 -24
- data/lib/shrine/plugins/upload_options.rb +14 -15
- data/lib/shrine/plugins/url_options.rb +31 -0
- data/lib/shrine/plugins/validation.rb +80 -0
- data/lib/shrine/plugins/validation_helpers.rb +35 -58
- data/lib/shrine/plugins/versions.rb +107 -87
- data/lib/shrine/plugins.rb +22 -0
- data/lib/shrine/storage/file_system.rb +46 -64
- data/lib/shrine/storage/linter.rb +42 -7
- data/lib/shrine/storage/memory.rb +49 -0
- data/lib/shrine/storage/s3.rb +173 -160
- data/lib/shrine/uploaded_file.rb +32 -32
- data/lib/shrine/version.rb +3 -3
- data/lib/shrine.rb +87 -150
- data/shrine.gemspec +11 -12
- metadata +92 -82
- data/doc/migrating_storage.md +0 -76
- data/doc/plugins/backup.md +0 -31
- data/doc/plugins/copy.md +0 -24
- data/doc/plugins/delete_promoted.md +0 -12
- data/doc/plugins/direct_upload.md +0 -172
- data/doc/plugins/hooks.md +0 -58
- data/doc/plugins/logging.md +0 -42
- data/doc/plugins/migration_helpers.md +0 -60
- data/doc/plugins/moving.md +0 -19
- data/doc/plugins/multi_delete.md +0 -20
- data/doc/plugins/parallelize.md +0 -16
- data/doc/plugins/parsed_json.md +0 -23
- data/doc/regenerating_versions.md +0 -143
- data/lib/shrine/plugins/background_helpers.rb +0 -5
- data/lib/shrine/plugins/backup.rb +0 -90
- data/lib/shrine/plugins/copy.rb +0 -50
- data/lib/shrine/plugins/delete_promoted.rb +0 -20
- data/lib/shrine/plugins/direct_upload.rb +0 -217
- data/lib/shrine/plugins/hooks.rb +0 -90
- data/lib/shrine/plugins/logging.rb +0 -142
- data/lib/shrine/plugins/migration_helpers.rb +0 -70
- data/lib/shrine/plugins/moving.rb +0 -57
- data/lib/shrine/plugins/multi_delete.rb +0 -32
- data/lib/shrine/plugins/parallelize.rb +0 -78
- data/lib/shrine/plugins/parsed_json.rb +0 -29
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: getting-started
|
|
3
|
+
title: Getting Started
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import Tabs from '@theme/Tabs';
|
|
7
|
+
import TabItem from '@theme/TabItem';
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
Add Shrine to the Gemfile and write an initializer which sets up the storage
|
|
12
|
+
and loads integration for your persistence library:
|
|
13
|
+
|
|
14
|
+
```rb
|
|
15
|
+
# Gemfile
|
|
16
|
+
gem "shrine", "~> 3.0"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```rb
|
|
20
|
+
require "shrine"
|
|
21
|
+
require "shrine/storage/file_system"
|
|
22
|
+
|
|
23
|
+
Shrine.storages = {
|
|
24
|
+
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"), # temporary
|
|
25
|
+
store: Shrine::Storage::FileSystem.new("public", prefix: "uploads"), # permanent
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
Shrine.plugin :sequel # or :activerecord
|
|
29
|
+
Shrine.plugin :cached_attachment_data # for retaining the cached file across form redisplays
|
|
30
|
+
Shrine.plugin :restore_cached_data # re-extract metadata when attaching a cached file
|
|
31
|
+
Shrine.plugin :rack_file # for non-Rails apps
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Next decide how you will name the attachment attribute on your model, and run a
|
|
35
|
+
migration that adds an `<attachment>_data` text or JSON column, which Shrine
|
|
36
|
+
will use to store all information about the attachment:
|
|
37
|
+
|
|
38
|
+
<Tabs>
|
|
39
|
+
<TabItem value="sequel" label="Sequel">
|
|
40
|
+
|
|
41
|
+
```rb
|
|
42
|
+
Sequel.migration do
|
|
43
|
+
change do
|
|
44
|
+
add_column :photos, :image_data, :text # or :jsonb
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
</TabItem>
|
|
50
|
+
<TabItem value="activerecord" label="Active Record">
|
|
51
|
+
|
|
52
|
+
```rb
|
|
53
|
+
class AddImageDataToPhotos < ActiveRecord::Migration
|
|
54
|
+
def change
|
|
55
|
+
add_column :photos, :image_data, :text # or :jsonb
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
</TabItem>
|
|
61
|
+
<TabItem value="rails" label="Rails">
|
|
62
|
+
|
|
63
|
+
```rb
|
|
64
|
+
$ rails generate migration add_image_data_to_photos image_data:text # or image_data:jsonb
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
</TabItem>
|
|
68
|
+
</Tabs>
|
|
69
|
+
|
|
70
|
+
If using `jsonb` consider adding a [gin index] for fast key-value pair searchability within `image_data`.
|
|
71
|
+
|
|
72
|
+
Now you can create an uploader class for the type of files you want to upload,
|
|
73
|
+
and add a virtual attribute for handling attachments using this uploader to
|
|
74
|
+
your model. If you do not care about adding plugins or additional processing,
|
|
75
|
+
you can use `Shrine::Attachment`.
|
|
76
|
+
|
|
77
|
+
```rb
|
|
78
|
+
class ImageUploader < Shrine
|
|
79
|
+
# plugins and uploading logic
|
|
80
|
+
end
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
<Tabs>
|
|
84
|
+
<TabItem value="sequel" label="Sequel">
|
|
85
|
+
|
|
86
|
+
```rb
|
|
87
|
+
class Photo < Sequel::Model
|
|
88
|
+
include ImageUploader::Attachment(:image) # adds an `image` virtual attribute
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
</TabItem>
|
|
93
|
+
<TabItem value="activerecord" label="Active Record">
|
|
94
|
+
|
|
95
|
+
```rb
|
|
96
|
+
class Photo < ActiveRecord::Base
|
|
97
|
+
include ImageUploader::Attachment(:image) # adds an `image` virtual attribute
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
</TabItem>
|
|
102
|
+
</Tabs>
|
|
103
|
+
|
|
104
|
+
Let's now add the form fields which will use this virtual attribute (NOT the
|
|
105
|
+
`<attachment>_data` column attribute). We need (1) a file field for choosing
|
|
106
|
+
files, and (2) a hidden field for retaining the uploaded file in case of
|
|
107
|
+
validation errors and for potential [direct uploads].
|
|
108
|
+
|
|
109
|
+
<Tabs>
|
|
110
|
+
<TabItem value="rails" label="Rails form builder">
|
|
111
|
+
|
|
112
|
+
```rb
|
|
113
|
+
form_for @photo do |f|
|
|
114
|
+
f.hidden_field :image, value: @photo.cached_image_data, id: nil
|
|
115
|
+
f.file_field :image
|
|
116
|
+
f.submit
|
|
117
|
+
end
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
</TabItem>
|
|
121
|
+
<TabItem value="simple_form" label="Simple Form">
|
|
122
|
+
|
|
123
|
+
```rb
|
|
124
|
+
simple_form_for @photo do |f|
|
|
125
|
+
f.input :image, as: :hidden, input_html: { value: @photo.cached_image_data }
|
|
126
|
+
f.input :image, as: :file
|
|
127
|
+
f.button :submit
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
</TabItem>
|
|
132
|
+
<TabItem value="form" label="Forme">
|
|
133
|
+
|
|
134
|
+
```rb
|
|
135
|
+
form @photo, action: "/photos", enctype: "multipart/form-data" do |f|
|
|
136
|
+
f.input :image, type: :hidden, value: @photo.cached_image_data
|
|
137
|
+
f.input :image, type: :file
|
|
138
|
+
f.button "Create"
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
</TabItem>
|
|
143
|
+
<TabItem value="html" label="HTML">
|
|
144
|
+
|
|
145
|
+
```erb
|
|
146
|
+
<form action="/photos" method="post" enctype="multipart/form-data">
|
|
147
|
+
<input name="photo[image]" type="hidden" value="<%= @photo.cached_image_data %>" />
|
|
148
|
+
<input name="photo[image] "type="file" />
|
|
149
|
+
<input type="submit" value="Create" />
|
|
150
|
+
</form>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
</TabItem>
|
|
154
|
+
</Tabs>
|
|
155
|
+
|
|
156
|
+
Note that the file field needs to go *after* the hidden field, so that
|
|
157
|
+
selecting a new file can always override the cached file in the hidden field.
|
|
158
|
+
Also notice the `enctype="multipart/form-data"` HTML attribute, which is
|
|
159
|
+
required for submitting files through the form (the Rails form builder
|
|
160
|
+
will automatically generate this for you).
|
|
161
|
+
|
|
162
|
+
When the form is submitted, in your router/controller you can assign the file
|
|
163
|
+
from request params to the attachment attribute on the model.
|
|
164
|
+
|
|
165
|
+
<Tabs>
|
|
166
|
+
<TabItem value="rails" label="Rails">
|
|
167
|
+
|
|
168
|
+
```rb
|
|
169
|
+
class PhotosController < ApplicationController
|
|
170
|
+
def create
|
|
171
|
+
Photo.create(photo_params)
|
|
172
|
+
# ...
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def photo_params
|
|
178
|
+
params.require(:photo).permit(:image)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
</TabItem>
|
|
184
|
+
<TabItem value="sinatra" label="Sinatra">
|
|
185
|
+
|
|
186
|
+
```rb
|
|
187
|
+
post "/photos" do
|
|
188
|
+
Photo.create(params[:photo])
|
|
189
|
+
# ...
|
|
190
|
+
end
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
</TabItem>
|
|
194
|
+
</Tabs>
|
|
195
|
+
|
|
196
|
+
Once a file is uploaded and attached to the record, you can retrieve a URL to
|
|
197
|
+
the uploaded file with `#<attachment>_url` and display it on the page:
|
|
198
|
+
|
|
199
|
+
<Tabs>
|
|
200
|
+
<TabItem value="rails" label="Rails">
|
|
201
|
+
|
|
202
|
+
```erb
|
|
203
|
+
<%= image_tag @photo.image_url %>
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
</TabItem>
|
|
207
|
+
<TabItem value="html" label="HTML">
|
|
208
|
+
|
|
209
|
+
```erb
|
|
210
|
+
<img src="<%= @photo.image_url %>" />
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
</TabItem>
|
|
214
|
+
</Tabs>
|
|
215
|
+
|
|
216
|
+
## Storage
|
|
217
|
+
|
|
218
|
+
A "storage" in Shrine is an object that encapsulates communication with a
|
|
219
|
+
specific storage service, by implementing a common public interface. Storage
|
|
220
|
+
instances are registered under an identifier in `Shrine.storages`, so that they
|
|
221
|
+
can later be used by [uploaders][uploader].
|
|
222
|
+
|
|
223
|
+
Shrine ships with the following storages:
|
|
224
|
+
|
|
225
|
+
* [`Shrine::Storage::FileSystem`][FileSystem] – stores files on disk
|
|
226
|
+
* [`Shrine::Storage::S3`][S3] – stores files on [AWS S3] (or [DigitalOcean Spaces], [MinIO], ...)
|
|
227
|
+
* [`Shrine::Storage::Memory`][Memory] – stores file in memory (convenient for [testing][Testing with Shrine])
|
|
228
|
+
|
|
229
|
+
Here is how we might configure Shrine with S3 storage:
|
|
230
|
+
|
|
231
|
+
```rb
|
|
232
|
+
# Gemfile
|
|
233
|
+
gem "aws-sdk-s3", "~> 1.14" # for AWS S3 storage
|
|
234
|
+
```
|
|
235
|
+
```rb
|
|
236
|
+
require "shrine/storage/s3"
|
|
237
|
+
|
|
238
|
+
s3_options = {
|
|
239
|
+
bucket: "<YOUR BUCKET>", # required
|
|
240
|
+
region: "<YOUR REGION>", # required
|
|
241
|
+
access_key_id: "<YOUR ACCESS KEY ID>",
|
|
242
|
+
secret_access_key: "<YOUR SECRET ACCESS KEY>",
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
Shrine.storages = {
|
|
246
|
+
cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options), # temporary
|
|
247
|
+
store: Shrine::Storage::S3.new(**s3_options), # permanent
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The above example sets up S3 for both temporary and permanent storage, which is
|
|
252
|
+
suitable for [direct uploads][presigned upload]. The `:cache` and `:store`
|
|
253
|
+
names are special only in terms that the [attacher] will automatically pick
|
|
254
|
+
them up, you can also register more storage objects under different names.
|
|
255
|
+
|
|
256
|
+
See the [FileSystem]/[S3]/[Memory] storage docs for more details. There are
|
|
257
|
+
[many more Shrine storages][storages] provided by external gems, and you can
|
|
258
|
+
also [create your own storage][Creating Storages].
|
|
259
|
+
|
|
260
|
+
## Uploader
|
|
261
|
+
|
|
262
|
+
Uploaders are subclasses of `Shrine`, and they wrap the actual upload to the
|
|
263
|
+
storage. They perform common tasks around upload that aren't related to a
|
|
264
|
+
particular storage.
|
|
265
|
+
|
|
266
|
+
```rb
|
|
267
|
+
class MyUploader < Shrine
|
|
268
|
+
# image attachment logic
|
|
269
|
+
end
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
It's common to create an uploader for each type of file that you want to handle
|
|
273
|
+
(`ImageUploader`, `VideoUploader`, `AudioUploader` etc), but really you can
|
|
274
|
+
organize them in any way you like.
|
|
275
|
+
|
|
276
|
+
### Uploading
|
|
277
|
+
|
|
278
|
+
The main method of the uploader is `Shrine.upload`, which takes an [IO-like
|
|
279
|
+
object][io abstraction] and a storage identifier on the input, and returns a
|
|
280
|
+
representation of the [uploaded file] on the output.
|
|
281
|
+
|
|
282
|
+
```rb
|
|
283
|
+
MyUploader.upload(file, :store) #=> #<Shrine::UploadedFile>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Internally this instantiates the uploader with the storage and calls
|
|
287
|
+
`Shrine#upload`:
|
|
288
|
+
|
|
289
|
+
```rb
|
|
290
|
+
uploader = MyUploader.new(:store)
|
|
291
|
+
uploader.upload(file) #=> #<Shrine::UploadedFile>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Some of the tasks performed by `#upload` include:
|
|
295
|
+
|
|
296
|
+
* extracting [metadata]
|
|
297
|
+
* generating [location]
|
|
298
|
+
* uploading (this is where the [storage] is called)
|
|
299
|
+
* closing the uploaded file
|
|
300
|
+
|
|
301
|
+
The second argument is a "context" hash which is forwarded to places like
|
|
302
|
+
metadata extraction and location generation, but it has a few special options:
|
|
303
|
+
|
|
304
|
+
```rb
|
|
305
|
+
uploader.upload(io, metadata: { "foo" => "bar" }) # add metadata
|
|
306
|
+
uploader.upload(io, location: "path/to/file") # specify custom location
|
|
307
|
+
uploader.upload(io, upload_options: { acl: "public-read" }) # add options to Storage#upload
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### IO abstraction
|
|
311
|
+
|
|
312
|
+
Shrine is able to upload any IO-like object that implement methods [`#read`],
|
|
313
|
+
[`#rewind`], [`#eof?`] and [`#close`] whose behaviour matches the [`IO`] class.
|
|
314
|
+
This includes but is not limited to the following objects:
|
|
315
|
+
|
|
316
|
+
* [`File`](https://ruby-doc.org/core/File.html)
|
|
317
|
+
* [`Tempfile`](https://ruby-doc.org/stdlib/libdoc/tempfile/rdoc/Tempfile.html)
|
|
318
|
+
* [`StringIO`](https://ruby-doc.org/stdlib/libdoc/stringio/rdoc/StringIO.html)
|
|
319
|
+
* [`ActionDispatch::Http::UploadedFile`](https://api.rubyonrails.org/classes/ActionDispatch/Http/UploadedFile.html)
|
|
320
|
+
* [`Shrine::RackFile`](https://shrinerb.com/docs/plugins/rack_file)
|
|
321
|
+
* [`Shrine::DataFile`](https://shrinerb.com/docs/plugins/data_uri)
|
|
322
|
+
* [`Shrine::UploadedFile`](#uploaded-file)
|
|
323
|
+
* [`Down::ChunkedIO`](https://github.com/janko/down#streaming)
|
|
324
|
+
* ...
|
|
325
|
+
|
|
326
|
+
```rb
|
|
327
|
+
uploader.upload File.open("/path/to/file", binmode: true) # upload from disk
|
|
328
|
+
uploader.upload StringIO.new("file content") # upload from memory
|
|
329
|
+
uploader.upload ActionDispatch::Http::UploadedFile.new(...) # upload from Rails controller
|
|
330
|
+
uploader.upload Shrine.rack_file({ tempfile: tempfile }) # upload from Rack controller
|
|
331
|
+
uploader.upload Rack::Test::UploadedFile.new(...) # upload from rack-test
|
|
332
|
+
uploader.upload Down.open("https://example.org/file") # upload from internet
|
|
333
|
+
uploader.upload Shrine::UploadedFile.new(...) # upload from Shrine storage
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Uploaded file
|
|
337
|
+
|
|
338
|
+
The `Shrine::UploadedFile` object represents the file that was uploaded to a
|
|
339
|
+
storage, and it's what's returned from `Shrine#upload` or when retrieving a
|
|
340
|
+
record [attachment].
|
|
341
|
+
|
|
342
|
+
```rb
|
|
343
|
+
uploader.upload(file) #=> #<Shrine::UploadedFile ...> (uploader)
|
|
344
|
+
photo.image #=> #<Shrine::UploadedFile ...> (attachment)
|
|
345
|
+
attacher.file #=> #<Shrine::UploadedFile ...> (attacher)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
An uploaded file object contains the following data:
|
|
349
|
+
|
|
350
|
+
| Key | Description |
|
|
351
|
+
| :------- | :---------- |
|
|
352
|
+
| `id` | location of the file on the storage |
|
|
353
|
+
| `storage` | identifier of the storage the file was uploaded to |
|
|
354
|
+
| `metadata` | file [metadata] that was extracted before upload |
|
|
355
|
+
|
|
356
|
+
```rb
|
|
357
|
+
uploaded_file #=> #<Shrine::UploadedFile id="949sdjg834.jpg" storage=:store metadata={...}>
|
|
358
|
+
|
|
359
|
+
uploaded_file.id #=> "949sdjg834.jpg"
|
|
360
|
+
uploaded_file.storage_key #=> :store
|
|
361
|
+
uploaded_file.storage #=> #<Shrine::Storage::S3>
|
|
362
|
+
uploaded_file.metadata #=> {...}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
It comes with many convenient methods that delegate to the storage:
|
|
366
|
+
|
|
367
|
+
```rb
|
|
368
|
+
uploaded_file.url #=> "https://my-bucket.s3.amazonaws.com/949sdjg834.jpg"
|
|
369
|
+
uploaded_file.open { |io| ... } # opens the uploaded file stream
|
|
370
|
+
uploaded_file.download { |file| ... } # downloads the uploaded file to disk
|
|
371
|
+
uploaded_file.stream(destination) # streams uploaded content into a writable destination
|
|
372
|
+
uploaded_file.exists? #=> true
|
|
373
|
+
uploaded_file.delete # deletes the uploaded file from the storage
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
It also implements the IO-like interface that conforms to Shrine's [IO
|
|
377
|
+
abstraction][io abstraction], which allows it to be uploaded again to other
|
|
378
|
+
storages.
|
|
379
|
+
|
|
380
|
+
```rb
|
|
381
|
+
uploaded_file.read # returns content of the uploaded file
|
|
382
|
+
uploaded_file.eof? # returns true if the whole IO was read
|
|
383
|
+
uploaded_file.rewind # rewinds the IO
|
|
384
|
+
uploaded_file.close # closes the IO
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
For more details, see the [Retrieving Uploads] guide and
|
|
388
|
+
[`Shrine::UploadedFile`] API docs.
|
|
389
|
+
|
|
390
|
+
## Attaching
|
|
391
|
+
|
|
392
|
+
To attach uploaded files to database records, Shrine offers an attachment
|
|
393
|
+
interface built on top of uploaders and uploaded files. There are integrations
|
|
394
|
+
for various persistence libraries ([ActiveRecord][activerecord plugin],
|
|
395
|
+
[Sequel][sequel plugin], [ROM][rom plugin], [Hanami][hanami plugin],
|
|
396
|
+
[Mongoid][mongoid plugin]), but you can also attach files to plain structs
|
|
397
|
+
([mutable][model plugin] or [immutable][entity plugin]).
|
|
398
|
+
|
|
399
|
+
```rb
|
|
400
|
+
Shrine.plugin :sequel # :activerecord
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
### Attachment module
|
|
404
|
+
|
|
405
|
+
The easiest way to attach files is with the `Shrine::Attachment` module:
|
|
406
|
+
|
|
407
|
+
```rb
|
|
408
|
+
class Photo < Sequel::Model # ActiveRecord::Base
|
|
409
|
+
include ImageUploader::Attachment.new(:image) #
|
|
410
|
+
include ImageUploader::Attachment[:image] # use your preferred syntax
|
|
411
|
+
include ImageUploader::Attachment(:image) #
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
The included module will add attachment methods for the specified attribute:
|
|
416
|
+
|
|
417
|
+
| Method | Description |
|
|
418
|
+
| :----- | :---------- |
|
|
419
|
+
| `#image=` | uploads the file to temporary storage and serializes the result into `image_data` |
|
|
420
|
+
| `#image` | returns [`Shrine::UploadedFile`][uploaded file] instantiated from `image_data` |
|
|
421
|
+
| `#image_url` | calls `url` on the attachment if it's present, otherwise returns nil |
|
|
422
|
+
| `#image_attacher` | returns instance of [`Shrine::Attacher`][attacher] which handles the attaching |
|
|
423
|
+
|
|
424
|
+
The persistence plugin we loaded will add callbacks that ensure cached files
|
|
425
|
+
are automatically promoted to permanent storage on when record is saved, and
|
|
426
|
+
that attachments are deleted when the record is destroyed.
|
|
427
|
+
|
|
428
|
+
```rb
|
|
429
|
+
# no file is attached
|
|
430
|
+
photo.image #=> nil
|
|
431
|
+
|
|
432
|
+
# the assigned file is cached to temporary storage and written to `image_data` column
|
|
433
|
+
photo.image = File.open("waterfall.jpg", "rb")
|
|
434
|
+
photo.image #=> #<Shrine::UploadedFile ...>
|
|
435
|
+
photo.image_url #=> "/uploads/cache/0sdfllasfi842.jpg"
|
|
436
|
+
photo.image_data #=> '{"id":"0sdfllasfi842.jpg","storage":"cache","metadata":{...}}'
|
|
437
|
+
|
|
438
|
+
# the cached file is promoted to permanent storage and saved to `image_data` column
|
|
439
|
+
photo.save
|
|
440
|
+
photo.image #=> #<Shrine::UploadedFile ...>
|
|
441
|
+
photo.image_url #=> "/uploads/store/l02kladf8jlda.jpg"
|
|
442
|
+
photo.image_data #=> '{"id":"l02kladf8jlda.jpg","storage":"store","metadata":{...}}'
|
|
443
|
+
|
|
444
|
+
# the attached file is deleted with the record
|
|
445
|
+
photo.destroy
|
|
446
|
+
photo.image.exists? #=> false
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
If there is already a file attached and a new file is attached, the previous
|
|
450
|
+
attachment will get deleted when the record gets saved.
|
|
451
|
+
|
|
452
|
+
```rb
|
|
453
|
+
photo.update(image: new_file) # changes the attachment and deletes previous
|
|
454
|
+
photo.update(image: nil) # removes the attachment and deletes previous
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Attacher
|
|
458
|
+
|
|
459
|
+
The methods and callbacks added by the `Shrine::Attachment` module just
|
|
460
|
+
delegate the behaviour to an underlying `Shrine::Attacher` object.
|
|
461
|
+
|
|
462
|
+
```rb
|
|
463
|
+
photo.image_attacher #=> #<Shrine::Attacher>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The `Shrine::Attacher` object can be instantiated and used directly:
|
|
467
|
+
|
|
468
|
+
```rb
|
|
469
|
+
attacher = ImageUploader::Attacher.from_model(photo, :image)
|
|
470
|
+
|
|
471
|
+
attacher.assign(file) # equivalent to `photo.image = file`
|
|
472
|
+
attacher.file # equivalent to `photo.image`
|
|
473
|
+
attacher.url # equivalent to `photo.image_url`
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
The attacher is what drives attaching files to model instances; you can use it
|
|
477
|
+
as a more explicit alternative to models' attachment interface, or when you
|
|
478
|
+
need something that's not available through the attachment methods.
|
|
479
|
+
|
|
480
|
+
See [Using Attacher] guide for more details.
|
|
481
|
+
|
|
482
|
+
### Temporary storage
|
|
483
|
+
|
|
484
|
+
Shrine uses temporary storage to support [file validation][validation] and
|
|
485
|
+
[direct uploads]. If you don't need these features, you can tell Shrine to
|
|
486
|
+
upload files directly to permanent storage:
|
|
487
|
+
|
|
488
|
+
```rb
|
|
489
|
+
Shrine.plugin :model, cache: false
|
|
490
|
+
```
|
|
491
|
+
```rb
|
|
492
|
+
photo.image = File.open("waterfall.jpg", "rb")
|
|
493
|
+
photo.image.storage_key #=> :store
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
If you're using the attacher directly, you can just use `Attacher#attach`
|
|
497
|
+
instead of `Attacher#assign`:
|
|
498
|
+
|
|
499
|
+
```rb
|
|
500
|
+
attacher.attach File.open("waterfall.jpg", "rb")
|
|
501
|
+
attacher.file.storage_key #=> :store
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## Plugin system
|
|
505
|
+
|
|
506
|
+
By default, Shrine comes with a small core which provides only the essential
|
|
507
|
+
functionality. All additional features are available via [plugins], which also
|
|
508
|
+
ship with Shrine. This way you can choose exactly what and how much Shrine does
|
|
509
|
+
for you, and you load the code only for features that you use.
|
|
510
|
+
|
|
511
|
+
```rb
|
|
512
|
+
Shrine.plugin :instrumentation # adds instrumentation
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Plugins add behaviour by extending Shrine core classes via module inclusion, and
|
|
516
|
+
many of them also accept configuration options. The plugin system respects
|
|
517
|
+
inheritance, so you can choose to load a plugin globally or per uploader.
|
|
518
|
+
|
|
519
|
+
```rb
|
|
520
|
+
class ImageUploader < Shrine
|
|
521
|
+
plugin :store_dimensions # extract image dimensions only for this uploader and its descendants
|
|
522
|
+
end
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
If you want to extend Shrine functionality with custom behaviour, you can also
|
|
526
|
+
[create your own plugin][Creating Plugins]. There are also additional [external
|
|
527
|
+
plugins] created by others.
|
|
528
|
+
|
|
529
|
+
> NOTE: An uploader class will inherit a copy of current superclass' plugin
|
|
530
|
+
> options at the time of subclassing. This means you should *not* load
|
|
531
|
+
> additional plugins on a superclass after the subclass has already been
|
|
532
|
+
> created, because new options will not get applied to the subclass, which
|
|
533
|
+
> can result in errors.
|
|
534
|
+
|
|
535
|
+
## Metadata
|
|
536
|
+
|
|
537
|
+
Shrine automatically extracts some basic file metadata and saves them to the
|
|
538
|
+
`Shrine::UploadedFile`. You can access them through the `#metadata` hash or via
|
|
539
|
+
metadata methods:
|
|
540
|
+
|
|
541
|
+
```rb
|
|
542
|
+
uploaded_file.metadata #=>
|
|
543
|
+
# {
|
|
544
|
+
# "filename" => "matrix.mp4",
|
|
545
|
+
# "mime_type" => "video/mp4",
|
|
546
|
+
# "size" => 345993,
|
|
547
|
+
# }
|
|
548
|
+
|
|
549
|
+
uploaded_file.original_filename #=> "matrix.mp4"
|
|
550
|
+
uploaded_file.extension #=> "mp4"
|
|
551
|
+
uploaded_file.mime_type #=> "video/mp4"
|
|
552
|
+
uploaded_file.size #=> 345993
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### MIME type
|
|
556
|
+
|
|
557
|
+
By default, `mime_type` metadata will be set from the `#content_type` attribute
|
|
558
|
+
of the uploaded file (if it exists), which is generally not secure and will
|
|
559
|
+
trigger a warning. You can load the [`determine_mime_type`][determine_mime_type
|
|
560
|
+
plugin] plugin to have MIME type extracted from file *content* instead.
|
|
561
|
+
|
|
562
|
+
```rb
|
|
563
|
+
# Gemfile
|
|
564
|
+
gem "marcel", "~> 0.3"
|
|
565
|
+
```
|
|
566
|
+
```rb
|
|
567
|
+
Shrine.plugin :determine_mime_type, analyzer: :marcel
|
|
568
|
+
```
|
|
569
|
+
```rb
|
|
570
|
+
photo = Photo.new(image: StringIO.new("<?php ... ?>"))
|
|
571
|
+
photo.image.mime_type #=> "application/x-php"
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Other metadata
|
|
575
|
+
|
|
576
|
+
In addition to basic metadata, you can also extract [image
|
|
577
|
+
dimensions][store_dimensions plugin], calculate [signatures][signature plugin],
|
|
578
|
+
and in general extract any [custom metadata][add_metadata plugin]. Check out
|
|
579
|
+
the [Extracting Metadata] guide for more details.
|
|
580
|
+
|
|
581
|
+
## Processing
|
|
582
|
+
|
|
583
|
+
Shrine allows you to process attached files both "eagerly" and "on-the-fly".
|
|
584
|
+
For example, if your app is accepting image uploads, you can generate a
|
|
585
|
+
predefined set of thumbnails when the image is attached to a record, or you
|
|
586
|
+
can have thumbnails generated dynamically as they're needed.
|
|
587
|
+
|
|
588
|
+
For image processing, it's recommended to use the **[ImageProcessing]** gem,
|
|
589
|
+
which is a high-level wrapper for processing with
|
|
590
|
+
[MiniMagick][ImageProcessing::MiniMagick] and [libvips][ImageProcessing::Vips].
|
|
591
|
+
|
|
592
|
+
```
|
|
593
|
+
$ brew install imagemagick vips
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Eager processing
|
|
597
|
+
|
|
598
|
+
We can use the [`derivatives`][derivatives plugin] plugin to generate a
|
|
599
|
+
pre-defined set of processed files (e.g. image thumbnails). We do this by
|
|
600
|
+
registering a derivatives processor block and then explicitly triggering
|
|
601
|
+
creation:
|
|
602
|
+
|
|
603
|
+
```rb
|
|
604
|
+
# Gemfile
|
|
605
|
+
gem "image_processing", "~> 1.8"
|
|
606
|
+
```
|
|
607
|
+
```rb
|
|
608
|
+
Shrine.plugin :derivatives, create_on_promote: true
|
|
609
|
+
```
|
|
610
|
+
```rb
|
|
611
|
+
require "image_processing/mini_magick"
|
|
612
|
+
|
|
613
|
+
class ImageUploader < Shrine
|
|
614
|
+
Attacher.derivatives do |original|
|
|
615
|
+
magick = ImageProcessing::MiniMagick.source(original)
|
|
616
|
+
|
|
617
|
+
{
|
|
618
|
+
large: magick.resize_to_limit!(800, 800),
|
|
619
|
+
medium: magick.resize_to_limit!(500, 500),
|
|
620
|
+
small: magick.resize_to_limit!(300, 300),
|
|
621
|
+
}
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
```
|
|
625
|
+
```rb
|
|
626
|
+
photo = Photo.new(image: file)
|
|
627
|
+
photo.save # automatically creates derivatives on promotion
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
You can then retrieve the URL of a processed derivative:
|
|
631
|
+
|
|
632
|
+
```rb
|
|
633
|
+
photo.image_url(:large) #=> "https://s3.amazonaws.com/path/to/large.jpg"
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
The derivatives data is stored in the `<attachment>_data` column, and you can
|
|
637
|
+
retrieve them as [`Shrine::UploadedFile`][uploaded file] objects:
|
|
638
|
+
|
|
639
|
+
```rb
|
|
640
|
+
photo.image(:large) #=> #<Shrine::UploadedFile id="path/to/large.jpg" storage=:store metadata={...}>
|
|
641
|
+
photo.image(:large).url #=> "https://s3.amazonaws.com/path/to/large.jpg"
|
|
642
|
+
photo.image(:large).size #=> 5825949
|
|
643
|
+
photo.image(:large).mime_type #=> "image/jpeg"
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
For more details, see the [File Processing] guide and the
|
|
647
|
+
[`derivatives`][derivatives plugin] plugin documentation.
|
|
648
|
+
|
|
649
|
+
### On-the-fly processing
|
|
650
|
+
|
|
651
|
+
On-the-fly processing is provided by the
|
|
652
|
+
[`derivation_endpoint`][derivation_endpoint plugin] plugin. To set it up, we
|
|
653
|
+
configure the plugin with a secret key and a path prefix, [mount][Mounting
|
|
654
|
+
Endpoints] its Rack app in our routes on the configured path prefix, and define
|
|
655
|
+
processing we want to perform:
|
|
656
|
+
|
|
657
|
+
```rb
|
|
658
|
+
# Gemfile
|
|
659
|
+
gem "image_processing", "~> 1.8"
|
|
660
|
+
```
|
|
661
|
+
```rb
|
|
662
|
+
# config/initializers/rails.rb (Rails)
|
|
663
|
+
# ...
|
|
664
|
+
Shrine.plugin :derivation_endpoint, secret_key: "<YOUR_SECRET_KEY>"
|
|
665
|
+
```
|
|
666
|
+
```rb
|
|
667
|
+
require "image_processing/mini_magick"
|
|
668
|
+
|
|
669
|
+
class ImageUploader < Shrine
|
|
670
|
+
plugin :derivation_endpoint, prefix: "derivations/image" # matches mount point
|
|
671
|
+
|
|
672
|
+
derivation :thumbnail do |file, width, height|
|
|
673
|
+
ImageProcessing::MiniMagick
|
|
674
|
+
.source(file)
|
|
675
|
+
.resize_to_limit!(width.to_i, height.to_i)
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
```
|
|
679
|
+
```rb
|
|
680
|
+
# config/routes.rb (Rails)
|
|
681
|
+
Rails.application.routes.draw do
|
|
682
|
+
# ...
|
|
683
|
+
mount ImageUploader.derivation_endpoint => "/derivations/image"
|
|
684
|
+
end
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Now we can generate URLs from attached files that will perform the desired
|
|
688
|
+
processing:
|
|
689
|
+
|
|
690
|
+
```rb
|
|
691
|
+
photo.image.derivation_url(:thumbnail, 600, 400)
|
|
692
|
+
#=> "/derivations/image/thumbnail/600/400/eyJpZCI6ImZvbyIsInN0b3JhZ2UiOiJzdG9yZSJ9?signature=..."
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
The on-the-fly processing feature is highly customizable, see the
|
|
696
|
+
[`derivation_endpoint`][derivation_endpoint plugin] plugin documentation for
|
|
697
|
+
more details.
|
|
698
|
+
|
|
699
|
+
## Validation
|
|
700
|
+
|
|
701
|
+
The [`validation`][validation plugin] plugin allows performing validation for
|
|
702
|
+
attached files. For common validations, the
|
|
703
|
+
[`validation_helpers`][validation_helpers plugin] plugin provides useful
|
|
704
|
+
validators for built in metadata:
|
|
705
|
+
|
|
706
|
+
```rb
|
|
707
|
+
Shrine.plugin :validation_helpers
|
|
708
|
+
```
|
|
709
|
+
```rb
|
|
710
|
+
class DocumentUploader < Shrine
|
|
711
|
+
Attacher.validate do
|
|
712
|
+
validate_max_size 5*1024*1024, message: "is too large (max is 5 MB)"
|
|
713
|
+
validate_mime_type %w[application/pdf]
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
```
|
|
717
|
+
|
|
718
|
+
```rb
|
|
719
|
+
user = User.new
|
|
720
|
+
user.cv = File.open("cv.pdf", "rb")
|
|
721
|
+
user.valid? #=> false
|
|
722
|
+
user.errors.to_hash #=> {:cv=>["is too large (max is 5 MB)"]}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
For more details, see the [File Validation] guide and
|
|
726
|
+
[`validation_helpers`][validation_helpers plugin] plugin docs.
|
|
727
|
+
|
|
728
|
+
## Location
|
|
729
|
+
|
|
730
|
+
Shrine automatically generates random locations before uploading files. By
|
|
731
|
+
default, the hierarchy is flat, meaning all files are stored in the root
|
|
732
|
+
directory of the storage.
|
|
733
|
+
|
|
734
|
+
```
|
|
735
|
+
024d9fe83bf4fafb.jpg
|
|
736
|
+
768a336bf54de219.jpg
|
|
737
|
+
adfaa363629f7fc5.png
|
|
738
|
+
...
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
The [`pretty_location`][pretty_location plugin] plugin provides a good default
|
|
742
|
+
hierarchy:
|
|
743
|
+
|
|
744
|
+
```rb
|
|
745
|
+
Shrine.plugin :pretty_location
|
|
746
|
+
```
|
|
747
|
+
```
|
|
748
|
+
user/
|
|
749
|
+
564/
|
|
750
|
+
avatar/
|
|
751
|
+
aa3e0cd715.jpg
|
|
752
|
+
thumb-493g82jf23.jpg
|
|
753
|
+
photo/
|
|
754
|
+
123/
|
|
755
|
+
image/
|
|
756
|
+
13f8a7bc18.png
|
|
757
|
+
thumb-9be62da67e.png
|
|
758
|
+
...
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
But you can also override `Shrine#generate_location` with a custom
|
|
762
|
+
implementation, for example:
|
|
763
|
+
|
|
764
|
+
```rb
|
|
765
|
+
class ImageUploader < Shrine
|
|
766
|
+
def generate_location(io, record: nil, derivative: nil, **)
|
|
767
|
+
return super unless record
|
|
768
|
+
|
|
769
|
+
table = record.class.table_name
|
|
770
|
+
id = record.id
|
|
771
|
+
prefix = derivative || "original"
|
|
772
|
+
|
|
773
|
+
"uploads/#{table}/#{id}/#{prefix}-#{super}"
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
```
|
|
777
|
+
```
|
|
778
|
+
uploads/
|
|
779
|
+
photos/
|
|
780
|
+
123/
|
|
781
|
+
original-afe929b8b4.jpg
|
|
782
|
+
small-ad61f25883.jpg
|
|
783
|
+
medium-41b75c42bb.jpg
|
|
784
|
+
large-73e67abe50.jpg
|
|
785
|
+
...
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
> There should always be a random component in the location, so that the ORM
|
|
789
|
+
dirty tracking is detected properly.
|
|
790
|
+
|
|
791
|
+
The `Shrine#generate_location` method contains a lot of useful context for the
|
|
792
|
+
upcoming upload:
|
|
793
|
+
|
|
794
|
+
```rb
|
|
795
|
+
class ImageUploader < Shrine
|
|
796
|
+
def generate_location(io, record: nil, name: nil, derivative: nil, metadata: {}, **options)
|
|
797
|
+
storage_key #=> :cache, :store, ...
|
|
798
|
+
io #=> #<File>, #<Shrine::UploadedFile>, ...
|
|
799
|
+
record #=> #<Photo>, #<User>, ...
|
|
800
|
+
name #=> :image, :avatar, ...
|
|
801
|
+
derivative #=> :small, :medium, :large, ... (derivatives plugin)
|
|
802
|
+
metadata #=> { "filename" => "nature.jpg", "mime_type" => "image/jpeg", "size" => 18573, ... }
|
|
803
|
+
options #=> { ... other uploader options ... }
|
|
804
|
+
|
|
805
|
+
# ...
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
## Direct uploads
|
|
811
|
+
|
|
812
|
+
To improve the user experience, it's recommended to upload files asynchronously
|
|
813
|
+
as soon as the user selects them. The direct uploads would go to temporary
|
|
814
|
+
storage, just like in the synchronous flow. Then, instead of attaching a raw
|
|
815
|
+
file to your model, you assign the cached file JSON data.
|
|
816
|
+
|
|
817
|
+
```rb
|
|
818
|
+
# in the regular synchronous flow
|
|
819
|
+
photo.image = file
|
|
820
|
+
|
|
821
|
+
# in the direct upload flow
|
|
822
|
+
photo.image = '{"id":"...","storage":"cache","metadata":{...}}'
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
On the client side it's highly recommended to use **[Uppy]**, a very flexible
|
|
826
|
+
modern JavaScript file upload library that happens to integrate nicely with
|
|
827
|
+
Shrine.
|
|
828
|
+
|
|
829
|
+
### Simple direct upload
|
|
830
|
+
|
|
831
|
+
The simplest approach is to upload directly to an endpoint in your app, which
|
|
832
|
+
forwards uploads to the specified storage. The
|
|
833
|
+
[`upload_endpoint`][upload_endpoint plugin] Shrine plugin provides a
|
|
834
|
+
[mountable][Mounting Endpoints] Rack app that implements this endpoint:
|
|
835
|
+
|
|
836
|
+
```rb
|
|
837
|
+
Shrine.plugin :upload_endpoint
|
|
838
|
+
```
|
|
839
|
+
```rb
|
|
840
|
+
# config/routes.rb (Rails)
|
|
841
|
+
Rails.application.routes.draw do
|
|
842
|
+
# ...
|
|
843
|
+
mount Shrine.upload_endpoint(:cache) => "/upload" # POST /upload
|
|
844
|
+
end
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Then you can configure Uppy's [XHR Upload][uppy xhr-upload] plugin to upload to
|
|
848
|
+
this endpoint. See [this walkthrough][Adding Direct App Uploads] for adding
|
|
849
|
+
simple direct uploads from scratch, it includes a complete JavaScript example
|
|
850
|
+
(there is also the [Roda][roda demo] / [Rails][rails demo] demo app).
|
|
851
|
+
|
|
852
|
+
### Presigned direct upload
|
|
853
|
+
|
|
854
|
+
For better performance, you can also upload files directly to your cloud
|
|
855
|
+
storage service (AWS S3, Google Cloud Storage etc). For this, your temporary
|
|
856
|
+
storage needs to be your cloud service:
|
|
857
|
+
|
|
858
|
+
```rb
|
|
859
|
+
require "shrine/storage/s3"
|
|
860
|
+
|
|
861
|
+
Shrine.storages = {
|
|
862
|
+
cache: Shrine::Storage::S3.new(prefix: "cache", **s3_options),
|
|
863
|
+
store: Shrine::Storage::S3.new(**s3_options)
|
|
864
|
+
}
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
In this flow, the client needs to first fetch upload parameters from the
|
|
868
|
+
server, and then use these parameters for the upload to the cloud service.
|
|
869
|
+
The [`presign_endpoint`][presign_endpoint plugin] Shrine plugin provides a
|
|
870
|
+
[mountable][Mounting Endpoints] Rack app that generates upload parameters:
|
|
871
|
+
|
|
872
|
+
```rb
|
|
873
|
+
Shrine.plugin :presign_endpoint
|
|
874
|
+
```
|
|
875
|
+
```rb
|
|
876
|
+
# config/routes.rb (Rails)
|
|
877
|
+
Rails.application.routes.draw do
|
|
878
|
+
# ...
|
|
879
|
+
mount Shrine.presign_endpoint(:cache) => "/s3/params" # GET /s3/params
|
|
880
|
+
end
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
Then you can configure Uppy's [AWS S3][uppy aws-s3] plugin to fetch params from
|
|
884
|
+
your endpoint before uploading to S3. See [this walkthrough][Adding Direct S3
|
|
885
|
+
Uploads] for adding direct uploads to S3 from scratch, it includes a complete
|
|
886
|
+
JavaScript example (there is also the [Roda][roda demo] / [Rails][rails demo]
|
|
887
|
+
demo). See also the [Direct Uploads to S3] guide for more details.
|
|
888
|
+
|
|
889
|
+
### Resumable direct upload
|
|
890
|
+
|
|
891
|
+
If your app is accepting large uploads, you can improve resilience by making
|
|
892
|
+
the uploads **resumable**. This can significantly improve experience for users
|
|
893
|
+
on slow and flaky internet connections.
|
|
894
|
+
|
|
895
|
+
#### Uppy S3 Multipart
|
|
896
|
+
|
|
897
|
+
You can achieve resumable uploads directly to S3 with the [AWS S3
|
|
898
|
+
Multipart][uppy aws-s3-multipart] Uppy plugin, accompanied with
|
|
899
|
+
`uppy_s3_multipart` Shrine plugin provided by the [uppy-s3_multipart] gem.
|
|
900
|
+
|
|
901
|
+
```rb
|
|
902
|
+
# Gemfile
|
|
903
|
+
gem "uppy-s3_multipart", "~> 0.3"
|
|
904
|
+
```
|
|
905
|
+
```rb
|
|
906
|
+
Shrine.plugin :uppy_s3_multipart
|
|
907
|
+
```
|
|
908
|
+
```rb
|
|
909
|
+
# config/routes.rb (Rails)
|
|
910
|
+
Rails.application.routes.draw do
|
|
911
|
+
# ...
|
|
912
|
+
mount Shrine.uppy_s3_multipart(:cache) => "/s3/multipart"
|
|
913
|
+
end
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
See the [uppy-s3_multipart] docs for more details.
|
|
917
|
+
|
|
918
|
+
#### Tus protocol
|
|
919
|
+
|
|
920
|
+
If you want a more generic approach, you can build your resumable uploads on
|
|
921
|
+
**[tus]** – an open resumable upload protocol. On the server side you can use
|
|
922
|
+
the [tus-ruby-server] gem, on the client side Uppy's [Tus][uppy tus] plugin,
|
|
923
|
+
and the [shrine-tus] gem for the glue.
|
|
924
|
+
|
|
925
|
+
```rb
|
|
926
|
+
# Gemfile
|
|
927
|
+
gem "tus-server", "~> 2.0"
|
|
928
|
+
gem "shrine-tus", "~> 2.1"
|
|
929
|
+
```
|
|
930
|
+
```rb
|
|
931
|
+
require "shrine/storage/tus"
|
|
932
|
+
|
|
933
|
+
Shrine.storages = {
|
|
934
|
+
cache: Shrine::Storage::Tus.new, # tus server acts as temporary storage
|
|
935
|
+
store: ..., # your permanent storage
|
|
936
|
+
}
|
|
937
|
+
```
|
|
938
|
+
```rb
|
|
939
|
+
# config/routes.rb (Rails)
|
|
940
|
+
Rails.application.routes.draw do
|
|
941
|
+
# ...
|
|
942
|
+
mount Tus::Server => "/files"
|
|
943
|
+
end
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
See [this walkthrough][Adding Resumable Uploads] for adding tus-powered
|
|
947
|
+
resumable uploads from scratch, it includes a complete JavaScript example
|
|
948
|
+
(there is also a [demo app][resumable demo]). See also [shrine-tus] and
|
|
949
|
+
[tus-ruby-server] docs for more details.
|
|
950
|
+
|
|
951
|
+
## Backgrounding
|
|
952
|
+
|
|
953
|
+
The [`backgrounding`][backgrounding plugin] plugin allows you to move file promotion
|
|
954
|
+
and deletion into a background job, using the backgrounding library [of your
|
|
955
|
+
choice][Backgrounding Libraries]:
|
|
956
|
+
|
|
957
|
+
```rb
|
|
958
|
+
Shrine.plugin :backgrounding
|
|
959
|
+
Shrine::Attacher.promote_block do
|
|
960
|
+
PromoteJob.perform_async(self.class.name, record.class.name, record.id, name, file_data)
|
|
961
|
+
end
|
|
962
|
+
Shrine::Attacher.destroy_block do
|
|
963
|
+
DestroyJob.perform_async(self.class.name, data)
|
|
964
|
+
end
|
|
965
|
+
```
|
|
966
|
+
```rb
|
|
967
|
+
class PromoteJob
|
|
968
|
+
include Sidekiq::Worker
|
|
969
|
+
|
|
970
|
+
def perform(attacher_class, record_class, record_id, name, file_data)
|
|
971
|
+
attacher_class = Object.const_get(attacher_class)
|
|
972
|
+
record = Object.const_get(record_class).find(record_id) # if using Active Record
|
|
973
|
+
|
|
974
|
+
attacher = attacher_class.retrieve(model: record, name: name, file: file_data)
|
|
975
|
+
attacher.atomic_promote
|
|
976
|
+
rescue Shrine::AttachmentChanged, ActiveRecord::RecordNotFound
|
|
977
|
+
# attachment has changed or the record has been deleted, nothing to do
|
|
978
|
+
end
|
|
979
|
+
end
|
|
980
|
+
```
|
|
981
|
+
```rb
|
|
982
|
+
class DestroyJob
|
|
983
|
+
include Sidekiq::Worker
|
|
984
|
+
|
|
985
|
+
def perform(attacher_class, data)
|
|
986
|
+
attacher_class = Object.const_get(attacher_class)
|
|
987
|
+
|
|
988
|
+
attacher = attacher_class.from_data(data)
|
|
989
|
+
attacher.destroy
|
|
990
|
+
end
|
|
991
|
+
end
|
|
992
|
+
```
|
|
993
|
+
|
|
994
|
+
## Clearing cache
|
|
995
|
+
|
|
996
|
+
Shrine doesn't automatically delete files uploaded to temporary storage, instead
|
|
997
|
+
you should set up a separate recurring task that will automatically delete old
|
|
998
|
+
cached files.
|
|
999
|
+
|
|
1000
|
+
Most Shrine storage classes come with a `#clear!` method, which you can call in
|
|
1001
|
+
a recurring script. For FileSystem and S3 storage it would look like this:
|
|
1002
|
+
|
|
1003
|
+
```rb
|
|
1004
|
+
# FileSystem storage
|
|
1005
|
+
file_system = Shrine.storages[:cache]
|
|
1006
|
+
file_system.clear! { |path| path.mtime < Time.now - 7*24*60*60 } # delete files older than 1 week
|
|
1007
|
+
```
|
|
1008
|
+
```rb
|
|
1009
|
+
# S3 storage
|
|
1010
|
+
s3 = Shrine.storages[:cache]
|
|
1011
|
+
s3.clear! { |object| object.last_modified < Time.now - 7*24*60*60 } # delete files older than 1 week
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
For S3, it may be easier and cheaper to use [S3 bucket lifecycle expiration rules](http://docs.aws.amazon.com/AmazonS3/latest/UG/lifecycle-configuration-bucket-no-versioning.html) instead.
|
|
1015
|
+
|
|
1016
|
+
## Logging
|
|
1017
|
+
|
|
1018
|
+
The [`instrumentation`][instrumentation plugin] plugin sends and logs events for
|
|
1019
|
+
important operations:
|
|
1020
|
+
|
|
1021
|
+
```rb
|
|
1022
|
+
Shrine.plugin :instrumentation, notifications: ActiveSupport::Notifications
|
|
1023
|
+
|
|
1024
|
+
uploaded_file = Shrine.upload(io, :store)
|
|
1025
|
+
uploaded_file.exists?
|
|
1026
|
+
uploaded_file.download
|
|
1027
|
+
uploaded_file.delete
|
|
1028
|
+
```
|
|
1029
|
+
```
|
|
1030
|
+
Metadata (32ms) – {:storage=>:store, :io=>StringIO, :uploader=>Shrine}
|
|
1031
|
+
Upload (1523ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :io=>StringIO, :upload_options=>{}, :uploader=>Shrine}
|
|
1032
|
+
Exists (755ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :uploader=>Shrine}
|
|
1033
|
+
Download (1002ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :download_options=>{}, :uploader=>Shrine}
|
|
1034
|
+
Delete (700ms) – {:storage=>:store, :location=>"ed0e30ddec8b97813f2c1f4cfd1700b4", :uploader=>Shrine}
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
Some plugins add their own instrumentation as well when they detect that the
|
|
1038
|
+
`instrumentation` plugin has been loaded. For that to work, the
|
|
1039
|
+
`instrumentation` plugin needs to be loaded *before* any of these plugins.
|
|
1040
|
+
|
|
1041
|
+
| Plugin | Instrumentation |
|
|
1042
|
+
| :----- | :-------------- |
|
|
1043
|
+
| `derivation_endpoint` | instruments file processing |
|
|
1044
|
+
| `derivatives` | instruments file processing |
|
|
1045
|
+
| `determine_mime_type` | instruments analyzing MIME type |
|
|
1046
|
+
| `store_dimensions` | instruments extracting image dimensions |
|
|
1047
|
+
| `signature` | instruments calculating signature |
|
|
1048
|
+
| `infer_extension` | instruments inferring extension |
|
|
1049
|
+
| `remote_url` | instruments remote URL downloading |
|
|
1050
|
+
| `data_uri` | instruments data URI parsing |
|
|
1051
|
+
|
|
1052
|
+
For instrumentation, warnings, and other logging, Shrine uses its internal
|
|
1053
|
+
logger. You can tell Shrine to use a different logger. For example, if you're
|
|
1054
|
+
using Rails, you might want to tell it to use the Rails logger:
|
|
1055
|
+
|
|
1056
|
+
```rb
|
|
1057
|
+
Shrine.logger = Rails.logger
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
In tests you might want to tell Shrine to log only warnings:
|
|
1061
|
+
|
|
1062
|
+
```rb
|
|
1063
|
+
Shrine.logger.level = Logger::WARN
|
|
1064
|
+
```
|
|
1065
|
+
|
|
1066
|
+
[Creating Plugins]: https://shrinerb.com/docs/creating-plugins
|
|
1067
|
+
[Creating Storages]: https://shrinerb.com/docs/creating-storages
|
|
1068
|
+
[Direct Uploads to S3]: https://shrinerb.com/docs/direct-s3
|
|
1069
|
+
[Extracting Metadata]: https://shrinerb.com/docs/metadata
|
|
1070
|
+
[File Processing]: https://shrinerb.com/docs/processing
|
|
1071
|
+
[File Validation]: https://shrinerb.com/docs/validation
|
|
1072
|
+
[Retrieving Uploads]: https://shrinerb.com/docs/retrieving-uploads
|
|
1073
|
+
[Using Attacher]: https://shrinerb.com/docs/attacher
|
|
1074
|
+
[FileSystem]: https://shrinerb.com/docs/storage/file-system
|
|
1075
|
+
[S3]: https://shrinerb.com/docs/storage/s3
|
|
1076
|
+
[Memory]: https://shrinerb.com/docs/storage/memory
|
|
1077
|
+
[Testing with Shrine]: https://shrinerb.com/docs/testing
|
|
1078
|
+
[`Shrine::UploadedFile`]: https://shrinerb.com/rdoc/classes/Shrine/UploadedFile/InstanceMethods.html
|
|
1079
|
+
|
|
1080
|
+
[attacher]: #attacher
|
|
1081
|
+
[attachment]: #attaching
|
|
1082
|
+
[direct uploads]: #direct-uploads
|
|
1083
|
+
[io abstraction]: #io-abstraction
|
|
1084
|
+
[location]: #location
|
|
1085
|
+
[metadata]: #metadata
|
|
1086
|
+
[presigned upload]: #presigned-direct-upload
|
|
1087
|
+
[storage]: #storage
|
|
1088
|
+
[uploaded file]: #uploaded-file
|
|
1089
|
+
[uploader]: #uploader
|
|
1090
|
+
[validation]: #validation
|
|
1091
|
+
|
|
1092
|
+
[Adding Direct App Uploads]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-App-Uploads
|
|
1093
|
+
[Adding Resumable Uploads]: https://github.com/shrinerb/shrine/wiki/Adding-Resumable-Uploads
|
|
1094
|
+
[Adding Direct S3 Uploads]: https://github.com/shrinerb/shrine/wiki/Adding-Direct-S3-Uploads
|
|
1095
|
+
[Backgrounding Libraries]: https://github.com/shrinerb/shrine/wiki/Backgrounding-Libraries
|
|
1096
|
+
[Mounting Endpoints]: https://github.com/shrinerb/shrine/wiki/Mounting-Endpoints
|
|
1097
|
+
|
|
1098
|
+
[AWS S3]: https://aws.amazon.com/s3/
|
|
1099
|
+
[MinIO]: https://min.io/
|
|
1100
|
+
[DigitalOcean Spaces]: https://www.digitalocean.com/products/spaces/
|
|
1101
|
+
[Cloudinary]: https://github.com/shrinerb/shrine-cloudinary
|
|
1102
|
+
[GCS]: https://github.com/renchap/shrine-google_cloud_storage
|
|
1103
|
+
[uppy-s3_multipart]: https://github.com/janko/uppy-s3_multipart
|
|
1104
|
+
[tus-ruby-server]: https://github.com/janko/tus-ruby-server
|
|
1105
|
+
|
|
1106
|
+
[Uppy]: https://uppy.io
|
|
1107
|
+
[shrine-tus]: https://github.com/shrinerb/shrine-tus
|
|
1108
|
+
[tus]: https://tus.io
|
|
1109
|
+
[uppy aws-s3-multipart]: https://uppy.io/docs/aws-s3-multipart/
|
|
1110
|
+
[uppy aws-s3]: https://uppy.io/docs/aws-s3/
|
|
1111
|
+
[uppy tus]: https://uppy.io/docs/tus/
|
|
1112
|
+
[uppy xhr-upload]: https://uppy.io/docs/xhr-upload/
|
|
1113
|
+
|
|
1114
|
+
[ImageProcessing]: https://github.com/janko/image_processing
|
|
1115
|
+
[ImageProcessing::MiniMagick]: https://github.com/janko/image_processing/blob/master/doc/minimagick.md#readme
|
|
1116
|
+
[ImageProcessing::Vips]: https://github.com/janko/image_processing/blob/master/doc/vips.md#readme
|
|
1117
|
+
[`file`]: http://linux.die.net/man/1/file
|
|
1118
|
+
[Down]: https://github.com/janko/down
|
|
1119
|
+
|
|
1120
|
+
[activerecord plugin]: https://shrinerb.com/docs/plugins/activerecord
|
|
1121
|
+
[add_metadata plugin]: https://shrinerb.com/docs/plugins/add_metadata
|
|
1122
|
+
[backgrounding plugin]: https://shrinerb.com/docs/plugins/backgrounding
|
|
1123
|
+
[data_uri plugin]: https://shrinerb.com/docs/plugins/data_uri
|
|
1124
|
+
[derivation_endpoint plugin]: https://shrinerb.com/docs/plugins/derivation_endpoint
|
|
1125
|
+
[derivatives plugin]: https://shrinerb.com/docs/plugins/derivatives
|
|
1126
|
+
[determine_mime_type plugin]: https://shrinerb.com/docs/plugins/determine_mime_type
|
|
1127
|
+
[instrumentation plugin]: https://shrinerb.com/docs/plugins/instrumentation
|
|
1128
|
+
[hanami plugin]: https://github.com/katafrakt/hanami-shrine
|
|
1129
|
+
[model plugin]: https://shrinerb.com/docs/plugins/model
|
|
1130
|
+
[entity plugin]: https://shrinerb.com/docs/plugins/entity
|
|
1131
|
+
[mongoid plugin]: https://github.com/shrinerb/shrine-mongoid
|
|
1132
|
+
[presign_endpoint plugin]: https://shrinerb.com/docs/plugins/presign_endpoint
|
|
1133
|
+
[pretty_location plugin]: https://shrinerb.com/docs/plugins/pretty_location
|
|
1134
|
+
[rack_file plugin]: https://shrinerb.com/docs/plugins/rack_file
|
|
1135
|
+
[rom plugin]: https://github.com/shrinerb/shrine-rom
|
|
1136
|
+
[sequel plugin]: https://shrinerb.com/docs/plugins/sequel
|
|
1137
|
+
[signature plugin]: https://shrinerb.com/docs/plugins/signature
|
|
1138
|
+
[store_dimensions plugin]: https://shrinerb.com/docs/plugins/store_dimensions
|
|
1139
|
+
[upload_endpoint plugin]: https://shrinerb.com/docs/plugins/upload_endpoint
|
|
1140
|
+
[validation_helpers plugin]: https://shrinerb.com/docs/plugins/validation_helpers
|
|
1141
|
+
[validation plugin]: https://shrinerb.com/docs/plugins/validation
|
|
1142
|
+
|
|
1143
|
+
[rails demo]: https://github.com/erikdahlstrand/shrine-rails-example
|
|
1144
|
+
[roda demo]: https://github.com/shrinerb/shrine/tree/master/demo
|
|
1145
|
+
[resumable demo]: https://github.com/shrinerb/shrine-tus-demo
|
|
1146
|
+
|
|
1147
|
+
[`#read`]: https://ruby-doc.org/core/IO.html#method-i-read
|
|
1148
|
+
[`#eof?`]: https://ruby-doc.org/core/IO.html#method-i-eof
|
|
1149
|
+
[`#rewind`]: https://ruby-doc.org/core/IO.html#method-i-rewind
|
|
1150
|
+
[`#close`]: https://ruby-doc.org/core/IO.html#method-i-close
|
|
1151
|
+
[`IO`]: https://ruby-doc.org/core/IO.html
|
|
1152
|
+
|
|
1153
|
+
[storages]: https://shrinerb.com/docs/external/extensions#storages
|
|
1154
|
+
[plugins]: https://shrinerb.com/plugins
|
|
1155
|
+
[external plugins]: https://shrinerb.com/docs/external/extensions#plugins
|
|
1156
|
+
[gin index]: https://www.postgresql.org/docs/current/datatype-json.html#JSON-INDEXING
|