dragonfly 0.1.6 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dragonfly might be problematic. Click here for more details.

Files changed (69) hide show
  1. data/.yardopts +4 -0
  2. data/{README.markdown → README.md} +12 -24
  3. data/Rakefile +6 -6
  4. data/VERSION +1 -1
  5. data/config.rb +1 -3
  6. data/docs.watchr +1 -1
  7. data/dragonfly-rails.gemspec +2 -5
  8. data/dragonfly.gemspec +32 -12
  9. data/extra_docs/ActiveRecord.md +195 -0
  10. data/extra_docs/Analysers.md +63 -0
  11. data/extra_docs/DataStorage.md +33 -0
  12. data/extra_docs/Encoding.md +58 -0
  13. data/extra_docs/GettingStarted.md +114 -0
  14. data/extra_docs/Processing.md +58 -0
  15. data/extra_docs/Shortcuts.md +118 -0
  16. data/extra_docs/UsingWithRails.md +104 -0
  17. data/features/{dragonfly.feature → images.feature} +14 -4
  18. data/features/no_processing.feature +20 -0
  19. data/features/steps/dragonfly_steps.rb +29 -8
  20. data/features/support/env.rb +20 -8
  21. data/generators/dragonfly_app/USAGE +0 -1
  22. data/generators/dragonfly_app/dragonfly_app_generator.rb +1 -13
  23. data/generators/dragonfly_app/templates/metal_file.erb +1 -1
  24. data/lib/dragonfly/active_record_extensions.rb +1 -0
  25. data/lib/dragonfly/active_record_extensions/attachment.rb +52 -6
  26. data/lib/dragonfly/active_record_extensions/validations.rb +26 -6
  27. data/lib/dragonfly/analysis/base.rb +6 -3
  28. data/lib/dragonfly/analysis/r_magick_analyser.rb +0 -6
  29. data/lib/dragonfly/app.rb +53 -35
  30. data/lib/dragonfly/configurable.rb +1 -1
  31. data/lib/dragonfly/data_storage/file_data_store.rb +8 -8
  32. data/lib/dragonfly/delegatable.rb +14 -0
  33. data/lib/dragonfly/delegator.rb +50 -0
  34. data/lib/dragonfly/encoding/base.rb +7 -7
  35. data/lib/dragonfly/encoding/r_magick_encoder.rb +3 -0
  36. data/lib/dragonfly/encoding/transparent_encoder.rb +1 -1
  37. data/lib/dragonfly/extended_temp_object.rb +13 -7
  38. data/lib/dragonfly/parameters.rb +17 -11
  39. data/lib/dragonfly/processing/base.rb +9 -0
  40. data/lib/dragonfly/processing/r_magick_processor.rb +15 -1
  41. data/lib/dragonfly/r_magick_configuration.rb +12 -8
  42. data/lib/dragonfly/rails/images.rb +1 -1
  43. data/lib/dragonfly/temp_object.rb +14 -2
  44. data/lib/dragonfly/url_handler.rb +5 -6
  45. data/samples/sample.docx +0 -0
  46. data/spec/dragonfly/active_record_extensions/model_spec.rb +175 -84
  47. data/spec/dragonfly/analysis/r_magick_analyser_spec.rb +0 -8
  48. data/spec/dragonfly/app_spec.rb +3 -3
  49. data/spec/dragonfly/configurable_spec.rb +1 -1
  50. data/spec/dragonfly/data_storage/file_data_store_spec.rb +55 -40
  51. data/spec/dragonfly/delegatable_spec.rb +32 -0
  52. data/spec/dragonfly/delegator_spec.rb +133 -0
  53. data/spec/dragonfly/encoding/r_magick_encoder_spec.rb +28 -0
  54. data/spec/dragonfly/extended_temp_object_spec.rb +5 -5
  55. data/spec/dragonfly/parameters_spec.rb +22 -32
  56. data/spec/dragonfly/processing/rmagick_processor_spec.rb +1 -2
  57. data/spec/dragonfly/temp_object_spec.rb +51 -0
  58. data/spec/dragonfly/url_handler_spec.rb +10 -15
  59. data/yard/handlers/configurable_attr_handler.rb +38 -0
  60. data/yard/setup.rb +9 -0
  61. data/yard/templates/default/fulldoc/html/css/common.css +27 -0
  62. data/yard/templates/default/module/html/configuration_summary.erb +31 -0
  63. data/yard/templates/default/module/setup.rb +17 -0
  64. metadata +31 -12
  65. data/features/support/image_helpers.rb +0 -9
  66. data/generators/dragonfly_app/templates/custom_processing.erb +0 -13
  67. data/lib/dragonfly/analysis/analyser.rb +0 -45
  68. data/lib/dragonfly/processing/processor.rb +0 -14
  69. data/spec/dragonfly/analysis/analyser_spec.rb +0 -85
@@ -0,0 +1,63 @@
1
+ Analysers
2
+ =========
3
+
4
+ Analysing data for things like width, mime_type, etc. come under the banner of Analysis.
5
+
6
+ Let's say we have a dragonfly app called 'images'
7
+
8
+ app = Dragonfly::App[:images]
9
+
10
+ Data gets passed around between the datastore, processor, analyser, etc. in the form of an {Dragonfly::ExtendedTempObject ExtendedTempObject}.
11
+
12
+ temp_object = app.create_object(File.new('path/to/image.png'))
13
+
14
+ This object will have any methods which have been registered with the analyser. For example, registering
15
+ the {Dragonfly::Analysis::RMagickAnalyser rmagick analyser}
16
+
17
+ app.register_analyser(Dragonfly::Analysis::RMagickAnalyser)
18
+
19
+ give us the methods `width`, `height`, `depth` and `number_of_colours` (or `number_of_colors`).
20
+
21
+ temp_object.width # => 280
22
+ # ...etc.
23
+
24
+ Registering the {Dragonfly::Analysis::FileCommandAnalyser 'file' command analyser}
25
+
26
+ app.register_analyser(Dragonfly::Analysis::FileCommandAnalyser)
27
+
28
+ gives us the method `mime_type`, which is necessary for the app to serve the file properly.
29
+
30
+ temp_object.mime_type # => 'image/png'
31
+
32
+ As the file command analyser is {Dragonfly::Configurable configurable}, we can configure it as we register it if we need to
33
+
34
+ app.register_analyser(Dragonfly::Analysis::FileCommandAnalyser) do |a|
35
+ a.file_command = '/usr/bin/file'
36
+ end
37
+
38
+ The saved configuration {Dragonfly::RMagickConfiguration RMagickConfiguration} registers the above two analysers automatically.
39
+
40
+ Custom Analysers
41
+ ----------------
42
+
43
+ To register a custom analyser, derive from {Dragonfly::Analysis::Base Analysis::Base} and register.
44
+ Each method takes the temp_object as its argument.
45
+
46
+ class MyAnalyser < Dragonfly::Analysis::Base
47
+
48
+ def coolness(temp_object)
49
+ # use temp_object.data, temp_object.path, etc...
50
+ temp_object.size / 30
51
+ end
52
+
53
+ # ... add as many methods as you wish
54
+
55
+ end
56
+
57
+ app.register_analyser(MyAnalyser)
58
+
59
+ temp_object = app.create_object(File.new('path/to/image.png'))
60
+
61
+ temp_object.coolness # => 2067
62
+
63
+ You can register multiple analysers.
@@ -0,0 +1,33 @@
1
+ Data Storage
2
+ ============
3
+
4
+ Each dragonfly app has a datastore.
5
+
6
+ Dragonfly::App[:my_app_name].datastore
7
+
8
+ By default it uses the file data store, but you can configure it to use
9
+ a custom data store (e.g. S3, SQL, CouchDB, etc.) by registering one with the correct interface, namely
10
+ having `store`, `retrieve` and `destroy`.
11
+
12
+ class MyDataStore < Dragonfly::DataStorage::Base
13
+
14
+ def store(temp_object)
15
+ # ... use temp_object.data, temp_object.file, or temp_object.path and store
16
+ 'return_some_unique_uid'
17
+ end
18
+
19
+ def retrieve(uid)
20
+ # find the content and return either a data string, a file, a tempfile or a Dragonfly::TempObject
21
+ end
22
+
23
+ def destroy(uid)
24
+ # find the content and destroy
25
+ end
26
+
27
+ end
28
+
29
+ You can now configure the app to use this datastore like so
30
+
31
+ Dragonfly::App[:my_app_name].datastore = MyDataStore.new
32
+
33
+ If you want your datastore to be configurable, you can include the {Dragonfly::Configurable Configurable} module.
@@ -0,0 +1,58 @@
1
+ Encoding
2
+ ========
3
+
4
+ 'Encoding' encapsulates the idea of a format (e.g. 'png', 'jpeg', 'doc', 'txt', etc.), and any encoding options
5
+ (e.g. bit_rate, etc.).
6
+
7
+ It is up to the encoder to modify the data according to the requested format (note the format is not the same as the mime-type;
8
+ the mime-type is detected by the analyser).
9
+
10
+ The encoder needs to implement a single method, `encode`, which takes a {Dragonfly::ExtendedTempObject temp_object}, a format, and encoding options as arguments.
11
+
12
+ def encode(temp_object, format, encoding={})
13
+ #... encode and return a String, File, Tempfile or TempObject
14
+ end
15
+
16
+ Let's say we register the {Dragonfly::Encoding::RMagickEncoder rmagick encoder} to our dragonfly app called 'images'
17
+
18
+ app = Dragonfly::App[:images]
19
+ app.register_encoder(Dragonfly::Encoding::RMagickEncoder)
20
+
21
+ Then we can encode {Dragonfly::ExtendedTempObject temp_objects} to formats recognised by the RMagickEncoder
22
+
23
+ temp_object = app.create_object(File.new('path/to/image.png'))
24
+
25
+ temp_object.encode(:png) # => returns a new temp_object with data encoded as 'png'
26
+ temp_object.encode!(:gif) # => encodes its own data as a 'png'
27
+
28
+ temp_object.encode(:doc) # => throws :unable_to_handle
29
+
30
+ The saved configuration {Dragonfly::RMagickConfiguration RMagickConfiguration} registers the above encoder automatically.
31
+
32
+ Custom Encoders
33
+ ---------------
34
+
35
+ To register a custom encoder, derive from {Dragonfly::Encoding::Base Encoding::Base} and register.
36
+ As described above, you need to implement the `encode` method.
37
+ If the encoder can't handle a format, it should throw `:unable_to_handle`, and control will pass to the previously
38
+ registered encoder, and so on.
39
+
40
+ class MyEncoder < Dragonfly::Encoding::Base
41
+
42
+ def encode(temp_object, format, encoding={})
43
+ if format.to_s == 'yo'
44
+ #... encode and return a String, File, Tempfile or TempObject
45
+ else
46
+ throw :unable_to_handle
47
+ end
48
+ end
49
+
50
+ end
51
+
52
+ app.register_encoder(MyEncoder)
53
+
54
+ If the encoder is {Dragonfly::Configurable configurable}, we can configure it as we register it if we need to
55
+
56
+ app.register_encoder(MyEncoder) do |e|
57
+ e.some_attribute = 'hello'
58
+ end
@@ -0,0 +1,114 @@
1
+ Getting Started
2
+ ===============
3
+
4
+ Below is a general guide for setting up and using Dragonfly.
5
+
6
+ For setting up with Ruby on Rails, see {file:UsingWithRails UsingWithRails}.
7
+
8
+ For more info about using Rack applications, see the docs at {http://rack.rubyforge.org/}
9
+
10
+ Running as a Standalone Rack Application
11
+ ----------------------------------------
12
+
13
+ Basic usage of a dragonfly app involves storing data (e.g. images),
14
+ then serving that data, either in its original form, processed, encoded or both.
15
+
16
+ A basic rackup file `config.ru`:
17
+
18
+ require 'rubygems'
19
+ require 'dragonfly'
20
+
21
+ Dragonfly::App[:my_app_name].configure do |c|
22
+ # ...
23
+ c.some_attribute = 'blah'
24
+ # ...
25
+ end
26
+
27
+ run Dragonfly:App[:my_app_name]
28
+
29
+ As you can see, this involves instantiating an app, configuring it (how data is stored,
30
+ processing, encoding, etc.), then running it.
31
+
32
+ You can have multiple dragonfly apps, each with their own configuration.
33
+ Each app has a name, and is referred to by that name.
34
+
35
+ Dragonfly::App[:images] # ===> Creates an app called 'images'
36
+ Dragonfly::App[:images] # ===> Refers to the already created app 'images'
37
+
38
+ Example: Using to serve resized images
39
+ --------------------------------------
40
+
41
+ `config.ru`:
42
+
43
+ require 'rubygems'
44
+ require 'dragonfly'
45
+ require 'rack/cache'
46
+
47
+ app = Dragonfly::App[:images]
48
+ app.configure_with(Dragonfly::RMagickConfiguration)
49
+
50
+ use Rack::Cache,
51
+ :verbose => true,
52
+ :metastore => 'file:/var/cache/rack/meta',
53
+ :entitystore => 'file:/var/cache/rack/body'
54
+
55
+ run app
56
+
57
+ This configures the app to use the RMagick {Dragonfly::Processing::RMagickProcessor processor},
58
+ {Dragonfly::Encoding::RMagickEncoder encoder} and {Dragonfly::Analysis::RMagickAnalyser analyser}.
59
+ By default the {Dragonfly::DataStorage::FileDataStore file data store} is used.
60
+
61
+ Elsewhere in our code:
62
+
63
+ app = Dragonfly::App[:images]
64
+
65
+ # Store
66
+ uid = app.store(File.new('path/to/image.png')) # ===> returns a unique uid for that image, "2009/11/29/145804_file"
67
+
68
+ # Get the url for a thumbnail
69
+ url = app.url_for(uid, '30x30', :gif) # ===> "/2009/11/29/145804_file.gif?m=resize&o[geometry]=30x30"
70
+
71
+ Now when we visit the url `/2009/11/29/145804_file.gif?m=resize&o[geometry]=30x30` in the browser, we get the resized
72
+ image!
73
+
74
+ Caching
75
+ -------
76
+ Processing and encoding can be an expensive operation. The first time we visit the url,
77
+ the image is processed, and there might be a short delay and getting the response.
78
+
79
+ However, dragonfly apps send `Cache-Control` and `ETag` headers in the response, so we can easily put a caching
80
+ proxy like {http://varnish.projects.linpro.no Varnish}, {http://www.squid-cache.org Squid},
81
+ {http://tomayko.com/src/rack-cache/ Rack::Cache}, etc. in front of the app.
82
+
83
+ In the example above, we've put the middleware {http://tomayko.com/src/rack-cache/ Rack::Cache} in front of the app.
84
+ So although the first time we access the url the content is processed, every time after that it is received from the
85
+ cache, and is served super quick!
86
+
87
+ Avoiding Denial-of-service attacks
88
+ ----------------------------------
89
+ The url given above, `/2009/11/29/145804_file.gif?m=resize&o[geometry]=30x30`, could easily be modified to
90
+ generate all different sizes of thumbnails, just by changing the size, e.g.
91
+
92
+ `/2009/11/29/145804_file.gif?m=resize&o[geometry]=30x31`,
93
+
94
+ `/2009/11/29/145804_file.gif?m=resize&o[geometry]=30x32`,
95
+
96
+ etc.
97
+
98
+ Therefore the app can protect the url by generating a unique sha from a secret specified by you
99
+
100
+ Dragonfly::App[:images].url_handler.configure do |c|
101
+ c.protect_from_dos_attacks = true # Actually this is true by default
102
+ c.secret = 'You should supply some random secret here'
103
+ end
104
+
105
+ Then the required urls become something more like
106
+
107
+ `/2009/12/10/215214_file.gif?m=resize&o[geometry]=30x30&s=aa78e877ad3f6bc9`,
108
+
109
+ with a sha parameter on the end.
110
+ If we try to hack this url to get a different thumbnail,
111
+
112
+ `/2009/12/10/215214_file.gif?m=resize&o[geometry]=30x31&s=aa78e877ad3f6bc9`,
113
+
114
+ then we get a 400 (bad parameters) error.
@@ -0,0 +1,58 @@
1
+ Processing
2
+ ==========
3
+
4
+ Processing is changing content in some way, and does not involve encoding.
5
+ For example, resizing an image is classed as processing, whereas converting it from 'png' to 'jpeg' is classed as encoding.
6
+
7
+ All processing jobs are defined by a processing method and a processing options hash (which is passed to the method).
8
+
9
+ Let's say we have a dragonfly app called 'images'
10
+
11
+ app = Dragonfly::App[:images]
12
+
13
+ Data gets passed around between the datastore, processor, analyser, etc. in the form of an {Dragonfly::ExtendedTempObject ExtendedTempObject}.
14
+
15
+ temp_object = app.create_object(File.new('path/to/image.png'))
16
+
17
+ We can process this object with any of the methods which have been registered with the app's processor.
18
+ For example, registering the {Dragonfly::Processing::RMagickProcessor rmagick processor}
19
+
20
+ app.register_processor(Dragonfly::Processing::RMagickProcessor)
21
+
22
+ give us the processing methods `resize`, `crop`, `resize_and_crop`, `rotate`, etc.
23
+
24
+ temp_object.process(:resize, :geometry => '30x30!') # => returns a new temp_object with width x height = 30x30
25
+ temp_object.process!(:resize, :geometry => '30x30!') # => resizes its own data
26
+
27
+ The saved configuration {Dragonfly::RMagickConfiguration RMagickConfiguration} registers the above processor automatically.
28
+
29
+ Custom Processing
30
+ -----------------
31
+
32
+ To register a custom processor, derive from {Dragonfly::Processing::Base Processing::Base} and register.
33
+ Each method takes the temp_object, and the (optional) processing options hash as its argument.
34
+
35
+ class MyProcessor < Dragonfly::Processing::Base
36
+
37
+ def black_and_white(temp_object, opts={})
38
+ # use temp_object.data, temp_object.path, etc...
39
+ # ...process and return a String, File, Tempfile or TempObject
40
+ end
41
+
42
+ # ... add as many methods as you wish
43
+
44
+ end
45
+
46
+ app.register_processor(MyProcessor)
47
+
48
+ temp_object = app.create_object(File.new('path/to/image.png'))
49
+
50
+ temp_object.process(:black_and_white, :some => 'option')
51
+
52
+ You can register multiple processors.
53
+
54
+ As with analysers and encoders, if the processor is {Dragonfly::Configurable configurable}, we can configure it as we register it if we need to
55
+
56
+ app.register_processor(MyProcessor) do |p|
57
+ p.some_attribute = 'hello'
58
+ end
@@ -0,0 +1,118 @@
1
+ Shortcuts
2
+ =========
3
+
4
+ When you call {Dragonfly::App#fetch fetch}, {Dragonfly::ExtendedTempObject#transform transform},
5
+ {Dragonfly::UrlHandler#url_for url_for}, (or {Dragonfly::ActiveRecordExtensions::Attachment#url url} on an ActiveRecord attachment), you can specify all the job parameters
6
+ necessary to fetch the appropriate content.
7
+
8
+ The parameters are the following:
9
+
10
+ - `:uid` - the uid used by the datastore to retrieve the data (was returned when stored)
11
+ - `:processing_method` - the method used to do the processing
12
+ - `:processing_options` - options hash passed to the processing method
13
+ - `:format` - the format to encode the data with, e.g. 'png'
14
+ - `:encoding` - an options hash passed to the encoder for other options, e.g. bitrate, etc.
15
+
16
+ Say we have an app configured with the {Dragonfly::RMagickConfiguration RMagickConfiguration}
17
+
18
+ app = Dragonfly::App[:my_app]
19
+ app.configure_with(Dragonfly::RMagickConfiguration)
20
+
21
+ we can call things like
22
+
23
+ app.fetch 'some_uid',
24
+ :processing_method => :resize_and_crop,
25
+ :processing_options => {:width => 100, :height => 50, :gravity => 'ne'},
26
+ :format => :jpg,
27
+ :encoding => {:some => 'option'} # => gets a processed and encoded temp_object
28
+
29
+ app.url_for 'some_uid',
30
+ :processing_method => :resize_and_crop,
31
+ :processing_options => {:width => 100, :height => 50, :gravity => 'ne'},
32
+ :format => :jpg,
33
+ :encoding => {:some => 'option'} # => "/images/some_uid.tif?m=resize_and_crop&o[width]=...."
34
+
35
+ YIKES!!!!!
36
+
37
+ That's an awful lot of code every time, especially if we reuse the same parameters over and over.
38
+ That's why for the arguments after the uid, you can register {Dragonfly::Parameters parameter shortcuts}.
39
+
40
+ Simple shortcuts
41
+ ----------------
42
+ If we were to use the parameters in the example above a number of times, we could register a simple named shortcut, say 'thumb', using a symbol.
43
+
44
+ app.parameters.add_shortcut(:thumb,
45
+ :processing_method => :resize_and_crop,
46
+ :processing_options => {:width => 100, :height => 50, :gravity => 'ne'},
47
+ :format => :jpg,
48
+ :encoding => {:some => 'option'}
49
+ )
50
+
51
+ We can now use this named shortcut, `:thumb`, in place of the parameters elsewhere
52
+
53
+ app.fetch 'some_uid', :thumb # => gets a processed and encoded temp_object as before
54
+ app.url_for 'some_uid', :thumb # => "/images/some_uid.tif?m=resize_and_crop&o[width]=....", as before
55
+
56
+ Complex shortcuts
57
+ -----------------
58
+ Rather than create a new named shortcut for every different set of parameters, we can make life easier by defining shortcuts
59
+ which match a set of arguments, then form parameters from them.
60
+
61
+ For example, take the shortcut
62
+
63
+ app.parameters.add_shortcut(/\d+x\d+/, Symbol) do |geometry, format|
64
+ {
65
+ :processing_method => :my_resize_method,
66
+ :processing_options => {:geometry => geometry},
67
+ :format => format
68
+ }
69
+ end
70
+
71
+ It will match where there are two arguments, a string matching `/\d+x\d+/` and a symbol, and convert to a hash of job parameters.
72
+ Note that the matching arguments are yielded to the block.
73
+ So, for example,
74
+
75
+ app.url_for('some_uid', '100x50', :jpg)
76
+
77
+ generates the same url as if we'd used
78
+
79
+ app.url_for 'some_uid',
80
+ :processing_method => :my_resize_method,
81
+ :processing_options => {:geometry => 100},
82
+ :format => :jpg
83
+
84
+ The following examples would not match
85
+
86
+ app.url_for('some_uid', '100xg50', :jpg) # '100xg50' doesn't match /\d+x\d+/
87
+ app.url_for('some_uid', '100x50', 'jpg') # 'jpg' is not a Symbol
88
+ app.url_for('some_uid', '100x50') # not enough arguments
89
+ app.url_for('some_uid', '100x50', :jpg, 4) # too many arguments
90
+
91
+ The arguments are matched using the `===` operator (as used in `case` statements), which is why, for example, `:jpg` matches `Symbol`.
92
+
93
+ Regexp shortcuts
94
+ ----------------
95
+ If we register a complex shortcut as a single regexp, then the match data is also yielded, for convenience.
96
+
97
+ app.parameters.add_shortcut(/(\d+)x(\d+)/) do |geometry, match_data|
98
+ {
99
+ :processing_method => :my_resize_method,
100
+ :processing_options => {:width => match_data[1], :height => match_data[2]}
101
+ }
102
+ end
103
+
104
+
105
+ Default parameters
106
+ ------------------
107
+ If we've configured a default parameter, e.g.
108
+
109
+ app.parameters.default_format = :jpg
110
+
111
+ then this will be used in these methods whenever that parameter is not given, such as in the example above.
112
+
113
+ Avoiding processing/encoding
114
+ ----------------------------
115
+ If `:processing_method` is set to nil, then no processing takes place when methods like {Dragonfly::App#fetch fetch}, {Dragonfly::ExtendedTempObject#transform transform},
116
+ {Dragonfly::UrlHandler#url_for url_for}, and {Dragonfly::ActiveRecordExtensions::Attachment#url url} are called.
117
+
118
+ Similarly, if `:format` is set to nil, then no encoding takes place.