shrine 2.3.1 → 2.4.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 +212 -89
- data/doc/attacher.md +227 -0
- data/doc/design.md +5 -19
- data/doc/paperclip.md +2 -1
- data/doc/testing.md +266 -0
- data/lib/shrine.rb +220 -168
- data/lib/shrine/plugins/activerecord.rb +23 -14
- data/lib/shrine/plugins/backgrounding.rb +3 -3
- data/lib/shrine/plugins/cached_attachment_data.rb +6 -5
- data/lib/shrine/plugins/copy.rb +10 -9
- data/lib/shrine/plugins/data_uri.rb +5 -0
- data/lib/shrine/plugins/default_url_options.rb +17 -4
- data/lib/shrine/plugins/direct_upload.rb +6 -11
- data/lib/shrine/plugins/download_endpoint.rb +8 -24
- data/lib/shrine/plugins/multi_delete.rb +1 -1
- data/lib/shrine/plugins/processing.rb +8 -0
- data/lib/shrine/plugins/remote_url.rb +5 -0
- data/lib/shrine/plugins/remove_attachment.rb +3 -10
- data/lib/shrine/plugins/sequel.rb +28 -25
- data/lib/shrine/plugins/versions.rb +12 -1
- data/lib/shrine/storage/file_system.rb +16 -14
- data/lib/shrine/storage/linter.rb +6 -0
- data/lib/shrine/storage/s3.rb +51 -25
- data/lib/shrine/version.rb +2 -2
- data/shrine.gemspec +2 -2
- metadata +8 -7
- data/lib/shrine/plugins/concatenation.rb +0 -73
data/doc/attacher.md
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
# Using Attacher
|
2
|
+
|
3
|
+
The most convenient way to use Shrine is through the model, using the interface
|
4
|
+
provided by Shrine's attachment module. This way you can interact with the
|
5
|
+
attachment just like with any other column attribute, and adding attachment
|
6
|
+
fields to the form just works.
|
7
|
+
|
8
|
+
```rb
|
9
|
+
class Photo < Sequel::Model
|
10
|
+
include ImageUploader[:image]
|
11
|
+
end
|
12
|
+
```
|
13
|
+
|
14
|
+
However, you don't want to add additional methods on the model and prefer
|
15
|
+
explicitness, or you need more control, you can achieve the same behaviour
|
16
|
+
using the `Shrine::Attacher` object, which is what the attachment interface
|
17
|
+
uses under the hood.
|
18
|
+
|
19
|
+
```rb
|
20
|
+
attacher = ImageUploader::Attacher.new(photo, :image) # equivalent to `photo.image_attacher`
|
21
|
+
attacher.assign(file) # equivalent to `photo.image = file`
|
22
|
+
attacher.get # equivalent to `photo.image`
|
23
|
+
```
|
24
|
+
|
25
|
+
## Attributes
|
26
|
+
|
27
|
+
The attacher object exposes the objects it uses:
|
28
|
+
|
29
|
+
```rb
|
30
|
+
attacher.record #=> #<Photo>
|
31
|
+
attacher.name #=> :image
|
32
|
+
attacher.cache #=> #<ImageUploader @storage_key=:cache>
|
33
|
+
attacher.store #=> #<ImageUploader @storage_key=:store>
|
34
|
+
```
|
35
|
+
|
36
|
+
The attacher will automatically use `:cache` and `:store` storages, but you can
|
37
|
+
also tell it to use different temporary and permanent storage:
|
38
|
+
|
39
|
+
```rb
|
40
|
+
ImageUploader::Attacher.new(photo, :image, cache: :other_cache, store: :other_store)
|
41
|
+
```
|
42
|
+
|
43
|
+
The attacher will use the `<attachment>_data` attribute for storing information
|
44
|
+
about the attachment.
|
45
|
+
|
46
|
+
```rb
|
47
|
+
attacher.data_attribute #=> :image_data
|
48
|
+
```
|
49
|
+
|
50
|
+
## Assignment
|
51
|
+
|
52
|
+
The `#assign` method accepts either an IO object to be cached, or an already
|
53
|
+
cached file in form of a JSON string, and assigns the cached result to record's
|
54
|
+
`<attachment>_data` attribute.
|
55
|
+
|
56
|
+
```rb
|
57
|
+
# uploads the `io` object to temporary storage, and writes to the data column
|
58
|
+
attacher.assign(io)
|
59
|
+
|
60
|
+
# writes the given cached file to the data column
|
61
|
+
attacher.assign '{
|
62
|
+
"storage": "cache",
|
63
|
+
"id": "9260ea09d8effd.jpg",
|
64
|
+
"metadata": { ... }
|
65
|
+
}'
|
66
|
+
```
|
67
|
+
|
68
|
+
For security reasons `#assign` doesn't accept files uploaded to permanent
|
69
|
+
storage, but you can also use `#set` to attach any `Shrine::UploadedFile`
|
70
|
+
object.
|
71
|
+
|
72
|
+
```rb
|
73
|
+
uploaded_file #=> #<Shrine::UploadedFile>
|
74
|
+
attacher.set(uploaded_file)
|
75
|
+
```
|
76
|
+
|
77
|
+
## Retrieval
|
78
|
+
|
79
|
+
The `#get` method reads record's `<attachment>_data` attribute, and constructs
|
80
|
+
a `Shrine::UploadedFile` object from it.
|
81
|
+
|
82
|
+
```rb
|
83
|
+
attacher.get #=> #<Shrine::UploadedFile>
|
84
|
+
```
|
85
|
+
|
86
|
+
The `#read` method will just return the value of the underlying
|
87
|
+
`<attachment>_data` attribute.
|
88
|
+
|
89
|
+
```rb
|
90
|
+
attacher.read #=> '{"storage":"cache","id":"dsg024lfs.jpg",...}'
|
91
|
+
```
|
92
|
+
|
93
|
+
In general you can use `#uploaded_file` to contruct a `Shrine::UploadedFile`
|
94
|
+
from a JSON string.
|
95
|
+
|
96
|
+
```rb
|
97
|
+
attacher.uploaded_file('{"storage":"cache","id":"dsg024lfs.jpg",...}') #=> #<Shrine::UploadedFile>
|
98
|
+
```
|
99
|
+
|
100
|
+
## URL
|
101
|
+
|
102
|
+
The `#url` method returns the URL to the attached file, and returns `nil` if
|
103
|
+
no file is attached.
|
104
|
+
|
105
|
+
```rb
|
106
|
+
attacher.url # calls `attacher.get.url`
|
107
|
+
```
|
108
|
+
|
109
|
+
## State
|
110
|
+
|
111
|
+
You can ask the attacher whether the currently attached file is cached or
|
112
|
+
stored.
|
113
|
+
|
114
|
+
```rb
|
115
|
+
attacher.cached?
|
116
|
+
attacher.stored?
|
117
|
+
```
|
118
|
+
|
119
|
+
## Validations
|
120
|
+
|
121
|
+
Whenever a file is assigned via `#assign` or `#set`, the file validations are
|
122
|
+
automatically run, and you can access the validation errors through `#errors`:
|
123
|
+
|
124
|
+
```rb
|
125
|
+
attacher.assign(large_file)
|
126
|
+
attacher.errors #=> ["is larger than 10 MB"]
|
127
|
+
```
|
128
|
+
|
129
|
+
## Promoting
|
130
|
+
|
131
|
+
After the attachment is assigned and you run validations, it should be promoted
|
132
|
+
to permanent storage after the record is saved. You can use `#finalize` for
|
133
|
+
that, since that will also automatically delete any previously attached files.
|
134
|
+
|
135
|
+
```rb
|
136
|
+
# We run the finalization only if a new file was attached
|
137
|
+
attacher.finalize if attacher.attached?
|
138
|
+
```
|
139
|
+
|
140
|
+
This is normally automatically added to a callback by the ORM plugin when going
|
141
|
+
through the model. Internally this calls `#promote`, which uploads a given
|
142
|
+
`Shrine::UploadedFile` to permanent storage, and swaps it with the current
|
143
|
+
attachment, unless a new file was attached in the meanwhile.
|
144
|
+
|
145
|
+
```rb
|
146
|
+
# uploads cached file to permanent storage and replaces the current one
|
147
|
+
attacher.promote(cached_file, action: :custom_name)
|
148
|
+
```
|
149
|
+
|
150
|
+
The `:action` parameter is optional; it can be used for triggering a certain
|
151
|
+
processing block, and it is also automatically printed by the `logging` plugin
|
152
|
+
to aid in debugging.
|
153
|
+
|
154
|
+
Internally this calls `#swap`, which will update the record with any uploaded
|
155
|
+
file, but will reload the record to check if the current attachment hasn't
|
156
|
+
changed (if the `backgrounding` plugin is loaded).
|
157
|
+
|
158
|
+
```rb
|
159
|
+
attacher.swap(uploaded_file)
|
160
|
+
```
|
161
|
+
|
162
|
+
Both `#promote` and `#swap` are useful for [file migrations].
|
163
|
+
|
164
|
+
## Backgrounding
|
165
|
+
|
166
|
+
When the `backgrounding` plugin is loaded, it allows you to promote and delete
|
167
|
+
files in the background, and the corresponding methods are prefixed with `_`:
|
168
|
+
|
169
|
+
```rb
|
170
|
+
Shrine.plugin :backgrounding
|
171
|
+
Shrine::Attacher.promote { |data| PromoteJob.perform_async(data) }
|
172
|
+
Shrine::Attacher.delete { |data| DeleteJob.perform_async(data) }
|
173
|
+
```
|
174
|
+
```rb
|
175
|
+
attacher._promote(cached_file) # calls the registered `Attacher.promote` block
|
176
|
+
attacher._delete(uploaded_file) # calls the registered `Attacher.delete` block
|
177
|
+
```
|
178
|
+
|
179
|
+
These are automatically used when using Shrine through models.
|
180
|
+
|
181
|
+
## Context
|
182
|
+
|
183
|
+
The attacher sends `#context` to each upload/delete call to the uploader. By
|
184
|
+
default it will hold `:record` and `:name`:
|
185
|
+
|
186
|
+
```rb
|
187
|
+
attacher.context #=>
|
188
|
+
# {
|
189
|
+
# record: #<Photo...>,
|
190
|
+
# name: :image,
|
191
|
+
# }
|
192
|
+
```
|
193
|
+
|
194
|
+
However, you can change/add additional context to be sent when calling the
|
195
|
+
uploaders:
|
196
|
+
|
197
|
+
```rb
|
198
|
+
attacher.context[:foo] = "bar"
|
199
|
+
```
|
200
|
+
|
201
|
+
This is useful for example if you have immutable model instances, and you want
|
202
|
+
to assign a new updated instance. For example both foreground and background
|
203
|
+
`#promote` requires that the record is persisted (and its `#id` is present).
|
204
|
+
|
205
|
+
## Uploading and deleting
|
206
|
+
|
207
|
+
Normally you can upload and delete directly by using the uploader.
|
208
|
+
|
209
|
+
```rb
|
210
|
+
uploader = ImageUploader.new(:store)
|
211
|
+
uploaded_file = uploader.upload(image) # uploads the file to `:store` storage
|
212
|
+
uploader.delete(uploaded_file) # deletes the file uploaded to `:store`
|
213
|
+
```
|
214
|
+
|
215
|
+
The attacher has methods for "caching", "storing" and "deleting" files, which
|
216
|
+
delegate to these uploader methods, but also pass in the `#context`:
|
217
|
+
|
218
|
+
```rb
|
219
|
+
cached_file = attacher.cache!(image) # delegates to `Shrine#upload`
|
220
|
+
stored_file = attacher.store!(image) # delegates to `Shrine#upload`
|
221
|
+
attacher.delete!(stored_file) # delegates to `Shrine#delete`
|
222
|
+
```
|
223
|
+
|
224
|
+
The `#cache!` and `#store!` only upload the file to the storage, they don't
|
225
|
+
write to record's data column.
|
226
|
+
|
227
|
+
[file migrations]: http://shrinerb.com/rdoc/files/doc/migrating_storage_md.html
|
data/doc/design.md
CHANGED
@@ -28,26 +28,14 @@ A storage is a PORO which responds to certain methods:
|
|
28
28
|
class Shrine
|
29
29
|
module Storage
|
30
30
|
class MyStorage
|
31
|
-
def initialize(*args)
|
32
|
-
# initializing logic
|
33
|
-
end
|
34
|
-
|
35
31
|
def upload(io, id, shrine_metadata: {}, **upload_options)
|
36
32
|
# uploads `io` to the location `id`
|
37
33
|
end
|
38
34
|
|
39
|
-
def download(id)
|
40
|
-
# downloads the file from the storage
|
41
|
-
end
|
42
|
-
|
43
35
|
def open(id)
|
44
36
|
# returns the remote file as an IO-like object
|
45
37
|
end
|
46
38
|
|
47
|
-
def read(id)
|
48
|
-
# returns the file contents as a string
|
49
|
-
end
|
50
|
-
|
51
39
|
def exists?(id)
|
52
40
|
# checks if the file exists on the storage
|
53
41
|
end
|
@@ -59,12 +47,6 @@ class Shrine
|
|
59
47
|
def url(id, options = {})
|
60
48
|
# URL to the remote file, accepts options for customizing the URL
|
61
49
|
end
|
62
|
-
|
63
|
-
def clear!(confirm = nil)
|
64
|
-
# deletes all the files in the storage
|
65
|
-
end
|
66
|
-
|
67
|
-
# ...
|
68
50
|
end
|
69
51
|
end
|
70
52
|
end
|
@@ -182,6 +164,8 @@ to `Shrine#upload`, which works because `Shrine::UploadedFile` is an IO-like
|
|
182
164
|
object. After both caching and promoting the data hash of the uploaded file is
|
183
165
|
assigned to the record's column as JSON.
|
184
166
|
|
167
|
+
For more details see [Using Attacher].
|
168
|
+
|
185
169
|
## `Shrine::Attachment`
|
186
170
|
|
187
171
|
`Shrine::Attachment` is the highest level of abstraction. A
|
@@ -219,5 +203,7 @@ automatically:
|
|
219
203
|
|
220
204
|
* syncs Shrine's validation errors with the record
|
221
205
|
* triggers promoting after record is saved
|
222
|
-
* deletes the uploaded file if attachment was replaced
|
206
|
+
* deletes the uploaded file if attachment was replaced/removed or the record
|
223
207
|
destroyed
|
208
|
+
|
209
|
+
[Using Attacher]: http://shrinerb.com/rdoc/files/doc/attacher_md.html
|
data/doc/paperclip.md
CHANGED
@@ -29,8 +29,9 @@ Shrine.storages[:store] = Shrine::Storage::S3.new(
|
|
29
29
|
bucket: "my-bucket",
|
30
30
|
access_key_id: "abc",
|
31
31
|
secret_access_key: "xyz",
|
32
|
-
host: "http://abc123.cloudfront.net",
|
33
32
|
)
|
33
|
+
|
34
|
+
Shrine.plugin :default_url_options, store: {host: "http://abc123.cloudfront.net"}
|
34
35
|
```
|
35
36
|
|
36
37
|
Paperclip doesn't have a concept of "temporary" storage, so it cannot retain
|
data/doc/testing.md
ADDED
@@ -0,0 +1,266 @@
|
|
1
|
+
# Testing with Shrine
|
2
|
+
|
3
|
+
The goal of this guide is to provide some useful tips for testing file
|
4
|
+
attachments implemented with Shrine in your application.
|
5
|
+
|
6
|
+
## Callbacks
|
7
|
+
|
8
|
+
When you first try to test file attachments, you might experience that files
|
9
|
+
are simply not being promoted (uploaded from temporary to permanent storage).
|
10
|
+
This is because your tests are likely setup to be wrapped inside database
|
11
|
+
transactions, and that doesn't work with Shrine callbacks.
|
12
|
+
|
13
|
+
Specifically, Shrine uses "after commit" callbacks for promoting and deleting
|
14
|
+
attached files. This means that if your tests are wrapped inside transactions,
|
15
|
+
those Shrine actions will happen only after those transactions commit, which
|
16
|
+
happens only after the test has already finished.
|
17
|
+
|
18
|
+
```rb
|
19
|
+
# Promoting will happen only after the test transaction commits
|
20
|
+
it "can attach images" do
|
21
|
+
photo = Photo.create(image: image_file)
|
22
|
+
photo.image.storage_key #=> :cache (we expected it to be promoted to permanent storage)
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
For file attachments to properly work, you'll need to disable transactions for
|
27
|
+
those tests. For Rails apps you can tell Rails not to use transactions, and
|
28
|
+
instead use libraries like [DatabaseCleaner] which allow you to use table
|
29
|
+
truncation or deletion strategies instead of transactions.
|
30
|
+
|
31
|
+
```rb
|
32
|
+
RSpec.configure do |config|
|
33
|
+
config.use_transactional_fixtures = false
|
34
|
+
end
|
35
|
+
```
|
36
|
+
|
37
|
+
## Storage
|
38
|
+
|
39
|
+
If you're using an external storage in development, it is common in tests to
|
40
|
+
switch to a filesystem storage. However, that means that you'll also have to
|
41
|
+
clean up the test directory between tests, and writing to filesystem a lot can
|
42
|
+
affect the performance of your tests.
|
43
|
+
|
44
|
+
Instead of filesystem you can use [memory storage][shrine-memory], which is
|
45
|
+
both faster and doesn't require you to clean up anything between tests.
|
46
|
+
|
47
|
+
```rb
|
48
|
+
gem "shrine-memory"
|
49
|
+
```
|
50
|
+
```rb
|
51
|
+
# test/test_helper.rb
|
52
|
+
require "shrine/storage/memory"
|
53
|
+
|
54
|
+
Shrine.storages = {
|
55
|
+
cache: Shrine::Storage::Memory.new,
|
56
|
+
store: Shrine::Storage::Memory.new,
|
57
|
+
}
|
58
|
+
```
|
59
|
+
|
60
|
+
## Test data
|
61
|
+
|
62
|
+
If you're creating test data dynamically using libraries like [factory_girl],
|
63
|
+
you can have the test file assigned dynamically when the record is created:
|
64
|
+
|
65
|
+
```rb
|
66
|
+
factory :photo do
|
67
|
+
image File.open("test/files/image.jpg")
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
On the other hand, if you're setting up test data using YAML fixtures, you
|
72
|
+
aren't that flexible, because you can only use primitive data types that are
|
73
|
+
part of the YAML language. In that case you can load the `data_uri` Shrine
|
74
|
+
plugin, and assign files in form of data URI strings through the
|
75
|
+
`<attachment>_data_uri` accessor provided by the plugin.
|
76
|
+
|
77
|
+
```rb
|
78
|
+
Shrine.plugin :data_uri
|
79
|
+
```
|
80
|
+
```yml
|
81
|
+
# test/fixtures/photos.yml
|
82
|
+
photo:
|
83
|
+
image_data_uri: "data:image/png,<%= File.read("test/files/image.png") %>"
|
84
|
+
```
|
85
|
+
|
86
|
+
## Background jobs
|
87
|
+
|
88
|
+
If you're using background jobs with Shrine, you probably want to make them
|
89
|
+
synchronous in tests. Your favourite backgrounding library should already
|
90
|
+
support this, examples:
|
91
|
+
|
92
|
+
```rb
|
93
|
+
# Sidekiq
|
94
|
+
require "sidekiq/testing"
|
95
|
+
Sidekiq::Testing.inline!
|
96
|
+
```
|
97
|
+
|
98
|
+
```rb
|
99
|
+
# SuckerPunch
|
100
|
+
require "sucker_punch/testing/inline"
|
101
|
+
```
|
102
|
+
|
103
|
+
```rb
|
104
|
+
# ActiveJob
|
105
|
+
ActiveJob::Base.queue_adapter = :inline
|
106
|
+
```
|
107
|
+
|
108
|
+
## Acceptance tests
|
109
|
+
|
110
|
+
In acceptance tests you're testing your app end-to-end, and you likely want to
|
111
|
+
also test file attachments here. There are a variety of libraries that you
|
112
|
+
might be using for your acceptance tests.
|
113
|
+
|
114
|
+
### Capybara
|
115
|
+
|
116
|
+
If you're testing with the [Capybara] acceptance test framework, you can use
|
117
|
+
[`#attach_file`] to select a file from your filesystem in the form:
|
118
|
+
|
119
|
+
```rb
|
120
|
+
attach_file("#image-field", "test/files/image.jpg")
|
121
|
+
```
|
122
|
+
|
123
|
+
### Rack::Test
|
124
|
+
|
125
|
+
Regular routing tests in Rails use [Rack::Test], in which case you can create
|
126
|
+
`Rack::Test::UploadedFile` objects and pass them as form parameters:
|
127
|
+
|
128
|
+
```rb
|
129
|
+
post "/photos", photo: {image: Rack::Test::UploadedFile.new("test/files/image.jpg", "image/jpeg")}
|
130
|
+
```
|
131
|
+
|
132
|
+
### Rack::TestApp
|
133
|
+
|
134
|
+
With [Rack::TestApp] you can create multipart file upload requests by using the
|
135
|
+
`:multipart` option and passing a `File` object:
|
136
|
+
|
137
|
+
```rb
|
138
|
+
http.post "/photos", multipart: {"photo[image]" => File.open("test/files/image.jpg")}
|
139
|
+
```
|
140
|
+
|
141
|
+
## Attachment
|
142
|
+
|
143
|
+
Even though all the file attachment logic is usually encapsulated in your
|
144
|
+
uploader classes, in general it's still best to test this logic through models.
|
145
|
+
|
146
|
+
In your controller the attachment attribute using the uploaded file from the
|
147
|
+
controller, in Rails case it's an `ActionDispatch::Http::UploadedFile`.
|
148
|
+
However, you can also assign plain `File` objects, or any other kind of IO-like
|
149
|
+
objects.
|
150
|
+
|
151
|
+
```rb
|
152
|
+
describe ImageUploader do
|
153
|
+
it "generates image thumbnails" do
|
154
|
+
photo = Photo.create(image: File.open("test/files/image.png"))
|
155
|
+
assert_equal [:small, :medium, :large], photo.image.keys
|
156
|
+
end
|
157
|
+
end
|
158
|
+
```
|
159
|
+
|
160
|
+
If you want test with an IO object that closely resembles the kind of IO that
|
161
|
+
is assigned by your web framework, you can use this:
|
162
|
+
|
163
|
+
```rb
|
164
|
+
require "forwardable"
|
165
|
+
require "stringio"
|
166
|
+
|
167
|
+
class FakeIO
|
168
|
+
attr_reader :original_filename, :content_type
|
169
|
+
|
170
|
+
def initialize(content, filename: nil, content_type: nil)
|
171
|
+
@io = StringIO.new(content)
|
172
|
+
@original_filename = filename
|
173
|
+
@content_type = content_type
|
174
|
+
end
|
175
|
+
|
176
|
+
extend Forwardable
|
177
|
+
delegate Shrine::IO_METHODS.keys => :@io
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
```rb
|
182
|
+
describe ImageUploader do
|
183
|
+
it "generates image thumbnails" do
|
184
|
+
photo = Photo.create(image: FakeIO.new(File.read("test/files/image.png")))
|
185
|
+
assert_equal [:small, :medium, :large], photo.image.keys
|
186
|
+
end
|
187
|
+
end
|
188
|
+
```
|
189
|
+
|
190
|
+
## Processing
|
191
|
+
|
192
|
+
In tests you usually don't want to perform processing, or at least don't want
|
193
|
+
it to be performed by default (only when you're actually testing it).
|
194
|
+
|
195
|
+
If you're processing only single files, you can override the `Shrine#process`
|
196
|
+
method in tests to return nil:
|
197
|
+
|
198
|
+
```rb
|
199
|
+
class ImageUploader
|
200
|
+
def process(io, context)
|
201
|
+
# don't do any processing
|
202
|
+
end
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
If you're processing versions, you can override `Shrine#process` to simply
|
207
|
+
return a hash of unprocessed original files:
|
208
|
+
|
209
|
+
```rb
|
210
|
+
class ImageUploader
|
211
|
+
def process(io, context)
|
212
|
+
if context[:action] == :store
|
213
|
+
{small: io, medium: io, large: io}
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
However, it's even better to design your processing code in such a way that
|
220
|
+
it's easier to swap out in tests. In your *application* code you could extract
|
221
|
+
processing into a single `#call`-able object, and register it inside uploader
|
222
|
+
generic `#opts` hash.
|
223
|
+
|
224
|
+
```rb
|
225
|
+
class ImageUploader < Shrine
|
226
|
+
opts[:processor] = ImageThumbnailsGenerator
|
227
|
+
|
228
|
+
process(:store) do |io, context|
|
229
|
+
opts[:processor].call(io, context)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
```
|
233
|
+
|
234
|
+
Now in your tests you can easily swap out `ImageThumbnailsGenerator` with
|
235
|
+
"fake" processing, which just returns the result in correct format (single file
|
236
|
+
or hash of versions). Since the only requirement of the processor is that it
|
237
|
+
responds to `#call`, we can just swap it out for a proc or a lambda:
|
238
|
+
|
239
|
+
```rb
|
240
|
+
ImageUploader.opts[:processor] = proc do |io, context|
|
241
|
+
# return unprocessed file(s)
|
242
|
+
end
|
243
|
+
```
|
244
|
+
|
245
|
+
This also has the benefit of allowing you to test `ImageThumbnailsGenerator` in
|
246
|
+
isolation.
|
247
|
+
|
248
|
+
## Direct upload
|
249
|
+
|
250
|
+
In case you're doing direct uploads to S3 on production and staging
|
251
|
+
environments, in development and test you might want to just store files on
|
252
|
+
the filesystem for speed.
|
253
|
+
|
254
|
+
In that case you can swap out S3 for FileSystem, and the `direct_upload` app
|
255
|
+
should still continue to work without any changes. This is because Shrine
|
256
|
+
detects that you're using a storage which isn't an external service, and in
|
257
|
+
that case the presign endpoint returns an URL to the upload route that's also
|
258
|
+
provided by the `direct_upload` app mounted in your routes.
|
259
|
+
|
260
|
+
[DatabaseCleaner]: https://github.com/DatabaseCleaner/database_cleaner
|
261
|
+
[shrine-memory]: https://github.com/janko-m/shrine-memory
|
262
|
+
[factory_girl]: https://github.com/thoughtbot/factory_girl
|
263
|
+
[Capybara]: https://github.com/jnicklas/capybara
|
264
|
+
[`#attach_file`]: http://www.rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Actions#attach_file-instance_method
|
265
|
+
[Rack::Test]: https://github.com/brynary/rack-test
|
266
|
+
[Rack::TestApp]: https://github.com/kwatch/rack-test_app
|