refile 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
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)