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.

@@ -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
@@ -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, removed or the record
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
@@ -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
@@ -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