dragonfly 0.7.7 → 0.8.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.
- data/Gemfile +1 -1
- data/Gemfile.rails.2.3.5 +0 -1
- data/History.md +12 -0
- data/README.md +4 -2
- data/VERSION +1 -1
- data/config.ru +1 -1
- data/dragonfly.gemspec +256 -179
- data/extra_docs/Analysers.md +15 -6
- data/extra_docs/Configuration.md +13 -2
- data/extra_docs/Encoding.md +20 -7
- data/extra_docs/GeneralUsage.md +8 -5
- data/extra_docs/Generators.md +17 -7
- data/extra_docs/Heroku.md +1 -2
- data/extra_docs/MimeTypes.md +1 -1
- data/extra_docs/Models.md +1 -1
- data/extra_docs/Mongo.md +2 -2
- data/extra_docs/Processing.md +15 -7
- data/extra_docs/Rack.md +2 -3
- data/extra_docs/Rails2.md +2 -3
- data/extra_docs/Rails3.md +2 -3
- data/extra_docs/Sinatra.md +2 -2
- data/extra_docs/URLs.md +6 -4
- data/features/3.0.3.feature +8 -0
- data/features/steps/rails_steps.rb +2 -2
- data/features/support/env.rb +1 -1
- data/fixtures/files/app/views/albums/new.html.erb +4 -4
- data/fixtures/rails_2.3.5/template.rb +0 -1
- data/fixtures/{rails_3.0.0 → rails_3.0.3}/template.rb +0 -1
- data/irbrc.rb +1 -1
- data/lib/dragonfly/analysis/image_magick_analyser.rb +47 -0
- data/lib/dragonfly/app.rb +2 -0
- data/lib/dragonfly/config/image_magick.rb +41 -0
- data/lib/dragonfly/data_storage/file_data_store.rb +4 -2
- data/lib/dragonfly/data_storage/s3data_store.rb +7 -3
- data/lib/dragonfly/encoding/image_magick_encoder.rb +57 -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 +0 -18
- data/lib/dragonfly/image_magick_utils.rb +81 -0
- data/lib/dragonfly/processing/image_magick_processor.rb +99 -0
- data/lib/dragonfly/rails/images.rb +1 -1
- data/lib/dragonfly/temp_object.rb +7 -6
- data/spec/dragonfly/analysis/image_magick_analyser_spec.rb +15 -0
- data/spec/dragonfly/analysis/r_magick_analyser_spec.rb +5 -49
- data/spec/dragonfly/analysis/shared_analyser_spec.rb +51 -0
- data/spec/dragonfly/app_spec.rb +2 -0
- data/spec/dragonfly/data_storage/data_store_spec.rb +6 -0
- data/spec/dragonfly/data_storage/file_data_store_spec.rb +1 -1
- data/spec/dragonfly/data_storage/s3_data_store_spec.rb +11 -1
- data/spec/dragonfly/deprecation_spec.rb +2 -2
- data/spec/dragonfly/encoding/image_magick_encoder_spec.rb +41 -0
- data/spec/dragonfly/encoding/r_magick_encoder_spec.rb +3 -6
- 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 +12 -123
- data/spec/dragonfly/generation/shared_generator_spec.rb +91 -0
- data/spec/dragonfly/image_magick_utils_spec.rb +16 -0
- data/spec/dragonfly/processing/image_magick_processor_spec.rb +29 -0
- data/spec/dragonfly/processing/r_magick_processor_spec.rb +2 -212
- data/spec/dragonfly/processing/shared_processing_spec.rb +215 -0
- data/spec/image_matchers.rb +6 -0
- data/spec/spec_helper.rb +11 -0
- data/yard/templates/default/fulldoc/html/css/common.css +9 -2
- data/yard/templates/default/layout/html/layout.erb +12 -1
- metadata +310 -11
- data/.gitignore +0 -15
- data/features/rails_3.0.0.feature +0 -8
data/irbrc.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "bundler/setup"
|
3
3
|
require File.dirname(__FILE__) + '/lib/dragonfly'
|
4
|
-
APP = Dragonfly[:images].configure_with(:
|
4
|
+
APP = Dragonfly[:images].configure_with(:imagemagick)
|
5
5
|
|
6
6
|
# available_uids = `find #{APP.datastore.root_path} ! -type d`.split("\n").map do |file|
|
7
7
|
# file.sub("#{APP.datastore.root_path}/", '')
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Dragonfly
|
2
|
+
module Analysis
|
3
|
+
class ImageMagickAnalyser
|
4
|
+
|
5
|
+
include ImageMagickUtils
|
6
|
+
include Configurable
|
7
|
+
|
8
|
+
def width(temp_object)
|
9
|
+
identify(temp_object)[:width]
|
10
|
+
end
|
11
|
+
|
12
|
+
def height(temp_object)
|
13
|
+
identify(temp_object)[:height]
|
14
|
+
end
|
15
|
+
|
16
|
+
def aspect_ratio(temp_object)
|
17
|
+
attrs = identify(temp_object)
|
18
|
+
attrs[:width].to_f / attrs[:height]
|
19
|
+
end
|
20
|
+
|
21
|
+
def portrait?(temp_object)
|
22
|
+
attrs = identify(temp_object)
|
23
|
+
attrs[:width] <= attrs[:height]
|
24
|
+
end
|
25
|
+
|
26
|
+
def landscape?(temp_object)
|
27
|
+
attrs = identify(temp_object)
|
28
|
+
attrs[:width] >= attrs[:height]
|
29
|
+
end
|
30
|
+
|
31
|
+
def depth(temp_object)
|
32
|
+
identify(temp_object)[:depth]
|
33
|
+
end
|
34
|
+
|
35
|
+
def number_of_colours(temp_object)
|
36
|
+
details = raw_identify(temp_object, '-verbose -unique')
|
37
|
+
details[/Colors: (\d+)/, 1].to_i
|
38
|
+
end
|
39
|
+
alias number_of_colors number_of_colours
|
40
|
+
|
41
|
+
def format(temp_object)
|
42
|
+
identify(temp_object)[:format]
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/dragonfly/app.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
module Dragonfly
|
2
|
+
module Config
|
3
|
+
|
4
|
+
# ImageMagick is a saved configuration for Dragonfly apps, which does the following:
|
5
|
+
# - registers an imagemagick analyser
|
6
|
+
# - registers an imagemagick processor
|
7
|
+
# - registers an imagemagick encoder
|
8
|
+
# - adds thumb shortcuts like '280x140!', etc.
|
9
|
+
# Look at the source code for apply_configuration to see exactly how it configures the app.
|
10
|
+
module ImageMagick
|
11
|
+
|
12
|
+
def self.apply_configuration(app, opts={})
|
13
|
+
app.configure do |c|
|
14
|
+
c.analyser.register(Analysis::ImageMagickAnalyser)
|
15
|
+
c.processor.register(Processing::ImageMagickProcessor)
|
16
|
+
c.encoder.register(Encoding::ImageMagickEncoder)
|
17
|
+
c.generator.register(Generation::ImageMagickGenerator)
|
18
|
+
|
19
|
+
c.job :thumb do |geometry, format|
|
20
|
+
process :thumb, geometry
|
21
|
+
encode format if format
|
22
|
+
end
|
23
|
+
c.job :gif do
|
24
|
+
encode :gif
|
25
|
+
end
|
26
|
+
c.job :jpg do
|
27
|
+
encode :jpg
|
28
|
+
end
|
29
|
+
c.job :png do
|
30
|
+
encode :png
|
31
|
+
end
|
32
|
+
c.job :convert do |args, format|
|
33
|
+
process :convert, args, format
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -67,7 +67,7 @@ module Dragonfly
|
|
67
67
|
end
|
68
68
|
|
69
69
|
def relative(absolute_path)
|
70
|
-
absolute_path[/^#{root_path}
|
70
|
+
absolute_path[/^#{root_path}\/?(.*)$/, 1]
|
71
71
|
end
|
72
72
|
|
73
73
|
def directory_empty?(path)
|
@@ -79,7 +79,9 @@ module Dragonfly
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def relative_path_for(filename)
|
82
|
-
|
82
|
+
time = Time.now
|
83
|
+
msec = time.usec / 1000
|
84
|
+
"#{time.strftime '%Y/%m/%d/%H_%M_%S'}_#{msec}_#{filename.gsub(/[^\w.]+/,'_')}"
|
83
85
|
end
|
84
86
|
|
85
87
|
def store_extra_data(data_path, temp_object)
|
@@ -34,10 +34,14 @@ module Dragonfly
|
|
34
34
|
def store(temp_object, opts={})
|
35
35
|
uid = opts[:path] || generate_uid(temp_object.name || 'file')
|
36
36
|
ensure_initialized
|
37
|
-
object = use_filesystem ? temp_object.file : temp_object.data
|
38
37
|
extra_data = temp_object.attributes
|
39
|
-
|
40
|
-
|
38
|
+
if use_filesystem
|
39
|
+
temp_object.file do |f|
|
40
|
+
S3Object.store(uid, f, bucket_name, s3_metadata_for(extra_data))
|
41
|
+
end
|
42
|
+
else
|
43
|
+
S3Object.store(uid, temp_object.data, bucket_name, s3_metadata_for(extra_data))
|
44
|
+
end
|
41
45
|
uid
|
42
46
|
end
|
43
47
|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Dragonfly
|
2
|
+
module Encoding
|
3
|
+
class ImageMagickEncoder
|
4
|
+
|
5
|
+
include Configurable
|
6
|
+
include ImageMagickUtils
|
7
|
+
|
8
|
+
configurable_attr :supported_formats, [
|
9
|
+
:ai,
|
10
|
+
:bmp,
|
11
|
+
:eps,
|
12
|
+
:gif,
|
13
|
+
:gif87,
|
14
|
+
:ico,
|
15
|
+
:j2c,
|
16
|
+
:jp2,
|
17
|
+
:jpeg,
|
18
|
+
:jpg,
|
19
|
+
:pbm,
|
20
|
+
:pcd,
|
21
|
+
:pct,
|
22
|
+
:pcx,
|
23
|
+
:pdf,
|
24
|
+
:pict,
|
25
|
+
:pjpeg,
|
26
|
+
:png,
|
27
|
+
:png24,
|
28
|
+
:png32,
|
29
|
+
:png8,
|
30
|
+
:pnm,
|
31
|
+
:ppm,
|
32
|
+
:ps,
|
33
|
+
:psd,
|
34
|
+
:ras,
|
35
|
+
:tga,
|
36
|
+
:tiff,
|
37
|
+
:wbmp,
|
38
|
+
:xbm,
|
39
|
+
:xpm,
|
40
|
+
:xwd
|
41
|
+
]
|
42
|
+
|
43
|
+
def encode(temp_object, format, args='')
|
44
|
+
format = format.to_s.downcase
|
45
|
+
throw :unable_to_handle unless supported_formats.include?(format.to_sym)
|
46
|
+
details = identify(temp_object)
|
47
|
+
|
48
|
+
if details[:format] == format.to_sym && args.empty?
|
49
|
+
temp_object
|
50
|
+
else
|
51
|
+
convert(temp_object, args, format)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Dragonfly
|
2
|
+
module Generation
|
3
|
+
|
4
|
+
# HashWithCssStyleKeys is solely for being able to access a hash
|
5
|
+
# which has css-style keys (e.g. 'font-size') with the underscore
|
6
|
+
# symbol version
|
7
|
+
# @example
|
8
|
+
# opts = {'font-size' => '23px', :color => 'white'}
|
9
|
+
# opts = HashWithCssStyleKeys[opts]
|
10
|
+
# opts[:font_size] # ===> '23px'
|
11
|
+
# opts[:color] # ===> 'white'
|
12
|
+
class HashWithCssStyleKeys < Hash
|
13
|
+
def [](key)
|
14
|
+
super || (
|
15
|
+
str_key = key.to_s
|
16
|
+
css_key = str_key.gsub('_','-')
|
17
|
+
super(str_key) || super(css_key) || super(css_key.to_sym)
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Dragonfly
|
2
|
+
module Generation
|
3
|
+
class ImageMagickGenerator
|
4
|
+
|
5
|
+
FONT_STYLES = {
|
6
|
+
'normal' => 'normal',
|
7
|
+
'italic' => 'italic',
|
8
|
+
'oblique' => 'oblique'
|
9
|
+
}
|
10
|
+
|
11
|
+
FONT_STRETCHES = {
|
12
|
+
'normal' => 'normal',
|
13
|
+
'semi-condensed' => 'semi-condensed',
|
14
|
+
'condensed' => 'condensed',
|
15
|
+
'extra-condensed' => 'extra-condensed',
|
16
|
+
'ultra-condensed' => 'ultra-condensed',
|
17
|
+
'semi-expanded' => 'semi-expanded',
|
18
|
+
'expanded' => 'expanded',
|
19
|
+
'extra-expanded' => 'extra-expanded',
|
20
|
+
'ultra-expanded' => 'ultra-expanded'
|
21
|
+
}
|
22
|
+
|
23
|
+
FONT_WEIGHTS = {
|
24
|
+
'normal' => 'normal',
|
25
|
+
'bold' => 'bold',
|
26
|
+
'bolder' => 'bolder',
|
27
|
+
'lighter' => 'lighter',
|
28
|
+
'100' => 100,
|
29
|
+
'200' => 200,
|
30
|
+
'300' => 300,
|
31
|
+
'400' => 400,
|
32
|
+
'500' => 500,
|
33
|
+
'600' => 600,
|
34
|
+
'700' => 700,
|
35
|
+
'800' => 800,
|
36
|
+
'900' => 900
|
37
|
+
}
|
38
|
+
|
39
|
+
include ImageMagickUtils
|
40
|
+
include Configurable
|
41
|
+
|
42
|
+
def plasma(width, height, format='png')
|
43
|
+
tempfile = new_tempfile(format)
|
44
|
+
run "#{convert_command} -size #{width}x#{height} plasma:fractal #{tempfile.path}"
|
45
|
+
[
|
46
|
+
tempfile,
|
47
|
+
{:format => format.to_sym, :name => "plasma.#{format}"}
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
def text(string, opts={})
|
52
|
+
opts = HashWithCssStyleKeys[opts]
|
53
|
+
args = []
|
54
|
+
format = (opts[:format] || :png)
|
55
|
+
background = opts[:background_color] || 'none'
|
56
|
+
font_size = (opts[:font_size] || 12).to_i
|
57
|
+
escaped_string = "\"#{string.gsub(/"/, '\"')}\""
|
58
|
+
|
59
|
+
# Settings
|
60
|
+
args.push("-gravity NorthWest")
|
61
|
+
args.push("-antialias")
|
62
|
+
args.push("-pointsize #{font_size}")
|
63
|
+
args.push("-font '#{opts[:font]}'") if opts[:font]
|
64
|
+
args.push("-family '#{opts[:font_family]}'") if opts[:font_family]
|
65
|
+
args.push("-fill #{opts[:color]}") if opts[:color]
|
66
|
+
args.push("-stroke #{opts[:stroke_color]}") if opts[:stroke_color]
|
67
|
+
args.push("-style #{FONT_STYLES[opts[:font_style]]}") if opts[:font_style]
|
68
|
+
args.push("-stretch #{FONT_STRETCHES[opts[:font_stretch]]}") if opts[:font_stretch]
|
69
|
+
args.push("-weight #{FONT_WEIGHTS[opts[:font_weight]]}") if opts[:font_weight]
|
70
|
+
args.push("-background #{background}")
|
71
|
+
args.push("label:#{escaped_string}")
|
72
|
+
|
73
|
+
# Padding
|
74
|
+
pt, pr, pb, pl = parse_padding_string(opts[:padding]) if opts[:padding]
|
75
|
+
padding_top = (opts[:padding_top] || pt || 0)
|
76
|
+
padding_right = (opts[:padding_right] || pr || 0)
|
77
|
+
padding_bottom = (opts[:padding_bottom] || pb || 0)
|
78
|
+
padding_left = (opts[:padding_left] || pl || 0)
|
79
|
+
|
80
|
+
tempfile = new_tempfile(format)
|
81
|
+
run "#{convert_command} #{args.join(' ')} #{tempfile.path}"
|
82
|
+
|
83
|
+
if (padding_top || padding_right || padding_bottom || padding_left)
|
84
|
+
attrs = identify(tempfile)
|
85
|
+
text_width = attrs[:width].to_i
|
86
|
+
text_height = attrs[:height].to_i
|
87
|
+
width = padding_left + text_width + padding_right
|
88
|
+
height = padding_top + text_height + padding_bottom
|
89
|
+
|
90
|
+
args = args.slice(0, args.length - 2)
|
91
|
+
args.push("-size #{width}x#{height}")
|
92
|
+
args.push("xc:#{background}")
|
93
|
+
args.push("-annotate 0x0+#{padding_left}+#{padding_top} #{escaped_string}")
|
94
|
+
run "#{convert_command} #{args.join(' ')} #{tempfile.path}"
|
95
|
+
end
|
96
|
+
|
97
|
+
[
|
98
|
+
tempfile,
|
99
|
+
{:format => format, :name => "text.#{format}"}
|
100
|
+
]
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Use css-style padding declaration, i.e.
|
106
|
+
# 10 (all sides)
|
107
|
+
# 10 5 (top/bottom, left/right)
|
108
|
+
# 10 5 10 (top, left/right, bottom)
|
109
|
+
# 10 5 10 5 (top, right, bottom, left)
|
110
|
+
def parse_padding_string(str)
|
111
|
+
padding_parts = str.gsub('px','').split(/\s+/).map{|px| px.to_i}
|
112
|
+
case padding_parts.size
|
113
|
+
when 1
|
114
|
+
p = padding_parts.first
|
115
|
+
[p,p,p,p]
|
116
|
+
when 2
|
117
|
+
p,q = padding_parts
|
118
|
+
[p,q,p,q]
|
119
|
+
when 3
|
120
|
+
p,q,r = padding_parts
|
121
|
+
[p,q,r,q]
|
122
|
+
when 4
|
123
|
+
padding_parts
|
124
|
+
else raise ArgumentError, "Couldn't parse padding string '#{str}' - should be a css-style string"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def scale_factor_for(font_size)
|
129
|
+
# Scale approximately to 64 if below
|
130
|
+
min_size = 64
|
131
|
+
if font_size < min_size
|
132
|
+
(min_size.to_f / font_size).ceil
|
133
|
+
else
|
134
|
+
1
|
135
|
+
end.to_f
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -38,24 +38,6 @@ module Dragonfly
|
|
38
38
|
'900' => 900
|
39
39
|
}
|
40
40
|
|
41
|
-
# HashWithCssStyleKeys is solely for being able to access a hash
|
42
|
-
# which has css-style keys (e.g. 'font-size') with the underscore
|
43
|
-
# symbol version
|
44
|
-
# @example
|
45
|
-
# opts = {'font-size' => '23px', :color => 'white'}
|
46
|
-
# opts = HashWithCssStyleKeys[opts]
|
47
|
-
# opts[:font_size] # ===> '23px'
|
48
|
-
# opts[:color] # ===> 'white'
|
49
|
-
class HashWithCssStyleKeys < Hash
|
50
|
-
def [](key)
|
51
|
-
super || (
|
52
|
-
str_key = key.to_s
|
53
|
-
css_key = str_key.gsub('_','-')
|
54
|
-
super(str_key) || super(css_key) || super(css_key.to_sym)
|
55
|
-
)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
41
|
include RMagickUtils
|
60
42
|
include Configurable
|
61
43
|
configurable_attr :use_filesystem, true
|
@@ -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
|