refile 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +27 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +8 -0
  5. data/Gemfile +3 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +476 -0
  8. data/Rakefile +11 -0
  9. data/app/assets/javascripts/refile.js +50 -0
  10. data/app/helpers/attachment_helper.rb +52 -0
  11. data/config.ru +8 -0
  12. data/config/locales/en.yml +5 -0
  13. data/config/routes.rb +3 -0
  14. data/lib/refile.rb +72 -0
  15. data/lib/refile/app.rb +97 -0
  16. data/lib/refile/attachment.rb +89 -0
  17. data/lib/refile/attachment/active_record.rb +24 -0
  18. data/lib/refile/backend/file_system.rb +70 -0
  19. data/lib/refile/backend/s3.rb +129 -0
  20. data/lib/refile/file.rb +65 -0
  21. data/lib/refile/image_processing.rb +73 -0
  22. data/lib/refile/rails.rb +36 -0
  23. data/lib/refile/random_hasher.rb +5 -0
  24. data/lib/refile/version.rb +3 -0
  25. data/refile.gemspec +34 -0
  26. data/spec/refile/app_spec.rb +151 -0
  27. data/spec/refile/attachment_spec.rb +141 -0
  28. data/spec/refile/backend/file_system_spec.rb +30 -0
  29. data/spec/refile/backend/s3_spec.rb +11 -0
  30. data/spec/refile/backend_examples.rb +215 -0
  31. data/spec/refile/features/direct_upload_spec.rb +29 -0
  32. data/spec/refile/features/normal_upload_spec.rb +36 -0
  33. data/spec/refile/features/presigned_upload_spec.rb +29 -0
  34. data/spec/refile/fixtures/hello.txt +1 -0
  35. data/spec/refile/fixtures/large.txt +44 -0
  36. data/spec/refile/spec_helper.rb +58 -0
  37. data/spec/refile/test_app.rb +46 -0
  38. data/spec/refile/test_app/app/assets/javascripts/application.js +40 -0
  39. data/spec/refile/test_app/app/controllers/application_controller.rb +2 -0
  40. data/spec/refile/test_app/app/controllers/direct_posts_controller.rb +15 -0
  41. data/spec/refile/test_app/app/controllers/home_controller.rb +4 -0
  42. data/spec/refile/test_app/app/controllers/normal_posts_controller.rb +19 -0
  43. data/spec/refile/test_app/app/controllers/presigned_posts_controller.rb +30 -0
  44. data/spec/refile/test_app/app/models/post.rb +5 -0
  45. data/spec/refile/test_app/app/views/direct_posts/new.html.erb +16 -0
  46. data/spec/refile/test_app/app/views/home/index.html.erb +1 -0
  47. data/spec/refile/test_app/app/views/layouts/application.html.erb +14 -0
  48. data/spec/refile/test_app/app/views/normal_posts/new.html.erb +20 -0
  49. data/spec/refile/test_app/app/views/normal_posts/show.html.erb +9 -0
  50. data/spec/refile/test_app/app/views/presigned_posts/new.html.erb +16 -0
  51. data/spec/refile/test_app/config/database.yml +7 -0
  52. data/spec/refile/test_app/config/routes.rb +17 -0
  53. data/spec/refile/test_app/public/favicon.ico +0 -0
  54. data/spec/refile_spec.rb +35 -0
  55. metadata +294 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c82ae33f74a7a012f5d04acd5c72ad9675b4e3b7
4
+ data.tar.gz: 984abf68f34e0725fb18164c0923eeaa2b992b86
5
+ SHA512:
6
+ metadata.gz: 7393be87fdddfc7fd09b46bfab369dc9364ef30b1f61ebd03de568bc826101206de81474ee6f03215672bd7794d1ce017deadc403b062cba545132d3177aa565
7
+ data.tar.gz: 53ff261be44222e0fe7afac11e30372ed488884695bcca01569f2a1e58e3f30100765cda72349f4cac54b24a6f8fba660ae6bbf7d2699b739cbb253f69194176
@@ -0,0 +1,27 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.bundle
19
+ *.so
20
+ *.o
21
+ *.a
22
+ mkmf.log
23
+
24
+ s3.yml
25
+ spec/refile/test_app/log
26
+ spec/refile/test_app/tmp
27
+ spec/refile/test_app/db/*.sqlite
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ -r refile/spec_helper
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.5
4
+ gemfile:
5
+ - Gemfile
6
+ before_script:
7
+ - export DISPLAY=:99.0
8
+ - sh -e /etc/init.d/xvfb start
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Jonas Nicklas
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,476 @@
1
+ # Refile
2
+
3
+ [![Build Status](https://travis-ci.org/elabs/refile.svg?branch=master)](https://travis-ci.org/elabs/refile)
4
+
5
+ Refile is a modern file upload library for Ruby applications. It is simple, yet
6
+ powerful. Refile is an attempt by CarrierWave's original author to fix the
7
+ design mistakes and overengineering in CarrierWave.
8
+
9
+ Features:
10
+
11
+ - Configurable backends, file system, S3, etc...
12
+ - Convenient integration with ORMs
13
+ - On the fly manipulation of images and other files
14
+ - Streaming IO for fast and memory friendly uploads
15
+ - Works across form redisplays, i.e. when validations fail, even on S3
16
+ - Effortless direct uploads, even to S3
17
+
18
+ ## Quick start, Rails
19
+
20
+ Add the gem:
21
+
22
+ ``` ruby
23
+ gem "mini_magick"
24
+ gem "refile", require: ["refile/rails", "refile/image_processing"]
25
+ ```
26
+
27
+ Use the `attachment` method to use Refile in a model:
28
+
29
+ ``` ruby
30
+ class User < ActiveRecord::Base
31
+ attachment :profile_image
32
+ end
33
+ ```
34
+
35
+ Generate a migration:
36
+
37
+ ``` sh
38
+ rails generate migration add_profile_image_to_users profile_image_id:string
39
+ rake db:migrate
40
+ ```
41
+
42
+ Add an attachment field to your form:
43
+
44
+ ``` erb
45
+ <%= form_for @user do |form| %>
46
+ <%= form.attachment_field :profile_image %>
47
+ <% end %>
48
+ ```
49
+
50
+ Set up strong parameters:
51
+
52
+ ``` ruby
53
+ def user_params
54
+ params.require(:user).permit(:profile_image, :profile_image_cache_id)
55
+ end
56
+ ```
57
+
58
+ And start uploading! Finally show the file in your view:
59
+
60
+ ``` erb
61
+ <%= image_tag attachment_url(@user, :profile_image, :fill, 300, 300) %>
62
+ ```
63
+
64
+ ## How it works
65
+
66
+ Refile consists of several parts:
67
+
68
+ 1. Backends: cache and persist files
69
+ 2. Model attachments: map files to model columns
70
+ 3. A Rack application: streams files and accepts uploads
71
+ 4. Rails helpers: conveniently generate markup in your views
72
+ 4. A JavaScript library: facilitates direct uploads
73
+
74
+ Let's look at each of these in more detail!
75
+
76
+ ## 1. Backend
77
+
78
+ Files are uploaded to a backend. The backend assigns an ID to this file, which
79
+ will be unique for this file within the backend.
80
+
81
+ Let's look at a simple example of using the backend:
82
+
83
+ ``` ruby
84
+ backend = Refile::Backend::FileSystem.new("tmp")
85
+
86
+ file = backend.upload(StringIO.new("hello"))
87
+ file.id # => "b205bc..."
88
+ file.read # => "hello"
89
+
90
+ backend.get(file.id).read # => "hello"
91
+ ```
92
+
93
+ As you may notice, backends are "flat". Files do not have directories, nor do
94
+ they have names or permissions, they are only identified by their ID.
95
+
96
+ Refile has a global registry of backends, accessed through `Refile.backends`.
97
+
98
+ There are two "special" backends, which are only really special in that they
99
+ are the default backends for attachments. They are `cache` and `store`.
100
+
101
+ The cache is intended to be transient. Files are added here before they are
102
+ meant to be permanently stored. Usually files are then moved to the store for
103
+ permanent storage, but this isn't always the case.
104
+
105
+ Suppose for example that a user uploads a file in a form and receives a
106
+ validation error. In that case the file has been temporarily stored in the
107
+ cache. The user might decide to fix the error and resubmit, at which point the
108
+ file will be promoted to the store. On the other hand, the user might simply
109
+ give up and leave, now the file is left in the cache for later cleanup.
110
+
111
+ Refile has convenient accessors for setting the `cache` and `store`, so for
112
+ example you can switch to the S3 backend like this:
113
+
114
+ ``` ruby
115
+ # config/initializers/refile.rb
116
+ require "refile/backend/s3"
117
+
118
+ aws = {
119
+ access_key_id: "xyz",
120
+ secret_access_key: "abc",
121
+ bucket: "my-bucket",
122
+ }
123
+ Refile.cache = Refile::Backend::S3.new(prefix: "cache", **aws)
124
+ Refile.store = Refile::Backend::S3.new(prefix: "store", **aws)
125
+ ```
126
+
127
+ Try this in the quick start example above and your files are now uploaded to
128
+ S3.
129
+
130
+ Backends also provide the option of restricting the size of files they accept.
131
+ For example:
132
+
133
+ ``` ruby
134
+ Refile.cache = Refile::Backend::S3.new(max_size: 10.megabytes, ...)
135
+ ```
136
+
137
+ ### Uploadable
138
+
139
+ The `upload` method on backends can be called with a variety of objects. It
140
+ requires that the object passed to it behaves similarly to Ruby IO objects, in
141
+ particular it must implement the methods `size`, `read(length = nil, buffer =
142
+ nil)`, `eof?` and `close`. All of `File`, `Tempfile`,
143
+ `ActionDispath::UploadedFile` and `StringIO` implement this interface, however
144
+ `String` does not. If you want to upload a file from a `String` you must wrap
145
+ it in a `StringIO` first.
146
+
147
+ ## 2. Attachments
148
+
149
+ You've already seen the `attachment` method:
150
+
151
+ ``` ruby
152
+ class User < ActiveRecord::Base
153
+ attachment :profile_image
154
+ end
155
+ ```
156
+
157
+ Calling `attachment` generates a getter and setter with the given name. When
158
+ you assign a file to the setter, it is uploaded to the cache:
159
+
160
+ ``` ruby
161
+ User.new
162
+
163
+ # with a ActionDispatch::UploadedFile
164
+ user.profile_image = params[:file]
165
+
166
+ # with a regular File object
167
+ File.open("/some/path", "rb") do |file|
168
+ user.profile_image = file
169
+ end
170
+
171
+ # or a StringIO
172
+ user.profile_image = StringIO.new("hello world")
173
+
174
+ user.profile_image.id # => "fec421..."
175
+ user.profile_image.read # => "hello world"
176
+ ```
177
+
178
+ When you call `save` on the record, the uploaded file is transferred from the
179
+ cache to the store. Where possible, Refile does this move efficiently. For example
180
+ if both `cache` and `store` are on the same S3 account, instead of downloading
181
+ the file and uploading it again, Refile will simply issue a copy command to S3.
182
+
183
+ ### Other ORMs
184
+
185
+ Refile is built to integrate with ORMs other than ActiveRecord, but this being
186
+ a very young gem, such integrations do not yet exist. Take a look at the [ActiveRecord
187
+ integration](lib/refile/attachment/active_record.rb), building your own should
188
+ not be too difficult.
189
+
190
+ ### Pure Ruby classes
191
+
192
+ You can also use attachments in pure Ruby classes like this:
193
+
194
+ ``` ruby
195
+ class User
196
+ extend Refile::Attachment
197
+
198
+ attr_accessor :profile_image_id
199
+
200
+ attachment :profile_image
201
+ end
202
+ ```
203
+
204
+ ## 3. Rack Application
205
+
206
+ Refile includes a Rack application (an endpoint, not a middleware). This application
207
+ streams files from backends and can even accept file uploads and upload them to
208
+ backends.
209
+
210
+ **Important:** Unlike other file upload solutions, Refile always streams your files thorugh your
211
+ application. It cannot generate URLs to your files. This means that you should
212
+ **always** put a CDN or other HTTP cache in front of your application. Serving
213
+ files through your app takes a lot of resources and you want it to happen rarely.
214
+
215
+ Setting this up is actually quite simple, you can use the same CDN you would use
216
+ for your application's static assets. [This blog post](http://www.happybearsoftware.com/use-cloudfront-and-the-rails-asset-pipeline-to-speed-up-your-app.html)
217
+ explains how to set this up (bonus: faster static assets!). Once you've set this
218
+ up, simply configure Refile to use your CDN:
219
+
220
+ ``` ruby
221
+ Refile.host = "//your-dist-url.cloudfront.net"
222
+ ```
223
+
224
+ Using a [procol-relative URL](http://www.paulirish.com/2010/the-protocol-relative-url/) for `Refile.host` is recommended.
225
+
226
+ ### Mounting
227
+
228
+ If you are using Rails and have required [refile/rails.rb](lib/refile/rails.rb),
229
+ then the Rack application is mounted for you at `/attachments`. You should be able
230
+ to see this when you run `rake routes`.
231
+
232
+ You could also run the application on its own, it doesn't need to be mounted to
233
+ work.
234
+
235
+ ### Retrieving files
236
+
237
+ Files can be retrieved from the application by calling:
238
+
239
+ ```
240
+ GET /attachments/:backend_name/:id/:filename
241
+ ```
242
+
243
+ The `:filename` serves no other purpose than generating a nice name when the user
244
+ downloads the file, it does not in any way affect the downloaded file. For caching
245
+ purposes you should always use the same filename for the same file. The Rails helpers
246
+ default this to the name of the column.
247
+
248
+ ### Processing
249
+
250
+ Refile provides on the fly processing of files. You can trigger it by calling
251
+ a URL like this:
252
+
253
+ ```
254
+ GET /attachments/:backend_name/:processor_name/*args/:id/:filename
255
+ ```
256
+
257
+ Suppose we have uploaded a file:
258
+
259
+ ``` ruby
260
+ Refile.cache.upload(StringIO.new("hello")).id # => "a4e8ce"
261
+ ```
262
+
263
+ And we've defined a processor like this:
264
+
265
+ ``` ruby
266
+ Refile.processor :reverse do |file|
267
+ StringIO.new(file.read.reverse)
268
+ end
269
+ ```
270
+
271
+ Then you could do the following.
272
+
273
+ ``` sh
274
+ curl http://127.0.0.1:3000/attachments/cache/reverse/a4e8ce/some_file.txt
275
+ elloh
276
+ ```
277
+
278
+ Refile calls `call` on the processor and passes in the retrieved file, as well
279
+ as all additional arguments sent through the URL. See the
280
+ [built in image processors](lib/refile/image_processing.rb) for a more advanced
281
+ example.
282
+
283
+ ## 4. Rails helpers
284
+
285
+ Refile provides the `attachment_field` form helper which generates a file field
286
+ as well as a hidden field, suffixed with `cache_id`. This field keeps track of
287
+ the file in case it is not yet permanently stored, for example if validations
288
+ fail. It is also used for direct and presigned uploads. For this reason it is
289
+ highly recommended to use `attachment_field` instead of `file_field`.
290
+
291
+ ``` erb
292
+ <%= form_for @user do |form| %>
293
+ <%= form.attachment_field :profile_image %>
294
+ <% end %>
295
+ ```
296
+
297
+ Will generate something like:
298
+
299
+ ``` html
300
+ <form action="/users" enctype="multipart/form-data" method="post">
301
+ <input name="user[profile_image_cache_id]" type="hidden">
302
+ <input name="user[profile_image]" type="file">
303
+ </form>
304
+ ```
305
+
306
+ The `attachment_url` helper can then be used for generating URLs for the uploaded
307
+ files:
308
+
309
+ ``` erb
310
+ <%= image_tag attachment_url(@user, :profile_image) %>
311
+ ```
312
+
313
+ Any additional arguments to it are included in the URL as processor arguments:
314
+
315
+ ``` erb
316
+ <%= image_tag attachment_url(@user, :profile_image, :fill, 300, 300) %>
317
+ ```
318
+
319
+ ## 5. JavaScript library
320
+
321
+ Refile's JavaScript library is small but powerful.
322
+
323
+ Uploading files is slow, so anything we can do to speed up the process is going
324
+ to lead to happier users. One way to cheat is to start uploading files directly
325
+ after the user has chosen a file, instead of waiting until they hit the submit
326
+ button. This provides a significantly better user experience. Implementing this
327
+ is usually tricky, but thankfully Refile makes it very easy.
328
+
329
+ First, load the JavaScript file. If you're using the asset pipeline, you can
330
+ simply include it like this:
331
+
332
+ ``` javascript
333
+ //= require refile
334
+ ```
335
+
336
+ Otherwise you can grab a copy [here](https://raw.githubusercontent.com/elabs/refile/master/app/assets/javascripts/refile.js).
337
+
338
+ Now mark the field for direct upload:
339
+
340
+ ``` erb
341
+ <%= form.attachment_field :profile_image, direct: true %>
342
+ ```
343
+
344
+ There is no step 3 ;)
345
+
346
+ The file is now uploaded to the `cache` immediately after the user chooses a file.
347
+ If you try this in the browser, you'll notice that an AJAX request is fired as
348
+ soon as you choose a file. Then when you submit to the server, the file is no
349
+ longer submitted, only its id.
350
+
351
+ If you want to improve the experience of this, the JavaScript library fires
352
+ a couple of custom DOM events. These events bubble, so you can also listen for
353
+ them on the form for example:
354
+
355
+ ``` javascript
356
+ form.addEventListener("upload:start", function() {
357
+ // ...
358
+ });
359
+
360
+ form.addEventListener("upload:success", function() {
361
+ // ...
362
+ });
363
+
364
+ input.addEventListener("upload:progress", function() {
365
+ // ...
366
+ });
367
+ ```
368
+
369
+ You can also listen for them with jQuery, even with event delegation:
370
+
371
+ ``` javascript
372
+ $(document).on("upload:start", "form", function(e) {
373
+ // ...
374
+ });
375
+ ```
376
+
377
+ This way you could for example disable the submit button until all files have
378
+ uploaded:
379
+
380
+ ``` javascript
381
+ $(document).on("upload:start", "form", function(e) {
382
+ $(this).find("input[type=submit]").attr("disabled", true)
383
+ });
384
+
385
+ $(document).on("upload:complete", "form", function(e) {
386
+ if(!$(this).find("input.uploading").length) {
387
+ $(this).find("input[type=submit]").removeAttr("disabled")
388
+ }
389
+ });
390
+ ```
391
+
392
+ ### Presigned uploads
393
+
394
+ Amazon S3 supports uploads directly from the browser to S3 buckets. With this
395
+ feature you can bypass your application entirely; uploads never hit your application
396
+ at all. Unfortunately the default configuration of S3 buckets does not allow
397
+ cross site AJAX requests from posting to buckets. Fixing this is easy though.
398
+
399
+ - Open the AWS S3 console and locate your bucket
400
+ - Right click on it and choose "Properties"
401
+ - Open the "Permission" section
402
+ - Click "Add CORS Configuration"
403
+
404
+ The default configuration only allows "GET", you'll want to allow "POST" as well.
405
+ It could look something like this:
406
+
407
+ ``` xml
408
+ <CORSConfiguration>
409
+ <CORSRule>
410
+ <AllowedOrigin>*</AllowedOrigin>
411
+ <AllowedMethod>GET</AllowedMethod>
412
+ <AllowedMethod>POST</AllowedMethod>
413
+ <MaxAgeSeconds>3000</MaxAgeSeconds>
414
+ <AllowedHeader>Authorization</AllowedHeader>
415
+ </CORSRule>
416
+ </CORSConfiguration>
417
+ ```
418
+
419
+ If you're paranoid you can restrict the allowed origin to only your domain, but
420
+ since your bucket is only writable with authentication anyway, this shouldn't
421
+ be necessary.
422
+
423
+ Note that you do not need to, and in fact you shouldn't, make your bucket world
424
+ writable.
425
+
426
+ Once you've put in the new configuration, click "Save".
427
+
428
+ Now you can enable presigned uploads:
429
+
430
+ ``` erb
431
+ <%= form.attachment_field :profile_image, presigned: true %>
432
+ ```
433
+
434
+ You can also enable both direct and presigned uploads, and it'll fall back to
435
+ direct uploads if presigned uploads aren't available. This is useful if you're
436
+ using the FileSystem backend in development or test mode and the S3 backend in
437
+ production mode.
438
+
439
+ ``` erb
440
+ <%= form.attachment_field :profile_image, direct: true, presigned: true %>
441
+ ```
442
+
443
+ ### Browser compatibility
444
+
445
+ Refile's JavaScript library requires HTML5 features which are unavailable on
446
+ IE9 and earlier versions. All other major browsers are supported. Note though
447
+ that it has not yet been extensively tested.
448
+
449
+ ## Cache expiry
450
+
451
+ Files will accumulate in your cache, and you'll probably want to remove them
452
+ after some time.
453
+
454
+ The FileSystem backend does not currently provide any method of doing this. PRs
455
+ welcome ;)
456
+
457
+ On S3 this can be conveniently handled through lifecycle rules. Exactly how
458
+ depends a bit on your setup. If you are using the suggested setup of having
459
+ one bucket with `cache` and `store` being directories in that bucket (or prefixes
460
+ in S3 parlance), then follow the following steps, otherwise adapt them to your
461
+ needs:
462
+
463
+ - Open the AWS S3 console and locate your bucket
464
+ - Right click on it and choose "Properties"
465
+ - Open the "Lifecycle" section
466
+ - Click "Add rule"
467
+ - Choose "Apply the rule to: A prefix"
468
+ - Enter "cache/" as the prefix (trailing slash!)
469
+ - Click "Configure rule"
470
+ - For "Action on Objects" you'll probably want to choose "Permanently Delete Only"
471
+ - Choose whatever number of days you're comfortable with, I chose "1"
472
+ - Click "Review" and finally "Create and activate Rule"
473
+
474
+ ## License
475
+
476
+ [MIT](LICENSE.txt)