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.

Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +236 -234
  3. data/doc/changing_location.md +6 -4
  4. data/doc/creating_storages.md +4 -4
  5. data/doc/design.md +223 -0
  6. data/doc/migrating_storage.md +6 -11
  7. data/doc/regenerating_versions.md +22 -40
  8. data/lib/shrine.rb +60 -77
  9. data/lib/shrine/plugins/activerecord.rb +37 -14
  10. data/lib/shrine/plugins/background_helpers.rb +1 -0
  11. data/lib/shrine/plugins/backgrounding.rb +49 -37
  12. data/lib/shrine/plugins/backup.rb +6 -4
  13. data/lib/shrine/plugins/cached_attachment_data.rb +5 -5
  14. data/lib/shrine/plugins/data_uri.rb +9 -9
  15. data/lib/shrine/plugins/default_storage.rb +4 -4
  16. data/lib/shrine/plugins/default_url.rb +7 -1
  17. data/lib/shrine/plugins/default_url_options.rb +1 -1
  18. data/lib/shrine/plugins/delete_promoted.rb +2 -2
  19. data/lib/shrine/plugins/delete_raw.rb +4 -4
  20. data/lib/shrine/plugins/determine_mime_type.rb +50 -43
  21. data/lib/shrine/plugins/direct_upload.rb +10 -20
  22. data/lib/shrine/plugins/download_endpoint.rb +16 -13
  23. data/lib/shrine/plugins/dynamic_storage.rb +4 -12
  24. data/lib/shrine/plugins/included.rb +6 -19
  25. data/lib/shrine/plugins/keep_files.rb +4 -4
  26. data/lib/shrine/plugins/logging.rb +4 -4
  27. data/lib/shrine/plugins/migration_helpers.rb +37 -34
  28. data/lib/shrine/plugins/moving.rb +19 -32
  29. data/lib/shrine/plugins/parallelize.rb +5 -5
  30. data/lib/shrine/plugins/pretty_location.rb +2 -6
  31. data/lib/shrine/plugins/remote_url.rb +31 -43
  32. data/lib/shrine/plugins/remove_attachment.rb +5 -5
  33. data/lib/shrine/plugins/remove_invalid.rb +1 -1
  34. data/lib/shrine/plugins/restore_cached_data.rb +4 -10
  35. data/lib/shrine/plugins/sequel.rb +46 -21
  36. data/lib/shrine/plugins/store_dimensions.rb +19 -20
  37. data/lib/shrine/plugins/upload_options.rb +11 -9
  38. data/lib/shrine/plugins/validation_helpers.rb +3 -3
  39. data/lib/shrine/plugins/versions.rb +18 -3
  40. data/lib/shrine/storage/file_system.rb +9 -11
  41. data/lib/shrine/storage/linter.rb +1 -7
  42. data/lib/shrine/storage/s3.rb +25 -19
  43. data/lib/shrine/version.rb +3 -3
  44. data/shrine.gemspec +13 -3
  45. metadata +28 -9
  46. data/lib/shrine/plugins/delete_uploaded.rb +0 -3
  47. data/lib/shrine/plugins/keep_location.rb +0 -46
  48. data/lib/shrine/plugins/restore_cached.rb +0 -3
@@ -1,4 +1,4 @@
1
- # Changing Location of Files
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.promote(user.avatar, phase: :change_location)
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
- Note that the phase has to be overriden, otherwise it defaults to `:store`
27
- which would trigger processing if you have it set up.
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.
@@ -13,7 +13,7 @@ class Shrine
13
13
  # initializing logic
14
14
  end
15
15
 
16
- def upload(io, id, metadata = {})
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, metadata = {})
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, metadata = {})
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, metadata = {})
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
@@ -1,4 +1,4 @@
1
- # Migrating to Another Storage
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.update_avatar do |avatar|
38
- user.avatar_store.upload(avatar, {record: user, name: :avatar})
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.update_avatar do |avatar|
73
- avatar.to_json.gsub('"new_store"', '"store"')
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) # replace with actual processing method
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.update_avatar do |avatar|
49
- unless avatar.is_a?(Hash)
50
- file = some_processing(avatar.download)
51
- thumb = user.avatar_store.upload(file, {record: user, name: :avatar, version: :thumb})
52
- {original: avatar, thumb: thumb}
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.update_avatar do |avatar|
71
- thumb = some_processing(avatar[:original].download)
72
- avatar.merge(thumb: avatar[:thumb].replace(thumb))
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.update_avatar do |avatar|
107
- unless avatar[:new]
108
- file = some_processing(avatar[:original].download, *args)
109
- new = user.avatar_store.upload(file, {record: user, name: :avatar, version: :new})
110
- avatar.merge(new: new)
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.update_avatar do |avatar|
136
- old_version = avatar.delete(:old_version)
137
- old_versions << old_version if old_version
138
- avatar
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.avatar && user.avatar_store.uploaded?(user.avatar)
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?(:mime_type)
298
- io.mime_type
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
- storage.upload(io, context[:location], context[:metadata])
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(30)
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 unless get == uploaded_file
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?("@old")
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("@old")
520
- _promote
505
+ remove_instance_variable(:@old)
506
+ _promote(phase: :store) if cached?
521
507
  end
522
508
 
523
- # Calls #promote if attached file is cached.
524
- def _promote
525
- promote(get) if promote?(get)
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(cached_file, phase: :store)
531
- stored_file = store!(cached_file, phase: phase)
532
- result = swap(stored_file) or delete!(stored_file, phase: :stored)
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
- delete!(@old, phase: :replaced) if @old && !cache.uploaded?(@old)
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
- delete!(get, phase: :destroyed) if get && !cache.uploaded?(get)
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). If the attachment is missing, it calls
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 uploaded_file = get
554
- uploaded_file.url(**options)
555
- else
556
- default_url(**options)
557
- end
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
- uploaded_file = uploaded_file(value)
581
- set(uploaded_file) if cache.uploaded?(uploaded_file)
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