dragonfly 0.1.0

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 (75) hide show
  1. data/.gitignore +7 -0
  2. data/LICENSE +20 -0
  3. data/README.markdown +95 -0
  4. data/Rakefile +60 -0
  5. data/VERSION +1 -0
  6. data/config.rb +7 -0
  7. data/config.ru +10 -0
  8. data/dragonfly-rails.gemspec +41 -0
  9. data/dragonfly.gemspec +137 -0
  10. data/features/dragonfly.feature +38 -0
  11. data/features/steps/common_steps.rb +8 -0
  12. data/features/steps/dragonfly_steps.rb +39 -0
  13. data/features/support/env.rb +25 -0
  14. data/features/support/image_helpers.rb +9 -0
  15. data/generators/dragonfly_app/USAGE +16 -0
  16. data/generators/dragonfly_app/dragonfly_app_generator.rb +54 -0
  17. data/generators/dragonfly_app/templates/custom_processing.erb +13 -0
  18. data/generators/dragonfly_app/templates/initializer.erb +7 -0
  19. data/generators/dragonfly_app/templates/metal_file.erb +32 -0
  20. data/irbrc.rb +20 -0
  21. data/lib/dragonfly/active_record_extensions/attachment.rb +117 -0
  22. data/lib/dragonfly/active_record_extensions/class_methods.rb +41 -0
  23. data/lib/dragonfly/active_record_extensions/instance_methods.rb +28 -0
  24. data/lib/dragonfly/active_record_extensions/validations.rb +19 -0
  25. data/lib/dragonfly/active_record_extensions.rb +12 -0
  26. data/lib/dragonfly/analysis/analyser.rb +45 -0
  27. data/lib/dragonfly/analysis/base.rb +10 -0
  28. data/lib/dragonfly/analysis/r_magick_analyser.rb +40 -0
  29. data/lib/dragonfly/app.rb +85 -0
  30. data/lib/dragonfly/app_configuration.rb +9 -0
  31. data/lib/dragonfly/configurable.rb +113 -0
  32. data/lib/dragonfly/core_ext/object.rb +8 -0
  33. data/lib/dragonfly/data_storage/base.rb +19 -0
  34. data/lib/dragonfly/data_storage/file_data_store.rb +72 -0
  35. data/lib/dragonfly/data_storage.rb +9 -0
  36. data/lib/dragonfly/encoding/base.rb +13 -0
  37. data/lib/dragonfly/encoding/r_magick_encoder.rb +17 -0
  38. data/lib/dragonfly/extended_temp_object.rb +94 -0
  39. data/lib/dragonfly/middleware.rb +27 -0
  40. data/lib/dragonfly/parameters.rb +152 -0
  41. data/lib/dragonfly/processing/processor.rb +14 -0
  42. data/lib/dragonfly/processing/r_magick_processor.rb +71 -0
  43. data/lib/dragonfly/r_magick_configuration.rb +47 -0
  44. data/lib/dragonfly/rails/images.rb +20 -0
  45. data/lib/dragonfly/temp_object.rb +118 -0
  46. data/lib/dragonfly/url_handler.rb +148 -0
  47. data/lib/dragonfly.rb +33 -0
  48. data/samples/beach.png +0 -0
  49. data/samples/egg.png +0 -0
  50. data/samples/round.gif +0 -0
  51. data/samples/taj.jpg +0 -0
  52. data/spec/argument_matchers.rb +29 -0
  53. data/spec/dragonfly/active_record_extensions/attachment_spec.rb +8 -0
  54. data/spec/dragonfly/active_record_extensions/initializer.rb +1 -0
  55. data/spec/dragonfly/active_record_extensions/migration.rb +21 -0
  56. data/spec/dragonfly/active_record_extensions/model_spec.rb +400 -0
  57. data/spec/dragonfly/active_record_extensions/models.rb +2 -0
  58. data/spec/dragonfly/active_record_extensions/spec_helper.rb +23 -0
  59. data/spec/dragonfly/analysis/analyser_spec.rb +85 -0
  60. data/spec/dragonfly/analysis/r_magick_analyser_spec.rb +35 -0
  61. data/spec/dragonfly/app_spec.rb +69 -0
  62. data/spec/dragonfly/configurable_spec.rb +193 -0
  63. data/spec/dragonfly/data_storage/data_store_spec.rb +47 -0
  64. data/spec/dragonfly/data_storage/file_data_store_spec.rb +93 -0
  65. data/spec/dragonfly/extended_temp_object_spec.rb +67 -0
  66. data/spec/dragonfly/middleware_spec.rb +44 -0
  67. data/spec/dragonfly/parameters_spec.rb +293 -0
  68. data/spec/dragonfly/processing/rmagick_processor_spec.rb +148 -0
  69. data/spec/dragonfly/temp_object_spec.rb +233 -0
  70. data/spec/dragonfly/url_handler_spec.rb +246 -0
  71. data/spec/dragonfly_spec.rb +4 -0
  72. data/spec/image_matchers.rb +31 -0
  73. data/spec/simple_matchers.rb +14 -0
  74. data/spec/spec_helper.rb +19 -0
  75. metadata +160 -0
@@ -0,0 +1,117 @@
1
+ module Dragonfly
2
+ module ActiveRecordExtensions
3
+
4
+ class PendingUID; def to_s; 'PENDING'; end; end
5
+
6
+ class Attachment
7
+
8
+ def initialize(app, parent_model, attribute_name)
9
+ @app, @parent_model, @attribute_name = app, parent_model, attribute_name
10
+ end
11
+
12
+ def assign(value)
13
+ if value.nil?
14
+ self.uid = nil
15
+ reset_magic_attributes
16
+ else
17
+ self.temp_object = app.create_object(value)
18
+ self.uid = PendingUID.new
19
+ set_magic_attributes
20
+ end
21
+ value
22
+ end
23
+
24
+ def destroy!
25
+ app.datastore.destroy(previous_uid) if previous_uid
26
+ rescue DataStorage::DataNotFound => e
27
+ app.log.warn("*** WARNING ***: tried to destroy data with uid #{previous_uid}, but got error: #{e}")
28
+ end
29
+
30
+ def fetch(*args)
31
+ app.fetch(uid, *args)
32
+ end
33
+
34
+ def save!
35
+ if changed?
36
+ destroy!
37
+ self.uid = app.datastore.store(temp_object)
38
+ end
39
+ end
40
+
41
+ def size
42
+ temp_object.size
43
+ end
44
+
45
+ def temp_object
46
+ if @temp_object
47
+ @temp_object
48
+ elsif been_persisted?
49
+ @temp_object = fetch
50
+ end
51
+ end
52
+
53
+ def to_value
54
+ self if been_assigned?
55
+ end
56
+
57
+ def url(*args)
58
+ unless uid.nil? || uid.is_a?(PendingUID)
59
+ app.url_for(uid, *args)
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def been_assigned?
66
+ uid
67
+ end
68
+
69
+ def been_persisted?
70
+ uid && !uid.is_a?(PendingUID)
71
+ end
72
+
73
+ def changed?
74
+ parent_model.send("#{attribute_name}_uid_changed?")
75
+ end
76
+
77
+ def uid=(uid)
78
+ parent_model.send("#{attribute_name}_uid=", uid)
79
+ end
80
+
81
+ def uid
82
+ parent_model.send("#{attribute_name}_uid")
83
+ end
84
+
85
+ def previous_uid
86
+ parent_model.send("#{attribute_name}_uid_was")
87
+ end
88
+
89
+ attr_reader :app, :parent_model, :attribute_name
90
+
91
+ attr_writer :temp_object
92
+
93
+ def analyser
94
+ app.analyser
95
+ end
96
+
97
+ def magic_attributes
98
+ parent_model.class.column_names.select { |name|
99
+ name =~ /^#{attribute_name}_(.+)$/ &&
100
+ analyser.has_analysis_method?($1) || $1 == 'size'
101
+ }
102
+ end
103
+
104
+ def set_magic_attributes
105
+ magic_attributes.each do |attribute|
106
+ method = attribute.sub("#{attribute_name}_", '')
107
+ parent_model.send("#{attribute}=", temp_object.send(method))
108
+ end
109
+ end
110
+
111
+ def reset_magic_attributes
112
+ magic_attributes.each{|attribute| parent_model.send("#{attribute}=", nil) }
113
+ end
114
+
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,41 @@
1
+ module Dragonfly
2
+ module ActiveRecordExtensions
3
+ module ClassMethods
4
+
5
+ include Validations
6
+
7
+ def register_dragonfly_app(accessor_prefix, app)
8
+ metaclass.class_eval do
9
+
10
+ # Defines e.g. 'image_accessor' for any activerecord class body
11
+ define_method "#{accessor_prefix}_accessor" do |attribute|
12
+
13
+ before_save :save_attached_files unless before_save_callback_chain.find(:save_attached_files)
14
+ before_destroy :destroy_attached_files unless before_destroy_callback_chain.find(:destroy_attached_files)
15
+
16
+ # Register the new attribute
17
+ dragonfly_apps_for_attributes[attribute] = app
18
+
19
+ # Define the setter for the attribute
20
+ define_method "#{attribute}=" do |value|
21
+ attachments[attribute].assign(value)
22
+ end
23
+
24
+ # Define the getter for the attribute
25
+ define_method attribute do
26
+ attachments[attribute].to_value
27
+ end
28
+
29
+ end
30
+
31
+ end
32
+ app
33
+ end
34
+
35
+ def dragonfly_apps_for_attributes
36
+ @dragonfly_apps_for_attributes ||= {}
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ module Dragonfly
2
+ module ActiveRecordExtensions
3
+ module InstanceMethods
4
+
5
+ def attachments
6
+ @attachments ||= self.class.dragonfly_apps_for_attributes.inject({}) do |hash, (attribute, app)|
7
+ hash[attribute] = Attachment.new(app, self, attribute)
8
+ hash
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def save_attached_files
15
+ attachments.each do |attribute, attachment|
16
+ attachment.save!
17
+ end
18
+ end
19
+
20
+ def destroy_attached_files
21
+ attachments.each do |attribute, attachment|
22
+ attachment.destroy!
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ module Dragonfly
2
+ module ActiveRecordExtensions
3
+ module Validations
4
+
5
+ def validates_mime_type_of(*args)
6
+ opts = args.last
7
+ raise ArgumentError, "you must provide either :in => [(mime-types)] or :as => '(mime-type)' to validates_mime_type_of" unless opts.is_a?(::Hash) &&
8
+ (allowed_mime_types = opts[:in] || [opts[:as]])
9
+ validates_each(*args) do |record, attr, attachment|
10
+ if attachment
11
+ mime_type = attachment.temp_object.mime_type
12
+ record.errors.add attr, "doesn't have the correct MIME-type. It needs to be #{'one of ' if allowed_mime_types.length > 1}'#{allowed_mime_types.join('\', \'')}', but was '#{mime_type || 'unknown'}'" unless allowed_mime_types.include?(mime_type)
13
+ end
14
+ end
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,12 @@
1
+ module Dragonfly
2
+ module ActiveRecordExtensions
3
+
4
+ def self.extended(klass)
5
+ unless klass.include?(InstanceMethods)
6
+ klass.extend(ClassMethods)
7
+ klass.class_eval{ include InstanceMethods }
8
+ end
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ module Dragonfly
2
+ module Analysis
3
+ class Analyser
4
+
5
+ def initialize
6
+ @analysers = []
7
+ end
8
+
9
+ include Configurable
10
+
11
+ def register(analyser)
12
+ analysers.unshift(analyser)
13
+ end
14
+ configuration_method :register
15
+
16
+ def mime_type(temp_object)
17
+ analysers.each do |analyser|
18
+ mime_type = analyser.mime_type(temp_object)
19
+ return mime_type if mime_type
20
+ end
21
+ nil
22
+ end
23
+
24
+ def analysis_methods
25
+ analysers.map{|a| a.public_methods(false) }.flatten.uniq
26
+ end
27
+
28
+ def has_analysis_method?(method)
29
+ analysis_methods.include?(method.to_s)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :analysers
35
+
36
+ def method_missing(meth, *args)
37
+ analysers.each do |analyser|
38
+ return analyser.send(meth, *args) if analyser.respond_to?(meth)
39
+ end
40
+ super
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,10 @@
1
+ module Dragonfly
2
+ module Analysis
3
+ class Base
4
+
5
+ def mime_type(temp_object)
6
+ end
7
+
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,40 @@
1
+ require 'rmagick'
2
+ require 'mime/types'
3
+
4
+ module Dragonfly
5
+ module Analysis
6
+
7
+ class RMagickAnalyser < Base
8
+
9
+ def width(image)
10
+ rmagick_image(image).columns
11
+ end
12
+
13
+ def height(image)
14
+ rmagick_image(image).rows
15
+ end
16
+
17
+ def mime_type(image)
18
+ MIME::Types.type_for(rmagick_image(image).format).first.to_s
19
+ rescue Magick::ImageMagickError
20
+ end
21
+
22
+ def depth(image)
23
+ rmagick_image(image).depth
24
+ end
25
+
26
+ def number_of_colours(image)
27
+ rmagick_image(image).number_colors
28
+ end
29
+ alias number_of_colors number_of_colours
30
+
31
+ private
32
+
33
+ def rmagick_image(image)
34
+ Magick::Image.from_blob(image.data).first
35
+ end
36
+
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,85 @@
1
+ require 'logger'
2
+ require 'forwardable'
3
+
4
+ module Dragonfly
5
+ class App
6
+
7
+ class << self
8
+
9
+ private :new # Hide 'new' - need to use 'instance'
10
+
11
+ def instance(name)
12
+ apps[name] ||= new
13
+ end
14
+
15
+ alias [] instance
16
+
17
+ private
18
+
19
+ def apps
20
+ @apps ||= {}
21
+ end
22
+
23
+ end
24
+
25
+ def initialize
26
+ @analyser = Analysis::Analyser.new
27
+ @processor = Processing::Processor.new
28
+ @parameters_class = Class.new(Parameters)
29
+ @url_handler = UrlHandler.new(@parameters_class)
30
+ initialize_temp_object_class
31
+ end
32
+
33
+ attr_reader :analyser,
34
+ :processor,
35
+ :encoder,
36
+ :url_handler,
37
+ :parameters_class,
38
+ :temp_object_class
39
+
40
+ alias parameters parameters_class
41
+
42
+ # Just for convenience so the user doesn't have to use url_handler
43
+ extend Forwardable
44
+ def_delegator :url_handler, :url_for
45
+
46
+ include Configurable
47
+
48
+ configurable_attr :datastore do DataStorage::FileDataStore.new end
49
+ configurable_attr :encoder do Encoding::Base.new end
50
+ configurable_attr :log do Logger.new('/var/tmp/dragonfly.log') end
51
+ configurable_attr :cache_duration, 3000
52
+
53
+ def call(env)
54
+ parameters = url_handler.url_to_parameters(env['PATH_INFO'], env['QUERY_STRING'])
55
+ temp_object = fetch(parameters.uid, parameters)
56
+ [200, {
57
+ "Content-Type" => temp_object.mime_type,
58
+ "Content-Length" => temp_object.size.to_s,
59
+ "ETag" => parameters.unique_signature,
60
+ "Cache-Control" => "public, max-age=#{cache_duration}"
61
+ }, temp_object]
62
+ rescue UrlHandler::IncorrectSHA, UrlHandler::SHANotGiven => e
63
+ [400, {"Content-Type" => "text/plain"}, [e.message]]
64
+ rescue UrlHandler::UnknownUrl, DataStorage::DataNotFound => e
65
+ [404, {"Content-Type" => 'text/plain'}, [e.message]]
66
+ end
67
+
68
+ def fetch(uid, *args)
69
+ temp_object = temp_object_class.new(datastore.retrieve(uid))
70
+ temp_object.transform(*args)
71
+ end
72
+
73
+ def create_object(initialization_object)
74
+ temp_object_class.new(initialization_object)
75
+ end
76
+
77
+ private
78
+
79
+ def initialize_temp_object_class
80
+ @temp_object_class = Class.new(ExtendedTempObject)
81
+ @temp_object_class.app = self
82
+ end
83
+
84
+ end
85
+ end
@@ -0,0 +1,9 @@
1
+ module Dragonfly
2
+ class AppConfiguration
3
+
4
+ def apply_configuration(app)
5
+ raise NotImplementedError
6
+ end
7
+
8
+ end
9
+ end
@@ -0,0 +1,113 @@
1
+ module Dragonfly
2
+ module Configurable
3
+
4
+ # Exceptions
5
+ class BadConfigAttribute < StandardError; end
6
+
7
+ def self.included(klass)
8
+ klass.class_eval do
9
+ include Configurable::InstanceMethods
10
+ extend Configurable::ClassMethods
11
+
12
+ # These aren't included in InstanceMethods because we need access to 'klass'
13
+ # We can't just put them into InstanceMethods and use 'self.class' because
14
+ # this won't always point to the class in which we've included Configurable,
15
+ # e.g. if we've included it in an eigenclasse
16
+ define_method :configuration_hash do
17
+ @configuration_hash ||= klass.default_configuration.dup
18
+ end
19
+ private :configuration_hash
20
+
21
+ define_method :configuration_methods do
22
+ klass.configuration_methods
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ module InstanceMethods
29
+
30
+ def configure(&blk)
31
+ yield ConfigurationProxy.new(self)
32
+ end
33
+
34
+ def configure_with(configurer)
35
+ configurer.apply_configuration(self)
36
+ end
37
+
38
+ def configuration
39
+ configuration_hash.dup
40
+ end
41
+
42
+ def has_configuration_method?(method_name)
43
+ configuration_methods.include?(method_name.to_sym)
44
+ end
45
+
46
+ end
47
+
48
+ module ClassMethods
49
+
50
+ def default_configuration
51
+ @default_configuration ||= {}
52
+ end
53
+
54
+ def configuration_methods
55
+ @configuration_methods ||= []
56
+ end
57
+
58
+ private
59
+
60
+ def configurable_attr attribute, default=nil, &blk
61
+ default_configuration[attribute] = blk || default
62
+
63
+ # Define the reader
64
+ define_method(attribute) do
65
+ if configuration_hash[attribute].respond_to? :call
66
+ configuration_hash[attribute] = configuration_hash[attribute].call
67
+ end
68
+ configuration_hash[attribute]
69
+ end
70
+
71
+ # Define the writer
72
+ define_method("#{attribute}=") do |value|
73
+ configuration_hash[attribute] = value
74
+ end
75
+
76
+ configuration_method attribute
77
+ configuration_method "#{attribute}="
78
+ end
79
+
80
+ def configuration_method(*method_names)
81
+ configuration_methods.push(*method_names.map{|n| n.to_sym })
82
+ end
83
+
84
+ end
85
+
86
+ class ConfigurationProxy
87
+
88
+ def initialize(owner)
89
+ @owner = owner
90
+ end
91
+
92
+ def method_missing(method_name, *args, &block)
93
+ if owner.has_configuration_method?(method_name)
94
+ owner.send(method_name, *args, &block)
95
+ elsif nested_configurable?(method_name, *args)
96
+ owner.send(method_name, *args).configure(&block)
97
+ else
98
+ raise BadConfigAttribute, "You tried to configure using '#{method_name.inspect}', but the valid config attributes are #{owner.configuration_methods.map{|a| %('#{a.inspect}') }.sort.join(', ')}"
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ attr_reader :owner
105
+
106
+ def nested_configurable?(method, *args)
107
+ owner.respond_to?(method) && owner.send(method, *args).is_a?(Configurable)
108
+ end
109
+
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,8 @@
1
+ class Object
2
+
3
+ # Will eventually get this by cherry-picking from activesupport
4
+ def blank?
5
+ respond_to?(:empty?) ? empty? : !self
6
+ end
7
+
8
+ end
@@ -0,0 +1,19 @@
1
+ module Dragonfly
2
+ module DataStorage
3
+ class Base
4
+
5
+ def store(temp_object)
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def retrieve(uid)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ def destroy(uid)
14
+ raise NotImplementedError
15
+ end
16
+
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,72 @@
1
+ module Dragonfly
2
+ module DataStorage
3
+
4
+ class FileDataStore < Base
5
+
6
+ include Configurable
7
+
8
+ configurable_attr :root_path, '/var/tmp/dragonfly'
9
+
10
+ def store(temp_object)
11
+
12
+ suffix = if temp_object.name.blank?
13
+ 'file'
14
+ else
15
+ temp_object.name.sub(/\.[^.]*?$/, '')
16
+ end
17
+ relative_path = relative_storage_path(suffix)
18
+
19
+ begin
20
+ while File.exist?(storage_path = absolute_storage_path(relative_path))
21
+ relative_path = increment_path(relative_path)
22
+ end
23
+ FileUtils.mkdir_p File.dirname(storage_path) unless File.exist?(storage_path)
24
+ FileUtils.cp temp_object.path, storage_path
25
+ rescue Errno::EACCES => e
26
+ raise UnableToStore, e.message
27
+ end
28
+
29
+ relative_path
30
+ end
31
+
32
+ def retrieve(relative_path)
33
+ begin
34
+ File.new(absolute_storage_path(relative_path))
35
+ rescue Errno::ENOENT => e
36
+ raise DataNotFound, e.message
37
+ end
38
+ end
39
+
40
+ def destroy(relative_path)
41
+ FileUtils.rm absolute_storage_path(relative_path)
42
+ containing_directory = Pathname.new(relative_path).dirname
43
+ containing_directory.ascend do |relative_dir|
44
+ dir = absolute_storage_path(relative_dir)
45
+ FileUtils.rmdir dir if directory_empty?(dir)
46
+ end
47
+ rescue Errno::ENOENT => e
48
+ raise DataNotFound, e.message
49
+ end
50
+
51
+ private
52
+
53
+ def increment_path(path)
54
+ path.sub(/(_(\d+))?$/){ $1 ? "_#{$2.to_i+1}" : '_2' }
55
+ end
56
+
57
+ def relative_storage_path(suffix)
58
+ "#{Time.now.strftime '%Y/%m/%d/%H%M%S'}_#{suffix}"
59
+ end
60
+
61
+ def absolute_storage_path(relative_path)
62
+ File.join(root_path, relative_path)
63
+ end
64
+
65
+ def directory_empty?(path)
66
+ Dir.entries(path) == ['.','..']
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,9 @@
1
+ module Dragonfly
2
+ module DataStorage
3
+
4
+ # Exceptions
5
+ class DataNotFound < StandardError; end
6
+ class UnableToStore < StandardError; end
7
+
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Dragonfly
2
+ module Encoding
3
+
4
+ class Base
5
+
6
+ def encode(temp_object, format, options={})
7
+ raise NotImplementedError
8
+ end
9
+
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ require 'rmagick'
2
+
3
+ module Dragonfly
4
+ module Encoding
5
+
6
+ class RMagickEncoder < Base
7
+
8
+ def encode(image, format, encoding={})
9
+ encoded_image = Magick::Image.from_blob(image.data).first
10
+ encoded_image.format = format.to_s
11
+ encoded_image.to_blob
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+ end