defile 0.2.0

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