fog-dragonfly 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of fog-dragonfly might be problematic. Click here for more details.
- data/.specopts +2 -0
- data/.yardopts +23 -0
- data/Gemfile +23 -0
- data/Gemfile.rails.2.3.5 +14 -0
- data/History.md +266 -0
- data/LICENSE +20 -0
- data/README.md +88 -0
- data/Rakefile +92 -0
- data/VERSION +1 -0
- data/config.ru +13 -0
- data/docs.watchr +1 -0
- data/dragonfly.gemspec +293 -0
- data/extra_docs/Analysers.md +108 -0
- data/extra_docs/Caching.md +23 -0
- data/extra_docs/Configuration.md +138 -0
- data/extra_docs/DataStorage.md +136 -0
- data/extra_docs/Encoding.md +96 -0
- data/extra_docs/GeneralUsage.md +121 -0
- data/extra_docs/Generators.md +102 -0
- data/extra_docs/Heroku.md +50 -0
- data/extra_docs/Index.md +36 -0
- data/extra_docs/MimeTypes.md +40 -0
- data/extra_docs/Models.md +266 -0
- data/extra_docs/Mongo.md +45 -0
- data/extra_docs/Processing.md +130 -0
- data/extra_docs/Rack.md +52 -0
- data/extra_docs/Rails2.md +55 -0
- data/extra_docs/Rails3.md +62 -0
- data/extra_docs/Sinatra.md +25 -0
- data/extra_docs/URLs.md +169 -0
- data/features/3.0.3.feature +8 -0
- data/features/images.feature +47 -0
- data/features/no_processing.feature +14 -0
- data/features/rails_2.3.5.feature +7 -0
- data/features/steps/common_steps.rb +8 -0
- data/features/steps/dragonfly_steps.rb +66 -0
- data/features/steps/rails_steps.rb +39 -0
- data/features/support/env.rb +40 -0
- data/fixtures/files/app/models/album.rb +3 -0
- data/fixtures/files/app/views/albums/new.html.erb +4 -0
- data/fixtures/files/app/views/albums/show.html.erb +4 -0
- data/fixtures/files/config/initializers/dragonfly.rb +4 -0
- data/fixtures/files/features/manage_album_images.feature +12 -0
- data/fixtures/files/features/step_definitions/image_steps.rb +15 -0
- data/fixtures/files/features/support/paths.rb +15 -0
- data/fixtures/files/features/text_images.feature +7 -0
- data/fixtures/rails_2.3.5/template.rb +10 -0
- data/fixtures/rails_3.0.3/template.rb +20 -0
- data/irbrc.rb +17 -0
- data/lib/dragonfly.rb +45 -0
- data/lib/dragonfly/active_model_extensions.rb +13 -0
- data/lib/dragonfly/active_model_extensions/attachment.rb +169 -0
- data/lib/dragonfly/active_model_extensions/class_methods.rb +45 -0
- data/lib/dragonfly/active_model_extensions/instance_methods.rb +28 -0
- data/lib/dragonfly/active_model_extensions/validations.rb +37 -0
- data/lib/dragonfly/analyser.rb +59 -0
- data/lib/dragonfly/analysis/file_command_analyser.rb +32 -0
- data/lib/dragonfly/analysis/image_magick_analyser.rb +47 -0
- data/lib/dragonfly/analysis/r_magick_analyser.rb +63 -0
- data/lib/dragonfly/app.rb +182 -0
- data/lib/dragonfly/config/heroku.rb +19 -0
- data/lib/dragonfly/config/image_magick.rb +41 -0
- data/lib/dragonfly/config/r_magick.rb +46 -0
- data/lib/dragonfly/config/rails.rb +17 -0
- data/lib/dragonfly/configurable.rb +119 -0
- data/lib/dragonfly/core_ext/object.rb +8 -0
- data/lib/dragonfly/core_ext/string.rb +9 -0
- data/lib/dragonfly/core_ext/symbol.rb +9 -0
- data/lib/dragonfly/data_storage.rb +9 -0
- data/lib/dragonfly/data_storage/file_data_store.rb +114 -0
- data/lib/dragonfly/data_storage/mongo_data_store.rb +82 -0
- data/lib/dragonfly/data_storage/s3data_store.rb +115 -0
- data/lib/dragonfly/encoder.rb +13 -0
- data/lib/dragonfly/encoding/image_magick_encoder.rb +57 -0
- data/lib/dragonfly/encoding/r_magick_encoder.rb +61 -0
- data/lib/dragonfly/function_manager.rb +69 -0
- data/lib/dragonfly/generation/hash_with_css_style_keys.rb +23 -0
- data/lib/dragonfly/generation/image_magick_generator.rb +140 -0
- data/lib/dragonfly/generation/r_magick_generator.rb +155 -0
- data/lib/dragonfly/generator.rb +9 -0
- data/lib/dragonfly/image_magick_utils.rb +81 -0
- data/lib/dragonfly/job.rb +371 -0
- data/lib/dragonfly/job_builder.rb +39 -0
- data/lib/dragonfly/job_definitions.rb +26 -0
- data/lib/dragonfly/job_endpoint.rb +15 -0
- data/lib/dragonfly/loggable.rb +28 -0
- data/lib/dragonfly/middleware.rb +34 -0
- data/lib/dragonfly/processing/image_magick_processor.rb +99 -0
- data/lib/dragonfly/processing/r_magick_processor.rb +126 -0
- data/lib/dragonfly/processor.rb +9 -0
- data/lib/dragonfly/r_magick_utils.rb +48 -0
- data/lib/dragonfly/rails/images.rb +22 -0
- data/lib/dragonfly/response.rb +82 -0
- data/lib/dragonfly/routed_endpoint.rb +40 -0
- data/lib/dragonfly/serializer.rb +32 -0
- data/lib/dragonfly/simple_cache.rb +23 -0
- data/lib/dragonfly/simple_endpoint.rb +63 -0
- data/lib/dragonfly/temp_object.rb +220 -0
- data/samples/beach.png +0 -0
- data/samples/egg.png +0 -0
- data/samples/round.gif +0 -0
- data/samples/sample.docx +0 -0
- data/samples/taj.jpg +0 -0
- data/spec/argument_matchers.rb +19 -0
- data/spec/dragonfly/active_model_extensions/active_model_setup.rb +97 -0
- data/spec/dragonfly/active_model_extensions/active_record_setup.rb +85 -0
- data/spec/dragonfly/active_model_extensions/model_spec.rb +723 -0
- data/spec/dragonfly/active_model_extensions/spec_helper.rb +11 -0
- data/spec/dragonfly/analyser_spec.rb +123 -0
- data/spec/dragonfly/analysis/file_command_analyser_spec.rb +57 -0
- data/spec/dragonfly/analysis/image_magick_analyser_spec.rb +15 -0
- data/spec/dragonfly/analysis/r_magick_analyser_spec.rb +27 -0
- data/spec/dragonfly/analysis/shared_analyser_spec.rb +51 -0
- data/spec/dragonfly/app_spec.rb +280 -0
- data/spec/dragonfly/config/r_magick_spec.rb +25 -0
- data/spec/dragonfly/configurable_spec.rb +220 -0
- data/spec/dragonfly/core_ext/string_spec.rb +17 -0
- data/spec/dragonfly/core_ext/symbol_spec.rb +17 -0
- data/spec/dragonfly/data_storage/data_store_spec.rb +76 -0
- data/spec/dragonfly/data_storage/file_data_store_spec.rb +169 -0
- data/spec/dragonfly/data_storage/mongo_data_store_spec.rb +38 -0
- data/spec/dragonfly/data_storage/s3_data_store_spec.rb +94 -0
- data/spec/dragonfly/deprecation_spec.rb +20 -0
- data/spec/dragonfly/encoding/image_magick_encoder_spec.rb +41 -0
- data/spec/dragonfly/encoding/r_magick_encoder_spec.rb +37 -0
- data/spec/dragonfly/function_manager_spec.rb +154 -0
- data/spec/dragonfly/generation/hash_with_css_style_keys_spec.rb +24 -0
- data/spec/dragonfly/generation/image_magick_generator_spec.rb +12 -0
- data/spec/dragonfly/generation/r_magick_generator_spec.rb +24 -0
- data/spec/dragonfly/generation/shared_generator_spec.rb +91 -0
- data/spec/dragonfly/image_magick_utils_spec.rb +16 -0
- data/spec/dragonfly/job_builder_spec.rb +37 -0
- data/spec/dragonfly/job_definitions_spec.rb +35 -0
- data/spec/dragonfly/job_endpoint_spec.rb +120 -0
- data/spec/dragonfly/job_spec.rb +773 -0
- data/spec/dragonfly/loggable_spec.rb +80 -0
- data/spec/dragonfly/middleware_spec.rb +68 -0
- data/spec/dragonfly/processing/image_magick_processor_spec.rb +29 -0
- data/spec/dragonfly/processing/r_magick_processor_spec.rb +26 -0
- data/spec/dragonfly/processing/shared_processing_spec.rb +215 -0
- data/spec/dragonfly/routed_endpoint_spec.rb +48 -0
- data/spec/dragonfly/serializer_spec.rb +61 -0
- data/spec/dragonfly/simple_cache_spec.rb +27 -0
- data/spec/dragonfly/simple_endpoint_spec.rb +89 -0
- data/spec/dragonfly/temp_object_spec.rb +352 -0
- data/spec/image_matchers.rb +47 -0
- data/spec/simple_matchers.rb +44 -0
- data/spec/spec_helper.rb +58 -0
- data/yard/handlers/configurable_attr_handler.rb +38 -0
- data/yard/setup.rb +15 -0
- data/yard/templates/default/fulldoc/html/css/common.css +107 -0
- data/yard/templates/default/layout/html/layout.erb +87 -0
- data/yard/templates/default/module/html/configuration_summary.erb +31 -0
- data/yard/templates/default/module/setup.rb +17 -0
- metadata +550 -0
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'RMagick'
|
2
|
+
|
3
|
+
module Dragonfly
|
4
|
+
module Generation
|
5
|
+
class RMagickGenerator
|
6
|
+
|
7
|
+
FONT_STYLES = {
|
8
|
+
'normal' => Magick::NormalStyle,
|
9
|
+
'italic' => Magick::ItalicStyle,
|
10
|
+
'oblique' => Magick::ObliqueStyle
|
11
|
+
}
|
12
|
+
|
13
|
+
FONT_STRETCHES = {
|
14
|
+
'normal' => Magick::NormalStretch,
|
15
|
+
'semi-condensed' => Magick::SemiCondensedStretch,
|
16
|
+
'condensed' => Magick::CondensedStretch,
|
17
|
+
'extra-condensed' => Magick::ExtraCondensedStretch,
|
18
|
+
'ultra-condensed' => Magick::UltraCondensedStretch,
|
19
|
+
'semi-expanded' => Magick::SemiExpandedStretch,
|
20
|
+
'expanded' => Magick::ExpandedStretch,
|
21
|
+
'extra-expanded' => Magick::ExtraExpandedStretch,
|
22
|
+
'ultra-expanded' => Magick::UltraExpandedStretch
|
23
|
+
}
|
24
|
+
|
25
|
+
FONT_WEIGHTS = {
|
26
|
+
'normal' => Magick::NormalWeight,
|
27
|
+
'bold' => Magick::BoldWeight,
|
28
|
+
'bolder' => Magick::BolderWeight,
|
29
|
+
'lighter' => Magick::LighterWeight,
|
30
|
+
'100' => 100,
|
31
|
+
'200' => 200,
|
32
|
+
'300' => 300,
|
33
|
+
'400' => 400,
|
34
|
+
'500' => 500,
|
35
|
+
'600' => 600,
|
36
|
+
'700' => 700,
|
37
|
+
'800' => 800,
|
38
|
+
'900' => 900
|
39
|
+
}
|
40
|
+
|
41
|
+
include RMagickUtils
|
42
|
+
include Configurable
|
43
|
+
configurable_attr :use_filesystem, true
|
44
|
+
|
45
|
+
def plasma(width, height, format='png')
|
46
|
+
image = Magick::Image.read("plasma:fractal"){self.size = "#{width}x#{height}"}.first
|
47
|
+
image.format = format.to_s
|
48
|
+
content = use_filesystem ? write_to_tempfile(image) : image.to_blob
|
49
|
+
image.destroy!
|
50
|
+
[
|
51
|
+
content,
|
52
|
+
{:format => format.to_sym, :name => "plasma.#{format}"}
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
def text(text_string, opts={})
|
57
|
+
opts = HashWithCssStyleKeys[opts]
|
58
|
+
|
59
|
+
draw = Magick::Draw.new
|
60
|
+
draw.gravity = Magick::CenterGravity
|
61
|
+
draw.text_antialias = true
|
62
|
+
|
63
|
+
# Font size
|
64
|
+
font_size = (opts[:font_size] || 12).to_i
|
65
|
+
|
66
|
+
# Scale up the text for better quality -
|
67
|
+
# it will be reshrunk at the end
|
68
|
+
s = scale_factor_for(font_size)
|
69
|
+
|
70
|
+
# Settings
|
71
|
+
draw.pointsize = font_size * s
|
72
|
+
draw.font = opts[:font] if opts[:font]
|
73
|
+
draw.font_family = opts[:font_family] if opts[:font_family]
|
74
|
+
draw.fill = opts[:color] if opts[:color]
|
75
|
+
draw.stroke = opts[:stroke_color] if opts[:stroke_color]
|
76
|
+
draw.font_style = FONT_STYLES[opts[:font_style]] if opts[:font_style]
|
77
|
+
draw.font_stretch = FONT_STRETCHES[opts[:font_stretch]] if opts[:font_stretch]
|
78
|
+
draw.font_weight = FONT_WEIGHTS[opts[:font_weight]] if opts[:font_weight]
|
79
|
+
|
80
|
+
# Padding
|
81
|
+
# NB the values are scaled up by the scale factor
|
82
|
+
pt, pr, pb, pl = parse_padding_string(opts[:padding]) if opts[:padding]
|
83
|
+
padding_top = (opts[:padding_top] || pt || 0) * s
|
84
|
+
padding_right = (opts[:padding_right] || pr || 0) * s
|
85
|
+
padding_bottom = (opts[:padding_bottom] || pb || 0) * s
|
86
|
+
padding_left = (opts[:padding_left] || pl || 0) * s
|
87
|
+
|
88
|
+
# Calculate (scaled up) dimensions
|
89
|
+
metrics = draw.get_type_metrics(text_string)
|
90
|
+
width, height = metrics.width, metrics.height
|
91
|
+
|
92
|
+
scaled_up_width = padding_left + width + padding_right
|
93
|
+
scaled_up_height = padding_top + height + padding_bottom
|
94
|
+
|
95
|
+
# Draw the background
|
96
|
+
image = Magick::Image.new(scaled_up_width, scaled_up_height){
|
97
|
+
self.background_color = opts[:background_color] || 'transparent'
|
98
|
+
}
|
99
|
+
# Draw the text
|
100
|
+
draw.annotate(image, width, height, padding_left, padding_top, text_string)
|
101
|
+
|
102
|
+
# Scale back down again
|
103
|
+
image.scale!(1/s)
|
104
|
+
|
105
|
+
format = opts[:format] || :png
|
106
|
+
image.format = format.to_s
|
107
|
+
|
108
|
+
# Output image either as a string or a tempfile
|
109
|
+
content = use_filesystem ? write_to_tempfile(image) : image.to_blob
|
110
|
+
image.destroy!
|
111
|
+
[
|
112
|
+
content,
|
113
|
+
{:format => format, :name => "text.#{format}"}
|
114
|
+
]
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# Use css-style padding declaration, i.e.
|
120
|
+
# 10 (all sides)
|
121
|
+
# 10 5 (top/bottom, left/right)
|
122
|
+
# 10 5 10 (top, left/right, bottom)
|
123
|
+
# 10 5 10 5 (top, right, bottom, left)
|
124
|
+
def parse_padding_string(str)
|
125
|
+
padding_parts = str.gsub('px','').split(/\s+/).map{|px| px.to_i}
|
126
|
+
case padding_parts.size
|
127
|
+
when 1
|
128
|
+
p = padding_parts.first
|
129
|
+
[p,p,p,p]
|
130
|
+
when 2
|
131
|
+
p,q = padding_parts
|
132
|
+
[p,q,p,q]
|
133
|
+
when 3
|
134
|
+
p,q,r = padding_parts
|
135
|
+
[p,q,r,q]
|
136
|
+
when 4
|
137
|
+
padding_parts
|
138
|
+
else raise ArgumentError, "Couldn't parse padding string '#{str}' - should be a css-style string"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def scale_factor_for(font_size)
|
143
|
+
# Scale approximately to 64 if below
|
144
|
+
min_size = 64
|
145
|
+
if font_size < min_size
|
146
|
+
(min_size.to_f / font_size).ceil
|
147
|
+
else
|
148
|
+
1
|
149
|
+
end.to_f
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
module Dragonfly
|
4
|
+
module ImageMagickUtils
|
5
|
+
|
6
|
+
# Exceptions
|
7
|
+
class ShellCommandFailed < RuntimeError; end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
include Configurable
|
11
|
+
configurable_attr :convert_command, "convert"
|
12
|
+
configurable_attr :identify_command, "identify"
|
13
|
+
configurable_attr :log_commands, false
|
14
|
+
end
|
15
|
+
|
16
|
+
include Loggable
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def convert(temp_object, args='', format=nil)
|
21
|
+
tempfile = new_tempfile(format)
|
22
|
+
run "#{convert_command} #{args} #{temp_object.path} #{tempfile.path}"
|
23
|
+
tempfile
|
24
|
+
end
|
25
|
+
|
26
|
+
def identify(temp_object)
|
27
|
+
# example of details string:
|
28
|
+
# myimage.png PNG 200x100 200x100+0+0 8-bit DirectClass 31.2kb
|
29
|
+
details = raw_identify(temp_object)
|
30
|
+
filename, format, geometry, geometry_2, depth, image_class, size = details.split(' ')
|
31
|
+
width, height = geometry.split('x')
|
32
|
+
{
|
33
|
+
:filename => filename,
|
34
|
+
:format => format.downcase.to_sym,
|
35
|
+
:width => width.to_i,
|
36
|
+
:height => height.to_i,
|
37
|
+
:depth => depth.to_i,
|
38
|
+
:image_class => image_class
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def raw_identify(temp_object, args='')
|
43
|
+
run "#{identify_command} #{args} #{temp_object.path}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def new_tempfile(ext=nil)
|
47
|
+
tempfile = ext ? Tempfile.new(['dragonfly', ".#{ext}"]) : Tempfile.new('dragonfly')
|
48
|
+
tempfile.binmode
|
49
|
+
tempfile.close
|
50
|
+
tempfile
|
51
|
+
end
|
52
|
+
|
53
|
+
def convert_command
|
54
|
+
ImageMagickUtils.convert_command
|
55
|
+
end
|
56
|
+
|
57
|
+
def identify_command
|
58
|
+
ImageMagickUtils.identify_command
|
59
|
+
end
|
60
|
+
|
61
|
+
def run(command)
|
62
|
+
log.debug("Running command: #{command}") if ImageMagickUtils.log_commands
|
63
|
+
begin
|
64
|
+
result = `#{command}`
|
65
|
+
rescue Errno::ENOENT
|
66
|
+
raise_shell_command_failed(command)
|
67
|
+
end
|
68
|
+
if $?.exitstatus == 1
|
69
|
+
throw :unable_to_handle
|
70
|
+
elsif !$?.success?
|
71
|
+
raise_shell_command_failed(command)
|
72
|
+
end
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def raise_shell_command_failed(command)
|
77
|
+
raise ShellCommandFailed, "Command failed (#{command}) with exit status #{$?.exitstatus}"
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,371 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Dragonfly
|
6
|
+
class Job
|
7
|
+
|
8
|
+
# Exceptions
|
9
|
+
class AppDoesNotMatch < StandardError; end
|
10
|
+
class JobAlreadyApplied < StandardError; end
|
11
|
+
class NothingToProcess < StandardError; end
|
12
|
+
class NothingToEncode < StandardError; end
|
13
|
+
class NothingToAnalyse < StandardError; end
|
14
|
+
class InvalidArray < StandardError; end
|
15
|
+
class NoSHAGiven < StandardError; end
|
16
|
+
class IncorrectSHA < StandardError; end
|
17
|
+
|
18
|
+
extend Forwardable
|
19
|
+
def_delegators :result, :data, :file, :tempfile, :path, :to_file, :size, :ext, :name, :name=, :basename, :meta, :meta=, :format, :_format
|
20
|
+
|
21
|
+
class Step
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Dragonfly::Job::Fetch -> 'Fetch'
|
25
|
+
def basename
|
26
|
+
@basename ||= name.split('::').last
|
27
|
+
end
|
28
|
+
# Dragonfly::Job::Fetch -> :fetch
|
29
|
+
def step_name
|
30
|
+
@step_name ||= basename.gsub(/[A-Z]/){ "_#{$&.downcase}" }.sub('_','').to_sym
|
31
|
+
end
|
32
|
+
# Dragonfly::Job::Fetch -> :f
|
33
|
+
def abbreviation
|
34
|
+
@abbreviation ||= basename.scan(/[A-Z]/).join.downcase.to_sym
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(*args)
|
39
|
+
@args = args
|
40
|
+
end
|
41
|
+
|
42
|
+
attr_reader :args
|
43
|
+
|
44
|
+
def update_temp_object(job, content, extra)
|
45
|
+
temp_object = TempObject.new(content, job.temp_object.attributes)
|
46
|
+
temp_object.extract_attributes_from(extra) if extra
|
47
|
+
job.temp_object = temp_object
|
48
|
+
end
|
49
|
+
|
50
|
+
def inspect
|
51
|
+
"#{self.class.step_name}(#{args.map{|a| a.inspect }.join(', ')})"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Fetch < Step
|
56
|
+
def uid
|
57
|
+
args.first
|
58
|
+
end
|
59
|
+
def apply(job)
|
60
|
+
content, extra = job.app.datastore.retrieve(uid)
|
61
|
+
job.temp_object = TempObject.new(content, extra)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class Process < Step
|
66
|
+
def name
|
67
|
+
args.first
|
68
|
+
end
|
69
|
+
def arguments
|
70
|
+
args[1..-1]
|
71
|
+
end
|
72
|
+
def apply(job)
|
73
|
+
raise NothingToProcess, "Can't process because temp object has not been initialized. Need to fetch first?" unless job.temp_object
|
74
|
+
content, extra = job.app.processor.process(job.temp_object, name, *arguments)
|
75
|
+
update_temp_object(job, content, extra)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class Encode < Step
|
80
|
+
def format
|
81
|
+
args.first
|
82
|
+
end
|
83
|
+
def arguments
|
84
|
+
args[1..-1]
|
85
|
+
end
|
86
|
+
def apply(job)
|
87
|
+
raise NothingToEncode, "Can't encode because temp object has not been initialized. Need to fetch first?" unless job.temp_object
|
88
|
+
content, extra = job.app.encoder.encode(job.temp_object, format, *arguments)
|
89
|
+
update_temp_object(job, content, extra)
|
90
|
+
job.temp_object.format = format
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Generate < Step
|
95
|
+
def apply(job)
|
96
|
+
content, extra = job.app.generator.generate(*args)
|
97
|
+
job.temp_object = TempObject.new(content, extra)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class FetchFile < Step
|
102
|
+
def path
|
103
|
+
File.expand_path(args.first)
|
104
|
+
end
|
105
|
+
def apply(job)
|
106
|
+
job.temp_object = TempObject.new(File.new(path))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
STEPS = [
|
111
|
+
Fetch,
|
112
|
+
Process,
|
113
|
+
Encode,
|
114
|
+
Generate,
|
115
|
+
FetchFile
|
116
|
+
]
|
117
|
+
|
118
|
+
# Class methods
|
119
|
+
class << self
|
120
|
+
|
121
|
+
def from_a(steps_array, app)
|
122
|
+
unless steps_array.is_a?(Array) &&
|
123
|
+
steps_array.all?{|s| s.is_a?(Array) && step_abbreviations[s.first] }
|
124
|
+
raise InvalidArray, "can't define a job from #{steps_array.inspect}"
|
125
|
+
end
|
126
|
+
job = app.new_job
|
127
|
+
steps_array.each do |step_array|
|
128
|
+
step_class = step_abbreviations[step_array.shift]
|
129
|
+
job.steps << step_class.new(*step_array)
|
130
|
+
end
|
131
|
+
job
|
132
|
+
end
|
133
|
+
|
134
|
+
def from_path(path, app)
|
135
|
+
path = path.dup
|
136
|
+
path.sub!(app.url_path_prefix, '') if app.url_path_prefix
|
137
|
+
path.sub!('/', '')
|
138
|
+
deserialize(path, app)
|
139
|
+
end
|
140
|
+
|
141
|
+
def deserialize(string, app)
|
142
|
+
from_a(Serializer.marshal_decode(string), app)
|
143
|
+
end
|
144
|
+
|
145
|
+
def step_abbreviations
|
146
|
+
@step_abbreviations ||= STEPS.inject({}){|hash, step_class| hash[step_class.abbreviation] = step_class; hash }
|
147
|
+
end
|
148
|
+
|
149
|
+
def step_names
|
150
|
+
@step_names ||= STEPS.map{|step_class| step_class.step_name }
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
# Instance methods
|
156
|
+
|
157
|
+
def initialize(app, temp_object=nil)
|
158
|
+
@app = app
|
159
|
+
self.extend app.analyser.analysis_methods
|
160
|
+
self.extend app.job_definitions
|
161
|
+
@steps = []
|
162
|
+
@next_step_index = 0
|
163
|
+
@temp_object = temp_object
|
164
|
+
end
|
165
|
+
|
166
|
+
# Used by 'dup' and 'clone'
|
167
|
+
def initialize_copy(other)
|
168
|
+
self.steps = other.steps.dup
|
169
|
+
self.extend app.analyser.analysis_methods
|
170
|
+
self.extend app.job_definitions
|
171
|
+
end
|
172
|
+
|
173
|
+
attr_accessor :temp_object
|
174
|
+
attr_reader :app, :steps
|
175
|
+
|
176
|
+
# define fetch(), fetch!(), process(), etc.
|
177
|
+
STEPS.each do |step_class|
|
178
|
+
class_eval %(
|
179
|
+
def #{step_class.step_name}(*args)
|
180
|
+
new_job = self.dup
|
181
|
+
new_job.steps << #{step_class}.new(*args)
|
182
|
+
new_job
|
183
|
+
end
|
184
|
+
|
185
|
+
def #{step_class.step_name}!(*args)
|
186
|
+
steps << #{step_class}.new(*args)
|
187
|
+
self
|
188
|
+
end
|
189
|
+
)
|
190
|
+
end
|
191
|
+
|
192
|
+
def analyse(method, *args)
|
193
|
+
unless result
|
194
|
+
raise NothingToAnalyse, "Can't analyse because temp object has not been initialized. Need to fetch first?"
|
195
|
+
end
|
196
|
+
# Hacky - wish there was a nicer way to do this without extending with yet another module
|
197
|
+
if method == :format
|
198
|
+
_format || analyser.analyse(result, method, *args)
|
199
|
+
else
|
200
|
+
analyser.analyse(result, method, *args)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def +(other_job)
|
205
|
+
unless app == other_job.app
|
206
|
+
raise AppDoesNotMatch, "Cannot add jobs belonging to different apps (#{app} is not #{other_job.app})"
|
207
|
+
end
|
208
|
+
unless other_job.applied_steps.empty?
|
209
|
+
raise JobAlreadyApplied, "Cannot add jobs when the second one has already been applied (#{other_job})"
|
210
|
+
end
|
211
|
+
new_job = self.class.new(app, temp_object)
|
212
|
+
new_job.steps = steps + other_job.steps
|
213
|
+
new_job.next_step_index = next_step_index
|
214
|
+
new_job
|
215
|
+
end
|
216
|
+
|
217
|
+
# Applying, etc.
|
218
|
+
|
219
|
+
def apply
|
220
|
+
pending_steps.each{|step| step.apply(self) }
|
221
|
+
self.next_step_index = steps.length
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
def result
|
226
|
+
apply
|
227
|
+
temp_object
|
228
|
+
end
|
229
|
+
|
230
|
+
def applied_steps
|
231
|
+
steps[0...next_step_index]
|
232
|
+
end
|
233
|
+
|
234
|
+
def pending_steps
|
235
|
+
steps[next_step_index..-1]
|
236
|
+
end
|
237
|
+
|
238
|
+
def to_a
|
239
|
+
steps.map{|step|
|
240
|
+
[step.class.abbreviation, *step.args]
|
241
|
+
}
|
242
|
+
end
|
243
|
+
|
244
|
+
# Serializing, etc.
|
245
|
+
|
246
|
+
def serialize
|
247
|
+
Serializer.marshal_encode(to_a)
|
248
|
+
end
|
249
|
+
|
250
|
+
def unique_signature
|
251
|
+
Digest::SHA1.hexdigest(serialize)
|
252
|
+
end
|
253
|
+
|
254
|
+
def sha
|
255
|
+
Digest::SHA1.hexdigest("#{serialize}#{app.secret}")[0...8]
|
256
|
+
end
|
257
|
+
|
258
|
+
def validate_sha!(sha)
|
259
|
+
case sha
|
260
|
+
when nil
|
261
|
+
raise NoSHAGiven
|
262
|
+
when self.sha
|
263
|
+
self
|
264
|
+
else
|
265
|
+
raise IncorrectSHA, sha
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# URLs, etc.
|
270
|
+
|
271
|
+
def url(*args)
|
272
|
+
app.url_for(self, *args) unless steps.empty?
|
273
|
+
end
|
274
|
+
|
275
|
+
def b64_data
|
276
|
+
"data:#{resolve_mime_type};base64,#{Base64.encode64(data)}"
|
277
|
+
end
|
278
|
+
|
279
|
+
# to_stuff...
|
280
|
+
|
281
|
+
def to_app
|
282
|
+
JobEndpoint.new(self)
|
283
|
+
end
|
284
|
+
|
285
|
+
def to_response(env={})
|
286
|
+
to_app.call(env)
|
287
|
+
end
|
288
|
+
|
289
|
+
def to_path
|
290
|
+
"/#{serialize}"
|
291
|
+
end
|
292
|
+
|
293
|
+
def to_fetched_job(uid)
|
294
|
+
new_job = self.class.new(app, temp_object)
|
295
|
+
new_job.fetch!(uid)
|
296
|
+
new_job.next_step_index = 1
|
297
|
+
new_job
|
298
|
+
end
|
299
|
+
|
300
|
+
# Step inspection
|
301
|
+
|
302
|
+
def fetch_step
|
303
|
+
last_step_of_type(Fetch)
|
304
|
+
end
|
305
|
+
|
306
|
+
def uid
|
307
|
+
step = fetch_step
|
308
|
+
step.uid if step
|
309
|
+
end
|
310
|
+
|
311
|
+
def uid_basename
|
312
|
+
File.basename(uid, '.*') if uid
|
313
|
+
end
|
314
|
+
|
315
|
+
def uid_extname
|
316
|
+
File.extname(uid) if uid
|
317
|
+
end
|
318
|
+
|
319
|
+
def generate_step
|
320
|
+
last_step_of_type(Generate)
|
321
|
+
end
|
322
|
+
|
323
|
+
def fetch_file_step
|
324
|
+
last_step_of_type(FetchFile)
|
325
|
+
end
|
326
|
+
|
327
|
+
def process_steps
|
328
|
+
steps.select{|s| s.is_a?(Process) }
|
329
|
+
end
|
330
|
+
|
331
|
+
def encode_step
|
332
|
+
last_step_of_type(Encode)
|
333
|
+
end
|
334
|
+
|
335
|
+
def encoded_format
|
336
|
+
step = encode_step
|
337
|
+
step.format if step
|
338
|
+
end
|
339
|
+
|
340
|
+
def encoded_extname
|
341
|
+
format = encoded_format
|
342
|
+
".#{format}" if format
|
343
|
+
end
|
344
|
+
|
345
|
+
# Misc
|
346
|
+
|
347
|
+
def store(opts={})
|
348
|
+
app.store(result, opts)
|
349
|
+
end
|
350
|
+
|
351
|
+
def resolve_mime_type
|
352
|
+
app.resolve_mime_type(result)
|
353
|
+
end
|
354
|
+
|
355
|
+
def inspect
|
356
|
+
to_s.sub(/>$/, " app=#{app}, steps=#{steps.inspect}, temp_object=#{temp_object.inspect}, steps applied:#{applied_steps.length}/#{steps.length} >")
|
357
|
+
end
|
358
|
+
|
359
|
+
protected
|
360
|
+
|
361
|
+
attr_writer :steps
|
362
|
+
attr_accessor :next_step_index
|
363
|
+
|
364
|
+
private
|
365
|
+
|
366
|
+
def last_step_of_type(type)
|
367
|
+
steps.select{|s| s.is_a?(type) }.last
|
368
|
+
end
|
369
|
+
|
370
|
+
end
|
371
|
+
end
|