shrine 2.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of shrine might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +73 -11
- data/doc/carrierwave.md +1 -1
- data/doc/creating_storages.md +19 -32
- data/doc/paperclip.md +1 -1
- data/doc/refile.md +1 -1
- data/doc/regenerating_versions.md +2 -2
- data/lib/shrine.rb +12 -12
- data/lib/shrine/plugins/cached_attachment_data.rb +9 -12
- data/lib/shrine/plugins/determine_mime_type.rb +13 -15
- data/lib/shrine/plugins/download_endpoint.rb +15 -15
- data/lib/shrine/plugins/included.rb +3 -7
- data/lib/shrine/plugins/logging.rb +6 -6
- data/lib/shrine/plugins/module_include.rb +19 -6
- data/lib/shrine/plugins/moving.rb +5 -2
- data/lib/shrine/plugins/parallelize.rb +28 -24
- data/lib/shrine/plugins/restore_cached_data.rb +6 -6
- data/lib/shrine/plugins/versions.rb +15 -10
- data/lib/shrine/storage/linter.rb +1 -15
- data/lib/shrine/storage/s3.rb +13 -8
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +3 -3
- metadata +12 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 24f4131513e38de7fad11e2b540643b268026427
|
4
|
+
data.tar.gz: daad25a613048ed9c5c66a818050dddcd007fb6f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 23591ec752213879f06ddd761b371276ac00c7eef5830ea2360e343044526a2717aaae540db89da61753630c4b5f3c2d8d05f79b72a266a25722a72bb4759395
|
7
|
+
data.tar.gz: c10ce66b0ba150e4c29678850b19154f67adac844fb7c2e3f34f5ff6d92c74901089c5f0f93b9642f736b8928f1a9ba4531a4a08d4074561138e13a74b530947
|
data/README.md
CHANGED
@@ -12,13 +12,70 @@ explains the motivation behind Shrine.
|
|
12
12
|
- Bugs: [github.com/janko-m/shrine/issues](https://github.com/janko-m/shrine/issues)
|
13
13
|
- Help & Discussion: [groups.google.com/group/ruby-shrine](https://groups.google.com/forum/#!forum/ruby-shrine)
|
14
14
|
|
15
|
-
##
|
15
|
+
## Quick start
|
16
|
+
|
17
|
+
Add Shrine to the Gemfile and write an initializer:
|
16
18
|
|
17
19
|
```rb
|
18
20
|
gem "shrine"
|
19
21
|
```
|
20
22
|
|
21
|
-
|
23
|
+
```rb
|
24
|
+
require "shrine"
|
25
|
+
require "shrine/storage/file_system"
|
26
|
+
|
27
|
+
Shrine.storages = {
|
28
|
+
cache: Shrine::Storage::FileSystem.new("public", prefix: "uploads/cache"),
|
29
|
+
store: Shrine::Storage::FileSystem.new("public", prefix: "uploads/store"),
|
30
|
+
}
|
31
|
+
|
32
|
+
Shrine.plugin :sequel # :activerecord
|
33
|
+
Shrine.plugin :cached_attachment_data # for forms
|
34
|
+
```
|
35
|
+
|
36
|
+
Next write a migration to add a column which will hold attachment data, and run
|
37
|
+
it:
|
38
|
+
|
39
|
+
```rb
|
40
|
+
Sequel.migration do # class AddImageDataToPhotos < ActiveRecord::Migration
|
41
|
+
change do # def change
|
42
|
+
add_column :photos, :image_data, :text # add_column :photos, :image_data, :text
|
43
|
+
end # end
|
44
|
+
end # end
|
45
|
+
```
|
46
|
+
|
47
|
+
Now you can create an uploader class for the type of files you want to upload,
|
48
|
+
and make your model handle attachments:
|
49
|
+
|
50
|
+
```rb
|
51
|
+
class ImageUploader < Shrine
|
52
|
+
# plugins and uploading logic
|
53
|
+
end
|
54
|
+
```
|
55
|
+
|
56
|
+
```rb
|
57
|
+
class Photo < Sequel::Model # ActiveRecord::Base
|
58
|
+
include ImageUploader[:image]
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Finally, you can add the attachment fields to your form:
|
63
|
+
|
64
|
+
```erb
|
65
|
+
<form action="/photos" method="post" enctype="multipart/form-data">
|
66
|
+
<input name="photo[image]" type="hidden" value="<%= @photo.cached_image_data %>">
|
67
|
+
<input name="photo[image]" type="file">
|
68
|
+
</form>
|
69
|
+
|
70
|
+
<!-- Rails: -->
|
71
|
+
|
72
|
+
<%= form_for @photo do |f| %>
|
73
|
+
<%= f.hidden_field :image, value: @photo.cached_image_data %>
|
74
|
+
<%= f.file_field :image %>
|
75
|
+
<% end %>
|
76
|
+
```
|
77
|
+
|
78
|
+
----------
|
22
79
|
|
23
80
|
## Basics
|
24
81
|
|
@@ -139,19 +196,24 @@ Shrine::Attachment.new(:document)
|
|
139
196
|
* `#image` – returns a `Shrine::UploadedFile` based on data from `image_data`
|
140
197
|
* `#image_url` – calls `image.url` if attachment is present, otherwise returns nil.
|
141
198
|
|
142
|
-
This is how you
|
199
|
+
This is how you should create a form for a `@photo`:
|
143
200
|
|
201
|
+
```rb
|
202
|
+
Shrine.plugin :cached_attachment_data
|
203
|
+
```
|
144
204
|
```erb
|
145
205
|
<form action="/photos" method="post" enctype="multipart/form-data">
|
146
|
-
<input name="photo[image]" type="hidden" value="<%= @photo.
|
206
|
+
<input name="photo[image]" type="hidden" value="<%= @photo.cached_image_data %>">
|
147
207
|
<input name="photo[image]" type="file">
|
148
208
|
</form>
|
149
209
|
```
|
150
210
|
|
151
211
|
The "file" field is for file upload, while the "hidden" field is to make the
|
152
|
-
file persist in case of validation errors, and for direct uploads.
|
153
|
-
|
154
|
-
|
212
|
+
file persist in case of validation errors, and for direct uploads. Note that
|
213
|
+
the hidden field should always be *before* the file field.
|
214
|
+
|
215
|
+
This code works because `#image=` also accepts an already cached file via its
|
216
|
+
JSON representation (which is what `#cached_image_data` returns):
|
155
217
|
|
156
218
|
```rb
|
157
219
|
photo.image = '{"id":"9jsdf02kd", "storage":"cache", "metadata": {...}}'
|
@@ -165,10 +227,10 @@ Sequel and ActiveRecord ORMs. It uses the `<attachment>_data` column for
|
|
165
227
|
storing data for uploaded files, so you'll need to add it in a migration.
|
166
228
|
|
167
229
|
```rb
|
168
|
-
|
230
|
+
Shrine.plugin :sequel # :activerecord
|
169
231
|
```
|
170
232
|
```rb
|
171
|
-
|
233
|
+
add_column :movies, :video_data, :text
|
172
234
|
```
|
173
235
|
```rb
|
174
236
|
class Movie < Sequel::Model
|
@@ -264,7 +326,7 @@ require "image_processing/mini_magick"
|
|
264
326
|
|
265
327
|
class ImageUploader < Shrine
|
266
328
|
include ImageProcessing::MiniMagick
|
267
|
-
plugin :versions
|
329
|
+
plugin :versions
|
268
330
|
|
269
331
|
def process(io, context)
|
270
332
|
if context[:phase] == :store
|
@@ -528,7 +590,7 @@ See the documentation of the plugin for more details, as well as the
|
|
528
590
|
[Roda](https://github.com/janko-m/shrine-example)/[Rails](https://github.com/erikdahlstrand/shrine-rails-example)
|
529
591
|
example app which demonstrates multiple uploads directly to S3.
|
530
592
|
|
531
|
-
##
|
593
|
+
## Backgrounding
|
532
594
|
|
533
595
|
Shrine is the first file upload library designed for backgrounding support.
|
534
596
|
Moving phases of managing attachments to background jobs is essential for
|
data/doc/carrierwave.md
CHANGED
@@ -50,7 +50,7 @@ require "image_processing/mini_magick" # part of the "image_processing" gem
|
|
50
50
|
|
51
51
|
class ImageUploader < Shrine
|
52
52
|
include ImageProcessing::MiniMagick
|
53
|
-
plugin :versions
|
53
|
+
plugin :versions
|
54
54
|
|
55
55
|
def process(io, context)
|
56
56
|
if context[:phase] == :store
|
data/doc/creating_storages.md
CHANGED
@@ -37,11 +37,11 @@ class Shrine
|
|
37
37
|
# deletes the file from the storage
|
38
38
|
end
|
39
39
|
|
40
|
-
def url(id, options
|
40
|
+
def url(id, **options)
|
41
41
|
# URL to the remote file, accepts options for customizing the URL
|
42
42
|
end
|
43
43
|
|
44
|
-
def clear!
|
44
|
+
def clear!
|
45
45
|
# deletes all the files in the storage
|
46
46
|
end
|
47
47
|
end
|
@@ -49,12 +49,19 @@ class Shrine
|
|
49
49
|
end
|
50
50
|
```
|
51
51
|
|
52
|
+
## Upload
|
53
|
+
|
54
|
+
The job of `Storage#upload` is to upload the given IO object to the storage.
|
55
|
+
It's good practice to test the storage with a [fake IO] object which responds
|
56
|
+
only to required methods. Some HTTP libraries don't support uploading non-file
|
57
|
+
IOs, although for [Faraday] and [REST client] you can work around that.
|
58
|
+
|
52
59
|
If your storage doesn't control which id the uploaded file will have, you
|
53
|
-
can modify the `id` variable:
|
60
|
+
can modify the `id` variable before returning:
|
54
61
|
|
55
62
|
```rb
|
56
63
|
def upload(io, id, shrine_metadata: {}, **upload_options)
|
57
|
-
|
64
|
+
# ...
|
58
65
|
id.replace(actual_id)
|
59
66
|
end
|
60
67
|
```
|
@@ -64,12 +71,12 @@ you can modify the metadata hash:
|
|
64
71
|
|
65
72
|
```rb
|
66
73
|
def upload(io, id, shrine_metadata: {}, **upload_options)
|
67
|
-
|
68
|
-
|
74
|
+
# ...
|
75
|
+
shrine_metadata.merge!(returned_metadata)
|
69
76
|
end
|
70
77
|
```
|
71
78
|
|
72
|
-
##
|
79
|
+
## Update
|
73
80
|
|
74
81
|
If your storage supports updating data of existing files (e.g. some metadata),
|
75
82
|
the convention is to create an `#update` method:
|
@@ -90,31 +97,7 @@ class Shrine
|
|
90
97
|
end
|
91
98
|
```
|
92
99
|
|
93
|
-
##
|
94
|
-
|
95
|
-
If your storage can stream files by yielding chunks, you can add an additional
|
96
|
-
`#stream` method (used by the `download_endpoint` plugin):
|
97
|
-
|
98
|
-
```rb
|
99
|
-
class Shrine
|
100
|
-
module Storage
|
101
|
-
class MyStorage
|
102
|
-
# ...
|
103
|
-
|
104
|
-
def stream(id)
|
105
|
-
# yields chunks of the file
|
106
|
-
end
|
107
|
-
|
108
|
-
# ...
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
```
|
113
|
-
|
114
|
-
You should also yield the total filesize as the second argument, so that
|
115
|
-
download_endpoint can set `Content-Length` before it starts streaming.
|
116
|
-
|
117
|
-
## Moving
|
100
|
+
## Move
|
118
101
|
|
119
102
|
If your storage can move files, you can add 2 additional methods, and they will
|
120
103
|
automatically get used by the `moving` plugin:
|
@@ -195,3 +178,7 @@ linter = Shrine::Storage::Linter.new(storage, action: :warn)
|
|
195
178
|
Note that using the linter doesn't mean that you shouldn't write any manual
|
196
179
|
tests for your storage. There will likely be some edge cases that won't be
|
197
180
|
tested by the linter.
|
181
|
+
|
182
|
+
[fake IO]: https://github.com/janko-m/shrine-cloudinary/blob/ca587c580ea0762992a2df33fd590c9a1e534905/test/test_helper.rb#L20-L27
|
183
|
+
[REST client]: https://github.com/janko-m/shrine-cloudinary/blob/ca587c580ea0762992a2df33fd590c9a1e534905/lib/shrine/storage/cloudinary.rb#L138-L141
|
184
|
+
[Faraday]: https://github.com/janko-m/shrine-uploadcare/blob/2038781ace0f54d82fa06cc04c4c2958919208ad/lib/shrine/storage/uploadcare.rb#L140
|
data/doc/paperclip.md
CHANGED
@@ -56,7 +56,7 @@ require "image_processing/mini_magick" # part of the "image_processing" gem
|
|
56
56
|
|
57
57
|
class ImageUploader < Shrine
|
58
58
|
include ImageProcessing::MiniMagick
|
59
|
-
plugin :versions
|
59
|
+
plugin :versions
|
60
60
|
|
61
61
|
def process(io, context)
|
62
62
|
if context[:phase] == :store
|
data/doc/refile.md
CHANGED
@@ -29,7 +29,7 @@ end
|
|
29
29
|
|
30
30
|
While in Refile you configure attachments by passing options to `.attachment`,
|
31
31
|
in Shrine you define all your uploading logic inside uploaders, and then
|
32
|
-
generate an
|
32
|
+
generate an attachment module with that uploader which is included into the
|
33
33
|
model:
|
34
34
|
|
35
35
|
```rb
|
@@ -15,7 +15,7 @@ versions:
|
|
15
15
|
|
16
16
|
```rb
|
17
17
|
class ImageUploader < Shrine
|
18
|
-
plugin :versions
|
18
|
+
plugin :versions
|
19
19
|
|
20
20
|
def process(io, context)
|
21
21
|
case context[:phase]
|
@@ -75,7 +75,7 @@ update your processing code to generate it, and deploy it:
|
|
75
75
|
|
76
76
|
```rb
|
77
77
|
class ImageUploader < Shrine
|
78
|
-
plugin :versions
|
78
|
+
plugin :versions
|
79
79
|
|
80
80
|
def process(io, context)
|
81
81
|
case context[:phase]
|
data/lib/shrine.rb
CHANGED
@@ -19,7 +19,7 @@ class Shrine
|
|
19
19
|
private
|
20
20
|
|
21
21
|
def missing_methods_string
|
22
|
-
@missing_methods.map { |m, args| "
|
22
|
+
@missing_methods.map { |m, args| "##{m}" }.join(", ")
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
@@ -322,8 +322,11 @@ class Shrine
|
|
322
322
|
|
323
323
|
# Does the actual uploading, calling `#upload` on the storage.
|
324
324
|
def copy(io, context)
|
325
|
-
|
326
|
-
|
325
|
+
location = context[:location]
|
326
|
+
metadata = context[:metadata]
|
327
|
+
upload_options = context[:upload_options] || {}
|
328
|
+
|
329
|
+
storage.upload(io, location, shrine_metadata: metadata, **upload_options)
|
327
330
|
ensure
|
328
331
|
io.close rescue nil
|
329
332
|
end
|
@@ -358,10 +361,7 @@ class Shrine
|
|
358
361
|
# `#read`, `#eof?`, `#rewind`, `#size` and `#close`, otherwise raises
|
359
362
|
# Shrine::InvalidFile.
|
360
363
|
def _enforce_io(io)
|
361
|
-
missing_methods = IO_METHODS.
|
362
|
-
io.respond_to?(m) && [a.count, -1].include?(io.method(m).arity)
|
363
|
-
end
|
364
|
-
|
364
|
+
missing_methods = IO_METHODS.select { |m, a| !io.respond_to?(m) }
|
365
365
|
raise InvalidFile.new(io, missing_methods) if missing_methods.any?
|
366
366
|
end
|
367
367
|
|
@@ -469,7 +469,8 @@ class Shrine
|
|
469
469
|
# Otherwise it assumes that it's an IO object and caches it.
|
470
470
|
def assign(value)
|
471
471
|
if value.is_a?(String)
|
472
|
-
|
472
|
+
return if value == "" || value == read || !cache.uploaded?(uploaded_file(value))
|
473
|
+
assign_cached(uploaded_file(value))
|
473
474
|
else
|
474
475
|
uploaded_file = cache!(value, phase: :cache) if value
|
475
476
|
set(uploaded_file)
|
@@ -593,9 +594,8 @@ class Shrine
|
|
593
594
|
private
|
594
595
|
|
595
596
|
# Assigns a cached file (refuses if the file is stored).
|
596
|
-
def assign_cached(
|
597
|
-
cached_file
|
598
|
-
set(cached_file) if cache.uploaded?(cached_file)
|
597
|
+
def assign_cached(cached_file)
|
598
|
+
set(cached_file)
|
599
599
|
end
|
600
600
|
|
601
601
|
# Sets and saves the uploaded file.
|
@@ -682,7 +682,7 @@ class Shrine
|
|
682
682
|
|
683
683
|
# The filesize of the original file.
|
684
684
|
def size
|
685
|
-
|
685
|
+
(@io && @io.size) || (metadata["size"] && Integer(metadata["size"]))
|
686
686
|
end
|
687
687
|
|
688
688
|
# The MIME type of the original file.
|
@@ -1,23 +1,19 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
|
-
# The cached_attachment_data adds
|
4
|
-
#
|
3
|
+
# The cached_attachment_data plugin adds the ability to retain the cached
|
4
|
+
# file across form redisplays, which means the file doesn't have to be
|
5
|
+
# reuploaded in case of validation errors.
|
5
6
|
#
|
6
7
|
# plugin :cached_attachment_data
|
7
8
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
# `:value`:
|
9
|
+
# The plugin adds `#cached_<attachment>_data` to the model, which returns
|
10
|
+
# the cached file as JSON, and should be used to set the value of the
|
11
|
+
# hidden form field:
|
12
12
|
#
|
13
13
|
# <%= form_for @user do |f| %>
|
14
|
-
# <%= f.hidden_field :cached_avatar_data %>
|
14
|
+
# <%= f.hidden_field :avatar, value: @user.cached_avatar_data %>
|
15
15
|
# <%= f.field_field :avatar %>
|
16
16
|
# <% end %>
|
17
|
-
#
|
18
|
-
# Additionally, the hidden field will only be set when the attachment is
|
19
|
-
# cached (as opposed to the default where `user.avatar_data` will return
|
20
|
-
# both cached and stored files). This keeps Rails logs cleaner.
|
21
17
|
module CachedAttachmentData
|
22
18
|
module AttachmentMethods
|
23
19
|
def initialize(*)
|
@@ -29,6 +25,7 @@ class Shrine
|
|
29
25
|
end
|
30
26
|
|
31
27
|
def cached_#{@name}_data=(value)
|
28
|
+
warn "Calling #cached_#{@name}_data= is deprecated and will be removed in Shrine 3. You should use the original field name: `f.hidden_field :#{@name}, value: record.cached_#{@name}_data`."
|
32
29
|
#{@name}_attacher.assign(value)
|
33
30
|
end
|
34
31
|
RUBY
|
@@ -37,7 +34,7 @@ class Shrine
|
|
37
34
|
|
38
35
|
module AttacherMethods
|
39
36
|
def read_cached
|
40
|
-
get.to_json if
|
37
|
+
get.to_json if cached? && attached?
|
41
38
|
end
|
42
39
|
end
|
43
40
|
end
|
@@ -50,12 +50,12 @@ class Shrine
|
|
50
50
|
# [mime-types]: https://github.com/mime-types/ruby-mime-types
|
51
51
|
module DetermineMimeType
|
52
52
|
def self.configure(uploader, opts = {})
|
53
|
-
uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, :file)
|
53
|
+
uploader.opts[:mime_type_analyzer] = opts.fetch(:analyzer, uploader.opts.fetch(:mime_type_analyzer, :file))
|
54
|
+
uploader.opts[:mime_type_magic_header] = opts.fetch(:magic_header, uploader.opts.fetch(:mime_type_magic_header, MAGIC_NUMBER))
|
54
55
|
end
|
55
56
|
|
56
|
-
# How many bytes we
|
57
|
-
|
58
|
-
MAGIC_NUMBER = 1024
|
57
|
+
# How many bytes we need to read in order to determine the MIME type.
|
58
|
+
MAGIC_NUMBER = 256 * 1024
|
59
59
|
|
60
60
|
module InstanceMethods
|
61
61
|
private
|
@@ -83,14 +83,8 @@ class Shrine
|
|
83
83
|
def _extract_mime_type_with_file(io)
|
84
84
|
require "open3"
|
85
85
|
|
86
|
-
|
87
|
-
|
88
|
-
if io.respond_to?(:path)
|
89
|
-
mime_type, * = Open3.capture2(*cmd, io.path)
|
90
|
-
else
|
91
|
-
mime_type, * = Open3.capture2(*cmd, "-", stdin_data: io.read(MAGIC_NUMBER), binmode: true)
|
92
|
-
io.rewind
|
93
|
-
end
|
86
|
+
mime_type, status = Open3.capture2("file", "--mime-type", "--brief", "-",
|
87
|
+
stdin_data: magic_header(io), binmode: true)
|
94
88
|
|
95
89
|
mime_type.strip unless mime_type.empty?
|
96
90
|
end
|
@@ -108,9 +102,7 @@ class Shrine
|
|
108
102
|
require "filemagic"
|
109
103
|
|
110
104
|
filemagic = FileMagic.new(FileMagic::MAGIC_MIME_TYPE)
|
111
|
-
mime_type = filemagic.buffer(io
|
112
|
-
|
113
|
-
io.rewind
|
105
|
+
mime_type = filemagic.buffer(magic_header(io))
|
114
106
|
filemagic.close
|
115
107
|
|
116
108
|
mime_type
|
@@ -128,6 +120,12 @@ class Shrine
|
|
128
120
|
mime_type.to_s if mime_type
|
129
121
|
end
|
130
122
|
end
|
123
|
+
|
124
|
+
def magic_header(io)
|
125
|
+
content = io.read(opts[:mime_type_magic_header])
|
126
|
+
io.rewind
|
127
|
+
content
|
128
|
+
end
|
131
129
|
end
|
132
130
|
end
|
133
131
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "roda"
|
2
|
+
require "down"
|
2
3
|
|
3
4
|
class Shrine
|
4
5
|
module Plugins
|
@@ -106,15 +107,17 @@ class Shrine
|
|
106
107
|
response["Content-Disposition"] = "#{disposition}; filename=#{filename.inspect}"
|
107
108
|
response["Content-Type"] = Rack::Mime.mime_type(extname)
|
108
109
|
|
109
|
-
|
110
|
-
|
111
|
-
response['Content-Length'] = content_length.to_s if content_length
|
110
|
+
io = get_stream_io(id)
|
111
|
+
response["Content-Length"] = io.size.to_s if io.size
|
112
112
|
|
113
113
|
chunks = Enumerator.new do |y|
|
114
|
-
|
115
|
-
|
116
|
-
|
114
|
+
if io.respond_to?(:each_chunk)
|
115
|
+
io.each_chunk { |chunk| y.yield(chunk) }
|
116
|
+
else
|
117
|
+
y.yield io.read(16*1024, buffer ||= "") until io.eof?
|
117
118
|
end
|
119
|
+
io.close
|
120
|
+
io.delete if io.class.name == "Tempfile"
|
118
121
|
end
|
119
122
|
|
120
123
|
r.halt response.finish_with_body(chunks)
|
@@ -131,17 +134,14 @@ class Shrine
|
|
131
134
|
shrine_class.find_storage(storage_key)
|
132
135
|
end
|
133
136
|
|
134
|
-
def
|
137
|
+
def get_stream_io(id)
|
135
138
|
if storage.respond_to?(:stream)
|
136
|
-
|
139
|
+
warn "Storage#stream is deprecated, in Shrine 3 the download_endpoint plugin will use only Storage#open. You should update your storage library."
|
140
|
+
stream = storage.enum_for(:stream, id)
|
141
|
+
chunks = Enumerator.new { |y| y << Array(stream.next)[0] }
|
142
|
+
Down::ChunkedIO.new(size: Array(stream.peek)[1], chunks: chunks)
|
137
143
|
else
|
138
|
-
|
139
|
-
io = storage.open(id)
|
140
|
-
buffer = ""
|
141
|
-
y.yield(io.read(16*1024, buffer), io.size) until io.eof?
|
142
|
-
io.close
|
143
|
-
io.delete if io.class.name == "Tempfile"
|
144
|
-
end
|
144
|
+
storage.open(id)
|
145
145
|
end
|
146
146
|
end
|
147
147
|
|
@@ -1,16 +1,12 @@
|
|
1
1
|
class Shrine
|
2
2
|
module Plugins
|
3
3
|
# The included plugin allows you to hook up to the `.included` hook of the
|
4
|
-
# attachment module, and
|
4
|
+
# attachment module, and call additional methods on the model which includes
|
5
5
|
# it.
|
6
6
|
#
|
7
7
|
# plugin :included do |name|
|
8
|
-
#
|
9
|
-
#
|
10
|
-
# end
|
11
|
-
#
|
12
|
-
# define_method("#{name}_height") do
|
13
|
-
# send(name).height if send(name)
|
8
|
+
# before_save do
|
9
|
+
# # ...
|
14
10
|
# end
|
15
11
|
# end
|
16
12
|
#
|
@@ -82,20 +82,20 @@ class Shrine
|
|
82
82
|
end
|
83
83
|
|
84
84
|
module InstanceMethods
|
85
|
-
def
|
86
|
-
log("process", io, context) { super }
|
87
|
-
end
|
88
|
-
|
89
|
-
def around_store(io, context)
|
85
|
+
def store(io, context = {})
|
90
86
|
log("store", io, context) { super }
|
91
87
|
end
|
92
88
|
|
93
|
-
def
|
89
|
+
def delete(io, context = {})
|
94
90
|
log("delete", io, context) { super }
|
95
91
|
end
|
96
92
|
|
97
93
|
private
|
98
94
|
|
95
|
+
def processed(io, context = {})
|
96
|
+
log("process", io, context) { super }
|
97
|
+
end
|
98
|
+
|
99
99
|
# Collects the data and sends it for logging.
|
100
100
|
def log(action, input, context)
|
101
101
|
result, duration = benchmark { yield }
|
@@ -7,17 +7,30 @@ class Shrine
|
|
7
7
|
#
|
8
8
|
# To add a module to a core class, call the appropriate method:
|
9
9
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
10
|
+
# attachment_module CustomAttachmentMethods
|
11
|
+
# attacher_module CustomAttacherMethods
|
12
|
+
# file_module CustomFileMethods
|
13
13
|
#
|
14
14
|
# Alternatively you can pass in a block (which internally creates a module):
|
15
15
|
#
|
16
|
-
#
|
17
|
-
# def
|
18
|
-
#
|
16
|
+
# attachment_module do
|
17
|
+
# def included(model)
|
18
|
+
# super
|
19
|
+
#
|
20
|
+
# module_eval <<-RUBY, __FILE__, __LINE + 1
|
21
|
+
# def #{@name}_size(version)
|
22
|
+
# if #{@name}.is_a?(Hash)
|
23
|
+
# #{@name}[version].size
|
24
|
+
# else
|
25
|
+
# #{@name}.size if #{@name}
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# RUBY
|
19
29
|
# end
|
20
30
|
# end
|
31
|
+
#
|
32
|
+
# The above defines an additional `#<attachment>_size` method on the
|
33
|
+
# attachment module, which is what is included in your model.
|
21
34
|
module ModuleInclude
|
22
35
|
module ClassMethods
|
23
36
|
def attachment_module(mod = nil, &block)
|
@@ -29,8 +29,11 @@ class Shrine
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def move(io, context)
|
32
|
-
|
33
|
-
|
32
|
+
location = context[:location]
|
33
|
+
metadata = context[:metadata]
|
34
|
+
upload_options = context[:upload_options] || {}
|
35
|
+
|
36
|
+
storage.move(io, location, shrine_metadata: metadata, **upload_options)
|
34
37
|
end
|
35
38
|
|
36
39
|
def movable?(io, context)
|
@@ -15,13 +15,17 @@ class Shrine
|
|
15
15
|
uploader.opts[:parallelize_threads] = opts.fetch(:threads, uploader.opts.fetch(:parallelize_threads, 3))
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.load_dependencies(uploader, opts = {})
|
19
|
+
uploader.plugin :hooks
|
20
|
+
end
|
21
|
+
|
18
22
|
module InstanceMethods
|
19
|
-
def
|
20
|
-
with_pool { |pool| super(io, thread_pool: pool
|
23
|
+
def around_store(io, context)
|
24
|
+
with_pool { |pool| super(io, context.update(thread_pool: pool)) }
|
21
25
|
end
|
22
26
|
|
23
|
-
def
|
24
|
-
with_pool { |pool| super(uploaded_file, thread_pool: pool
|
27
|
+
def around_delete(uploaded_file, context)
|
28
|
+
with_pool { |pool| super(uploaded_file, context.update(thread_pool: pool)) }
|
25
29
|
end
|
26
30
|
|
27
31
|
private
|
@@ -41,31 +45,31 @@ class Shrine
|
|
41
45
|
pool.perform
|
42
46
|
result
|
43
47
|
end
|
48
|
+
end
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
+
class ThreadPool
|
51
|
+
def initialize(size)
|
52
|
+
@size = size
|
53
|
+
@tasks = Queue.new
|
54
|
+
end
|
50
55
|
|
51
|
-
|
52
|
-
|
53
|
-
|
56
|
+
def enqueue(&task)
|
57
|
+
@tasks.enq(task)
|
58
|
+
end
|
54
59
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
60
|
+
def perform
|
61
|
+
threads = @size.times.map { spawn_thread }
|
62
|
+
threads.each(&:join)
|
63
|
+
end
|
59
64
|
|
60
|
-
|
65
|
+
private
|
61
66
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
end
|
67
|
+
def spawn_thread
|
68
|
+
Thread.new do
|
69
|
+
Thread.current.abort_on_exception = true
|
70
|
+
loop do
|
71
|
+
task = @tasks.deq(true) rescue break
|
72
|
+
task.call
|
69
73
|
end
|
70
74
|
end
|
71
75
|
end
|
@@ -10,12 +10,12 @@ class Shrine
|
|
10
10
|
module AttacherMethods
|
11
11
|
private
|
12
12
|
|
13
|
-
def assign_cached(
|
14
|
-
|
15
|
-
|
16
|
-
real_metadata = cache.extract_metadata(
|
17
|
-
|
18
|
-
|
13
|
+
def assign_cached(cached_file)
|
14
|
+
uploaded_file(cached_file) do |file|
|
15
|
+
file.to_io # open
|
16
|
+
real_metadata = cache.extract_metadata(file, context)
|
17
|
+
file.metadata.update(real_metadata)
|
18
|
+
file.close
|
19
19
|
end
|
20
20
|
|
21
21
|
super(cached_file)
|
@@ -9,7 +9,7 @@ class Shrine
|
|
9
9
|
#
|
10
10
|
# class ImageUploader < Shrine
|
11
11
|
# include ImageProcessing::MiniMagick
|
12
|
-
# plugin :versions
|
12
|
+
# plugin :versions
|
13
13
|
#
|
14
14
|
# def process(io, context)
|
15
15
|
# if context[:phase] == :store
|
@@ -77,9 +77,7 @@ class Shrine
|
|
77
77
|
# If you already have some versions processed in the foreground when a
|
78
78
|
# background job is kicked off, you can setup explicit URL fallbacks:
|
79
79
|
#
|
80
|
-
# plugin :versions,
|
81
|
-
# names: [:thumb, :thumb_2x, :large, :large_2x],
|
82
|
-
# fallbacks: {:thumb_2x => :thumb, :large_2x => :large}
|
80
|
+
# plugin :versions, fallbacks: {:thumb_2x => :thumb, :large_2x => :large}
|
83
81
|
#
|
84
82
|
# # ... (background job is kicked off)
|
85
83
|
#
|
@@ -112,14 +110,15 @@ class Shrine
|
|
112
110
|
end
|
113
111
|
|
114
112
|
def self.configure(uploader, opts = {})
|
113
|
+
warn "The versions Shrine plugin doesn't need the :names option anymore, you can safely remove it." if opts.key?(:names)
|
114
|
+
|
115
115
|
uploader.opts[:version_names] = opts.fetch(:names, uploader.opts[:version_names])
|
116
116
|
uploader.opts[:version_fallbacks] = opts.fetch(:fallbacks, uploader.opts.fetch(:version_fallbacks, {}))
|
117
|
-
|
118
|
-
raise Error, "The :names option is required for versions plugin" if uploader.opts[:version_names].nil?
|
119
117
|
end
|
120
118
|
|
121
119
|
module ClassMethods
|
122
120
|
def version_names
|
121
|
+
warn "Shrine.version_names is deprecated and will be removed in Shrine 3."
|
123
122
|
opts[:version_names]
|
124
123
|
end
|
125
124
|
|
@@ -129,14 +128,14 @@ class Shrine
|
|
129
128
|
|
130
129
|
# Checks that the identifier is a registered version.
|
131
130
|
def version?(name)
|
132
|
-
|
131
|
+
warn "Shrine.version? is deprecated and will be removed in Shrine 3."
|
132
|
+
version_names.nil? || version_names.map(&:to_s).include?(name.to_s)
|
133
133
|
end
|
134
134
|
|
135
135
|
# Converts a hash of data into a hash of versions.
|
136
136
|
def uploaded_file(object, &block)
|
137
137
|
if (hash = object).is_a?(Hash) && !hash.key?("storage")
|
138
138
|
hash.inject({}) do |result, (name, data)|
|
139
|
-
next result if !version?(name)
|
140
139
|
result.update(name.to_sym => uploaded_file(data, &block))
|
141
140
|
end
|
142
141
|
else
|
@@ -165,7 +164,6 @@ class Shrine
|
|
165
164
|
if (hash = io).is_a?(Hash)
|
166
165
|
raise Error, ":location is not applicable to versions" if context.key?(:location)
|
167
166
|
hash.inject({}) do |result, (name, version)|
|
168
|
-
raise Error, "unknown version: #{name.inspect}" if !self.class.version?(name)
|
169
167
|
result.update(name => _store(version, version: name, **context))
|
170
168
|
end
|
171
169
|
else
|
@@ -190,7 +188,6 @@ class Shrine
|
|
190
188
|
def url(version = nil, **options)
|
191
189
|
if get.is_a?(Hash)
|
192
190
|
if version
|
193
|
-
raise Error, "unknown version: #{version.inspect}" if !shrine_class.version?(version)
|
194
191
|
if file = get[version]
|
195
192
|
file.url(**options)
|
196
193
|
elsif fallback = shrine_class.version_fallbacks[version]
|
@@ -209,6 +206,14 @@ class Shrine
|
|
209
206
|
end
|
210
207
|
end
|
211
208
|
end
|
209
|
+
|
210
|
+
private
|
211
|
+
|
212
|
+
def assign_cached(value)
|
213
|
+
cached_file = uploaded_file(value)
|
214
|
+
warn "Generating versions in the :cache phase is deprecated and will be forbidden in Shrine 3." if cached_file.is_a?(Hash)
|
215
|
+
super(cached_file)
|
216
|
+
end
|
212
217
|
end
|
213
218
|
end
|
214
219
|
|
@@ -40,7 +40,6 @@ class Shrine
|
|
40
40
|
lint_read(id)
|
41
41
|
lint_exists(id)
|
42
42
|
lint_url(id)
|
43
|
-
lint_stream(id) if storage.respond_to?(:stream)
|
44
43
|
lint_delete(id)
|
45
44
|
|
46
45
|
if storage.respond_to?(:move)
|
@@ -67,6 +66,7 @@ class Shrine
|
|
67
66
|
opened = storage.open(id)
|
68
67
|
error :open, "doesn't return a valid IO object" if !io?(opened)
|
69
68
|
error :open, "returns an empty IO object" if opened.read.empty?
|
69
|
+
opened.close
|
70
70
|
end
|
71
71
|
|
72
72
|
def lint_read(id)
|
@@ -85,20 +85,6 @@ class Shrine
|
|
85
85
|
error :url, "should return either nil or a string" if !(url.nil? || url.is_a?(String))
|
86
86
|
end
|
87
87
|
|
88
|
-
def lint_stream(id)
|
89
|
-
streamed = storage.enum_for(:stream, id).to_a
|
90
|
-
chunks = streamed.map { |(chunk, _)| chunk }
|
91
|
-
content_length = Array(streamed[0])[1]
|
92
|
-
|
93
|
-
error :stream, "doesn't yield any chunks" if chunks.empty?
|
94
|
-
error :stream, "yielded chunks sum up to empty content" if chunks.inject("", :+).empty?
|
95
|
-
|
96
|
-
if Array(streamed.first).size == 2
|
97
|
-
error :stream, "yielded content length isn't a number" if !content_length.is_a?(Integer)
|
98
|
-
error :stream, "yielded chunks don't sum up to given content length" if content_length != chunks.inject("", :+).length
|
99
|
-
end
|
100
|
-
end
|
101
|
-
|
102
88
|
def lint_delete(id)
|
103
89
|
storage.delete(id)
|
104
90
|
error :delete, "file still #exists? after deleting" if storage.exists?(id)
|
data/lib/shrine/storage/s3.rb
CHANGED
@@ -75,7 +75,7 @@ class Shrine
|
|
75
75
|
# [presigning]: http://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#presigned_post-instance_method
|
76
76
|
# [aws-sdk]: https://github.com/aws/aws-sdk-ruby
|
77
77
|
class S3
|
78
|
-
attr_reader :
|
78
|
+
attr_reader :s3, :bucket, :prefix, :host, :upload_options
|
79
79
|
|
80
80
|
# Initializes a storage for uploading to S3.
|
81
81
|
#
|
@@ -133,15 +133,9 @@ class Shrine
|
|
133
133
|
Down.download(url(id), ssl_ca_cert: Aws.config[:ssl_ca_bundle])
|
134
134
|
end
|
135
135
|
|
136
|
-
# Streams the object from S3, yielding downloaded chunks.
|
137
|
-
def stream(id)
|
138
|
-
object = object(id)
|
139
|
-
object.get { |chunk| yield chunk, object.content_length }
|
140
|
-
end
|
141
|
-
|
142
136
|
# Alias for #download.
|
143
137
|
def open(id)
|
144
|
-
|
138
|
+
Down.open(url(id), ssl_ca_cert: Aws.config[:ssl_ca_bundle])
|
145
139
|
end
|
146
140
|
|
147
141
|
# Returns the contents of the file as a String.
|
@@ -218,6 +212,17 @@ class Shrine
|
|
218
212
|
object(id).presigned_post(options)
|
219
213
|
end
|
220
214
|
|
215
|
+
# Catches the deprecated `#stream` method.
|
216
|
+
def method_missing(name, *args)
|
217
|
+
if name == :stream
|
218
|
+
warn "Shrine::Storage::S3#stream is deprecated over calling #each_chunk on S3#open."
|
219
|
+
object = object(*args)
|
220
|
+
object.get { |chunk| yield chunk, object.content_length }
|
221
|
+
else
|
222
|
+
super
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
221
226
|
protected
|
222
227
|
|
223
228
|
# Returns the S3 object.
|
data/lib/shrine/version.rb
CHANGED
data/shrine.gemspec
CHANGED
@@ -25,7 +25,7 @@ column, and tying them to record's lifecycle.
|
|
25
25
|
gem.files = Dir["README.md", "LICENSE.txt", "lib/**/*.rb", "shrine.gemspec", "doc/*.md"]
|
26
26
|
gem.require_path = "lib"
|
27
27
|
|
28
|
-
gem.add_dependency "down", ">= 2.
|
28
|
+
gem.add_dependency "down", ">= 2.3.3"
|
29
29
|
|
30
30
|
gem.add_development_dependency "rake", "~> 11.1"
|
31
31
|
gem.add_development_dependency "minitest", "~> 5.8"
|
@@ -34,13 +34,13 @@ column, and tying them to record's lifecycle.
|
|
34
34
|
gem.add_development_dependency "webmock"
|
35
35
|
gem.add_development_dependency "rack-test_app"
|
36
36
|
gem.add_development_dependency "dotenv"
|
37
|
-
gem.add_development_dependency "shrine-memory", "
|
37
|
+
gem.add_development_dependency "shrine-memory", ">= 0.2.1"
|
38
38
|
|
39
39
|
gem.add_development_dependency "roda"
|
40
40
|
gem.add_development_dependency "mimemagic"
|
41
41
|
gem.add_development_dependency "mime-types"
|
42
42
|
gem.add_development_dependency "fastimage"
|
43
|
-
gem.add_development_dependency "aws-sdk"
|
43
|
+
gem.add_development_dependency "aws-sdk"
|
44
44
|
|
45
45
|
unless RUBY_ENGINE == "jruby" || ENV["CI"]
|
46
46
|
gem.add_development_dependency "ruby-filemagic", "~> 0.7"
|
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: 2.0
|
4
|
+
version: 2.1.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-
|
11
|
+
date: 2016-06-26 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: 2.
|
19
|
+
version: 2.3.3
|
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: 2.
|
26
|
+
version: 2.3.3
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -126,16 +126,16 @@ dependencies:
|
|
126
126
|
name: shrine-memory
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
|
-
- - "
|
129
|
+
- - ">="
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: 0.2.
|
131
|
+
version: 0.2.1
|
132
132
|
type: :development
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
|
-
- - "
|
136
|
+
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version: 0.2.
|
138
|
+
version: 0.2.1
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
140
|
name: roda
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -196,16 +196,16 @@ dependencies:
|
|
196
196
|
name: aws-sdk
|
197
197
|
requirement: !ruby/object:Gem::Requirement
|
198
198
|
requirements:
|
199
|
-
- - "
|
199
|
+
- - ">="
|
200
200
|
- !ruby/object:Gem::Version
|
201
|
-
version:
|
201
|
+
version: '0'
|
202
202
|
type: :development
|
203
203
|
prerelease: false
|
204
204
|
version_requirements: !ruby/object:Gem::Requirement
|
205
205
|
requirements:
|
206
|
-
- - "
|
206
|
+
- - ">="
|
207
207
|
- !ruby/object:Gem::Version
|
208
|
-
version:
|
208
|
+
version: '0'
|
209
209
|
- !ruby/object:Gem::Dependency
|
210
210
|
name: ruby-filemagic
|
211
211
|
requirement: !ruby/object:Gem::Requirement
|