shrine 1.4.2 → 2.0.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 +236 -234
- data/doc/changing_location.md +6 -4
- data/doc/creating_storages.md +4 -4
- data/doc/design.md +223 -0
- data/doc/migrating_storage.md +6 -11
- data/doc/regenerating_versions.md +22 -40
- data/lib/shrine.rb +60 -77
- data/lib/shrine/plugins/activerecord.rb +37 -14
- data/lib/shrine/plugins/background_helpers.rb +1 -0
- data/lib/shrine/plugins/backgrounding.rb +49 -37
- data/lib/shrine/plugins/backup.rb +6 -4
- data/lib/shrine/plugins/cached_attachment_data.rb +5 -5
- data/lib/shrine/plugins/data_uri.rb +9 -9
- data/lib/shrine/plugins/default_storage.rb +4 -4
- data/lib/shrine/plugins/default_url.rb +7 -1
- data/lib/shrine/plugins/default_url_options.rb +1 -1
- data/lib/shrine/plugins/delete_promoted.rb +2 -2
- data/lib/shrine/plugins/delete_raw.rb +4 -4
- data/lib/shrine/plugins/determine_mime_type.rb +50 -43
- data/lib/shrine/plugins/direct_upload.rb +10 -20
- data/lib/shrine/plugins/download_endpoint.rb +16 -13
- data/lib/shrine/plugins/dynamic_storage.rb +4 -12
- data/lib/shrine/plugins/included.rb +6 -19
- data/lib/shrine/plugins/keep_files.rb +4 -4
- data/lib/shrine/plugins/logging.rb +4 -4
- data/lib/shrine/plugins/migration_helpers.rb +37 -34
- data/lib/shrine/plugins/moving.rb +19 -32
- data/lib/shrine/plugins/parallelize.rb +5 -5
- data/lib/shrine/plugins/pretty_location.rb +2 -6
- data/lib/shrine/plugins/remote_url.rb +31 -43
- data/lib/shrine/plugins/remove_attachment.rb +5 -5
- data/lib/shrine/plugins/remove_invalid.rb +1 -1
- data/lib/shrine/plugins/restore_cached_data.rb +4 -10
- data/lib/shrine/plugins/sequel.rb +46 -21
- data/lib/shrine/plugins/store_dimensions.rb +19 -20
- data/lib/shrine/plugins/upload_options.rb +11 -9
- data/lib/shrine/plugins/validation_helpers.rb +3 -3
- data/lib/shrine/plugins/versions.rb +18 -3
- data/lib/shrine/storage/file_system.rb +9 -11
- data/lib/shrine/storage/linter.rb +1 -7
- data/lib/shrine/storage/s3.rb +25 -19
- data/lib/shrine/version.rb +3 -3
- data/shrine.gemspec +13 -3
- metadata +28 -9
- data/lib/shrine/plugins/delete_uploaded.rb +0 -3
- data/lib/shrine/plugins/keep_location.rb +0 -46
- data/lib/shrine/plugins/restore_cached.rb +0 -3
data/doc/changing_location.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
#
|
1
|
+
# Migrating to Different Location
|
2
2
|
|
3
3
|
You have a production app with already uploaded attachments. However, you've
|
4
4
|
realized that the existing store folder structure for attachments isn't working
|
@@ -19,11 +19,13 @@ live production).
|
|
19
19
|
Shrine.plugin :delete_promoted
|
20
20
|
|
21
21
|
User.paged_each do |user|
|
22
|
-
user.
|
22
|
+
attacher = user.avatar_attacher
|
23
|
+
attacher.promote(phase: :migrate) if attacher.stored?
|
24
|
+
# use `attacher._promote(phase: :migrate)` if you want promoting to be backgrounded
|
23
25
|
end
|
24
26
|
```
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
+
The `:phase` is not mandatory, it's just for better introspection when
|
29
|
+
monitoring background jobs and logs.
|
28
30
|
|
29
31
|
Now all your existing attachments should be happily living on new locations.
|
data/doc/creating_storages.md
CHANGED
@@ -13,7 +13,7 @@ class Shrine
|
|
13
13
|
# initializing logic
|
14
14
|
end
|
15
15
|
|
16
|
-
def upload(io, id,
|
16
|
+
def upload(io, id, shrine_metadata: {}, **upload_options)
|
17
17
|
# uploads `io` to the location `id`
|
18
18
|
end
|
19
19
|
|
@@ -53,7 +53,7 @@ If your storage doesn't control which id the uploaded file will have, you
|
|
53
53
|
can modify the `id` variable:
|
54
54
|
|
55
55
|
```rb
|
56
|
-
def upload(io, id,
|
56
|
+
def upload(io, id, shrine_metadata: {}, **upload_options)
|
57
57
|
actual_id = do_upload(io, id, metadata)
|
58
58
|
id.replace(actual_id)
|
59
59
|
end
|
@@ -63,7 +63,7 @@ Likewise, if you need to save some information into the metadata after upload,
|
|
63
63
|
you can modify the metadata hash:
|
64
64
|
|
65
65
|
```rb
|
66
|
-
def upload(io, id,
|
66
|
+
def upload(io, id, shrine_metadata: {}, **upload_options)
|
67
67
|
additional_metadata = do_upload(io, id, metadata)
|
68
68
|
metadata.merge!(additional_metadata)
|
69
69
|
end
|
@@ -125,7 +125,7 @@ class Shrine
|
|
125
125
|
class MyStorage
|
126
126
|
# ...
|
127
127
|
|
128
|
-
def move(io, id,
|
128
|
+
def move(io, id, shrine_metadata: {}, **upload_options)
|
129
129
|
# does the moving of the `io` to the location `id`
|
130
130
|
end
|
131
131
|
|
data/doc/design.md
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
# The Design of Shrine
|
2
|
+
|
3
|
+
There are five main types of objects that you deal with in Shrine:
|
4
|
+
|
5
|
+
* Storage
|
6
|
+
* `Shrine`
|
7
|
+
* `Shrine::UploadedFile`
|
8
|
+
* `Shrine::Attacher`
|
9
|
+
* `Shrine::Attachment`
|
10
|
+
|
11
|
+
## Storage
|
12
|
+
|
13
|
+
On the lowest level we have a storage. A storage class encapsulates file
|
14
|
+
management logic on a particular service. It is what actually performs uploads,
|
15
|
+
generation of URLs, deletions and similar. By convention it is namespaced under
|
16
|
+
`Shrine::Storage`.
|
17
|
+
|
18
|
+
```rb
|
19
|
+
filesystem = Shrine::Storage::FileSystem.new("uploads")
|
20
|
+
filesystem.upload(file, "foo")
|
21
|
+
filesystem.url("foo") #=> "uploads/foo"
|
22
|
+
filesystem.delete("foo")
|
23
|
+
```
|
24
|
+
|
25
|
+
A storage is a PORO which responds to certain methods:
|
26
|
+
|
27
|
+
```rb
|
28
|
+
class Shrine
|
29
|
+
module Storage
|
30
|
+
class MyStorage
|
31
|
+
def initialize(*args)
|
32
|
+
# initializing logic
|
33
|
+
end
|
34
|
+
|
35
|
+
def upload(io, id, shrine_metadata: {}, **upload_options)
|
36
|
+
# uploads `io` to the location `id`
|
37
|
+
end
|
38
|
+
|
39
|
+
def download(id)
|
40
|
+
# downloads the file from the storage
|
41
|
+
end
|
42
|
+
|
43
|
+
def open(id)
|
44
|
+
# returns the remote file as an IO-like object
|
45
|
+
end
|
46
|
+
|
47
|
+
def read(id)
|
48
|
+
# returns the file contents as a string
|
49
|
+
end
|
50
|
+
|
51
|
+
def exists?(id)
|
52
|
+
# checks if the file exists on the storage
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete(id)
|
56
|
+
# deletes the file from the storage
|
57
|
+
end
|
58
|
+
|
59
|
+
def url(id, options = {})
|
60
|
+
# URL to the remote file, accepts options for customizing the URL
|
61
|
+
end
|
62
|
+
|
63
|
+
def clear!(confirm = nil)
|
64
|
+
# deletes all the files in the storage
|
65
|
+
end
|
66
|
+
|
67
|
+
# ...
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
Storages are typically not used directly, but through `Shrine`.
|
74
|
+
|
75
|
+
## `Shrine`
|
76
|
+
|
77
|
+
A `Shrine` object (also called an "uploader") acts as a wrapper around a
|
78
|
+
storage. First the storage needs to be registered under a name:
|
79
|
+
|
80
|
+
```rb
|
81
|
+
Shrine.storages[:file_system] = Shrine::Storage::FileSystem.new("uploads")
|
82
|
+
```
|
83
|
+
|
84
|
+
Now we can instantiate an uploader with this identifier, and upload files:
|
85
|
+
|
86
|
+
```rb
|
87
|
+
uploader = Shrine.new(:file_system)
|
88
|
+
uploaded_file = uploader.upload(file)
|
89
|
+
uploaded_file #=> #<Shrine::UploadedFile>
|
90
|
+
```
|
91
|
+
|
92
|
+
The argument to `Shrine#upload` must be an IO-like object. The method does the
|
93
|
+
following:
|
94
|
+
|
95
|
+
* generates a unique location
|
96
|
+
* extracts metadata
|
97
|
+
* uploads the file
|
98
|
+
* closes the file
|
99
|
+
* creates a `Shrine::UploadedFile` from the data
|
100
|
+
|
101
|
+
In applications it's common to create subclasses of `Shrine`, in order to allow
|
102
|
+
having different uploading logic for different types of files.
|
103
|
+
|
104
|
+
## `Shrine::UploadedFile`
|
105
|
+
|
106
|
+
`Shrine::UploadedFile` represents a file that was uploaded to a storage, and is
|
107
|
+
the result of `Shrine#upload`. It is essentially a wrapper around a data hash
|
108
|
+
containing information about the uploaded file.
|
109
|
+
|
110
|
+
```rb
|
111
|
+
uploaded_file #=> #<Shrine::UploadedFile>
|
112
|
+
uploaded_file.data #=>
|
113
|
+
# {
|
114
|
+
# "storage" => "file_system",
|
115
|
+
# "id" => "9260ea09d8effd.pdf",
|
116
|
+
# "metadata" => {
|
117
|
+
# "filename" => "resume.pdf",
|
118
|
+
# "mime_type" => "application/pdf",
|
119
|
+
# "size" => 983294,
|
120
|
+
# },
|
121
|
+
# }
|
122
|
+
```
|
123
|
+
|
124
|
+
The data hash contains the storage the file was uploaded to, the location, and
|
125
|
+
some metadata: original filename, MIME type and filesize. The
|
126
|
+
`Shrine::UploadedFile` object has handy methods which use this data:
|
127
|
+
|
128
|
+
```rb
|
129
|
+
# metadata methods
|
130
|
+
uploaded_file.original_filename
|
131
|
+
uploaded_file.mime_type
|
132
|
+
uploaded_file.size
|
133
|
+
# ...
|
134
|
+
|
135
|
+
# storage methods
|
136
|
+
uploaded_file.url
|
137
|
+
uploaded_file.exists?
|
138
|
+
uploaded_file.download
|
139
|
+
uploaded_file.delete
|
140
|
+
# ...
|
141
|
+
```
|
142
|
+
|
143
|
+
A `Shrine::UploadedFile` is itself an IO-like object (representing the
|
144
|
+
remote file), so it can be passed to `Shrine#upload` as well.
|
145
|
+
|
146
|
+
## `Shrine::Attacher`
|
147
|
+
|
148
|
+
We usually want to treat uploaded files as *attachments* to records, saving
|
149
|
+
their data into a database column. This is the responsibility of
|
150
|
+
`Shrine::Attacher`. A `Shrine::Attacher` uses `Shrine` uploaders and
|
151
|
+
`Shrine::UploadedFile` objects internally.
|
152
|
+
|
153
|
+
The attaching process requires a temporary and a permanent storage to be
|
154
|
+
registered (by default that's `:cache` and `:store`):
|
155
|
+
|
156
|
+
```rb
|
157
|
+
Shrine.storages = {
|
158
|
+
cache: Shrine::Storage::FileSystem.new("uploads/cache"),
|
159
|
+
store: Shrine::Storage::FileSystem.new("uploads/store"),
|
160
|
+
}
|
161
|
+
```
|
162
|
+
|
163
|
+
A `Shrine::Attacher` is instantiated with a model instance and an attachment
|
164
|
+
name (an "image" attachment will be saved to `image_data` field):
|
165
|
+
|
166
|
+
```rb
|
167
|
+
attacher = Shrine::Attacher.new(photo, :image)
|
168
|
+
|
169
|
+
attacher.assign(file)
|
170
|
+
attacher.get #=> #<Shrine::UploadedFile>
|
171
|
+
attacher.record.image_data #=> "{\"storage\":\"cache\",\"id\":\"9260ea09d8effd.jpg\",\"metadata\":{...}}"
|
172
|
+
|
173
|
+
attacher._promote
|
174
|
+
attacher.get #=> #<Shrine::UploadedFile>
|
175
|
+
attacher.record.image_data #=> "{\"storage\":\"store\",\"id\":\"ksdf02lr9sf3la.jpg\",\"metadata\":{...}}"
|
176
|
+
```
|
177
|
+
|
178
|
+
Above a file is assigned by the attacher, which "caches" (uploads) the file to
|
179
|
+
the temporary storage. The cached file is then "promoted" (uploaded) to
|
180
|
+
permanent storage. Behind the scenes a cached `Shrine::UploadedFile` is given
|
181
|
+
to `Shrine#upload`, which works because `Shrine::UploadedFile` is an IO-like
|
182
|
+
object. After both caching and promoting the data hash of the uploaded file is
|
183
|
+
assigned to the record's column as JSON.
|
184
|
+
|
185
|
+
## `Shrine::Attachment`
|
186
|
+
|
187
|
+
`Shrine::Attachment` is the highest level of abstraction. A
|
188
|
+
`Shrine::Attachment` module exposes the `Shrine::Attacher` object through the
|
189
|
+
model instance. The `Shrine::Attachment` class is a sublcass of `Module`, which
|
190
|
+
means that an instance of `Shrine::Attachment` is a module:
|
191
|
+
|
192
|
+
```rb
|
193
|
+
Shrine::Attachment.new(:image).is_a?(Module) #=> true
|
194
|
+
Shrine::Attachment.new(:image).instance_methods #=> [:image=, :image, :image_url, :image_attacher]
|
195
|
+
|
196
|
+
# equivalents
|
197
|
+
Shrine::Attachment.new(:image)
|
198
|
+
Shrine.attachment(:image)
|
199
|
+
Shrine[:image]
|
200
|
+
```
|
201
|
+
|
202
|
+
We can include this module to a model:
|
203
|
+
|
204
|
+
```rb
|
205
|
+
class Photo
|
206
|
+
include Shrine[:image]
|
207
|
+
end
|
208
|
+
```
|
209
|
+
```rb
|
210
|
+
photo.image = file # shorthand for `photo.image_attacher.assign(file)`
|
211
|
+
photo.image # shorthand for `photo.image_attacher.get`
|
212
|
+
photo.image_url # shorthand for `photo.image_attacher.url`
|
213
|
+
|
214
|
+
photo.image_attacher #=> #<Shrine::Attacher>
|
215
|
+
```
|
216
|
+
|
217
|
+
When an ORM plugin is loaded, the `Shrine::Attachment` module also
|
218
|
+
automatically:
|
219
|
+
|
220
|
+
* syncs Shrine's validation errors with the record
|
221
|
+
* triggers promoting after record is saved
|
222
|
+
* deletes the uploaded file if attachment was replaced, removed or the record
|
223
|
+
destroyed
|
data/doc/migrating_storage.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Migrating to
|
1
|
+
# Migrating to Different Storage
|
2
2
|
|
3
3
|
While your application is live in production and performing uploads, it may
|
4
4
|
happen that you decide you want to change your storage (the `:store`). Shrine
|
@@ -29,13 +29,11 @@ After you've deployed the previous change, it's time to copy all the existing
|
|
29
29
|
files to the new storage, and update the records. This is how you can do it
|
30
30
|
if you're using Sequel:
|
31
31
|
|
32
|
-
```rb
|
33
|
-
Shrine.plugin :migration_helpers # before the model is loaded
|
34
|
-
```
|
35
32
|
```rb
|
36
33
|
User.paged_each do |user|
|
37
|
-
user.
|
38
|
-
|
34
|
+
if (attacher = user.avatar_attacher).stored?
|
35
|
+
uploaded_file = attacher.store!(user.avatar)
|
36
|
+
attacher.swap(uploaded_file)
|
39
37
|
end
|
40
38
|
end
|
41
39
|
|
@@ -64,13 +62,10 @@ Shrine.storages[:new_store] = Shrine.storages[:store]
|
|
64
62
|
**Second**, you should rename the storage names on existing records. With
|
65
63
|
Sequel it would be something like:
|
66
64
|
|
67
|
-
```rb
|
68
|
-
Shrine.plugin :migration_helper # before the model is loaded
|
69
|
-
```
|
70
65
|
```rb
|
71
66
|
User.paged_each do |user|
|
72
|
-
user.
|
73
|
-
|
67
|
+
if user.avatar_attacher.stored?
|
68
|
+
user.update(avatar_data: user.avatar_data.gsub('"new_store"', '"store"'))
|
74
69
|
end
|
75
70
|
end
|
76
71
|
|
@@ -20,7 +20,7 @@ class ImageUploader < Shrine
|
|
20
20
|
def process(io, context)
|
21
21
|
case context[:phase]
|
22
22
|
when :store
|
23
|
-
thumb = process_thumb(io.download)
|
23
|
+
thumb = process_thumb(io.download)
|
24
24
|
{original: io, thumb: thumb}
|
25
25
|
end
|
26
26
|
end
|
@@ -40,17 +40,13 @@ unprocessed file URL.
|
|
40
40
|
Afterwards you should run a script which reprocesses the versions for existing
|
41
41
|
files:
|
42
42
|
|
43
|
-
```rb
|
44
|
-
Shrine.plugin :migration_helpers # before the model is loaded
|
45
|
-
```
|
46
43
|
```rb
|
47
44
|
User.paged_each do |user|
|
48
|
-
user.
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
end
|
45
|
+
attacher, attachment = user.avatar_attacher, user.avatar
|
46
|
+
if attacher.stored? && !attachment.is_a?(Hash)
|
47
|
+
file = some_processing(attachment.download)
|
48
|
+
thumb = attacher.store!(file, version: :thumb)
|
49
|
+
attacher.swap({original: avatar, thumb: thumb})
|
54
50
|
end
|
55
51
|
end
|
56
52
|
```
|
@@ -61,15 +57,13 @@ The simplest scenario is where you need to regenerate an existing version.
|
|
61
57
|
First you need to change and deploy your updated processing code, and
|
62
58
|
afterwards you can run a script like this on your production database:
|
63
59
|
|
64
|
-
```rb
|
65
|
-
Shrine.plugin :migration_helpers # before the model is loaded
|
66
|
-
```
|
67
|
-
|
68
60
|
```rb
|
69
61
|
User.paged_each do |user|
|
70
|
-
user.
|
71
|
-
|
72
|
-
|
62
|
+
attacher, attachment = user.avatar_attacher, user.avatar
|
63
|
+
if attacher.stored?
|
64
|
+
file = some_processing(attachment[:original].download)
|
65
|
+
thumb = attachment[:thumb].replace(thumb)
|
66
|
+
attacher.swap(attachment.merge(thumb: thumb))
|
73
67
|
end
|
74
68
|
end
|
75
69
|
```
|
@@ -97,18 +91,13 @@ end
|
|
97
91
|
After you've deployed this change, you should run a script that will generate
|
98
92
|
the new version for all existing records:
|
99
93
|
|
100
|
-
```rb
|
101
|
-
Shrine.plugin :migration_helpers # before the model is loaded
|
102
|
-
```
|
103
|
-
|
104
94
|
```rb
|
105
95
|
User.paged_each do |user|
|
106
|
-
user.
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
end
|
96
|
+
attacher, attachment = user.avatar_attacher, user.avatar
|
97
|
+
if attacher.stored? && !attachment[:new]
|
98
|
+
file = some_processing(attachment[:original].download, *args)
|
99
|
+
new = attacher.store!(file, version: :new)
|
100
|
+
attacher.swap(attachment.merge(new: new))
|
112
101
|
end
|
113
102
|
end
|
114
103
|
```
|
@@ -124,18 +113,14 @@ generate it (but keep the version name in the list), as well as update your app
|
|
124
113
|
not to use the new version, and deploy that code. After you've done that, you
|
125
114
|
can run a script which removes that version:
|
126
115
|
|
127
|
-
```rb
|
128
|
-
Shrine.plugin :migration_helpers # before the model is loaded
|
129
|
-
```
|
130
|
-
|
131
116
|
```rb
|
132
117
|
old_versions = []
|
133
118
|
|
134
119
|
User.paged_each do |user|
|
135
|
-
user.
|
136
|
-
|
137
|
-
old_versions << old_version
|
138
|
-
|
120
|
+
attacher, attachment = user.avatar_attacher, user.avatar
|
121
|
+
if attacher.stored? && attachment[:old_version]
|
122
|
+
old_versions << attachment.delete(:old_version)
|
123
|
+
attacher.swap(attachment)
|
139
124
|
end
|
140
125
|
end
|
141
126
|
|
@@ -154,13 +139,10 @@ If you made a lot of changes to versions, it might make sense to simply
|
|
154
139
|
regenerate all versions. After you've deployed the change in processing, you
|
155
140
|
can run a script which updates existing records:
|
156
141
|
|
157
|
-
```rb
|
158
|
-
Shrine.plugin :migration_helpers # before the model is loaded
|
159
|
-
```
|
160
|
-
|
161
142
|
```rb
|
162
143
|
User.paged_each do |user|
|
163
|
-
if user.
|
144
|
+
if user.avatar_attacher.stored?
|
145
|
+
# assuming your largest version is named ":original"
|
164
146
|
user.update(avatar: user.avatar[:original])
|
165
147
|
end
|
166
148
|
end
|
data/lib/shrine.rb
CHANGED
@@ -23,13 +23,6 @@ class Shrine
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
-
# Raised by storages in method `#clear!` when confirmation wasn't passed in.
|
27
|
-
class Confirm < Error
|
28
|
-
def message
|
29
|
-
"Are you sure you want to delete all files from the storage? (confirm with `clear!(:confirm)`)"
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
26
|
# Methods which an object has to respond to in order to be considered
|
34
27
|
# an IO object. Keys are method names, and values are arguments.
|
35
28
|
IO_METHODS = {
|
@@ -276,11 +269,6 @@ class Shrine
|
|
276
269
|
}
|
277
270
|
end
|
278
271
|
|
279
|
-
# User-defined default URL for when a file is missing (called by
|
280
|
-
# `Attacher#url`).
|
281
|
-
def default_url(context)
|
282
|
-
end
|
283
|
-
|
284
272
|
private
|
285
273
|
|
286
274
|
# Extracts the filename from the IO using some basic heuristics.
|
@@ -294,9 +282,8 @@ class Shrine
|
|
294
282
|
|
295
283
|
# Extracts the MIME type from the IO using some basic heuristics.
|
296
284
|
def extract_mime_type(io)
|
297
|
-
if io.respond_to?(:
|
298
|
-
|
299
|
-
elsif io.respond_to?(:content_type)
|
285
|
+
if io.respond_to?(:content_type)
|
286
|
+
warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content." unless opts.key?(:mime_type_analyzer)
|
300
287
|
io.content_type
|
301
288
|
end
|
302
289
|
end
|
@@ -335,7 +322,8 @@ class Shrine
|
|
335
322
|
|
336
323
|
# Does the actual uploading, calling `#upload` on the storage.
|
337
324
|
def copy(io, context)
|
338
|
-
|
325
|
+
context[:upload_options] = (context[:upload_options] || {}).merge(shrine_metadata: context[:metadata])
|
326
|
+
storage.upload(io, context[:location], context[:upload_options])
|
339
327
|
ensure
|
340
328
|
io.close rescue nil
|
341
329
|
end
|
@@ -379,7 +367,7 @@ class Shrine
|
|
379
367
|
|
380
368
|
# Generates a UID to use in location for uploaded files.
|
381
369
|
def generate_uid(io)
|
382
|
-
SecureRandom.hex
|
370
|
+
SecureRandom.hex
|
383
371
|
end
|
384
372
|
end
|
385
373
|
|
@@ -481,7 +469,7 @@ class Shrine
|
|
481
469
|
# Otherwise it assumes that it's an IO object and caches it.
|
482
470
|
def assign(value)
|
483
471
|
if value.is_a?(String)
|
484
|
-
assign_cached(value) unless value == ""
|
472
|
+
assign_cached(value) unless value == "" || value == read
|
485
473
|
else
|
486
474
|
uploaded_file = cache!(value, phase: :cache) if value
|
487
475
|
set(uploaded_file)
|
@@ -491,11 +479,9 @@ class Shrine
|
|
491
479
|
# Assigns a Shrine::UploadedFile, runs validation and schedules the
|
492
480
|
# old file for deletion.
|
493
481
|
def set(uploaded_file)
|
494
|
-
@old = get
|
482
|
+
@old = get
|
495
483
|
_set(uploaded_file)
|
496
484
|
validate
|
497
|
-
|
498
|
-
get
|
499
485
|
end
|
500
486
|
|
501
487
|
# Retrieves the uploaded file from the record column.
|
@@ -505,7 +491,7 @@ class Shrine
|
|
505
491
|
|
506
492
|
# Returns true if a new file has been attached.
|
507
493
|
def attached?
|
508
|
-
instance_variable_defined?(
|
494
|
+
instance_variable_defined?(:@old)
|
509
495
|
end
|
510
496
|
|
511
497
|
# Plugins can override this if they want something to be done on save.
|
@@ -516,45 +502,61 @@ class Shrine
|
|
516
502
|
# be called after saving.
|
517
503
|
def finalize
|
518
504
|
replace
|
519
|
-
remove_instance_variable(
|
520
|
-
_promote
|
505
|
+
remove_instance_variable(:@old)
|
506
|
+
_promote(phase: :store) if cached?
|
521
507
|
end
|
522
508
|
|
523
|
-
#
|
524
|
-
def _promote
|
525
|
-
promote(
|
509
|
+
# Promotes the file.
|
510
|
+
def _promote(uploaded_file = get, phase: nil)
|
511
|
+
promote(uploaded_file, phase: phase)
|
526
512
|
end
|
527
513
|
|
528
514
|
# Uploads the cached file to store, and updates the record with the
|
529
515
|
# stored file.
|
530
|
-
def promote(
|
531
|
-
stored_file = store!(
|
532
|
-
result = swap(stored_file) or
|
516
|
+
def promote(uploaded_file = get, **options)
|
517
|
+
stored_file = store!(uploaded_file, **options)
|
518
|
+
result = swap(stored_file) or _delete(stored_file, phase: :abort)
|
533
519
|
result
|
534
520
|
end
|
535
521
|
|
522
|
+
# Calls #update, overriden in ORM plugins.
|
523
|
+
def swap(uploaded_file)
|
524
|
+
update(uploaded_file)
|
525
|
+
uploaded_file if uploaded_file == get
|
526
|
+
end
|
527
|
+
|
536
528
|
# Deletes the attachment that was replaced, and is called after saving
|
537
529
|
# by ORM integrations. If also removes `@old` so that #save and #finalize
|
538
530
|
# don't get called for the current attachment anymore.
|
539
531
|
def replace
|
540
|
-
|
532
|
+
_delete(@old, phase: :replace) if @old && !cache.uploaded?(@old)
|
541
533
|
end
|
542
534
|
|
543
535
|
# Deletes the attachment. Typically this should be called after
|
544
536
|
# destroying a record.
|
545
537
|
def destroy
|
546
|
-
|
538
|
+
_delete(get, phase: :destroy) if get && !cache.uploaded?(get)
|
539
|
+
end
|
540
|
+
|
541
|
+
# Deletes the uploaded file.
|
542
|
+
def _delete(uploaded_file, phase: nil)
|
543
|
+
delete!(uploaded_file, phase: phase)
|
547
544
|
end
|
548
545
|
|
549
546
|
# Returns the URL to the attached file (internally calls `#url` on the
|
550
|
-
# storage)
|
551
|
-
# `Shrine#default_url`. Forwards any URL options to the storage.
|
547
|
+
# storage), forwarding any URL options to the storage.
|
552
548
|
def url(**options)
|
553
|
-
if
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
549
|
+
get.url(**options) if read
|
550
|
+
end
|
551
|
+
|
552
|
+
# Returns true if attachment is present and cached.
|
553
|
+
def cached?
|
554
|
+
get && cache.uploaded?(get)
|
555
|
+
end
|
556
|
+
|
557
|
+
# Returns true if attachment is present and stored.
|
558
|
+
def stored?
|
559
|
+
get && store.uploaded?(get)
|
558
560
|
end
|
559
561
|
|
560
562
|
# Runs the validations defined by `Attacher.validate`.
|
@@ -568,6 +570,21 @@ class Shrine
|
|
568
570
|
shrine_class.uploaded_file(*args, &block)
|
569
571
|
end
|
570
572
|
|
573
|
+
# Uploads the file to cache passing context.
|
574
|
+
def cache!(io, **options)
|
575
|
+
cache.upload(io, context.merge(options))
|
576
|
+
end
|
577
|
+
|
578
|
+
# Uploads the file to store passing context.
|
579
|
+
def store!(io, **options)
|
580
|
+
store.upload(io, context.merge(options))
|
581
|
+
end
|
582
|
+
|
583
|
+
# Deletes the file passing context.
|
584
|
+
def delete!(uploaded_file, **options)
|
585
|
+
store.delete(uploaded_file, context.merge(options))
|
586
|
+
end
|
587
|
+
|
571
588
|
# Returns the Shrine class related to this attacher.
|
572
589
|
def shrine_class
|
573
590
|
self.class.shrine_class
|
@@ -577,20 +594,8 @@ class Shrine
|
|
577
594
|
|
578
595
|
# Assigns a cached file (refuses if the file is stored).
|
579
596
|
def assign_cached(value)
|
580
|
-
|
581
|
-
set(
|
582
|
-
end
|
583
|
-
|
584
|
-
# Returns true if uploaded_file exists and is cached. If it's true,
|
585
|
-
# \#promote will be called.
|
586
|
-
def promote?(uploaded_file)
|
587
|
-
uploaded_file && cache.uploaded?(uploaded_file)
|
588
|
-
end
|
589
|
-
|
590
|
-
# Calls #update, overriden in ORM plugins.
|
591
|
-
def swap(uploaded_file)
|
592
|
-
update(uploaded_file)
|
593
|
-
uploaded_file if get == uploaded_file
|
597
|
+
cached_file = uploaded_file(value)
|
598
|
+
set(cached_file) if cache.uploaded?(cached_file)
|
594
599
|
end
|
595
600
|
|
596
601
|
# Sets and saves the uploaded file.
|
@@ -598,28 +603,6 @@ class Shrine
|
|
598
603
|
_set(uploaded_file)
|
599
604
|
end
|
600
605
|
|
601
|
-
# Uploads the file to cache (calls `Shrine#upload`).
|
602
|
-
def cache!(io, phase:)
|
603
|
-
cache.upload(io, context.merge(phase: phase))
|
604
|
-
end
|
605
|
-
|
606
|
-
# Uploads the file to store (calls `Shrine#upload`).
|
607
|
-
def store!(io, phase:)
|
608
|
-
store.upload(io, context.merge(phase: phase))
|
609
|
-
end
|
610
|
-
|
611
|
-
# Deletes the file (calls `Shrine#delete`).
|
612
|
-
def delete!(uploaded_file, phase:)
|
613
|
-
store.delete(uploaded_file, context.merge(phase: phase))
|
614
|
-
end
|
615
|
-
|
616
|
-
# Delegates to `Shrine#default_url`.
|
617
|
-
def default_url(**options)
|
618
|
-
url = store.default_url(options.merge(context))
|
619
|
-
warn "Overriding Shrine#default_url is deprecated and will be removed in Shrine 2. You should use the default_url plugin." if url
|
620
|
-
url
|
621
|
-
end
|
622
|
-
|
623
606
|
# The validation block provided by `Shrine.validate`.
|
624
607
|
def validate_block
|
625
608
|
shrine_class.opts[:validate]
|
@@ -632,12 +615,12 @@ class Shrine
|
|
632
615
|
|
633
616
|
# It writes to record's `<attachment>_data` column.
|
634
617
|
def write(value)
|
635
|
-
record.send("#{name}_data=", value)
|
618
|
+
record.send(:"#{name}_data=", value)
|
636
619
|
end
|
637
620
|
|
638
621
|
# It reads from the record's `<attachment>_data` column.
|
639
622
|
def read
|
640
|
-
value = record.send("#{name}_data")
|
623
|
+
value = record.send(:"#{name}_data")
|
641
624
|
value unless value.nil? || value.empty?
|
642
625
|
end
|
643
626
|
|