inkcite 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +110 -0
- data/Rakefile +8 -0
- data/assets/facebook-like.css +62 -0
- data/assets/facebook-like.js +59 -0
- data/assets/init/config.yml +97 -0
- data/assets/init/helpers.tsv +31 -0
- data/assets/init/source.html +60 -0
- data/assets/init/source.txt +6 -0
- data/bin/inkcite +6 -0
- data/inkcite.gemspec +42 -0
- data/lib/inkcite.rb +32 -0
- data/lib/inkcite/cli/base.rb +128 -0
- data/lib/inkcite/cli/build.rb +130 -0
- data/lib/inkcite/cli/init.rb +58 -0
- data/lib/inkcite/cli/preview.rb +30 -0
- data/lib/inkcite/cli/server.rb +123 -0
- data/lib/inkcite/cli/test.rb +61 -0
- data/lib/inkcite/email.rb +219 -0
- data/lib/inkcite/mailer.rb +140 -0
- data/lib/inkcite/minifier.rb +151 -0
- data/lib/inkcite/parser.rb +111 -0
- data/lib/inkcite/renderer.rb +177 -0
- data/lib/inkcite/renderer/base.rb +186 -0
- data/lib/inkcite/renderer/button.rb +168 -0
- data/lib/inkcite/renderer/div.rb +29 -0
- data/lib/inkcite/renderer/element.rb +82 -0
- data/lib/inkcite/renderer/footnote.rb +132 -0
- data/lib/inkcite/renderer/google_analytics.rb +35 -0
- data/lib/inkcite/renderer/image.rb +95 -0
- data/lib/inkcite/renderer/image_base.rb +82 -0
- data/lib/inkcite/renderer/in_browser.rb +38 -0
- data/lib/inkcite/renderer/like.rb +73 -0
- data/lib/inkcite/renderer/link.rb +243 -0
- data/lib/inkcite/renderer/litmus.rb +33 -0
- data/lib/inkcite/renderer/lorem.rb +39 -0
- data/lib/inkcite/renderer/mobile_image.rb +67 -0
- data/lib/inkcite/renderer/mobile_style.rb +40 -0
- data/lib/inkcite/renderer/mobile_toggle.rb +27 -0
- data/lib/inkcite/renderer/outlook_background.rb +48 -0
- data/lib/inkcite/renderer/partial.rb +31 -0
- data/lib/inkcite/renderer/preheader.rb +22 -0
- data/lib/inkcite/renderer/property.rb +39 -0
- data/lib/inkcite/renderer/responsive.rb +334 -0
- data/lib/inkcite/renderer/span.rb +21 -0
- data/lib/inkcite/renderer/table.rb +67 -0
- data/lib/inkcite/renderer/table_base.rb +149 -0
- data/lib/inkcite/renderer/td.rb +92 -0
- data/lib/inkcite/uploader.rb +173 -0
- data/lib/inkcite/util.rb +85 -0
- data/lib/inkcite/version.rb +3 -0
- data/lib/inkcite/view.rb +745 -0
- data/lib/inkcite/view/context.rb +38 -0
- data/lib/inkcite/view/media_query.rb +60 -0
- data/lib/inkcite/view/tag_stack.rb +38 -0
- data/test/email_spec.rb +16 -0
- data/test/parser_spec.rb +72 -0
- data/test/project/config.yml +98 -0
- data/test/project/helpers.tsv +56 -0
- data/test/project/images/inkcite.jpg +0 -0
- data/test/project/source.html +58 -0
- data/test/project/source.txt +6 -0
- data/test/renderer/button_spec.rb +45 -0
- data/test/renderer/div_spec.rb +101 -0
- data/test/renderer/element_spec.rb +31 -0
- data/test/renderer/footnote_spec.rb +57 -0
- data/test/renderer/image_spec.rb +82 -0
- data/test/renderer/link_spec.rb +84 -0
- data/test/renderer/mobile_image_spec.rb +27 -0
- data/test/renderer/mobile_style_spec.rb +37 -0
- data/test/renderer/td_spec.rb +126 -0
- data/test/renderer_spec.rb +28 -0
- data/test/view_spec.rb +15 -0
- metadata +333 -0
@@ -0,0 +1,21 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
class Span < Responsive
|
4
|
+
|
5
|
+
def render tag, opt, ctx
|
6
|
+
|
7
|
+
return '</span>' if tag == '/span'
|
8
|
+
|
9
|
+
span = Element.new('span')
|
10
|
+
|
11
|
+
mix_font span, opt, ctx
|
12
|
+
|
13
|
+
mix_responsive span, opt, ctx
|
14
|
+
|
15
|
+
span.to_s
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
class Table < TableBase
|
4
|
+
|
5
|
+
def render tag, opt, ctx
|
6
|
+
|
7
|
+
tag_stack = ctx.tag_stack(:table)
|
8
|
+
|
9
|
+
if tag == CLOSE_TABLE
|
10
|
+
|
11
|
+
# Remove this table from the stack of previously open tags.
|
12
|
+
tag_stack.pop
|
13
|
+
|
14
|
+
return '</tr></table>'
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
# Push this table onto the stack which will make it's declaration
|
19
|
+
# available to its child TDs.
|
20
|
+
tag_stack << opt
|
21
|
+
|
22
|
+
table = Element.new(tag, { :border => 0, :cellspacing => 0 })
|
23
|
+
|
24
|
+
# Inherit base cell attributes - border, background color and image, etc.
|
25
|
+
mix_all table, opt, ctx
|
26
|
+
|
27
|
+
# Text shadowing
|
28
|
+
mix_text_shadow table, opt, ctx
|
29
|
+
|
30
|
+
# Conveniently accept padding (easier to type and consistent with CSS)or
|
31
|
+
# cellpadding which must always be declared.
|
32
|
+
table[:cellpadding] = (opt[:padding] || opt[:cellpadding]).to_i
|
33
|
+
|
34
|
+
# Conveniently accept both float and align to mean the same thing.
|
35
|
+
align = opt[:align] || opt[:float]
|
36
|
+
table[:align] = align unless align.blank?
|
37
|
+
|
38
|
+
border_radius = opt[BORDER_RADIUS].to_i
|
39
|
+
table.style[BORDER_RADIUS] = px(border_radius) if border_radius > 0
|
40
|
+
|
41
|
+
border_collapse = opt[BORDER_COLLAPSE]
|
42
|
+
table.style[BORDER_COLLAPSE] = border_collapse unless border_collapse.blank?
|
43
|
+
|
44
|
+
margin_top = opt[MARGIN_TOP].to_i
|
45
|
+
table.style[MARGIN_TOP] = px(margin_top) if margin_top > 0
|
46
|
+
|
47
|
+
mobile = opt[:mobile]
|
48
|
+
|
49
|
+
# When a Table is configured to have it's cells DROP then it
|
50
|
+
# actually needs to FILL on mobile and it's child Tds will
|
51
|
+
# be DROP'd. Override the local mobile klass so the child Tds
|
52
|
+
# see the parent as DROP.
|
53
|
+
mobile = FILL if mobile == DROP || mobile == SWITCH
|
54
|
+
|
55
|
+
mix_responsive table, opt, ctx, mobile
|
56
|
+
|
57
|
+
table.to_s + '<tr>'
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
CLOSE_TABLE = '/table'
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
class TableBase < Responsive
|
4
|
+
|
5
|
+
protected
|
6
|
+
|
7
|
+
def mix_all element, opt, ctx
|
8
|
+
|
9
|
+
mix_background element, opt, ctx
|
10
|
+
mix_border element, opt, ctx
|
11
|
+
mix_dimensions element, opt, ctx
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
def mix_background element, opt, ctx
|
16
|
+
|
17
|
+
bgcolor = opt[:bgcolor]
|
18
|
+
bgcolor = nil if bgcolor == NONE
|
19
|
+
|
20
|
+
# Set the bgcolor attribute of the element as a fallback if
|
21
|
+
# css isn't supported.
|
22
|
+
element[:bgcolor] = hex(bgcolor) unless bgcolor.blank?
|
23
|
+
|
24
|
+
# Assisted background image handling for maximum compatibility.
|
25
|
+
bgimage = opt[:background]
|
26
|
+
bgposition = opt[BACKGROUND_POSITION]
|
27
|
+
bgrepeat = opt[BACKGROUND_REPEAT]
|
28
|
+
|
29
|
+
# No need to set any CSS if there is no background image present on this
|
30
|
+
# element. Previously, it would also set the background-color attribute
|
31
|
+
# for unnecessary duplication.
|
32
|
+
background_css(element.style, bgcolor, bgimage, bgposition, bgrepeat, nil, false, ctx) unless bgimage.blank?
|
33
|
+
|
34
|
+
m_bgcolor = detect(opt[MOBILE_BACKGROUND_COLOR], opt[MOBILE_BGCOLOR])
|
35
|
+
m_bgimage = detect(opt[MOBILE_BACKGROUND_IMAGE], opt[MOBILE_BACKGROUND])
|
36
|
+
|
37
|
+
mobile_background = background_css(
|
38
|
+
{},
|
39
|
+
m_bgcolor,
|
40
|
+
m_bgimage,
|
41
|
+
detect(opt[MOBILE_BACKGROUND_POSITION], bgposition),
|
42
|
+
detect(opt[MOBILE_BACKGROUND_REPEAT], bgrepeat),
|
43
|
+
detect(opt[MOBILE_BACKGROUND_SIZE]),
|
44
|
+
(m_bgcolor && bgcolor) || (m_bgimage && bgimage),
|
45
|
+
ctx
|
46
|
+
)
|
47
|
+
|
48
|
+
unless mobile_background.blank?
|
49
|
+
|
50
|
+
# Add the responsive rule that applies to this element.
|
51
|
+
rule = Rule.new(element.tag, unique_klass(ctx), mobile_background)
|
52
|
+
|
53
|
+
# Add the rule to the view and the element
|
54
|
+
ctx.media_query << rule
|
55
|
+
element.add_rule rule
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
def mix_border element, opt, ctx
|
62
|
+
|
63
|
+
border = opt[:border]
|
64
|
+
element.style[:border] = border unless border.blank?
|
65
|
+
|
66
|
+
# Iterate through each of the possible borders and apply them individually
|
67
|
+
# to the style if they are defined.
|
68
|
+
DIRECTIONS.each do |dir|
|
69
|
+
key = :"border-#{dir}"
|
70
|
+
border = opt[key]
|
71
|
+
element.style[key] = border unless border.blank? || border == NONE
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
def mix_dimensions element, opt, ctx
|
77
|
+
|
78
|
+
# Not taking .to_i because we want to accept both integer values
|
79
|
+
# or percentages - e.g. 50%
|
80
|
+
width = opt[:width]
|
81
|
+
element[:width] = width unless width.blank?
|
82
|
+
|
83
|
+
height = opt[:height].to_i
|
84
|
+
element[:height] = height if height > 0
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def background_css into, bgcolor, img, position, repeat, size, important, ctx
|
91
|
+
|
92
|
+
unless bgcolor.blank? && img.blank?
|
93
|
+
|
94
|
+
bgcolor = hex(bgcolor) unless bgcolor.blank?
|
95
|
+
|
96
|
+
# If no image has been provided or if the image provided is equal
|
97
|
+
# to "none" then we'll set the values independently. Otherwise
|
98
|
+
# we'll use a composite background declaration.
|
99
|
+
if none?(img)
|
100
|
+
|
101
|
+
unless bgcolor.blank?
|
102
|
+
bgcolor << ' !important' if important
|
103
|
+
into[BACKGROUND_COLOR] = bgcolor
|
104
|
+
end
|
105
|
+
|
106
|
+
# Check specifically for a value of "none" which allows the email
|
107
|
+
# designer to the background that is otherwise present on the
|
108
|
+
# desktop version of the email.
|
109
|
+
if img == NONE
|
110
|
+
img = 'none'
|
111
|
+
img << ' !important' if important
|
112
|
+
into[BACKGROUND_IMAGE] = img
|
113
|
+
end
|
114
|
+
|
115
|
+
else
|
116
|
+
|
117
|
+
# Default to no-repeat if a position has been supplied or replace
|
118
|
+
# 'none' as a convenience (cause none is easier to type than no-repeat).
|
119
|
+
repeat = 'no-repeat' if (repeat.blank? && !position.blank?) || repeat == NONE
|
120
|
+
|
121
|
+
sty = []
|
122
|
+
sty << bgcolor unless bgcolor.blank?
|
123
|
+
|
124
|
+
ctx.assert_image_exists(img)
|
125
|
+
|
126
|
+
sty << "url(#{ctx.image_url(img)})"
|
127
|
+
sty << position unless position.blank?
|
128
|
+
sty << repeat unless repeat.blank?
|
129
|
+
sty << '!important' if important
|
130
|
+
|
131
|
+
into[:background] = sty.join(' ')
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
# Background size needs to be set independently. Perhaps it can be
|
136
|
+
# mixed into background: but I couldn't make it work.
|
137
|
+
unless size.blank?
|
138
|
+
into[BACKGROUND_SIZE] = size
|
139
|
+
into[BACKGROUND_SIZE] << ' !important' if important
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
into
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module Inkcite
|
2
|
+
module Renderer
|
3
|
+
class Td < TableBase
|
4
|
+
|
5
|
+
def render tag, opt, ctx
|
6
|
+
|
7
|
+
tag_stack = ctx.tag_stack(:td)
|
8
|
+
|
9
|
+
if tag == CLOSE_TD
|
10
|
+
tag_stack.pop
|
11
|
+
return '</td>'
|
12
|
+
end
|
13
|
+
|
14
|
+
# Push this tag onto the stack so that child elements (e.g. links)
|
15
|
+
# can have access to its attributes.
|
16
|
+
tag_stack << opt
|
17
|
+
|
18
|
+
# Grab the attributes of the parent table so that the TD can inherit
|
19
|
+
# specific values like padding, valign, responsiveness, etc.
|
20
|
+
parent = ctx.parent_opts(:table)
|
21
|
+
|
22
|
+
td = Element.new('td')
|
23
|
+
|
24
|
+
# Inherit base cell attributes - border, background color and image, etc.
|
25
|
+
mix_all td, opt, ctx
|
26
|
+
|
27
|
+
# Force the td to collapse to a single pixel to support images that
|
28
|
+
# are less than 15 pixels.
|
29
|
+
opt.merge!({
|
30
|
+
:font => NONE,
|
31
|
+
:color => NONE,
|
32
|
+
FONT_SIZE => 1,
|
33
|
+
LINE_HEIGHT => 1
|
34
|
+
}) unless opt[:flush].blank?
|
35
|
+
|
36
|
+
# It is a best-practice to declare the same padding on all cells in a
|
37
|
+
# table. Check to see if padding was declared on the parent.
|
38
|
+
padding = parent[:padding].to_i
|
39
|
+
td.style[:padding] = px(padding) if padding > 0
|
40
|
+
|
41
|
+
# Custom handling for text align on TDs rather than Base's mix_text_align
|
42
|
+
# because if possible, using align= rather than a style keeps emails
|
43
|
+
# smaller. But for left-aligned text, you gotta use a style because
|
44
|
+
# you know, Outlook.
|
45
|
+
align = opt[:align]
|
46
|
+
unless align.blank?
|
47
|
+
td[:align] = align
|
48
|
+
|
49
|
+
# Must use style to reinforce left-align text in certain email clients.
|
50
|
+
# All other alignments are accepted naturally.
|
51
|
+
td.style[TEXT_ALIGN] = align if align == LEFT
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
valign = detect(opt[:valign], parent[:valign])
|
56
|
+
td[:valign] = valign unless valign.blank?
|
57
|
+
|
58
|
+
rowspan = opt[:rowspan].to_i
|
59
|
+
td[:rowspan] = rowspan if rowspan > 0
|
60
|
+
|
61
|
+
mix_font td, opt, ctx, parent
|
62
|
+
|
63
|
+
mobile = opt[:mobile]
|
64
|
+
if mobile.blank?
|
65
|
+
|
66
|
+
# If the cell doesn't define it's own responsive behavior, check to
|
67
|
+
# see if it inherits from its parent table. DROP and SWITCH declared
|
68
|
+
# at the table-level descend to their tds.
|
69
|
+
pm = parent[:mobile]
|
70
|
+
mobile = pm if pm == DROP || pm == SWITCH
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
mix_responsive td, opt, ctx, mobile
|
75
|
+
|
76
|
+
#outlook-bg <!--[if gte mso 9]>[n]<v:rect style="width:%width%px;height:%height%px;" strokecolor="none"><v:fill type="tile" src="%src%" /></v:fill></v:rect><v:shape id="theText[rnd]" style="position:absolute;width:%width%px;height:%height%px;margin:0;padding:0;%style%">[n]<![endif]-->
|
77
|
+
#/outlook-bg <!--[if gte mso 9]></v:shape><![endif]-->
|
78
|
+
|
79
|
+
td.to_s
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
CLOSE_TD = '/td'
|
85
|
+
LEFT = 'left'
|
86
|
+
|
87
|
+
# Property which controls the color of text
|
88
|
+
TEXT_COLOR = :'#text'
|
89
|
+
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
require 'net/sftp'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module Inkcite
|
5
|
+
class Uploader
|
6
|
+
|
7
|
+
def self.upload email
|
8
|
+
|
9
|
+
times = []
|
10
|
+
|
11
|
+
[ 'source.html', 'source.txt', 'helpers.tsv' ].each do |file|
|
12
|
+
file = email.project_file(file)
|
13
|
+
times << File.mtime(file).to_i if File.exists?(file)
|
14
|
+
end
|
15
|
+
|
16
|
+
local_images = email.image_dir
|
17
|
+
if File.exists?(local_images)
|
18
|
+
Dir.foreach(local_images) do |file|
|
19
|
+
times << File.mtime(File.join(local_images, file)).to_i unless file.starts_with?('.')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Get the most recently updated file.
|
24
|
+
last_update = times.max
|
25
|
+
|
26
|
+
# Determine when the last upload was completed.
|
27
|
+
last_upload = email.meta(:last_upload).to_i
|
28
|
+
|
29
|
+
self.do_upload(email, false) if last_update > last_upload
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.upload! email
|
34
|
+
self.do_upload(email, true)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
IMAGE_PATH = :'image-path'
|
40
|
+
|
41
|
+
def self.copy! sftp, local, remote, force=true
|
42
|
+
|
43
|
+
# Nothing to copy unless the local directory exists (e.g. some emails don't
|
44
|
+
# have an images directory.)
|
45
|
+
return unless File.exists?(local)
|
46
|
+
|
47
|
+
Dir.foreach(local) do |file|
|
48
|
+
next if file.starts_with?('.')
|
49
|
+
|
50
|
+
local_file = File.join(local, file)
|
51
|
+
unless File.directory?(local_file)
|
52
|
+
|
53
|
+
remote_file = File.join(remote, file)
|
54
|
+
|
55
|
+
unless force
|
56
|
+
next unless begin
|
57
|
+
File.stat(local_file).mtime > Time.at(sftp.stat!(remote_file).mtime)
|
58
|
+
rescue Net::SFTP::StatusException
|
59
|
+
true # File doesn't exist, so assume it's changed.
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
puts "Uploading #{local_file} -> #{remote_file} ..."
|
64
|
+
sftp.upload!(local_file, remote_file)
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Internal method responsive for doing the actual upload and
|
72
|
+
# forcing (if necessary) the update of the graphics.
|
73
|
+
def self.do_upload email, force
|
74
|
+
|
75
|
+
# The preview version defines the configuration for the server to which
|
76
|
+
# the files will be sftp'd.
|
77
|
+
config = email.config[:sftp]
|
78
|
+
|
79
|
+
# TODO: Verify SFTP configuration
|
80
|
+
host = config[:host]
|
81
|
+
path = config[:path]
|
82
|
+
username = config[:username]
|
83
|
+
password = config[:password]
|
84
|
+
|
85
|
+
# Pre-optimize images before we upload them to the CDN.
|
86
|
+
email.optimize_images
|
87
|
+
|
88
|
+
# This is the directory from which images will be uploaded.
|
89
|
+
# The email provides us with the correct directory based on
|
90
|
+
# whether or not image optimization is enabled.
|
91
|
+
local_images = email.optimized_image_dir
|
92
|
+
|
93
|
+
# This is the last location of image upload. If we're working
|
94
|
+
# on multiple versions but the images all point to the same
|
95
|
+
# location, it isn't necessary to re-upload images each time.
|
96
|
+
last_remote_root = nil
|
97
|
+
|
98
|
+
puts "Uploading to #{host} ..."
|
99
|
+
|
100
|
+
# Get a local handle on the litmus configuration.
|
101
|
+
Net::SFTP.start(host, username, :password => password) do |sftp|
|
102
|
+
|
103
|
+
# Upload each version of the email.
|
104
|
+
email.versions.each do |version|
|
105
|
+
|
106
|
+
view = email.view(:preview, :browser, version)
|
107
|
+
|
108
|
+
# Need to pass the upload path through the renderer to ensure
|
109
|
+
# that embedded tags will be converted into data.
|
110
|
+
remote_root = Inkcite::Renderer.render(path, view)
|
111
|
+
|
112
|
+
# Recursively ensure that the full directory structure necessary for
|
113
|
+
# the content and images is present.
|
114
|
+
mkdir! sftp, remote_root
|
115
|
+
|
116
|
+
# Check to see if there is a HTML version of this preview. Some emails
|
117
|
+
# do not have a hosted version and so it is not necessary to upload the
|
118
|
+
# HTML version of the email - but this is a bad practice.
|
119
|
+
file_name = view.file_name
|
120
|
+
unless file_name.blank?
|
121
|
+
|
122
|
+
remote_file_name = File.join(remote_root, file_name)
|
123
|
+
puts "Uploading #{remote_file_name}"
|
124
|
+
|
125
|
+
# We need to use StringIO to write the email to a buffer in order to upload
|
126
|
+
# the email's content in binary so that its encoding is honored. SFTP defaults
|
127
|
+
# to ASCII-8bit in non-binary mode, so it was blowing up on UTF-8 special
|
128
|
+
# characters (e.g. "Mäkinen").
|
129
|
+
# http://stackoverflow.com/questions/9439289/netsftp-transfer-mode-binary-vs-text
|
130
|
+
io = StringIO.new(view.render!)
|
131
|
+
sftp.upload!(io, remote_file_name)
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
# Upload the images to the remote directory
|
136
|
+
copy! sftp, local_images, remote_root, force && last_remote_root != remote_root
|
137
|
+
last_remote_root = remote_root
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
# Timestamp to indicate we uploaded now
|
144
|
+
email.set_meta :last_upload, Time.now.to_i
|
145
|
+
|
146
|
+
true
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.mkdir! sftp, path
|
150
|
+
|
151
|
+
_path = File::SEPARATOR
|
152
|
+
|
153
|
+
path.split(File::SEPARATOR).each do |dir|
|
154
|
+
|
155
|
+
# Add the child directory on to the path.
|
156
|
+
_path = File.join(_path, dir)
|
157
|
+
|
158
|
+
begin
|
159
|
+
sftp.stat!(_path).directory?
|
160
|
+
rescue Net::SFTP::StatusException
|
161
|
+
begin
|
162
|
+
puts "Creating directory: #{_path}"
|
163
|
+
sftp.mkdir!(_path)
|
164
|
+
rescue
|
165
|
+
raise "Error creating #{_path}: #{$!}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
end
|
173
|
+
end
|