inkcite 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|