shrine 2.13.0 → 2.14.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/CHANGELOG.md +72 -0
- data/README.md +20 -16
- data/doc/creating_storages.md +0 -21
- data/doc/design.md +1 -0
- data/doc/direct_s3.md +26 -15
- data/doc/metadata.md +67 -22
- data/doc/multiple_files.md +3 -3
- data/doc/processing.md +1 -1
- data/doc/retrieving_uploads.md +184 -0
- data/lib/shrine.rb +268 -900
- data/lib/shrine/attacher.rb +271 -0
- data/lib/shrine/attachment.rb +97 -0
- data/lib/shrine/plugins.rb +29 -0
- data/lib/shrine/plugins/_urlsafe_serialization.rb +182 -0
- data/lib/shrine/plugins/activerecord.rb +16 -14
- data/lib/shrine/plugins/add_metadata.rb +58 -24
- data/lib/shrine/plugins/backgrounding.rb +6 -1
- data/lib/shrine/plugins/cached_attachment_data.rb +9 -9
- data/lib/shrine/plugins/copy.rb +12 -8
- data/lib/shrine/plugins/data_uri.rb +23 -20
- data/lib/shrine/plugins/default_url_options.rb +5 -4
- data/lib/shrine/plugins/determine_mime_type.rb +24 -23
- data/lib/shrine/plugins/download_endpoint.rb +61 -73
- data/lib/shrine/plugins/migration_helpers.rb +17 -17
- data/lib/shrine/plugins/module_include.rb +9 -8
- data/lib/shrine/plugins/presign_endpoint.rb +13 -7
- data/lib/shrine/plugins/processing.rb +1 -1
- data/lib/shrine/plugins/rack_response.rb +128 -36
- data/lib/shrine/plugins/refresh_metadata.rb +20 -5
- data/lib/shrine/plugins/remote_url.rb +8 -8
- data/lib/shrine/plugins/remove_attachment.rb +9 -9
- data/lib/shrine/plugins/sequel.rb +21 -18
- data/lib/shrine/plugins/tempfile.rb +68 -0
- data/lib/shrine/plugins/upload_endpoint.rb +3 -2
- data/lib/shrine/plugins/upload_options.rb +7 -6
- data/lib/shrine/plugins/validation_helpers.rb +2 -1
- data/lib/shrine/storage/file_system.rb +20 -17
- data/lib/shrine/storage/linter.rb +0 -7
- data/lib/shrine/storage/s3.rb +159 -50
- data/lib/shrine/uploaded_file.rb +258 -0
- data/lib/shrine/version.rb +1 -1
- data/shrine.gemspec +7 -19
- metadata +41 -21
data/doc/multiple_files.md
CHANGED
@@ -36,9 +36,9 @@ attributes:
|
|
36
36
|
Album.create(
|
37
37
|
title: "My Album",
|
38
38
|
photos_attributes: [
|
39
|
-
{ image: File.open("image1.jpg",
|
40
|
-
{ image: File.open("image2.jpg",
|
41
|
-
{ image: File.open("image3.jpg",
|
39
|
+
{ image: File.open("image1.jpg", binmode: true) },
|
40
|
+
{ image: File.open("image2.jpg", binmode: true) },
|
41
|
+
{ image: File.open("image3.jpg", binmode: true) },
|
42
42
|
]
|
43
43
|
)
|
44
44
|
```
|
data/doc/processing.md
CHANGED
@@ -0,0 +1,184 @@
|
|
1
|
+
# Retrieving Uploads
|
2
|
+
|
3
|
+
Uploaded file content is typically retrieved from the storage using a
|
4
|
+
`Shrine::UploadedFile` object. This guide explains the various methods of
|
5
|
+
retrieving file content and how do they work.
|
6
|
+
|
7
|
+
For context, `Shrine::UploadedFile` object is what is returned by the
|
8
|
+
attachment reader method on the model instance (e.g. `photo.image`),
|
9
|
+
`Shrine::Attacher#get` if you're using the attacher directly, or
|
10
|
+
`Shrine#upload` if you're using the uploader directly.
|
11
|
+
|
12
|
+
## IO-like interface
|
13
|
+
|
14
|
+
In order for `Shrine::UploadedFile` objects to be uploadable to a storage, they
|
15
|
+
too conform to Shrine's IO-like interface, meaning they implement `#read`,
|
16
|
+
`#rewind`, `#eof?`, and `#close` matching the behaviour of the same methods on
|
17
|
+
Ruby's IO class.
|
18
|
+
|
19
|
+
```rb
|
20
|
+
uploaded_file.eof? # => false
|
21
|
+
uploaded_file.read # => "..."
|
22
|
+
uploaded_file.eof? # => true
|
23
|
+
uploaded_file.rewind # rewinds the underlying IO object
|
24
|
+
uploaded_file.eof? # => false
|
25
|
+
uploaded_file.close # closes the underlying IO object (this should be called when you're done)
|
26
|
+
```
|
27
|
+
|
28
|
+
In reality these methods are simply delegated on the IO object returned by the
|
29
|
+
`Storage#open` method of the underlying Shrine storage. For
|
30
|
+
`Shrine::Storage::FileSystem` this IO object will be a `File` object, while for
|
31
|
+
`Shrine::Storage::S3` (and most other remote storages) it will be a
|
32
|
+
[`Down::ChunkedIO`] object. `Storage#open` is implicitly called when any of
|
33
|
+
these IO methods are called for the first time.
|
34
|
+
|
35
|
+
```rb
|
36
|
+
uploaded_file.read(10) # calls `Storage#open` and assigns result to an instance variable
|
37
|
+
uploaded_file.read(10)
|
38
|
+
# ...
|
39
|
+
```
|
40
|
+
|
41
|
+
You can retrieve the underlying IO object returned by `Storage#open` with
|
42
|
+
`#to_io`:
|
43
|
+
|
44
|
+
```rb
|
45
|
+
uploaded_file.to_io # the underlying IO object returned by `Storage#open`
|
46
|
+
```
|
47
|
+
|
48
|
+
## Opening
|
49
|
+
|
50
|
+
The `Shrine::UploadedFile#open` method can be used to open the uploaded file
|
51
|
+
explicitly:
|
52
|
+
|
53
|
+
```rb
|
54
|
+
uploaded_file.open # calls `Storage#open` and assigns result to an instance variable
|
55
|
+
uploaded_file.read
|
56
|
+
uploaded_file.close
|
57
|
+
```
|
58
|
+
|
59
|
+
This is useful if you want to control where `Storage#open` will be called. It's
|
60
|
+
also useful if you want to pass additional parameters to `Storage#open`, which
|
61
|
+
will depend on the storage. For example, if you're using S3 storage and
|
62
|
+
server-side encryption, you can pass the necessary server-side-encryption
|
63
|
+
parameters to `Shrine::Storage::S3#open`:
|
64
|
+
|
65
|
+
```rb
|
66
|
+
# server-side encryption parameters for S3 storage
|
67
|
+
uploaded_file.open(
|
68
|
+
sse_customer_algorithm: "AES256",
|
69
|
+
sse_customer_key: "secret_key",
|
70
|
+
sse_customer_key_md5: "secret_key_md5",
|
71
|
+
)
|
72
|
+
```
|
73
|
+
|
74
|
+
`Shrine::UploadedFile#open` also accepts a block, which will ensure that the
|
75
|
+
underlying IO object is closed at the end of the block.
|
76
|
+
|
77
|
+
```rb
|
78
|
+
uploaded_file.open do
|
79
|
+
uploaded_file.read(1000)
|
80
|
+
# ...
|
81
|
+
end # underlying IO object is closed
|
82
|
+
```
|
83
|
+
|
84
|
+
`Shrine::UploadedFile#open` will return the result of a given block.
|
85
|
+
block. We can use that to safely retrieve the whole content of a file, without
|
86
|
+
leaving any temporary files lying around.
|
87
|
+
|
88
|
+
```rb
|
89
|
+
content = uploaded_file.open(&:read) # open, read, and close
|
90
|
+
content # uploaded file content
|
91
|
+
```
|
92
|
+
|
93
|
+
## Streaming
|
94
|
+
|
95
|
+
The `Shrine::UploadedFile#stream` method can be used to stream uploaded file
|
96
|
+
content to a writable destination object.
|
97
|
+
|
98
|
+
```rb
|
99
|
+
destination = StringIO.new # from the "stringio" standard library
|
100
|
+
uploaded_file.stream(destination)
|
101
|
+
destination.rewind
|
102
|
+
|
103
|
+
destination # holds the file content
|
104
|
+
```
|
105
|
+
|
106
|
+
The destination object can be any object that responds to `#write` and returns
|
107
|
+
number of bytes written, or a path string.
|
108
|
+
|
109
|
+
`Shrine::UploadedFile#stream` will play nicely with
|
110
|
+
`Shrine::UploadedFile#open`, meaning it will not re-open the uploaded file if
|
111
|
+
it's already opened.
|
112
|
+
|
113
|
+
```rb
|
114
|
+
uploaded_file.open do
|
115
|
+
uploaded_file.stream(destination)
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
Any additional parameters to `Shrine::UploadeFile#stream` are forwarded to
|
120
|
+
`Storage#open`. For example, if you're using S3 storage, you can tell AWS S3 to
|
121
|
+
use HTTP compression for the download request:
|
122
|
+
|
123
|
+
```rb
|
124
|
+
uploaded_file.stream(destination, response_content_encoding: "gzip")
|
125
|
+
```
|
126
|
+
|
127
|
+
If you want to stream uploaded file content to the response body in a Rack
|
128
|
+
application (Rails, Sinatra, Roda etc), see the `rack_response` plugin.
|
129
|
+
|
130
|
+
## Downloading
|
131
|
+
|
132
|
+
The `Shrine::UploadedFile#download` method can be used to download uploaded
|
133
|
+
file content do disk. Internally a temporary file will be created (using the
|
134
|
+
`tempfile` standard library) and passed to `Shrine::UploadedFile#stream`. The
|
135
|
+
return value is an open `Tempfile` object (a delegate of the `File` class).
|
136
|
+
|
137
|
+
```rb
|
138
|
+
tempfile = uploaded_file.download
|
139
|
+
tempfile #=> #<Tempfile:...>
|
140
|
+
|
141
|
+
tempfile.path #=> "/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20181227-2915-m2l6c1"
|
142
|
+
tempfile.read #=> "..."
|
143
|
+
tempfile.close! # close and unlink
|
144
|
+
```
|
145
|
+
|
146
|
+
Like `Shrine::UploadedFile#open`, `Shrine::UploadedFile#download` accepts a
|
147
|
+
block as well. The `Tempfile` object is yielded to the block, and after the
|
148
|
+
block finishes it's automatically closed and deleted.
|
149
|
+
|
150
|
+
```rb
|
151
|
+
uploaded_file.download do |tempfile|
|
152
|
+
tempfile.path #=> "/var/folders/k7/6zx6dx6x7ys3rv3srh0nyfj00000gn/T/20181227-2915-m2l6c1"
|
153
|
+
tempfile.read #=> "..."
|
154
|
+
# ...
|
155
|
+
end # tempfile is closed and deleted
|
156
|
+
```
|
157
|
+
|
158
|
+
Since `Shrine::UploadedFile#download` internally uses
|
159
|
+
`Shrine::UploadedFile#stream`, it plays nicely with `Shrine::UploadedFile#open`
|
160
|
+
as well, meaning it will only open the uploaded file if it's not already
|
161
|
+
opened.
|
162
|
+
|
163
|
+
```rb
|
164
|
+
uploaded_file.open do
|
165
|
+
tempfile = uploaded_file.download
|
166
|
+
# ...
|
167
|
+
end
|
168
|
+
```
|
169
|
+
|
170
|
+
Any options passed to `Shrine::UploadedFile#download` are forwarded to
|
171
|
+
`Storage#open` (unless the uploaded file was already opened, in which case
|
172
|
+
`Storage#open` was already called). For example, if you're using S3 storage,
|
173
|
+
you can tell AWS S3 to use HTTP compression for the download request:
|
174
|
+
|
175
|
+
```rb
|
176
|
+
uploaded_file.download(response_content_encoding: "gzip")
|
177
|
+
```
|
178
|
+
|
179
|
+
Every time `Shrine::UploadedFile#download` is called, it will make a new copy
|
180
|
+
of the uploaded file content. If you plan to retrieve uploaded file content
|
181
|
+
multiple times for the same `Shrine::UploadedFile` instance, consider using the
|
182
|
+
`tempfile` plugin.
|
183
|
+
|
184
|
+
[`Down::ChunkedIO`]: https://github.com/janko-m/down#streaming
|
data/lib/shrine.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "shrine/version"
|
4
|
+
require "shrine/uploaded_file"
|
5
|
+
require "shrine/attacher"
|
6
|
+
require "shrine/attachment"
|
7
|
+
require "shrine/plugins"
|
4
8
|
|
5
9
|
require "securerandom"
|
6
10
|
require "json"
|
7
11
|
require "tempfile"
|
8
12
|
|
13
|
+
# Core class that represents uploader.
|
14
|
+
# Base implementation is defined in InstanceMethods and ClassMethods.
|
9
15
|
class Shrine
|
10
16
|
# A generic exception used by Shrine.
|
11
17
|
class Error < StandardError; end
|
@@ -27,960 +33,322 @@ class Shrine
|
|
27
33
|
size: [],
|
28
34
|
close: [],
|
29
35
|
}
|
30
|
-
deprecate_constant(:IO_METHODS)
|
31
|
-
|
32
|
-
# Core class that represents a file uploaded to a storage. The instance
|
33
|
-
# methods for this class are added by Shrine::Plugins::Base::FileMethods, the
|
34
|
-
# class methods are added by Shrine::Plugins::Base::FileClassMethods.
|
35
|
-
class UploadedFile
|
36
|
-
@shrine_class = ::Shrine
|
37
|
-
end
|
38
|
-
|
39
|
-
# Core class which creates attachment modules for specified attribute names
|
40
|
-
# that are included into model classes. The instance methods for this class
|
41
|
-
# are added by Shrine::Plugins::Base::AttachmentMethods, the class methods
|
42
|
-
# are added by Shrine::Plugins::Base::AttachmentClassMethods.
|
43
|
-
class Attachment < Module
|
44
|
-
@shrine_class = ::Shrine
|
45
|
-
end
|
46
|
-
|
47
|
-
# Core class which handles attaching files to model instances. The instance
|
48
|
-
# methods for this class are added by Shrine::Plugins::Base::AttacherMethods,
|
49
|
-
# the class methods are added by Shrine::Plugins::Base::AttacherClassMethods.
|
50
|
-
class Attacher
|
51
|
-
@shrine_class = ::Shrine
|
52
|
-
end
|
36
|
+
deprecate_constant(:IO_METHODS)
|
53
37
|
|
54
38
|
@opts = {}
|
55
39
|
@storages = {}
|
56
40
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
@plugins = {}
|
61
|
-
|
62
|
-
# If the registered plugin already exists, use it. Otherwise, require it
|
63
|
-
# and return it. This raises a LoadError if such a plugin doesn't exist,
|
64
|
-
# or a Shrine::Error if it exists but it does not register itself
|
65
|
-
# correctly.
|
66
|
-
def self.load_plugin(name)
|
67
|
-
unless plugin = @plugins[name]
|
68
|
-
require "shrine/plugins/#{name}"
|
69
|
-
raise Error, "plugin #{name} did not register itself correctly in Shrine::Plugins" unless plugin = @plugins[name]
|
70
|
-
end
|
71
|
-
plugin
|
72
|
-
end
|
73
|
-
|
74
|
-
# Register the given plugin with Shrine, so that it can be loaded using
|
75
|
-
# `Shrine.plugin` with a symbol. Should be used by plugin files. Example:
|
76
|
-
#
|
77
|
-
# Shrine::Plugins.register_plugin(:plugin_name, PluginModule)
|
78
|
-
def self.register_plugin(name, mod)
|
79
|
-
@plugins[name] = mod
|
80
|
-
end
|
81
|
-
|
82
|
-
# The base plugin for Shrine, implementing all default functionality.
|
83
|
-
# Methods are put into a plugin so future plugins can easily override
|
84
|
-
# them and call `super` to get the default behavior.
|
85
|
-
module Base
|
86
|
-
module ClassMethods
|
87
|
-
# Generic options for this class, plugins store their options here.
|
88
|
-
attr_reader :opts
|
89
|
-
|
90
|
-
# A hash of storages with their symbol identifiers.
|
91
|
-
attr_accessor :storages
|
92
|
-
|
93
|
-
# When inheriting Shrine, copy the instance variables into the subclass,
|
94
|
-
# and create subclasses of core classes.
|
95
|
-
def inherited(subclass)
|
96
|
-
subclass.instance_variable_set(:@opts, opts.dup)
|
97
|
-
subclass.opts.each do |key, value|
|
98
|
-
if value.is_a?(Enumerable) && !value.frozen?
|
99
|
-
subclass.opts[key] = value.dup
|
100
|
-
end
|
101
|
-
end
|
102
|
-
subclass.instance_variable_set(:@storages, storages.dup)
|
103
|
-
|
104
|
-
file_class = Class.new(self::UploadedFile)
|
105
|
-
file_class.shrine_class = subclass
|
106
|
-
subclass.const_set(:UploadedFile, file_class)
|
107
|
-
|
108
|
-
attachment_class = Class.new(self::Attachment)
|
109
|
-
attachment_class.shrine_class = subclass
|
110
|
-
subclass.const_set(:Attachment, attachment_class)
|
111
|
-
|
112
|
-
attacher_class = Class.new(self::Attacher)
|
113
|
-
attacher_class.shrine_class = subclass
|
114
|
-
subclass.const_set(:Attacher, attacher_class)
|
115
|
-
end
|
116
|
-
|
117
|
-
# Load a new plugin into the current class. A plugin can be a module
|
118
|
-
# which is used directly, or a symbol representing a registered plugin
|
119
|
-
# which will be required and then loaded.
|
120
|
-
#
|
121
|
-
# Shrine.plugin MyPlugin
|
122
|
-
# Shrine.plugin :my_plugin
|
123
|
-
def plugin(plugin, *args, &block)
|
124
|
-
plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
|
125
|
-
plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
|
126
|
-
self.include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
|
127
|
-
self.extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
|
128
|
-
self::UploadedFile.include(plugin::FileMethods) if defined?(plugin::FileMethods)
|
129
|
-
self::UploadedFile.extend(plugin::FileClassMethods) if defined?(plugin::FileClassMethods)
|
130
|
-
self::Attachment.include(plugin::AttachmentMethods) if defined?(plugin::AttachmentMethods)
|
131
|
-
self::Attachment.extend(plugin::AttachmentClassMethods) if defined?(plugin::AttachmentClassMethods)
|
132
|
-
self::Attacher.include(plugin::AttacherMethods) if defined?(plugin::AttacherMethods)
|
133
|
-
self::Attacher.extend(plugin::AttacherClassMethods) if defined?(plugin::AttacherClassMethods)
|
134
|
-
plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
|
135
|
-
plugin
|
136
|
-
end
|
137
|
-
|
138
|
-
# Retrieves the storage under the given identifier (can be a Symbol or
|
139
|
-
# a String), and raises Shrine::Error if the storage is missing.
|
140
|
-
def find_storage(name)
|
141
|
-
storages.each { |key, value| return value if key.to_s == name.to_s }
|
142
|
-
raise Error, "storage #{name.inspect} isn't registered on #{self}"
|
143
|
-
end
|
144
|
-
|
145
|
-
# Generates an instance of Shrine::Attachment to be included in the
|
146
|
-
# model class. Example:
|
147
|
-
#
|
148
|
-
# class Photo
|
149
|
-
# include Shrine.attachment(:image) # creates a Shrine::Attachment object
|
150
|
-
# end
|
151
|
-
def attachment(name, *args)
|
152
|
-
self::Attachment.new(name, *args)
|
153
|
-
end
|
154
|
-
alias [] attachment
|
155
|
-
|
156
|
-
# Instantiates a Shrine::UploadedFile from a hash, and optionally
|
157
|
-
# yields the returned object.
|
158
|
-
#
|
159
|
-
# data = {"storage" => "cache", "id" => "abc123.jpg", "metadata" => {}}
|
160
|
-
# Shrine.uploaded_file(data) #=> #<Shrine::UploadedFile>
|
161
|
-
def uploaded_file(object, &block)
|
162
|
-
case object
|
163
|
-
when String
|
164
|
-
uploaded_file(JSON.parse(object), &block)
|
165
|
-
when Hash
|
166
|
-
uploaded_file(self::UploadedFile.new(object), &block)
|
167
|
-
when self::UploadedFile
|
168
|
-
object.tap { |f| yield(f) if block_given? }
|
169
|
-
else
|
170
|
-
raise Error, "cannot convert #{object.inspect} to a #{self}::UploadedFile"
|
171
|
-
end
|
172
|
-
end
|
173
|
-
|
174
|
-
# Temporarily converts an IO-like object into a file. If the input IO
|
175
|
-
# object is already a file, it simply yields it to the block, otherwise
|
176
|
-
# it copies IO content into a Tempfile object which is then yielded and
|
177
|
-
# afterwards deleted.
|
178
|
-
#
|
179
|
-
# Shrine.with_file(io) { |file| file.path }
|
180
|
-
def with_file(io)
|
181
|
-
if io.respond_to?(:path)
|
182
|
-
yield io
|
183
|
-
elsif io.is_a?(UploadedFile)
|
184
|
-
io.download { |tempfile| yield tempfile }
|
185
|
-
else
|
186
|
-
Tempfile.create("shrine-file", binmode: true) do |file|
|
187
|
-
IO.copy_stream(io, file.path)
|
188
|
-
io.rewind
|
189
|
-
|
190
|
-
yield file
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
# Prints a deprecation warning to standard error.
|
196
|
-
def deprecation(message)
|
197
|
-
warn "SHRINE DEPRECATION WARNING: #{message}"
|
198
|
-
end
|
199
|
-
end
|
200
|
-
|
201
|
-
module InstanceMethods
|
202
|
-
# The symbol identifier for the storage used by the uploader.
|
203
|
-
attr_reader :storage_key
|
204
|
-
|
205
|
-
# The storage object used by the uploader.
|
206
|
-
attr_reader :storage
|
207
|
-
|
208
|
-
# Accepts a storage symbol registered in `Shrine.storages`.
|
209
|
-
def initialize(storage_key)
|
210
|
-
@storage = self.class.find_storage(storage_key)
|
211
|
-
@storage_key = storage_key.to_sym
|
212
|
-
end
|
213
|
-
|
214
|
-
# The class-level options hash. This should probably not be modified at
|
215
|
-
# the instance level.
|
216
|
-
def opts
|
217
|
-
self.class.opts
|
218
|
-
end
|
219
|
-
|
220
|
-
# The main method for uploading files. Takes an IO-like object and an
|
221
|
-
# optional context hash (used internally by Shrine::Attacher). It calls
|
222
|
-
# user-defined #process, and afterwards it calls #store. The `io` is
|
223
|
-
# closed after upload.
|
224
|
-
def upload(io, context = {})
|
225
|
-
io = processed(io, context) || io
|
226
|
-
store(io, context)
|
227
|
-
end
|
228
|
-
|
229
|
-
# User is expected to perform processing inside this method, and
|
230
|
-
# return the processed files. Returning nil signals that no proccessing
|
231
|
-
# has been done and that the original file should be used.
|
232
|
-
#
|
233
|
-
# class ImageUploader < Shrine
|
234
|
-
# def process(io, context)
|
235
|
-
# # do processing and return processed files
|
236
|
-
# end
|
237
|
-
# end
|
238
|
-
def process(io, context = {})
|
239
|
-
end
|
240
|
-
|
241
|
-
# Uploads the file and returns an instance of Shrine::UploadedFile. By
|
242
|
-
# default the location of the file is automatically generated by
|
243
|
-
# \#generate_location, but you can pass in `:location` to upload to
|
244
|
-
# a specific location.
|
245
|
-
#
|
246
|
-
# uploader.store(io)
|
247
|
-
def store(io, context = {})
|
248
|
-
_store(io, context)
|
249
|
-
end
|
250
|
-
|
251
|
-
# Returns true if the storage of the given uploaded file matches the
|
252
|
-
# storage of this uploader.
|
253
|
-
def uploaded?(uploaded_file)
|
254
|
-
uploaded_file.storage_key == storage_key.to_s
|
255
|
-
end
|
256
|
-
|
257
|
-
# Deletes the given uploaded file and returns it.
|
258
|
-
def delete(uploaded_file, context = {})
|
259
|
-
_delete(uploaded_file, context)
|
260
|
-
uploaded_file
|
261
|
-
end
|
262
|
-
|
263
|
-
# Generates a unique location for the uploaded file, preserving the
|
264
|
-
# file extension. Can be overriden in uploaders for generating custom
|
265
|
-
# location.
|
266
|
-
def generate_location(io, context = {})
|
267
|
-
extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
|
268
|
-
extension ||= File.extname(extract_filename(io).to_s).downcase
|
269
|
-
basename = generate_uid(io)
|
270
|
-
|
271
|
-
basename + extension
|
272
|
-
end
|
273
|
-
|
274
|
-
# Extracts filename, size and MIME type from the file, which is later
|
275
|
-
# accessible through UploadedFile#metadata.
|
276
|
-
def extract_metadata(io, context = {})
|
277
|
-
{
|
278
|
-
"filename" => extract_filename(io),
|
279
|
-
"size" => extract_size(io),
|
280
|
-
"mime_type" => extract_mime_type(io),
|
281
|
-
}
|
282
|
-
end
|
283
|
-
|
284
|
-
private
|
285
|
-
|
286
|
-
# Attempts to extract the appropriate filename from the IO object.
|
287
|
-
def extract_filename(io)
|
288
|
-
if io.respond_to?(:original_filename)
|
289
|
-
io.original_filename
|
290
|
-
elsif io.respond_to?(:path)
|
291
|
-
File.basename(io.path)
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
# Attempts to extract the MIME type from the IO object.
|
296
|
-
def extract_mime_type(io)
|
297
|
-
if io.respond_to?(:content_type)
|
298
|
-
warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
|
299
|
-
io.content_type
|
300
|
-
end
|
301
|
-
end
|
302
|
-
|
303
|
-
# Extracts the filesize from the IO object.
|
304
|
-
def extract_size(io)
|
305
|
-
io.size if io.respond_to?(:size)
|
306
|
-
end
|
307
|
-
|
308
|
-
# It first asserts that `io` is a valid IO object. It then extracts
|
309
|
-
# metadata and generates the location, before calling the storage to
|
310
|
-
# upload the IO object, passing the extracted metadata and location.
|
311
|
-
# Finally it returns a Shrine::UploadedFile object which represents the
|
312
|
-
# file that was uploaded.
|
313
|
-
def _store(io, context)
|
314
|
-
_enforce_io(io)
|
315
|
-
|
316
|
-
metadata = get_metadata(io, context)
|
317
|
-
metadata = metadata.merge(context[:metadata]) if context[:metadata]
|
318
|
-
|
319
|
-
location = get_location(io, context.merge(metadata: metadata))
|
41
|
+
module ClassMethods
|
42
|
+
# Generic options for this class, plugins store their options here.
|
43
|
+
attr_reader :opts
|
320
44
|
|
321
|
-
|
45
|
+
# A hash of storages with their symbol identifiers.
|
46
|
+
attr_accessor :storages
|
322
47
|
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
# Delegates to #remove.
|
331
|
-
def _delete(uploaded_file, context)
|
332
|
-
remove(uploaded_file, context)
|
333
|
-
end
|
334
|
-
|
335
|
-
# Delegates to #copy.
|
336
|
-
def put(io, context)
|
337
|
-
copy(io, context)
|
338
|
-
end
|
339
|
-
|
340
|
-
# Calls `#upload` on the storage, passing to it the location, metadata
|
341
|
-
# and any upload options. The storage might modify the location or
|
342
|
-
# metadata that were passed in. The uploaded IO is then closed.
|
343
|
-
def copy(io, context)
|
344
|
-
location = context[:location]
|
345
|
-
metadata = context[:metadata]
|
346
|
-
upload_options = context[:upload_options] || {}
|
347
|
-
|
348
|
-
storage.upload(io, location, shrine_metadata: metadata, **upload_options)
|
349
|
-
ensure
|
350
|
-
io.close rescue nil
|
351
|
-
end
|
352
|
-
|
353
|
-
# Delegates to `UploadedFile#delete`.
|
354
|
-
def remove(uploaded_file, context)
|
355
|
-
uploaded_file.delete
|
356
|
-
end
|
357
|
-
|
358
|
-
# Delegates to #process.
|
359
|
-
def processed(io, context)
|
360
|
-
process(io, context)
|
361
|
-
end
|
362
|
-
|
363
|
-
# Retrieves the location for the given IO and context. First it looks
|
364
|
-
# for the `:location` option, otherwise it calls #generate_location.
|
365
|
-
def get_location(io, context)
|
366
|
-
location = context[:location] || generate_location(io, context)
|
367
|
-
location or raise Error, "location generated for #{io.inspect} was nil (context = #{context})"
|
368
|
-
end
|
369
|
-
|
370
|
-
# If the IO object is a Shrine::UploadedFile, it simply copies over its
|
371
|
-
# metadata, otherwise it calls #extract_metadata.
|
372
|
-
def get_metadata(io, context)
|
373
|
-
if io.is_a?(UploadedFile)
|
374
|
-
io.metadata.dup
|
375
|
-
else
|
376
|
-
extract_metadata(io, context)
|
377
|
-
end
|
378
|
-
end
|
379
|
-
|
380
|
-
# Asserts that the object is a valid IO object, specifically that it
|
381
|
-
# responds to `#read`, `#eof?`, `#rewind`, `#size` and `#close`. If the
|
382
|
-
# object doesn't respond to one of these methods, a Shrine::InvalidFile
|
383
|
-
# error is raised.
|
384
|
-
def _enforce_io(io)
|
385
|
-
missing_methods = %i[read eof? rewind close].select { |m| !io.respond_to?(m) }
|
386
|
-
raise InvalidFile.new(io, missing_methods) if missing_methods.any?
|
387
|
-
end
|
388
|
-
|
389
|
-
# Generates a unique identifier that can be used for a location.
|
390
|
-
def generate_uid(io)
|
391
|
-
SecureRandom.hex
|
48
|
+
# When inheriting Shrine, copy the instance variables into the subclass,
|
49
|
+
# and create subclasses of core classes.
|
50
|
+
def inherited(subclass)
|
51
|
+
subclass.instance_variable_set(:@opts, opts.dup)
|
52
|
+
subclass.opts.each do |key, value|
|
53
|
+
if value.is_a?(Enumerable) && !value.frozen?
|
54
|
+
subclass.opts[key] = value.dup
|
392
55
|
end
|
393
56
|
end
|
57
|
+
subclass.instance_variable_set(:@storages, storages.dup)
|
394
58
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
attr_accessor :shrine_class
|
59
|
+
file_class = Class.new(self::UploadedFile)
|
60
|
+
file_class.shrine_class = subclass
|
61
|
+
subclass.const_set(:UploadedFile, file_class)
|
399
62
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
def inspect
|
404
|
-
"#{shrine_class.inspect}::Attachment"
|
405
|
-
end
|
406
|
-
end
|
407
|
-
|
408
|
-
module AttachmentMethods
|
409
|
-
# Instantiates an attachment module for a given attribute name, which
|
410
|
-
# can then be included to a model class. Second argument will be passed
|
411
|
-
# to an attacher module.
|
412
|
-
def initialize(name, **options)
|
413
|
-
@name = name
|
414
|
-
@options = options
|
415
|
-
|
416
|
-
module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
417
|
-
def #{name}_attacher(options = {})
|
418
|
-
@#{name}_attacher = nil if options.any?
|
419
|
-
@#{name}_attacher ||= (
|
420
|
-
attachments = self.class.ancestors.grep(Shrine::Attachment)
|
421
|
-
attachment = attachments.find { |mod| mod.attachment_name == :#{name} }
|
422
|
-
attacher_class = attachment.shrine_class::Attacher
|
423
|
-
options = attachment.options.merge(options)
|
424
|
-
|
425
|
-
attacher_class.new(self, :#{name}, options)
|
426
|
-
)
|
427
|
-
end
|
428
|
-
|
429
|
-
def #{name}=(value)
|
430
|
-
#{name}_attacher.assign(value)
|
431
|
-
end
|
432
|
-
|
433
|
-
def #{name}
|
434
|
-
#{name}_attacher.get
|
435
|
-
end
|
436
|
-
|
437
|
-
def #{name}_url(*args)
|
438
|
-
#{name}_attacher.url(*args)
|
439
|
-
end
|
440
|
-
RUBY
|
441
|
-
end
|
63
|
+
attachment_class = Class.new(self::Attachment)
|
64
|
+
attachment_class.shrine_class = subclass
|
65
|
+
subclass.const_set(:Attachment, attachment_class)
|
442
66
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
67
|
+
attacher_class = Class.new(self::Attacher)
|
68
|
+
attacher_class.shrine_class = subclass
|
69
|
+
subclass.const_set(:Attacher, attacher_class)
|
70
|
+
end
|
447
71
|
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
72
|
+
# Load a new plugin into the current class. A plugin can be a module
|
73
|
+
# which is used directly, or a symbol representing a registered plugin
|
74
|
+
# which will be required and then loaded.
|
75
|
+
#
|
76
|
+
# Shrine.plugin MyPlugin
|
77
|
+
# Shrine.plugin :my_plugin
|
78
|
+
def plugin(plugin, *args, &block)
|
79
|
+
plugin = Plugins.load_plugin(plugin) if plugin.is_a?(Symbol)
|
80
|
+
plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
|
81
|
+
self.include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
|
82
|
+
self.extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
|
83
|
+
self::UploadedFile.include(plugin::FileMethods) if defined?(plugin::FileMethods)
|
84
|
+
self::UploadedFile.extend(plugin::FileClassMethods) if defined?(plugin::FileClassMethods)
|
85
|
+
self::Attachment.include(plugin::AttachmentMethods) if defined?(plugin::AttachmentMethods)
|
86
|
+
self::Attachment.extend(plugin::AttachmentClassMethods) if defined?(plugin::AttachmentClassMethods)
|
87
|
+
self::Attacher.include(plugin::AttacherMethods) if defined?(plugin::AttacherMethods)
|
88
|
+
self::Attacher.extend(plugin::AttacherClassMethods) if defined?(plugin::AttacherClassMethods)
|
89
|
+
plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
|
90
|
+
plugin
|
91
|
+
end
|
452
92
|
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
93
|
+
# Retrieves the storage under the given identifier (can be a Symbol or
|
94
|
+
# a String), and raises Shrine::Error if the storage is missing.
|
95
|
+
def find_storage(name)
|
96
|
+
storages.each { |key, value| return value if key.to_s == name.to_s }
|
97
|
+
raise Error, "storage #{name.inspect} isn't registered on #{self}"
|
98
|
+
end
|
459
99
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
100
|
+
# Generates an instance of Shrine::Attachment to be included in the
|
101
|
+
# model class. Example:
|
102
|
+
#
|
103
|
+
# class Photo
|
104
|
+
# include Shrine.attachment(:image) # creates a Shrine::Attachment object
|
105
|
+
# end
|
106
|
+
def attachment(name, *args)
|
107
|
+
self::Attachment.new(name, *args)
|
108
|
+
end
|
109
|
+
alias [] attachment
|
466
110
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
111
|
+
# Instantiates a Shrine::UploadedFile from a hash, and optionally
|
112
|
+
# yields the returned object.
|
113
|
+
#
|
114
|
+
# data = {"storage" => "cache", "id" => "abc123.jpg", "metadata" => {}}
|
115
|
+
# Shrine.uploaded_file(data) #=> #<Shrine::UploadedFile>
|
116
|
+
def uploaded_file(object, &block)
|
117
|
+
case object
|
118
|
+
when String
|
119
|
+
uploaded_file(JSON.parse(object), &block)
|
120
|
+
when Hash
|
121
|
+
uploaded_file(self::UploadedFile.new(object), &block)
|
122
|
+
when self::UploadedFile
|
123
|
+
object.tap { |f| yield(f) if block_given? }
|
124
|
+
else
|
125
|
+
raise Error, "cannot convert #{object.inspect} to a #{self}::UploadedFile"
|
472
126
|
end
|
127
|
+
end
|
473
128
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
129
|
+
# Temporarily converts an IO-like object into a file. If the input IO
|
130
|
+
# object is already a file, it simply yields it to the block, otherwise
|
131
|
+
# it copies IO content into a Tempfile object which is then yielded and
|
132
|
+
# afterwards deleted.
|
133
|
+
#
|
134
|
+
# Shrine.with_file(io) { |file| file.path }
|
135
|
+
def with_file(io)
|
136
|
+
if io.respond_to?(:path)
|
137
|
+
yield io
|
138
|
+
elsif io.is_a?(UploadedFile)
|
139
|
+
io.download { |tempfile| yield tempfile }
|
140
|
+
else
|
141
|
+
Tempfile.create("shrine-file", binmode: true) do |file|
|
142
|
+
IO.copy_stream(io, file.path)
|
143
|
+
io.rewind
|
485
144
|
|
486
|
-
|
487
|
-
# validation. Example:
|
488
|
-
#
|
489
|
-
# Shrine::Attacher.validate do
|
490
|
-
# if get.size > 5*1024*1024
|
491
|
-
# errors << "is too big (max is 5 MB)"
|
492
|
-
# end
|
493
|
-
# end
|
494
|
-
def validate(&block)
|
495
|
-
define_method(:validate_block, &block)
|
496
|
-
private :validate_block
|
145
|
+
yield file
|
497
146
|
end
|
498
147
|
end
|
148
|
+
end
|
499
149
|
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
attr_reader :store
|
506
|
-
|
507
|
-
# Returns the context that will be sent to the uploader when uploading
|
508
|
-
# and deleting. Can be modified with additional data to be sent to the
|
509
|
-
# uploader.
|
510
|
-
attr_reader :context
|
511
|
-
|
512
|
-
# Returns an array of validation errors created on file assignment in
|
513
|
-
# the `Attacher.validate` block.
|
514
|
-
attr_reader :errors
|
515
|
-
|
516
|
-
# Initializes the necessary attributes.
|
517
|
-
def initialize(record, name, cache: :cache, store: :store)
|
518
|
-
@cache = shrine_class.new(cache)
|
519
|
-
@store = shrine_class.new(store)
|
520
|
-
@context = {record: record, name: name}
|
521
|
-
@errors = []
|
522
|
-
end
|
523
|
-
|
524
|
-
# Returns the model instance associated with the attacher.
|
525
|
-
def record; context[:record]; end
|
526
|
-
|
527
|
-
# Returns the attachment name associated with the attacher.
|
528
|
-
def name; context[:name]; end
|
529
|
-
|
530
|
-
# Receives the attachment value from the form. It can receive an
|
531
|
-
# already cached file as a JSON string, otherwise it assumes that it's
|
532
|
-
# an IO object and uploads it to the temporary storage. The cached file
|
533
|
-
# is then written to the attachment attribute in the JSON format.
|
534
|
-
def assign(value, **options)
|
535
|
-
if value.is_a?(String)
|
536
|
-
return if value == "" || !cache.uploaded?(uploaded_file(value))
|
537
|
-
assign_cached(uploaded_file(value))
|
538
|
-
else
|
539
|
-
uploaded_file = cache!(value, action: :cache, **options) if value
|
540
|
-
set(uploaded_file)
|
541
|
-
end
|
542
|
-
end
|
543
|
-
|
544
|
-
# Accepts a Shrine::UploadedFile object and writes it to the attachment
|
545
|
-
# attribute. It then runs file validations, and records that the
|
546
|
-
# attachment has changed.
|
547
|
-
def set(uploaded_file)
|
548
|
-
file = get
|
549
|
-
@old = file unless uploaded_file == file
|
550
|
-
_set(uploaded_file)
|
551
|
-
validate
|
552
|
-
end
|
553
|
-
|
554
|
-
# Runs the validations defined by `Attacher.validate`.
|
555
|
-
def validate
|
556
|
-
errors.clear
|
557
|
-
validate_block if get
|
558
|
-
end
|
559
|
-
|
560
|
-
# Returns true if a new file has been attached.
|
561
|
-
def changed?
|
562
|
-
instance_variable_defined?(:@old)
|
563
|
-
end
|
564
|
-
alias attached? changed?
|
565
|
-
|
566
|
-
# Plugins can override this if they want something to be done before
|
567
|
-
# save.
|
568
|
-
def save
|
569
|
-
end
|
570
|
-
|
571
|
-
# Deletes the old file and promotes the new one. Typically this should
|
572
|
-
# be called after saving the model instance.
|
573
|
-
def finalize
|
574
|
-
return if !instance_variable_defined?(:@old)
|
575
|
-
replace
|
576
|
-
remove_instance_variable(:@old)
|
577
|
-
_promote(action: :store) if cached?
|
578
|
-
end
|
579
|
-
|
580
|
-
# Delegates to #promote, overriden for backgrounding.
|
581
|
-
def _promote(uploaded_file = get, **options)
|
582
|
-
promote(uploaded_file, **options)
|
583
|
-
end
|
584
|
-
|
585
|
-
# Uploads the cached file to store, and writes the stored file to the
|
586
|
-
# attachment attribute.
|
587
|
-
def promote(uploaded_file = get, **options)
|
588
|
-
stored_file = store!(uploaded_file, **options)
|
589
|
-
result = swap(stored_file) or _delete(stored_file, action: :abort)
|
590
|
-
result
|
591
|
-
end
|
592
|
-
|
593
|
-
# Calls #update, overriden in ORM plugins, and returns true if the
|
594
|
-
# attachment was successfully updated.
|
595
|
-
def swap(uploaded_file)
|
596
|
-
update(uploaded_file)
|
597
|
-
uploaded_file if uploaded_file == get
|
598
|
-
end
|
599
|
-
|
600
|
-
# Deletes the previous attachment that was replaced, typically called
|
601
|
-
# after the model instance is saved with the new attachment.
|
602
|
-
def replace
|
603
|
-
_delete(@old, action: :replace) if @old && !cache.uploaded?(@old)
|
604
|
-
end
|
605
|
-
|
606
|
-
# Deletes the current attachment, typically called after destroying the
|
607
|
-
# record.
|
608
|
-
def destroy
|
609
|
-
file = get
|
610
|
-
_delete(file, action: :destroy) if file && !cache.uploaded?(file)
|
611
|
-
end
|
612
|
-
|
613
|
-
# Delegates to #delete!, overriden for backgrounding.
|
614
|
-
def _delete(uploaded_file, **options)
|
615
|
-
delete!(uploaded_file, **options)
|
616
|
-
end
|
617
|
-
|
618
|
-
# Returns the URL to the attached file if it's present. It forwards any
|
619
|
-
# given URL options to the storage.
|
620
|
-
def url(**options)
|
621
|
-
get.url(**options) if read
|
622
|
-
end
|
623
|
-
|
624
|
-
# Returns true if attachment is present and cached.
|
625
|
-
def cached?
|
626
|
-
file = get
|
627
|
-
file && cache.uploaded?(file)
|
628
|
-
end
|
629
|
-
|
630
|
-
# Returns true if attachment is present and stored.
|
631
|
-
def stored?
|
632
|
-
file = get
|
633
|
-
file && store.uploaded?(file)
|
634
|
-
end
|
635
|
-
|
636
|
-
# Returns a Shrine::UploadedFile instantiated from the data written to
|
637
|
-
# the attachment attribute.
|
638
|
-
def get
|
639
|
-
uploaded_file(read) if read
|
640
|
-
end
|
641
|
-
|
642
|
-
# Reads from the `<attachment>_data` attribute on the model instance.
|
643
|
-
# It returns nil if the value is blank.
|
644
|
-
def read
|
645
|
-
value = record.send(data_attribute)
|
646
|
-
convert_after_read(value) unless value.nil? || value.empty?
|
647
|
-
end
|
648
|
-
|
649
|
-
# Uploads the file using the #cache uploader, passing the #context.
|
650
|
-
def cache!(io, **options)
|
651
|
-
Shrine.deprecation("Sending :phase to Attacher#cache! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
652
|
-
cache.upload(io, context.merge(_equalize_phase_and_action(options)))
|
653
|
-
end
|
150
|
+
# Prints a deprecation warning to standard error.
|
151
|
+
def deprecation(message)
|
152
|
+
warn "SHRINE DEPRECATION WARNING: #{message}"
|
153
|
+
end
|
154
|
+
end
|
654
155
|
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
store.upload(io, context.merge(_equalize_phase_and_action(options)))
|
659
|
-
end
|
156
|
+
module InstanceMethods
|
157
|
+
# The symbol identifier for the storage used by the uploader.
|
158
|
+
attr_reader :storage_key
|
660
159
|
|
661
|
-
|
662
|
-
|
663
|
-
Shrine.deprecation("Sending :phase to Attacher#delete! is deprecated and will not be supported in Shrine 3. Use :action instead.") if options[:phase]
|
664
|
-
store.delete(uploaded_file, context.merge(_equalize_phase_and_action(options)))
|
665
|
-
end
|
160
|
+
# The storage object used by the uploader.
|
161
|
+
attr_reader :storage
|
666
162
|
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
# The name of the attribute on the model instance that is used to store
|
674
|
-
# the attachment data. Defaults to `<attachment>_data`.
|
675
|
-
def data_attribute
|
676
|
-
:"#{name}_data"
|
677
|
-
end
|
163
|
+
# Accepts a storage symbol registered in `Shrine.storages`.
|
164
|
+
def initialize(storage_key)
|
165
|
+
@storage = self.class.find_storage(storage_key)
|
166
|
+
@storage_key = storage_key.to_sym
|
167
|
+
end
|
678
168
|
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
169
|
+
# The class-level options hash. This should probably not be modified at
|
170
|
+
# the instance level.
|
171
|
+
def opts
|
172
|
+
self.class.opts
|
173
|
+
end
|
684
174
|
|
685
|
-
|
175
|
+
# The main method for uploading files. Takes an IO-like object and an
|
176
|
+
# optional context hash (used internally by Shrine::Attacher). It calls
|
177
|
+
# user-defined #process, and afterwards it calls #store. The `io` is
|
178
|
+
# closed after upload.
|
179
|
+
def upload(io, context = {})
|
180
|
+
io = processed(io, context) || io
|
181
|
+
store(io, context)
|
182
|
+
end
|
686
183
|
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
184
|
+
# User is expected to perform processing inside this method, and
|
185
|
+
# return the processed files. Returning nil signals that no proccessing
|
186
|
+
# has been done and that the original file should be used.
|
187
|
+
#
|
188
|
+
# class ImageUploader < Shrine
|
189
|
+
# def process(io, context)
|
190
|
+
# # do processing and return processed files
|
191
|
+
# end
|
192
|
+
# end
|
193
|
+
def process(io, context = {})
|
194
|
+
end
|
691
195
|
|
692
|
-
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
196
|
+
# Uploads the file and returns an instance of Shrine::UploadedFile. By
|
197
|
+
# default the location of the file is automatically generated by
|
198
|
+
# \#generate_location, but you can pass in `:location` to upload to
|
199
|
+
# a specific location.
|
200
|
+
#
|
201
|
+
# uploader.store(io)
|
202
|
+
def store(io, context = {})
|
203
|
+
_store(io, context)
|
204
|
+
end
|
697
205
|
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
206
|
+
# Returns true if the storage of the given uploaded file matches the
|
207
|
+
# storage of this uploader.
|
208
|
+
def uploaded?(uploaded_file)
|
209
|
+
uploaded_file.storage_key == storage_key.to_s
|
210
|
+
end
|
702
211
|
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
end
|
212
|
+
# Deletes the given uploaded file and returns it.
|
213
|
+
def delete(uploaded_file, context = {})
|
214
|
+
_delete(uploaded_file, context)
|
215
|
+
uploaded_file
|
216
|
+
end
|
709
217
|
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
218
|
+
# Generates a unique location for the uploaded file, preserving the
|
219
|
+
# file extension. Can be overriden in uploaders for generating custom
|
220
|
+
# location.
|
221
|
+
def generate_location(io, context = {})
|
222
|
+
extension = ".#{io.extension}" if io.is_a?(UploadedFile) && io.extension
|
223
|
+
extension ||= File.extname(extract_filename(io).to_s).downcase
|
224
|
+
basename = generate_uid(io)
|
714
225
|
|
715
|
-
|
716
|
-
|
717
|
-
uploaded_file.data
|
718
|
-
end
|
226
|
+
basename + extension
|
227
|
+
end
|
719
228
|
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
229
|
+
# Extracts filename, size and MIME type from the file, which is later
|
230
|
+
# accessible through UploadedFile#metadata.
|
231
|
+
def extract_metadata(io, context = {})
|
232
|
+
{
|
233
|
+
"filename" => extract_filename(io),
|
234
|
+
"size" => extract_size(io),
|
235
|
+
"mime_type" => extract_mime_type(io),
|
236
|
+
}
|
237
|
+
end
|
724
238
|
|
725
|
-
|
726
|
-
def convert_after_read(value)
|
727
|
-
value
|
728
|
-
end
|
239
|
+
private
|
729
240
|
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
241
|
+
# Attempts to extract the appropriate filename from the IO object.
|
242
|
+
def extract_filename(io)
|
243
|
+
if io.respond_to?(:original_filename)
|
244
|
+
io.original_filename
|
245
|
+
elsif io.respond_to?(:path)
|
246
|
+
File.basename(io.path)
|
736
247
|
end
|
248
|
+
end
|
737
249
|
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
#
|
743
|
-
# and then assigned to a constant of the Shrine subclass, make inspect
|
744
|
-
# reflect the likely name for the class.
|
745
|
-
def inspect
|
746
|
-
"#{shrine_class.inspect}::UploadedFile"
|
747
|
-
end
|
250
|
+
# Attempts to extract the MIME type from the IO object.
|
251
|
+
def extract_mime_type(io)
|
252
|
+
if io.respond_to?(:content_type) && io.content_type
|
253
|
+
warn "The \"mime_type\" Shrine metadata field will be set from the \"Content-Type\" request header, which might not hold the actual MIME type of the file. It is recommended to load the determine_mime_type plugin which determines MIME type from file content."
|
254
|
+
io.content_type.split(";").first # exclude media type parameters
|
748
255
|
end
|
256
|
+
end
|
749
257
|
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
# Initializes the uploaded file with the given data hash.
|
755
|
-
def initialize(data)
|
756
|
-
raise Error, "#{data.inspect} isn't valid uploaded file data" unless data["id"] && data["storage"]
|
757
|
-
|
758
|
-
@data = data
|
759
|
-
@data["metadata"] ||= {}
|
760
|
-
storage # ensure storage is registered
|
761
|
-
end
|
762
|
-
|
763
|
-
# The location where the file was uploaded to the storage.
|
764
|
-
def id
|
765
|
-
@data.fetch("id")
|
766
|
-
end
|
767
|
-
|
768
|
-
# The string identifier of the storage the file is uploaded to.
|
769
|
-
def storage_key
|
770
|
-
@data.fetch("storage")
|
771
|
-
end
|
772
|
-
|
773
|
-
# A hash of file metadata that was extracted during upload.
|
774
|
-
def metadata
|
775
|
-
@data.fetch("metadata")
|
776
|
-
end
|
777
|
-
|
778
|
-
# The filename that was extracted from the uploaded file.
|
779
|
-
def original_filename
|
780
|
-
metadata["filename"]
|
781
|
-
end
|
782
|
-
|
783
|
-
# The extension derived from #id if present, otherwise it's derived
|
784
|
-
# from #original_filename.
|
785
|
-
def extension
|
786
|
-
result = File.extname(id)[1..-1] || File.extname(original_filename.to_s)[1..-1]
|
787
|
-
result.downcase if result
|
788
|
-
end
|
789
|
-
|
790
|
-
# The filesize of the uploaded file.
|
791
|
-
def size
|
792
|
-
(@io && @io.size) || (metadata["size"] && Integer(metadata["size"]))
|
793
|
-
end
|
794
|
-
|
795
|
-
# The MIME type of the uploaded file.
|
796
|
-
def mime_type
|
797
|
-
metadata["mime_type"]
|
798
|
-
end
|
799
|
-
alias content_type mime_type
|
800
|
-
|
801
|
-
# Calls `#open` on the storage to open the uploaded file for reading.
|
802
|
-
# Most storages will return a lazy IO object which dynamically
|
803
|
-
# retrieves file content from the storage as the object is being read.
|
804
|
-
#
|
805
|
-
# If a block is given, the opened IO object is yielded to the block,
|
806
|
-
# and at the end of the block it's automatically closed. In this case
|
807
|
-
# the return value of the method is the block return value.
|
808
|
-
#
|
809
|
-
# If no block is given, the opened IO object is returned.
|
810
|
-
#
|
811
|
-
# uploaded_file.open #=> IO object returned by the storage
|
812
|
-
# uploaded_file.read #=> "..."
|
813
|
-
# uploaded_file.close
|
814
|
-
#
|
815
|
-
# # or
|
816
|
-
#
|
817
|
-
# uploaded_file.open { |io| io.read } # the IO is automatically closed
|
818
|
-
def open(*args)
|
819
|
-
@io.close if @io && !(@io.respond_to?(:closed?) && @io.closed?)
|
820
|
-
@io = storage.open(id, *args)
|
821
|
-
|
822
|
-
return @io unless block_given?
|
823
|
-
|
824
|
-
begin
|
825
|
-
yield @io
|
826
|
-
ensure
|
827
|
-
@io.close
|
828
|
-
@io = nil
|
829
|
-
end
|
830
|
-
end
|
831
|
-
|
832
|
-
# Calls `#download` on the storage if the storage implements it,
|
833
|
-
# otherwise streams content into a newly created Tempfile.
|
834
|
-
#
|
835
|
-
# If a block is given, the opened Tempfile object is yielded to the
|
836
|
-
# block, and at the end of the block it's automatically closed and
|
837
|
-
# deleted. In this case the return value of the method is the block
|
838
|
-
# return value.
|
839
|
-
#
|
840
|
-
# If no block is given, the opened Tempfile is returned.
|
841
|
-
#
|
842
|
-
# uploaded_file.download
|
843
|
-
# #=> #<File:/var/folders/.../20180302-33119-1h1vjbq.jpg>
|
844
|
-
#
|
845
|
-
# # or
|
846
|
-
#
|
847
|
-
# uploaded_file.download { |tempfile| tempfile.read } # tempfile is deleted
|
848
|
-
def download(*args)
|
849
|
-
if storage.respond_to?(:download)
|
850
|
-
tempfile = storage.download(id, *args)
|
851
|
-
else
|
852
|
-
tempfile = Tempfile.new(["shrine", ".#{extension}"], binmode: true)
|
853
|
-
stream(tempfile, *args)
|
854
|
-
tempfile.open
|
855
|
-
end
|
856
|
-
|
857
|
-
block_given? ? yield(tempfile) : tempfile
|
858
|
-
ensure
|
859
|
-
tempfile.close! if ($! || block_given?) && tempfile
|
860
|
-
end
|
861
|
-
|
862
|
-
# Streams uploaded file content into the specified destination. The
|
863
|
-
# destination object is given directly to `IO.copy_stream`, so it can
|
864
|
-
# be either a path on disk or an object that responds to `#write`.
|
865
|
-
#
|
866
|
-
# If the uploaded file is already opened, it will be simply rewinded
|
867
|
-
# after streaming finishes. Otherwise the uploaded file is opened and
|
868
|
-
# then closed after streaming.
|
869
|
-
#
|
870
|
-
# uploaded_file.stream(StringIO.new)
|
871
|
-
# # or
|
872
|
-
# uploaded_file.stream("/path/to/destination")
|
873
|
-
def stream(destination, *args)
|
874
|
-
if @io
|
875
|
-
IO.copy_stream(io, destination)
|
876
|
-
io.rewind
|
877
|
-
else
|
878
|
-
open(*args) { |io| IO.copy_stream(io, destination) }
|
879
|
-
end
|
880
|
-
end
|
881
|
-
|
882
|
-
# Part of complying to the IO interface. It delegates to the internally
|
883
|
-
# opened IO object.
|
884
|
-
def read(*args)
|
885
|
-
io.read(*args)
|
886
|
-
end
|
887
|
-
|
888
|
-
# Part of complying to the IO interface. It delegates to the internally
|
889
|
-
# opened IO object.
|
890
|
-
def eof?
|
891
|
-
io.eof?
|
892
|
-
end
|
893
|
-
|
894
|
-
# Part of complying to the IO interface. It delegates to the internally
|
895
|
-
# opened IO object.
|
896
|
-
def close
|
897
|
-
io.close if @io
|
898
|
-
end
|
899
|
-
|
900
|
-
# Part of complying to the IO interface. It delegates to the internally
|
901
|
-
# opened IO object.
|
902
|
-
def rewind
|
903
|
-
io.rewind
|
904
|
-
end
|
258
|
+
# Extracts the filesize from the IO object.
|
259
|
+
def extract_size(io)
|
260
|
+
io.size if io.respond_to?(:size)
|
261
|
+
end
|
905
262
|
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
263
|
+
# It first asserts that `io` is a valid IO object. It then extracts
|
264
|
+
# metadata and generates the location, before calling the storage to
|
265
|
+
# upload the IO object, passing the extracted metadata and location.
|
266
|
+
# Finally it returns a Shrine::UploadedFile object which represents the
|
267
|
+
# file that was uploaded.
|
268
|
+
def _store(io, context)
|
269
|
+
_enforce_io(io)
|
910
270
|
|
911
|
-
|
912
|
-
|
913
|
-
def exists?
|
914
|
-
storage.exists?(id)
|
915
|
-
end
|
271
|
+
metadata = get_metadata(io, context)
|
272
|
+
metadata = metadata.merge(context[:metadata]) if context[:metadata]
|
916
273
|
|
917
|
-
|
918
|
-
def replace(io, context = {})
|
919
|
-
uploader.upload(io, context.merge(location: id))
|
920
|
-
end
|
274
|
+
location = get_location(io, context.merge(metadata: metadata))
|
921
275
|
|
922
|
-
|
923
|
-
# storage.
|
924
|
-
def delete
|
925
|
-
storage.delete(id)
|
926
|
-
end
|
276
|
+
put(io, context.merge(location: location, metadata: metadata))
|
927
277
|
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
278
|
+
self.class.uploaded_file(
|
279
|
+
"id" => location,
|
280
|
+
"storage" => storage_key.to_s,
|
281
|
+
"metadata" => metadata,
|
282
|
+
)
|
283
|
+
end
|
932
284
|
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
end
|
285
|
+
# Delegates to #remove.
|
286
|
+
def _delete(uploaded_file, context)
|
287
|
+
remove(uploaded_file, context)
|
288
|
+
end
|
938
289
|
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
290
|
+
# Delegates to #copy.
|
291
|
+
def put(io, context)
|
292
|
+
copy(io, context)
|
293
|
+
end
|
943
294
|
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
295
|
+
# Calls `#upload` on the storage, passing to it the location, metadata
|
296
|
+
# and any upload options. The storage might modify the location or
|
297
|
+
# metadata that were passed in. The uploaded IO is then closed.
|
298
|
+
def copy(io, context)
|
299
|
+
location = context[:location]
|
300
|
+
metadata = context[:metadata]
|
301
|
+
upload_options = context[:upload_options] || {}
|
302
|
+
|
303
|
+
storage.upload(io, location, shrine_metadata: metadata, **upload_options)
|
304
|
+
ensure
|
305
|
+
io.close rescue nil
|
306
|
+
end
|
952
307
|
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
308
|
+
# Delegates to `UploadedFile#delete`.
|
309
|
+
def remove(uploaded_file, context)
|
310
|
+
uploaded_file.delete
|
311
|
+
end
|
957
312
|
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
313
|
+
# Delegates to #process.
|
314
|
+
def processed(io, context)
|
315
|
+
process(io, context)
|
316
|
+
end
|
962
317
|
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
318
|
+
# Retrieves the location for the given IO and context. First it looks
|
319
|
+
# for the `:location` option, otherwise it calls #generate_location.
|
320
|
+
def get_location(io, context)
|
321
|
+
location = context[:location] || generate_location(io, context)
|
322
|
+
location or raise Error, "location generated for #{io.inspect} was nil (context = #{context})"
|
323
|
+
end
|
967
324
|
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
325
|
+
# If the IO object is a Shrine::UploadedFile, it simply copies over its
|
326
|
+
# metadata, otherwise it calls #extract_metadata.
|
327
|
+
def get_metadata(io, context)
|
328
|
+
if io.is_a?(UploadedFile)
|
329
|
+
io.metadata.dup
|
330
|
+
else
|
331
|
+
extract_metadata(io, context)
|
332
|
+
end
|
333
|
+
end
|
972
334
|
|
973
|
-
|
335
|
+
# Asserts that the object is a valid IO object, specifically that it
|
336
|
+
# responds to `#read`, `#eof?`, `#rewind`, `#size` and `#close`. If the
|
337
|
+
# object doesn't respond to one of these methods, a Shrine::InvalidFile
|
338
|
+
# error is raised.
|
339
|
+
def _enforce_io(io)
|
340
|
+
missing_methods = %i[read eof? rewind close].select { |m| !io.respond_to?(m) }
|
341
|
+
raise InvalidFile.new(io, missing_methods) if missing_methods.any?
|
342
|
+
end
|
974
343
|
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
@io || open
|
979
|
-
end
|
980
|
-
end
|
344
|
+
# Generates a unique identifier that can be used for a location.
|
345
|
+
def generate_uid(io)
|
346
|
+
SecureRandom.hex
|
981
347
|
end
|
982
348
|
end
|
349
|
+
end
|
983
350
|
|
984
|
-
|
985
|
-
|
351
|
+
[Shrine, Shrine::UploadedFile, Shrine::Attacher, Shrine::Attachment].each do |core_class|
|
352
|
+
core_class.include core_class.const_get(:InstanceMethods)
|
353
|
+
core_class.extend core_class.const_get(:ClassMethods)
|
986
354
|
end
|