defile 0.2.0

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 +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)