shrine 1.2.0 → 1.3.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: 685289a1b672f363582842cdb2109c4197f9e735
4
- data.tar.gz: 37a64e5704dcc6125074dc2333ecf570ba5eaede
3
+ metadata.gz: d59731ea1e8a0376a6f8ad86e8d627250dec2f44
4
+ data.tar.gz: 14bd2696930a028e157cc1535995a5342958170b
5
5
  SHA512:
6
- metadata.gz: 15150fb3c6d90f397c6e35f714b4eeed02d9c30ee8497fc9763f689b7e7732612e96a896c9d912cf04161a564e5196e73ba20f87df8d489d1562ffe4381dcf7b
7
- data.tar.gz: eb4f056cf14cbacb40ee272008c08623e72767965b808d376f4880b83088a0bd57a20282ac7eb05138aea6dc6c0eae9d3c75c16b8bb8faae940816ee1d079fdc
6
+ metadata.gz: a0ca4170b242be2e01fa6abdeb9ff0e3380e1271c0783099e738182e4d6f995ac7b8042884c52147052a751b6a7143eb16766879165c87f221ed6e84b7deb53a
7
+ data.tar.gz: 6aee5bb9a3a44f162c9d2da791c498c2d592a05aa3efc7bf058d38def80040af8856db96431eb558ed93538cc327f718874f59a376e3b55e11778189c87401be
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015 Janko Marohnić
3
+ Copyright (c) 2015-2016 Janko Marohnić
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -18,7 +18,7 @@ explains the motivation behind Shrine.
18
18
  gem "shrine"
19
19
  ```
20
20
 
21
- Shrine has been tested on MRI 2.1, MRI 2.2, JRuby and Rubinius.
21
+ Shrine has been tested on MRI 2.1, MRI 2.2, MRI 2.3, JRuby and Rubinius.
22
22
 
23
23
  ## Basics
24
24
 
@@ -47,8 +47,8 @@ First we add the storage we want to use to Shrine's registry. Storages are
47
47
  simple Ruby classes which perform the actual uploads. We instantiate a `Shrine`
48
48
  with the storage name, and when we call `#upload` Shrine does the following:
49
49
 
50
- * generates a unique location for the file
51
50
  * extracts metadata from the file
51
+ * generates a unique location for the file
52
52
  * uploads the file using the underlying storage
53
53
  * closes the file
54
54
  * returns a `Shrine::UploadedFile` with relevant data
@@ -78,8 +78,8 @@ To read about the metadata that is stored with the uploaded file, see the
78
78
  ## Attachment
79
79
 
80
80
  In web applications, instead of managing files directly, we rather want to
81
- treat them as "attachments" to recod tie them to their lifecycle. In Shrine we
82
- do this by generating and including "attachment" modules.
81
+ treat them as "attachments" to records and tie them to their lifecycle. In
82
+ Shrine we do this by generating and including "attachment" modules.
83
83
 
84
84
  Firstly we need to assign the special `:cache` and `:store` storages:
85
85
 
@@ -245,7 +245,8 @@ assigned and saved, an "upload" actually happens two times. First the file is
245
245
  "uploaded" to cache on assignment, and then the cached file is reuploaded to
246
246
  store on save. You could theoretically do processing in both phases, depending
247
247
  on your preferences (although it's generally not recommended to process on
248
- caching).
248
+ caching, because it happens before file validations; use the `recache` plugin
249
+ instead).
249
250
 
250
251
  Ok, now how do we do the actual processing? Well, Shrine actually doesn't ship
251
252
  with any file processing functionality, because that is a generic problem that
@@ -477,9 +478,11 @@ end
477
478
 
478
479
  Note that there should always be a random component in the location, otherwise
479
480
  dirty tracking won't be detected properly (you can use `Shrine#generate_uid`).
481
+ Also note that you can access the extracted metadata here through
482
+ `context[:metadata]`.
480
483
 
481
- When using `Shrine` directly you can bypass `#generate_location` by passing in
482
- `:location`
484
+ When using the uploader directly, it's possible to bypass `#generate_location`
485
+ by passing in `:location`:
483
486
 
484
487
  ```rb
485
488
  file = File.open("avatar.jpg")
@@ -527,7 +530,7 @@ and for FileSystem you can put something like this in your Rake task:
527
530
 
528
531
  ```rb
529
532
  file_system = Shrine.storages[:cache]
530
- file_system.clear!(older_than: 1.week.ago) # adjust the time
533
+ file_system.clear!(older_than: Time.now - 7*24*60*60) # delete files older than 1 week
531
534
  ```
532
535
 
533
536
  ## Background jobs
@@ -569,7 +572,7 @@ libraries are:
569
572
  * **User experience** – After starting the background job, Shrine will save the
570
573
  record with the cached attachment so that it can be immediately shown to the
571
574
  user. With other file upload libraries users cannot see the file until the
572
- background job has finished, which is really lame.
575
+ background job has finished.
573
576
  * **Simplicity** – Instead of writing the workers for you, Shrine allows you
574
577
  to use your own workers in a very simple way. Also, no extra columns are
575
578
  required.
@@ -1,24 +1,24 @@
1
1
  # Shrine for CarrierWave Users
2
2
 
3
- This guide is aimed at helping CarrierWave users transition to Shrine. We will
4
- first generally mention what are the key differences. Afterwards there is an
5
- extensive reference of CarrierWave's interface and what is the equivalent in
6
- Shrine.
3
+ This guide is aimed at helping CarrierWave users transition to Shrine. First it
4
+ explains some key differences in the design between the two libraries.
5
+ Afterwards it explains how you can transition an existing app that uses
6
+ CarrierWave to Shrine. It then finishes off with an extensive reference of
7
+ CarrierWave's interface and what is the equivalent in Shrine.
7
8
 
8
9
  ## Uploaders
9
10
 
10
- Shrine has a concept of uploaders similar to CarrierWave's, but instead of
11
- inheriting from `CarrierWave::Uploader::Base`, you inherit from `Shrine`
12
- directly:
11
+ Shrine has a concept of uploaders similar to CarrierWave's, which allows you to
12
+ have different uploading logic for different types of files:
13
13
 
14
14
  ```rb
15
15
  class ImageUploader < Shrine
16
- # ...
16
+ # uploading logic
17
17
  end
18
18
  ```
19
19
 
20
20
  While in CarrierWave you choose a storages for uploaders directly, in Shrine
21
- you first register storages globally (under a symbol name), and then you
21
+ you first register storages globally (under a symbol name), and then you can
22
22
  instantiate uploaders with a specific storage.
23
23
 
24
24
  ```rb
@@ -35,15 +35,15 @@ store_uploader = Shrine.new(:store)
35
35
  ```
36
36
 
37
37
  CarrierWave uses symbols for referencing storages (`:file`, `:fog`, ...), but
38
- in Shrine you instantiate storages directly. This makes storages much more
39
- flexible, because this way they can have their own options that are specific to
40
- them.
38
+ in Shrine storages are simple Ruby classes which you can instantiate directly.
39
+ This makes storages much more flexible, because this way they can have their
40
+ own options that are specific to them.
41
41
 
42
42
  ### Processing
43
43
 
44
- In Shrine processing is done instance-level in the `#process` method. To
45
- generate versions, you simply return a hash, and also load the `versions`
46
- plugin to make your uploader recognize versions:
44
+ In Shrine processing is done instance-level in the `#process` method, and can
45
+ be specified for each phase. You can return a single processed file or a hash of
46
+ versions (with the `versions` plugin):
47
47
 
48
48
  ```rb
49
49
  require "image_processing/mini_magick" # part of the "image_processing" gem
@@ -73,7 +73,7 @@ Shrine.plugin :activerecord # If you're using ActiveRecord
73
73
  ```
74
74
 
75
75
  Instead of giving you class methods for "mounting" uploaders, in Shrine you
76
- generate "attachment modules" which you include in your models:
76
+ generate attachment modules which you simply include in your models:
77
77
 
78
78
  ```rb
79
79
  class User < Sequel::Model
@@ -82,8 +82,8 @@ end
82
82
  ```
83
83
 
84
84
  You models are required to have the `<attachment>_data` column, in the above
85
- case `avatar_data`. It contains the storage and location of the file, as well
86
- as additional metadata.
85
+ case `avatar_data`. Shrine stores storage, location, and additional metadata of
86
+ the uploaded file to that column.
87
87
 
88
88
  ### Multiple uploads
89
89
 
@@ -94,6 +94,99 @@ you're using, and it's analogous to how you would implement adding items to any
94
94
  dynamic one-to-many relationship. Take a look at the [example app] which
95
95
  demonstrates how easy it is to implement multiple uploads.
96
96
 
97
+ ## Migrating from CarrierWave
98
+
99
+ You have an existing app using CarrierWave and you want to transfer it to
100
+ Shrine. Let's assume we have a `Photo` model with the "image" attachment. First
101
+ we need to create the `image_data` column for Shrine:
102
+
103
+ ```rb
104
+ add_column :photos, :image_data, :text
105
+ ```
106
+
107
+ Afterwards we need to make new uploads write to the `image_data` column. This
108
+ can be done by including the below module to all models that have CarrierWave
109
+ attachments:
110
+
111
+ ```rb
112
+ require "fastimage"
113
+
114
+ module CarrierwaveShrineSynchronization
115
+ def self.included(model)
116
+ model.before_save do
117
+ self.class.uploaders.each_key do |name|
118
+ write_shrine_data(name) if changes.key?(name)
119
+ end
120
+ end
121
+ end
122
+
123
+ def write_shrine_data(name)
124
+ uploader = send(name)
125
+
126
+ if read_attribute(name).present?
127
+ data = uploader_to_shrine_data(uploader)
128
+
129
+ if uploader.versions.any?
130
+ data = {original: data}
131
+ uploader.versions.each do |name, version|
132
+ data[name] = uploader_to_shrine_data(version)
133
+ end
134
+ end
135
+
136
+ write_attribute(:"#{name}_data", data.to_json)
137
+ else
138
+ write_attribute(:"#{name}_data", nil)
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ # If you'll be using `:prefix` on your Shrine storage, make sure to
145
+ # subtract it from the path assigned as `:id`.
146
+ def uploader_to_shrine_data(uploader)
147
+ path = uploader.store_path(read_attribute(uploader.mounted_as))
148
+
149
+ size = uploader.file.size if changes.key?(uploader.mounted_as)
150
+ size ||= FastImage.new(uploader.url).content_length
151
+ size ||= File.size(File.join(uploader.root, path))
152
+ filename = File.basename(path)
153
+ mime_type = MIME::Types.type_for(path).first.to_s.presence
154
+
155
+ {
156
+ storage: :store,
157
+ id: path,
158
+ metadata: {
159
+ size: size,
160
+ filename: filename,
161
+ mime_type: mime_type,
162
+ },
163
+ }
164
+ end
165
+ end
166
+ ```
167
+ ```rb
168
+ class Photo < ActiveRecord::Base
169
+ mount_uploader :image, ImageUploader
170
+ include CarrierwaveShrineSynchronization # needs to be after `mount_uploader`
171
+ end
172
+ ```
173
+
174
+ After you deploy this code, the `image_data` column should now be successfully
175
+ synchronized with new attachments. Next step is to run a script which writes
176
+ all existing CarrierWave attachments to `image_data`:
177
+
178
+ ```rb
179
+ Photo.find_each do |photo|
180
+ Photo.uploaders.each_key { |name| photo.write_shrine_data(name) }
181
+ photo.save!
182
+ end
183
+ ```
184
+
185
+ Now you should be able to rewrite your application so that it uses Shrine
186
+ instead of CarrierWave, using equivalent Shrine storages. For help with
187
+ translating the code from CarrierWave to Shrine, you can consult the reference
188
+ below.
189
+
97
190
  ## CarrierWave to Shrine direct mapping
98
191
 
99
192
  ### `CarrierWave::Uploader::Base`
@@ -107,7 +200,6 @@ plugin:
107
200
  ```rb
108
201
  Shrine.storages[:foo] = Shrine::Storage::Foo.new(*args)
109
202
  ```
110
-
111
203
  ```rb
112
204
  class ImageUploader
113
205
  plugin :default_storage, store: :foo
@@ -4,26 +4,12 @@ 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
5
5
  for you.
6
6
 
7
- The first step is to change the location, either by using the `pretty_location`
8
- plugin:
7
+ The first step is to change the location (by overriding `#generate_location` or
8
+ with the pretty_location plugin), and deploy that change. Attachments on old
9
+ locations will still continue to work properly.
9
10
 
10
- ```rb
11
- Shrine.plugin :pretty_location
12
- ```
13
-
14
- Or by overriding `#generate_location`:
15
-
16
- ```rb
17
- class MyUploader < Shrine
18
- def generate_location(io, context)
19
- "#{context[:record].class}/#{context[:record].id}/#{io.original_filename}"
20
- end
21
- end
22
- ```
23
-
24
- After you've deployed this change, all existing attachments on old locations
25
- will continue to work properly. The next step is to run a script that will
26
- move those to new locations. The easiest way to do that is to reupload them:
11
+ The next step is to run a script that will move those to new locations. The
12
+ easiest way to do that is to reupload them, and afterwards delete them:
27
13
 
28
14
  ```rb
29
15
  Shrine.plugin :migration_helpers # before the model is loaded
@@ -69,10 +69,31 @@ def upload(io, id, metadata = {})
69
69
  end
70
70
  ```
71
71
 
72
+ ## Updating
73
+
74
+ If your storage supports updating data of existing files (e.g. some metadata),
75
+ the convention is to create an `#update` method:
76
+
77
+ ```rb
78
+ class Shrine
79
+ module Storage
80
+ class MyStorage
81
+ # ...
82
+
83
+ def update(id, options = {})
84
+ # update data of the file
85
+ end
86
+
87
+ # ...
88
+ end
89
+ end
90
+ end
91
+ ```
92
+
72
93
  ## Streaming
73
94
 
74
95
  If your storage can stream files by yielding chunks, you can add an additional
75
- `#stream` method:
96
+ `#stream` method (used by the `download_endpoint` plugin):
76
97
 
77
98
  ```rb
78
99
  class Shrine
@@ -39,7 +39,7 @@ Shrine's JSON representation of an uploaded file looks like this:
39
39
  "id": "349234854924394", # requied
40
40
  "storage": "cache", # required
41
41
  "metadata": {
42
- "size": 45461, # required
42
+ "size": 45461, # optional
43
43
  "filename": "foo.jpg", # optional
44
44
  "mime_type": "image/jpeg", # optional
45
45
  }
@@ -141,10 +141,8 @@ include the object key as a query param:
141
141
  <%
142
142
  cached_file = {
143
143
  storage: "cache",
144
- id: params[:key][/cache\/(.+)/, 1], # we have to remove the prefix part,
145
- metadata: {
146
- size: Shrine.storages[:cache].bucket.object(params[:key]).size,
147
- }
144
+ id: params[:key][/cache\/(.+)/, 1], # we have to remove the prefix part
145
+ metadata: {},
148
146
  }
149
147
  %>
150
148
 
@@ -103,10 +103,9 @@ class User < Sequel::Model
103
103
  end
104
104
  ```
105
105
 
106
- Unlike in Paperclip which requires you to have `<attachment>_file_name`,
107
- `<attachment>_file_size`, `<attachment>_content_type` and
108
- `<attachment>_updated_at` columns, in Shrine you only need to have an
109
- `<attachment>_data` text column, and all information will be stored there.
106
+ Unlike in Paperclip which requires you to have 4 `<attachment>_*` columns, in
107
+ Shrine you only need to have an `<attachment>_data` text column, and all
108
+ information will be stored there (in the above case `avatar_data`).
110
109
 
111
110
  The attachments use `:store` for storing the files, and `:cache` for caching.
112
111
  The latter is something Paperclip doesn't do, but caching before storing is
@@ -179,6 +178,120 @@ class ImageUploader < Shrine
179
178
  end
180
179
  ```
181
180
 
181
+ ## Migrating from Paperclip
182
+
183
+ You have an existing app using Paperclip and you want to transfer it to Shrine.
184
+ First we need to make new uploads write to the `<attachment>_data` column.
185
+ Let's assume we have a `Photo` model with the "image" attachment:
186
+
187
+ ```rb
188
+ add_column :photos, :image_data, :text
189
+ ```
190
+
191
+ Afterwards we need to make new uploads write to the `image_data` column. This
192
+ can be done by including the below module to all models that have Paperclip
193
+ attachments:
194
+
195
+ ```rb
196
+ require "fastimage"
197
+
198
+ module PaperclipShrineSynchronization
199
+ def self.included(model)
200
+ model.before_save do
201
+ Paperclip::AttachmentRegistry.each_definition do |klass, name, options|
202
+ write_shrine_data(name) if changes.key?(:"#{name}_file_name") && klass == self.class
203
+ end
204
+ end
205
+ end
206
+
207
+ def write_shrine_data(name)
208
+ attachment = send(name)
209
+
210
+ if attachment.size.present?
211
+ data = attachment_to_shrine_data(attachment)
212
+
213
+ if attachment.styles.any?
214
+ data = {original: data}
215
+ attachment.styles.each do |name, style|
216
+ data[name] = style_to_shrine_data(style)
217
+ end
218
+ end
219
+
220
+ write_attribute(:"#{name}_data", data.to_json)
221
+ else
222
+ write_attribute(:"#{name}_data", nil)
223
+ end
224
+ end
225
+
226
+ private
227
+
228
+ # If you'll be using a `:prefix` on your Shrine storage, or you're storing
229
+ # files on the filesystem, make sure to subtract the appropriate part
230
+ # from the path assigned to `:id`.
231
+ def attachment_to_shrine_data(attachment)
232
+ {
233
+ storage: :store,
234
+ id: attachment.path,
235
+ metadata: {
236
+ size: attachment.size,
237
+ filename: attachment.original_filename,
238
+ content_type: attachment.content_type,
239
+ },
240
+ }
241
+ end
242
+
243
+ # If you'll be using a `:prefix` on your Shrine storage, or you're storing
244
+ # files on the filesystem, make sure to subtract the appropriate part
245
+ # from the path assigned to `:id`.
246
+ def style_to_shrine_data(style)
247
+ attachment = style.attachment
248
+ path = attachment.path(style.name)
249
+ url = attachment.url(style.name)
250
+ file = attachment.instance_variable_get("@queued_for_write")[style.name]
251
+
252
+ size = file.size if file
253
+ size ||= FastImage.new(url).content_length
254
+ size ||= File.size(path)
255
+ filename = File.basename(path)
256
+ mime_type = MIME::Types.type_for(path).first.to_s.presence
257
+
258
+ {
259
+ storage: :store,
260
+ id: path,
261
+ metadata: {
262
+ size: size,
263
+ filename: filename,
264
+ mime_type: mime_type,
265
+ }
266
+ }
267
+ end
268
+ end
269
+ ```
270
+ ```rb
271
+ class Photo < ActiveRecord::Base
272
+ has_attached_file :image
273
+ include PaperclipShrineSynchronization # needs to be after `has_attached_file`
274
+ end
275
+ ```
276
+
277
+ After you deploy this code, the `image_data` column should now be successfully
278
+ synchronized with new attachments. Next step is to run a script which writes
279
+ all existing CarrierWave attachments to `image_data`:
280
+
281
+ ```rb
282
+ Photo.find_each do |photo|
283
+ Paperclip::AttachmentRegistry.each_definition do |klass, name, options|
284
+ photo.write_shrine_data(name) if klass == Photo
285
+ end
286
+ photo.save!
287
+ end
288
+ ```
289
+
290
+ Now you should be able to rewrite your application so that it uses Shrine
291
+ instead of Paperclip, using equivalent Shrine storages. For help with
292
+ translating the code from Paperclip to Shrine, you can consult the reference
293
+ below.
294
+
182
295
  ## Paperclip to Shrine direct mapping
183
296
 
184
297
  ### `has_attached_file`
@@ -188,6 +188,67 @@ Shrine doesn't have a built-in solution for accepting multiple uploads, but
188
188
  it's actually very easy to do manually, see the [example app] on how you can do
189
189
  multiple uploads directly to S3.
190
190
 
191
+ ## Migrating from Refile
192
+
193
+ You have an existing app using Refile and you want to transfer it to
194
+ Shrine. Let's assume we have a `Photo` model with the "image" attachment. First
195
+ we need to create the `image_data` column for Shrine:
196
+
197
+ ```rb
198
+ add_column :photos, :image_data, :text
199
+ ```
200
+
201
+ Afterwards we need to make new uploads write to the `image_data` column. This
202
+ can be done by including the below module to all models that have Refile
203
+ attachments:
204
+
205
+ ```rb
206
+ module RefileShrineSynchronization
207
+ def write_shrine_data(name)
208
+ if read_attribute("#{name}_id").present?
209
+ data = {
210
+ storage: :store,
211
+ id: send("#{name}_id"),
212
+ metadata: {
213
+ size: (send("#{name}_size") if respond_to?("#{name}_size")),
214
+ filename: (send("#{name}_filename") if respond_to?("#{name}_filename")),
215
+ mime_type: (send("#{name}_content_type") if respond_to?("#{name}_content_type")),
216
+ }
217
+ }
218
+
219
+ write_attribute(:"#{name}_data", data.to_json)
220
+ else
221
+ write_attribute(:"#{name}_data", nil)
222
+ end
223
+ end
224
+ end
225
+ ```
226
+ ```rb
227
+ class Photo < ActiveRecord::Base
228
+ attachment :image
229
+ include RefileShrineSynchronization
230
+
231
+ before_save do
232
+ write_shrine_data(:image) if changes.key?(:image_id)
233
+ end
234
+ end
235
+ ```
236
+
237
+ After you deploy this code, the `image_data` column should now be successfully
238
+ synchronized with new attachments. Next step is to run a script which writes
239
+ all existing Refile attachments to `image_data`:
240
+
241
+ ```rb
242
+ Photo.find_each do |photo|
243
+ photo.write_shrine_data(:image)
244
+ photo.save!
245
+ end
246
+ ```
247
+
248
+ Now you should be able to rewrite your application so that it uses Shrine
249
+ instead of Refile, using equivalent Shrine storages. For help with translating
250
+ the code from Refile to Shrine, you can consult the reference below.
251
+
191
252
  ## Refile to Shrine direct mapping
192
253
 
193
254
  ### `Refile`
@@ -258,10 +258,11 @@ class Shrine
258
258
  # Generates a unique location for the uploaded file, and preserves an
259
259
  # optional extension.
260
260
  def generate_location(io, context = {})
261
- extension = File.extname(extract_filename(io).to_s)
261
+ extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
262
+ extension ||= File.extname(extract_filename(io).to_s)
262
263
  basename = generate_uid(io)
263
264
 
264
- basename + extension
265
+ basename + extension.to_s
265
266
  end
266
267
 
267
268
  # Extracts filename, size and MIME type from the file, which is later
@@ -310,15 +311,15 @@ class Shrine
310
311
  # the metadata, stores the file, and returns a Shrine::UploadedFile.
311
312
  def _store(io, context)
312
313
  _enforce_io(io)
313
- context[:location] ||= get_location(io, context)
314
- context[:metadata] ||= get_metadata(io, context)
314
+ metadata = get_metadata(io, context)
315
+ location = get_location(io, context.merge(metadata: metadata))
315
316
 
316
- put(io, context)
317
+ put(io, context.merge(location: location, metadata: metadata))
317
318
 
318
319
  self.class::UploadedFile.new(
319
- "id" => context[:location],
320
+ "id" => location,
320
321
  "storage" => storage_key.to_s,
321
- "metadata" => context[:metadata],
322
+ "metadata" => metadata,
322
323
  )
323
324
  end
324
325
 
@@ -352,7 +353,7 @@ class Shrine
352
353
  # Retrieves the location for the given io and context. First it looks
353
354
  # for the `:location` option, otherwise it calls #generate_location.
354
355
  def get_location(io, context)
355
- generate_location(io, context)
356
+ context[:location] || generate_location(io, context)
356
357
  end
357
358
 
358
359
  # Copies the metadata over from an UploadedFile or calls
@@ -527,7 +528,7 @@ class Shrine
527
528
  # stored file.
528
529
  def promote(cached_file)
529
530
  stored_file = store!(cached_file, phase: :store)
530
- (result = swap(stored_file)) or delete!(stored_file, phase: :stored)
531
+ result = swap(stored_file) or delete!(stored_file, phase: :stored)
531
532
  result
532
533
  end
533
534
 
@@ -562,6 +563,7 @@ class Shrine
562
563
  instance_exec(&validate_block) if validate_block && get
563
564
  end
564
565
 
566
+ # Delegates to `Shrine.uploaded_file`.
565
567
  def uploaded_file(*args, &block)
566
568
  shrine_class.uploaded_file(*args, &block)
567
569
  end
@@ -585,7 +587,7 @@ class Shrine
585
587
  uploaded_file && cache.uploaded?(uploaded_file)
586
588
  end
587
589
 
588
- # Alias to #update, overriden in ORM plugins.
590
+ # Calls #update, overriden in ORM plugins.
589
591
  def swap(uploaded_file)
590
592
  update(uploaded_file)
591
593
  uploaded_file
@@ -642,7 +644,7 @@ class Shrine
642
644
  # The context that's sent to Shrine on upload and delete. It holds the
643
645
  # record and the name of the attachment.
644
646
  def context
645
- @context ||= {name: name, record: record}
647
+ {name: name, record: record}
646
648
  end
647
649
  end
648
650
 
@@ -659,46 +661,50 @@ class Shrine
659
661
  end
660
662
 
661
663
  module FileMethods
662
- # The ID of the uploaded file, which holds the location of the actual
663
- # file on the storage
664
- attr_reader :id
665
-
666
- # The storage key as a string.
667
- attr_reader :storage_key
668
-
669
- # A hash of metadata, returned from `Shrine#extract_metadata`.
670
- attr_reader :metadata
671
-
672
664
  # The entire data hash which identifies this uploaded file.
673
665
  attr_reader :data
674
666
 
675
667
  def initialize(data)
676
- @data = data
677
- @id = data.fetch("id")
678
- @storage_key = data.fetch("storage")
679
- @metadata = data.fetch("metadata")
680
-
668
+ @data = data
669
+ @data["metadata"] ||= {}
681
670
  storage # ensure storage exists
682
671
  end
683
672
 
673
+ # The ID of the uploaded file, which holds the location of the actual
674
+ # file on the storage
675
+ def id
676
+ @data.fetch("id")
677
+ end
678
+
679
+ # The storage key as a string.
680
+ def storage_key
681
+ @data.fetch("storage")
682
+ end
683
+
684
+ # A hash of metadata.
685
+ def metadata
686
+ @data.fetch("metadata")
687
+ end
688
+
684
689
  # The filename that was extracted from the original file.
685
690
  def original_filename
686
- metadata.fetch("filename")
691
+ metadata["filename"]
687
692
  end
688
693
 
689
- # The extension derived from `#original_filename`.
694
+ # The extension derived from #id if present, otherwise from
695
+ # #original_filename.
690
696
  def extension
691
697
  File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
692
698
  end
693
699
 
694
700
  # The filesize of the original file.
695
701
  def size
696
- Integer(metadata.fetch("size"))
702
+ Integer(metadata["size"]) if metadata["size"]
697
703
  end
698
704
 
699
705
  # The MIME type of the original file.
700
706
  def mime_type
701
- metadata.fetch("mime_type")
707
+ metadata["mime_type"]
702
708
  end
703
709
  alias content_type mime_type
704
710
 
@@ -717,8 +723,10 @@ class Shrine
717
723
  # Part of Shrine::UploadedFile's complying to the IO interface. It
718
724
  # delegates to the internally downloaded file.
719
725
  def close
720
- io.close
721
- io.delete if io.class.name == "Tempfile"
726
+ if @io
727
+ io.close
728
+ io.delete if io.class.name == "Tempfile"
729
+ end
722
730
  end
723
731
 
724
732
  # Part of Shrine::UploadedFile's complying to the IO interface. It
@@ -783,12 +791,12 @@ class Shrine
783
791
 
784
792
  # The instance of `Shrine` with the corresponding storage.
785
793
  def uploader
786
- @uploader ||= shrine_class.new(storage_key)
794
+ shrine_class.new(storage_key)
787
795
  end
788
796
 
789
797
  # The storage class this file was uploaded to.
790
798
  def storage
791
- uploader.storage
799
+ shrine_class.find_storage(storage_key)
792
800
  end
793
801
 
794
802
  # Returns the Shrine class related to this uploaded file.
@@ -28,9 +28,9 @@ class Shrine
28
28
  # If you want to put some parts of this lifecycle into a background job, see
29
29
  # the backgrounding plugin.
30
30
  #
31
- # Additionally, any Shrine validation errors will added to ActiveRecord's
32
- # errors upon validation. Note that if you want to validate presence of the
33
- # attachment, you can do it directly on the model.
31
+ # Additionally, any Shrine validation errors will be added to
32
+ # ActiveRecord's errors upon validation. If you want to validate presence
33
+ # of the attachment, you can do it directly on the model.
34
34
  #
35
35
  # class User < ActiveRecord::Base
36
36
  # include ImageUploader[:avatar]
@@ -80,7 +80,7 @@ class Shrine
80
80
  break if record.send("#{name}_data") != record.reload.send("#{name}_data")
81
81
  super
82
82
  end
83
- rescue ActiveRecord::RecordNotFound
83
+ rescue ::ActiveRecord::RecordNotFound
84
84
  end
85
85
 
86
86
  # We save the record after updating, raising any validation errors.
@@ -30,20 +30,25 @@ class Shrine
30
30
 
31
31
  module AttacherMethods
32
32
  def backup_file(uploaded_file)
33
- uploaded_file(uploaded_file) { |f| f.data["storage"] = backup_storage.to_s }
34
- uploaded_file(uploaded_file.to_json)
33
+ uploaded_file(uploaded_file.to_json) do |file|
34
+ file.data["storage"] = backup_storage.to_s
35
+ end
35
36
  end
36
37
 
37
38
  private
38
39
 
39
40
  # Back up the stored file and return it.
40
41
  def store!(io, phase:)
41
- super.tap { |stored_file| store_backup!(stored_file) }
42
+ stored_file = super
43
+ store_backup!(stored_file)
44
+ stored_file
42
45
  end
43
46
 
44
47
  # Delete the backed up file unless `:delete` was set to false.
45
48
  def delete!(uploaded_file, phase:)
46
- super.tap { |deleted_file| delete_backup!(deleted_file) if backup_delete? }
49
+ deleted_file = super
50
+ delete_backup!(deleted_file) if backup_delete?
51
+ deleted_file
47
52
  end
48
53
 
49
54
  # Upload the stored file to the backup storage.
@@ -74,7 +74,7 @@ class Shrine
74
74
  else
75
75
  message = shrine_class.opts[:data_uri_error_message]
76
76
  message = message.call(uri) if message.respond_to?(:call)
77
- errors << message
77
+ errors.replace [message]
78
78
  @data_uri = uri
79
79
  end
80
80
  end
@@ -23,7 +23,7 @@ class Shrine
23
23
  private
24
24
 
25
25
  def default_url(**options)
26
- default_url_block.call(options.merge(context))
26
+ default_url_block.call(context.merge(options){|k,old,new|old})
27
27
  end
28
28
 
29
29
  def default_url_block
@@ -107,13 +107,16 @@ class Shrine
107
107
  # Uses the ruby-filemagic gem to magically extract the MIME type.
108
108
  def _extract_mime_type_with_filemagic(io)
109
109
  filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
110
- data = io.read(MAGIC_NUMBER); io.rewind
110
+ data = io.read(MAGIC_NUMBER)
111
+ io.rewind
111
112
  filemagic.buffer(data)
112
113
  end
113
114
 
114
115
  # Uses the mimemagic gem to extract the MIME type.
115
116
  def _extract_mime_type_with_mimemagic(io)
116
- MimeMagic.by_magic(io).type
117
+ result = MimeMagic.by_magic(io).type
118
+ io.rewind
119
+ result
117
120
  end
118
121
 
119
122
  # Uses the mime-types gem to determine MIME type from file extension.
@@ -55,9 +55,9 @@ class Shrine
55
55
  #
56
56
  # plugin :direct_upload, presign: true
57
57
  #
58
- # This will disable the default `POST /:storage/:name` route (for security
59
- # reasons), and enable `GET /:storage/presign`. The response for that
60
- # request looks something like this:
58
+ # This will add `GET /:storage/presign`, and disable the default `POST
59
+ # /:storage/:name` (for security reasons) The response for that request
60
+ # looks something like this:
61
61
  #
62
62
  # {
63
63
  # "url" => "https://my-bucket.s3-eu-west-1.amazonaws.com",
@@ -1,3 +1,6 @@
1
+ warn "The keep_location Shrine plugin is deprecated and will be removed in Shrine 2. " \
2
+ "You can easily implement the same behaviour in Shrine#generate_location."
3
+
1
4
  class Shrine
2
5
  module Plugins
3
6
  # The keep_location plugin allows you to preserve locations when
@@ -22,7 +25,7 @@ class Shrine
22
25
  private
23
26
 
24
27
  def get_location(io, context)
25
- if io.is_a?(UploadedFile) && keep_location?(io)
28
+ if !context[:location] && io.is_a?(UploadedFile) && keep_location?(io)
26
29
  io.id
27
30
  else
28
31
  super
@@ -5,6 +5,8 @@ class Shrine
5
5
  #
6
6
  # plugin :migration_helpers
7
7
  #
8
+ # ## `<attachment>_cache` and `<attachment>_store`
9
+ #
8
10
  # If your attachment's name is "avatar", the model will get `#avatar_cache`
9
11
  # and `#avatar_store` methods.
10
12
  #
@@ -12,6 +14,16 @@ class Shrine
12
14
  # user.avatar_cache #=> #<Shrine @storage_key=:cache @storage=#<Shrine::Storage::FileSystem @directory=public/uploads>>
13
15
  # user.avatar_store #=> #<Shrine @storage_key=:store @storage=#<Shrine::Storage::S3:0x007fb8343397c8 @bucket=#<Aws::S3::Bucket name="foo">>>
14
16
  #
17
+ # ## `<attachment>_cached?` and `<attachment>_stored?`
18
+ #
19
+ # You can use these methods to check whether attachment exists and is
20
+ # cached/stored:
21
+ #
22
+ # user.avatar_cached? # user.avatar && user.avatar_cache.uploaded?(user.avatar)
23
+ # user.avatar_stored? # user.avatar && user.avatar_store.uploaded?(user.avatar)
24
+ #
25
+ # ## `update_<attachment>`
26
+ #
15
27
  # The model will also get `#update_avatar` method, which can be used when
16
28
  # doing attachment migrations. It will update the record's attachment with
17
29
  # the result of the passed in block.
@@ -41,6 +53,14 @@ class Shrine
41
53
  def #{name}_store
42
54
  #{name}_attacher.store
43
55
  end
56
+
57
+ def #{name}_cached?
58
+ #{name}_attacher.cached?
59
+ end
60
+
61
+ def #{name}_stored?
62
+ #{name}_attacher.stored?
63
+ end
44
64
  RUBY
45
65
  end
46
66
  end
@@ -53,6 +73,18 @@ class Shrine
53
73
  new_attachment = block.call(get)
54
74
  swap(new_attachment)
55
75
  end
76
+
77
+ # Returns true if the attachment is present and is uploaded by the
78
+ # temporary storage.
79
+ def cached?
80
+ get && cache.uploaded?(get)
81
+ end
82
+
83
+ # Returns true if the attachment is present and is uploaded by the
84
+ # permanent storage.
85
+ def stored?
86
+ get && store.uploaded?(get)
87
+ end
56
88
  end
57
89
  end
58
90
 
@@ -17,12 +17,12 @@ class Shrine
17
17
  #
18
18
  # plugin :moving, storages: [:cache, :store]
19
19
  #
20
- # What exactly means "moving"? Usually this means that the file which is
21
- # being uploaded will be deleted afterwards. However, if both the file
22
- # being uploaded and the destination are on the filesystem, a `mv` command
23
- # will be executed instead. Some other storages may implement moving as
24
- # well, usually if also both the cache and store are using the same
25
- # storage.
20
+ # What exactly means "moving"? If both the file being uploaded and the
21
+ # destination are on the filesystem, a `mv` command will be executed
22
+ # (making the transfer instantaneous). Some other storages may implement
23
+ # moving as well, usually only for files which are on the same storage.
24
+ # If moving isn't implemented by the storage, the file will be simply
25
+ # deleted after upload.
26
26
  module Moving
27
27
  def self.configure(uploader, storages:)
28
28
  uploader.opts[:move_files_to_storages] = storages
@@ -24,7 +24,7 @@ class Shrine
24
24
  if storage.respond_to?(:multi_delete)
25
25
  storage.multi_delete(uploaded_file.map(&:id))
26
26
  else
27
- uploaded_file.map { |file| _delete(file, context) }
27
+ uploaded_file.each { |file| _delete(file, context) }
28
28
  end
29
29
  else
30
30
  super
@@ -11,11 +11,28 @@ class Shrine
11
11
  #
12
12
  # "user/564/avatar/thumb-493g82jf23.jpg"
13
13
  # # :model/:id/:attachment/:version-:uid.:extension
14
+ #
15
+ # By default if a record class is inside a namespace, only the "inner"
16
+ # class name is used in the location. If you want to include the namespace,
17
+ # you can pass in the `:namespace` option with the desired separator as the
18
+ # value:
19
+ #
20
+ # plugin :pretty_location, namespace: "_"
21
+ # # "blog_user/.../493g82jf23.jpg"
22
+ #
23
+ # plugin :pretty_location, namespace: "/"
24
+ # # "blog/user/.../493g82jf23.jpg"
14
25
  module PrettyLocation
26
+ def self.configure(uploader, namespace: nil)
27
+ uploader.opts[:pretty_location_namespace] = namespace
28
+ end
29
+
15
30
  module InstanceMethods
16
31
  def generate_location(io, context)
17
- type = context[:record].class.name.downcase if context[:record] && context[:record].class.name
18
- id = context[:record].id if context[:record].respond_to?(:id)
32
+ if context[:record]
33
+ type = class_location(context[:record].class) if context[:record].class.name
34
+ id = context[:record].id if context[:record].respond_to?(:id)
35
+ end
19
36
  name = context[:name]
20
37
 
21
38
  dirname, slash, basename = super.rpartition("/")
@@ -30,6 +47,15 @@ class Shrine
30
47
  def generate_uid(io)
31
48
  SecureRandom.hex(5)
32
49
  end
50
+
51
+ def class_location(klass)
52
+ parts = klass.name.downcase.split("::")
53
+ if separator = opts[:pretty_location_namespace]
54
+ parts.join(separator)
55
+ else
56
+ parts.last
57
+ end
58
+ end
33
59
  end
34
60
  end
35
61
 
@@ -29,34 +29,38 @@ class Shrine
29
29
  #
30
30
  # plugin :remote_url, max_size: nil
31
31
  #
32
- # If download fails, either because the remote file wasn't found, was too
33
- # large, or the request redirected, an error will be added to the
34
- # attachment. You can change the default error message:
35
- #
36
- # plugin :remote_url, error_message: "download failed"
37
- # plugin :remote_url, error_message: ->(url) { I18n.t("errors.download_failed") }
38
- #
39
32
  # Finally, if for some reason the way the file is downloaded doesn't suit
40
33
  # your needs, you can provide a custom downloader:
41
34
  #
42
- # plugin :remote_url, downloader: ->(url) do
35
+ # plugin :remote_url, downloader: ->(url, max_size:) do
43
36
  # request = RestClient::Request.new(method: :get, url: url, raw_response: true)
44
37
  # response = request.execute
45
38
  # response.file
46
39
  # end
40
+ #
41
+ # If download errors, the error is rescued and a validation error is added
42
+ # equal to the error message. You can change the default error message:
43
+ #
44
+ # plugin :remote_url, error_message: "download failed"
45
+ # plugin :remote_url, error_message: ->(url) { I18n.t("errors.download_failed") }
46
+ #
47
+ # If you need the error instance for generating the error message, passing
48
+ # the `:include_error` option will additionally yield the error to the
49
+ # block:
50
+ #
51
+ # plugin :remote_url, include_error: true, error_message: ->(url, error) { "..." }
47
52
  module RemoteUrl
48
- DEFAULT_ERROR_MESSAGE = "file was not found or was too large"
49
-
50
53
  def self.load_dependencies(uploader, downloader: :open_uri, **)
51
54
  case downloader
52
55
  when :open_uri then require "down"
53
56
  end
54
57
  end
55
58
 
56
- def self.configure(uploader, downloader: :open_uri, error_message: nil, max_size:)
59
+ def self.configure(uploader, downloader: :open_uri, max_size:, error_message: nil, include_error: false)
57
60
  uploader.opts[:remote_url_downloader] = downloader
58
- uploader.opts[:remote_url_error_message] = error_message || DEFAULT_ERROR_MESSAGE
59
61
  uploader.opts[:remote_url_max_size] = max_size
62
+ uploader.opts[:remote_url_error_message] = error_message
63
+ uploader.opts[:remote_url_include_error] = include_error
60
64
  end
61
65
 
62
66
  module AttachmentMethods
@@ -82,12 +86,17 @@ class Shrine
82
86
  def remote_url=(url)
83
87
  return if url == ""
84
88
 
85
- if downloaded_file = download(url)
89
+ begin
90
+ downloaded_file = download(url)
91
+ rescue => error
92
+ download_error = error
93
+ end
94
+
95
+ if downloaded_file
86
96
  assign(downloaded_file)
87
97
  else
88
- message = shrine_class.opts[:remote_url_error_message]
89
- message = message.call(url) if message.respond_to?(:call)
90
- errors << message
98
+ message = download_error_message(url, download_error)
99
+ errors.replace [message]
91
100
  @remote_url = url
92
101
  end
93
102
  end
@@ -117,7 +126,19 @@ class Shrine
117
126
  # the download simply failed.
118
127
  def download_with_open_uri(url, max_size:)
119
128
  Down.download(url, max_size: max_size)
120
- rescue Down::Error
129
+ end
130
+
131
+ def download_error_message(url, error)
132
+ if message = shrine_class.opts[:remote_url_error_message]
133
+ args = [url]
134
+ args << error if shrine_class.opts[:remote_url_include_error]
135
+ message = message.call(*args) if message.respond_to?(:call)
136
+ else
137
+ message = "download failed"
138
+ message = "#{message}: #{error.message}" if error
139
+ end
140
+
141
+ message
121
142
  end
122
143
  end
123
144
  end
@@ -78,7 +78,7 @@ class Shrine
78
78
  break if record.send("#{name}_data") != record.reload.send("#{name}_data")
79
79
  super
80
80
  end
81
- rescue Sequel::Error
81
+ rescue ::Sequel::Error
82
82
  end
83
83
 
84
84
  # We save the record after updating, raising any validation errors.
@@ -62,7 +62,9 @@ class Shrine
62
62
  private
63
63
 
64
64
  def _extract_dimensions_with_fastimage(io)
65
- FastImage.size(io)
65
+ result = FastImage.size(io)
66
+ io.rewind # https://github.com/sdsykes/fastimage/pull/66
67
+ result
66
68
  end
67
69
  end
68
70
 
@@ -154,7 +154,6 @@ class Shrine
154
154
  def _delete(uploaded_file, context)
155
155
  if (versions = uploaded_file).is_a?(Hash)
156
156
  _delete(versions.values, context)
157
- versions
158
157
  else
159
158
  super
160
159
  end
@@ -40,8 +40,8 @@ class Shrine
40
40
  # These options will be passed to aws-sdk's methods for [uploading],
41
41
  # [copying] and [presigning].
42
42
  #
43
- # You can also forward additional upload options per upload with the
44
- # `upload_options` plugin:
43
+ # You can also generate upload options per upload with the `upload_options`
44
+ # plugin:
45
45
  #
46
46
  # class MyUploader < Shrine
47
47
  # plugin :upload_options, store: ->(io, context) do
@@ -53,6 +53,9 @@ class Shrine
53
53
  # end
54
54
  # end
55
55
  #
56
+ # Note that these aren't applied to presigns, since presigns are generated
57
+ # using the storage directly.
58
+ #
56
59
  # ## CDN
57
60
  #
58
61
  # If you're using a CDN with S3 like Amazon CloudFront, you can specify
@@ -217,7 +220,7 @@ class Shrine
217
220
 
218
221
  # This is used to check whether an S3 file is copyable.
219
222
  def access_key_id
220
- @s3.client.config.credentials.access_key_id
223
+ @s3.client.config.credentials.credentials.access_key_id
221
224
  end
222
225
 
223
226
  private
@@ -242,7 +245,7 @@ class Shrine
242
245
 
243
246
  # Amazon requires multipart copy from S3 objects larger than 5 GB.
244
247
  def large?(io)
245
- io.size >= 5*1024*1024*1024 # 5GB
248
+ io.size && io.size >= 5*1024*1024*1024 # 5GB
246
249
  end
247
250
  end
248
251
  end
@@ -5,7 +5,7 @@ class Shrine
5
5
 
6
6
  module VERSION
7
7
  MAJOR = 1
8
- MINOR = 2
8
+ MINOR = 3
9
9
  TINY = 0
10
10
  PRE = nil
11
11
 
@@ -16,13 +16,12 @@ Gem::Specification.new do |gem|
16
16
  gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "shrine.gemspec", "doc/*.md"]
17
17
  gem.require_path = "lib"
18
18
 
19
- gem.add_dependency "down", ">= 1.0.5"
19
+ gem.add_dependency "down", ">= 2.0.1"
20
20
 
21
21
  gem.add_development_dependency "rake"
22
22
  gem.add_development_dependency "minitest", "~> 5.8"
23
23
  gem.add_development_dependency "minitest-hooks", "~> 1.3.0"
24
24
  gem.add_development_dependency "mocha"
25
- gem.add_development_dependency "vcr", "~> 2.9"
26
25
  gem.add_development_dependency "webmock"
27
26
  gem.add_development_dependency "rack-test_app"
28
27
  gem.add_development_dependency "dotenv"
@@ -39,7 +38,7 @@ Gem::Specification.new do |gem|
39
38
  end
40
39
 
41
40
  gem.add_development_dependency "sequel"
42
- gem.add_development_dependency "activerecord"
41
+ gem.add_development_dependency "activerecord", "~> 4.2"
43
42
 
44
43
  if RUBY_ENGINE == "jruby"
45
44
  gem.add_development_dependency "activerecord-jdbcsqlite3-adapter"
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.2.0
4
+ version: 1.3.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: 2016-01-26 00:00:00.000000000 Z
11
+ date: 2016-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 1.0.5
19
+ version: 2.0.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 1.0.5
26
+ version: 2.0.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: vcr
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '2.9'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '2.9'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: webmock
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -252,16 +238,16 @@ dependencies:
252
238
  name: activerecord
253
239
  requirement: !ruby/object:Gem::Requirement
254
240
  requirements:
255
- - - ">="
241
+ - - "~>"
256
242
  - !ruby/object:Gem::Version
257
- version: '0'
243
+ version: '4.2'
258
244
  type: :development
259
245
  prerelease: false
260
246
  version_requirements: !ruby/object:Gem::Requirement
261
247
  requirements:
262
- - - ">="
248
+ - - "~>"
263
249
  - !ruby/object:Gem::Version
264
- version: '0'
250
+ version: '4.2'
265
251
  - !ruby/object:Gem::Dependency
266
252
  name: sqlite3
267
253
  requirement: !ruby/object:Gem::Requirement
@@ -358,9 +344,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
358
344
  version: '0'
359
345
  requirements: []
360
346
  rubyforge_project:
361
- rubygems_version: 2.4.5
347
+ rubygems_version: 2.5.1
362
348
  signing_key:
363
349
  specification_version: 4
364
350
  summary: Toolkit for file uploads in Ruby
365
351
  test_files: []
366
- has_rdoc: