shrine 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of shrine might be problematic. Click here for more details.

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