mecano
Version:
Common functions for system deployment.
837 lines (807 loc) • 30.4 kB
text/coffeescript
crypto = require 'crypto'
fs = require 'fs'
path = require 'path'
each = require 'each'
util = require 'util'
Stream = require 'stream'
exec = require 'ssh2-exec'
connect = require 'ssh2-connect'
buffer = require 'buffer'
rimraf = require 'rimraf'
ini = require 'ini'
tilde = require 'tilde-expansion'
ssh2fs = require 'ssh2-fs'
jsesc = require 'jsesc'
misc = module.exports =
array:
intersect: (array) ->
return [] if array is null
result = []
for item, i in array
continue if result.indexOf(item) isnt -1
for argument, j in arguments
break if argument.indexOf(item) is -1
result.push item if j is arguments.length
result
regexp:
escape: (str) ->
str.replace /[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"
object:
equals: (obj1, obj2, keys) ->
keys1 = Object.keys obj1
keys2 = Object.keys obj2
if keys
keys1 = keys1.filter (k) -> keys.indexOf(k) isnt -1
keys2 = keys2.filter (k) -> keys.indexOf(k) isnt -1
else keys = keys1
return false if keys1.length isnt keys2.length
for k in keys
return false if obj1[k] isnt obj2[k]
return true
path:
normalize: (location, callback) ->
tilde location, (location) ->
callback path.normalize location
resolve: (locations..., callback) ->
normalized = []
each(locations)
.on 'item', (location, next) ->
misc.path.normalize location, (location) ->
normalized.push location
next()
.on 'end', ->
callback path.resolve normalized...
iptables:
# add_properties: ['target', 'protocol', 'dport', 'in-interface', 'out-interface', 'source', 'destination']
add_properties: ['-p', '-s', '-d', '-j', '-g', '-i', '-o', '-f']
# modify_properties: ['state', 'comment']
modify_properties: [
'-c', 'state|--state', 'comment|--comment'
'tcp|--source-port', 'tcp|--sport', 'tcp|--destination-port', 'tcp|--dport', 'tcp|--tcp-flags', 'tcp|--syn', 'tcp|--tcp-option'
'udp|--source-port', 'udp|--sport', 'udp|--destination-port', 'udp|--dport']
commands: # Used to compute rulenum
'-A': ['chain']
'-D': ['chain']
'-I': ['chain']
'-R': ['chain']
'-P': ['chain', 'target']
'-L': true, '-S': true, '-F': true, '-Z': true, '-N': true, '-X': true, '-E': true
parameters: ['-p', '-s', '-d', '-j', '-g', '-i', '-o', '-f', '-c']
parameters_inverted:
'--protocol': '-p', '--source': '-s', '--destination': '-d', '--jump': '-j'
'--goto': '-g', '--in-interface': '-i', '--out-interface': '-o',
'--fragment': '-f', '--set-counters': '-c'
protocols:
tcp: ['--source-port', '--sport', '--destination-port', '--dport', '--tcp-flags', '--syn', '--tcp-option']
udp: ['--source-port', '--sport', '--destination-port', '--dport']
udplite: []
icmp: []
esp: []
ah: []
sctp: []
all: []
modules:
state: ['--state']
comment: ['--comment']
cmd_args: (cmd, rule) ->
for k, v of rule
continue if ['chain', 'rulenum'].indexOf(k) isnt -1
if match = /^([\w]+)\|([-\w]+)$/.exec k
module = match[1]
arg = match[2]
cmd += " -m #{module}"
cmd += " #{arg} #{v}"
else
cmd += " #{k} #{v}"
cmd
cmd_modify: (rule) ->
rule.rulenum ?= 1
misc.iptables.cmd_args "iptables -R #{rule.chain} #{rule.rulenum}", rule
cmd_add: (rule) ->
rule.rulenum ?= 1
misc.iptables.cmd_args "iptables -I #{rule.chain} #{rule.rulenum}", rule
cmd: (newrules, oldrules) ->
cmds = []
for newrule in newrules
create = true
add_properties = misc.array.intersect misc.iptables.add_properties, Object.keys newrule
for oldrule in oldrules
if misc.object.equals newrule, oldrule, add_properties
create = false
if not misc.object.equals newrule, oldrule, misc.iptables.modify_properties
cmds.push misc.iptables.cmd_modify newrule
if create
cmds.push misc.iptables.cmd_add newrule
cmds
normalize: (rules) ->
nrules = []
for rule in rules
nrule = {}
# nrule.rulenum = rule.rulenum or 1
# Search for commands and parameters
for k, v of rule
v = rule[k] = "#{v}" if typeof v is 'number'
if k is 'chain' or k is 'rulenum'
v = misc.iptables.normalize v if nk is 'rulenum' and typeof v is 'object'
nk = k
else if k[0..1] is '--'
nk = misc.iptables.parameters_inverted[nk]
else if k[0] isnt '-'
nk = misc.iptables.parameters_inverted["--#{k}"]
else if misc.iptables.parameters.indexOf(k) isnt -1
nk = k
if nk
nrule[nk] = v
rule[k] = null
if protocol = nrule['-p']
for k in misc.iptables.protocols[protocol]
if rule[k]
nrule["#{protocol}|k"] = rule[k]
rule[k] = null
else if rule[k[2..]]
nrule["#{protocol}|#{k}"] = rule[k[2..]]
rule[k[2..]] = null
for k, v of rule
continue unless v
k = "--#{k}" unless k[0..2] is '--'
for mk, mvs of misc.iptables.modules
for mv in mvs
if k is mv
nrule["#{mk}|#{k}"] = v
rule[k] = null
for k, v of nrule
nrule[k] = jsesc v, quotes: 'double', wrap: true unless /^[A-Za-z0-9_\/-]+$/.test v
nrules.push nrule
nrules
###
Parse the result of `iptables -S`
###
parse: (stdout) ->
rules = []
command = null
command_index = 1
for line in stdout.split /\r\n|[\n\r\u0085\u2028\u2029]/g
command_index++
rule = {}
i = 0
key = ''
value = ''
module = null
while i <= line.length
char = line[i]
forceflush = i is line.length
newarg = (i is 0 and char is '-') or line[(i-1)..i] is ' -'
if newarg or forceflush
if value
value = value.trim()
if key is '-m'
module = value
else
key = "#{module}|#{key}" if module
rule[key] = value
# First key is a command
if misc.iptables.commands[key]
if key isnt command
command = key
command_index = 1
# Determine rule number
rule.rulenum = command_index
if Array.isArray misc.iptables.commands[key]
for v, j in value.split ' '
rule[misc.iptables.commands[key][j]] = v
key = ''
value = ''
break if forceflush
key += char
while (char = line[++i]) isnt ' '
key += char
if misc.iptables.parameters.indexOf(key) isnt -1
module = null
continue
if char is '"'
while (char = line[++i]) isnt '"'
value += char
i++
continue
while (char = line[++i]) isnt '-' and i < line.length
process.exit() unless char
value += char
# value += char
# i++
rules.push rule
rules
file:
copyFile: (ssh, source, destination, callback) ->
s = (ssh, callback) ->
unless ssh
then callback null, fs
else ssh.sftp callback
s ssh, (err, fs) ->
rs = fs.createReadStream source
ws = rs.pipe fs.createWriteStream destination
ws.on 'close', ->
fs.end() if fs.end
modified = true
callback()
ws.on 'error', callback
###
Compare modes
-------------
###
cmpmod: (modes...) ->
ref = modes[0]
ref = ref.toString(8) if typeof ref is 'number'
for i in [1...modes.length]
mode = modes[i]
mode = mode.toString(8) if typeof mode is 'number'
l = Math.min ref.length, mode.length
return false if mode.substr(-l) isnt ref.substr(-l)
true
copy: (ssh, source, destination, callback) ->
unless ssh
source = fs.createReadStream(u.pathname)
source.pipe destination
destination.on 'close', callback
destination.on 'error', callback
else
# todo: use cp to copy over ssh
callback new Error 'Copy over SSH not yet implemented'
###
`files.hash(file, [algorithm], callback)`
-----------------------------------------
Retrieve the hash of a supplied file in hexadecimal
form. If the provided file is a directory, the returned hash
is the sum of all the hashs of the files it recursively
contains. The default algorithm to compute the hash is md5.
Throw an error if file does not exist unless it is a directory.
misc.file.hash ssh, '/path/to/file', (err, md5) ->
md5.should.eql '287621a8df3c3f6c99c7b7645bd09ffd'
###
hash: (ssh, file, algorithm, callback) ->
if arguments.length is 3
callback = algorithm
algorithm = 'md5'
hasher = (ssh, path, callback) ->
shasum = crypto.createHash algorithm
ssh2fs.createReadStream ssh, path, (err, stream) ->
return callback err if err
stream
.on 'data', (data) ->
shasum.update data
.on 'error', (err) ->
return callback() if err.code is 'EISDIR'
callback err
.on 'end', ->
callback err, shasum.digest 'hex'
hashs = []
ssh2fs.stat ssh, file, (err, stat) ->
return callback new Error "Does not exist: #{file}" if err?.code is 'ENOENT'
return callback err if err
file += '/**' if stat.isDirectory()
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# This is not working over ssh, we
# need to implement the "glob" module
# over ssh
# |||||||||||||||||||||||||||||||||
# Temp fix, we support file md5 over
# ssh, but not directory:
if ssh and stat.isFile()
return hasher ssh, file, callback
each()
.files(file)
.on 'item', (item, next) ->
hasher ssh, item, (err, h) ->
return next err if err
hashs.push h if h?
next()
.on 'error', (err) ->
callback err
.on 'end', ->
switch hashs.length
when 0
if stat.isFile()
then callback new Error "Does not exist: #{file}"
else callback null, crypto.createHash(algorithm).update('').digest('hex')
when 1
return callback null, hashs[0]
else
hashs = crypto.createHash(algorithm).update(hashs.join('')).digest('hex')
return callback null, hashs
###
`files.compare(files, callback)`
--------------------------------
Compare the hash of multiple file. Return the file md5
if the file are the same or false otherwise.
###
compare: (ssh, files, callback) ->
return callback new Error 'Minimum of 2 files' if files.length < 2
result = null
each(files)
.parallel(true)
.on 'item', (file, next) ->
misc.file.hash ssh, file, (err, md5) ->
return next err if err
if result is null
result = md5
else if result isnt md5
result = false
next()
.on 'error', (err) ->
callback err
.on 'end', ->
callback null, result
###
remove(ssh, path, callback)
---------------------------
Remove a file or directory
###
remove: (ssh, path, callback) ->
unless ssh
rimraf path, callback
else
# Not very pretty but fast and no time to try make a version of rimraf over ssh
child = exec ssh, "rm -rdf #{path}"
child.on 'exit', (code) ->
callback null, code
string:
escapeshellarg: (arg) ->
result = arg.replace /[^\\]'/g, (match) ->
match.slice(0, 1) + '\\\''
"'#{result}'"
###
`string.hash(file, [algorithm], callback)`
------------------------------------------
Output the hash of a supplied string in hexadecimal
form. The default algorithm to compute the hash is md5.
###
hash: (data, algorithm) ->
if arguments.length is 1
algorithm = 'md5'
crypto.createHash(algorithm).update(data).digest('hex')
repeat: (str, l) ->
Array(l+1).join str
###
`string.endsWith(search, [position])`
-------------------------------------
Determines whether a string ends with the characters of another string,
returning true or false as appropriate.
This method has been added to the ECMAScript 6 specification and its code
was borrowed from [Mozilla](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith)
###
endsWith: (str, search, position) ->
position = position or str.length
position = position - search.length
lastIndex = str.lastIndexOf search
return lastIndex isnt -1 and lastIndex is position
ssh:
###
passwd(ssh, [user], callback)
----------------------
Return information present in '/etc/passwd' and cache the
result in the provided ssh instance as "passwd".
Result look like:
{ root: {
uid: '0',
gid: '0',
comment: 'root',
home: '/root',
shell: '/bin/bash' }, ... }
###
passwd: (ssh, username, callback) ->
if arguments.length is 3
# Username may be null, stop here
return callback null, null unless username
# Is user present in cache
if ssh.passwd and ssh.passwd[username]
return callback null, ssh.passwd[username]
# Reload the cache and check if user is here
ssh.passwd = null
return misc.ssh.passwd ssh, (err, users) ->
return callback err if err
user = users[username]
# Dont throw exception, just return undefined
# return callback new Error "User #{username} does not exists" unless user
callback null, user
callback = username
username = null
# Grab passwd from the cache
return callback null, ssh.passwd if ssh.passwd
# Alternative is to use the id command, eg `id -u ubuntu`
ssh2fs.readFile ssh, '/etc/passwd', 'ascii', (err, lines) ->
return callback err if err
passwd = []
for line in lines.split '\n'
info = /(.*)\:\w\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)/.exec line
continue unless info
passwd[info[1]] = uid: parseInt(info[2]), gid: parseInt(info[3]), comment: info[4], home: info[5], shell: info[6]
ssh.passwd = passwd
callback null, passwd
###
group(ssh, [group], callback)
----------------------
Return information present in '/etc/group' and cache the
result in the provided ssh instance as "group".
Result look like:
{ root: {
password: 'x'
gid: 0,
user_list: [] },
bin: {
password: 'x',
gid: 1,
user_list: ['bin','daemon'] } }
###
group: (ssh, group, callback) ->
if arguments.length is 3
# Group may be null, stop here
return callback null, null unless group
# Is group present in cache
if ssh.cache_group and ssh.cache_group[group]
return callback null, ssh.cache_group[group]
# Reload the cache and check if user is here
ssh.cache_group = null
return misc.ssh.group ssh, (err, groups) ->
return err if err
gid = groups[group]
# Dont throw exception, just return undefined
# return callback new Error "Group does not exists: #{group}" unless gid
callback null, gid
callback = group
group = null
# Grab group from the cache
return callback null, ssh.cache_group if ssh.cache_group
# Alternative is to use the id command, eg `id -g admin`
ssh2fs.readFile ssh, '/etc/group', 'ascii', (err, lines) ->
return callback err if err
group = []
for line in lines.split '\n'
info = /(.*)\:(.*)\:(.*)\:(.*)/.exec line
continue unless info
group[info[1]] = password: info[2], gid: parseInt(info[3]), user_list: if info[4] then info[4].split ',' else []
ssh.cache_group = group
callback null, group
###
`pidfileStatus(ssh, pidfile, [options], callback)`
---------------------------------------
Return a status code after reading a status file. Any existing
pidfile referencing a dead process will be removed.
The callback is called with an error and a status code. Values
expected as status code are:
* 0 if pidfile math a running process
* 1 if pidfile does not exists
* 2 if pidfile exists but match no process
###
pidfileStatus: (ssh, pidfile, options, callback) ->
if arguments.length is 3
callback = options
options = {}
ssh2fs.readFile ssh, pidfile, 'ascii', (err, pid) ->
# pidfile does not exists
return callback null, 1 if err and err.code is 'ENOENT'
return callback err if err
stdout = []
run = exec
cmd: "ps aux | grep #{pid.trim()} | grep -v grep | awk '{print $2}'"
ssh: ssh
run.stdout.on 'data', (data) ->
stdout.push data
if options.stdout
run.stdout.pipe options.stdout
if options.stderr
run.stderr.pipe options.stderr
run.on "exit", (code) ->
stdout = stdout.join('')
# pidfile math a running process
return callback null, 0 unless stdout is ''
misc.file.remove ssh, pidfile, (err, removed) ->
return callback err if err
# pidfile exists but match no process
callback null, 2
###
`isPortOpen(port, host, callback)`: Check if a port is already open
###
isPortOpen: (port, host, callback) ->
if arguments.length is 2
callback = host
host = '127.0.0.1'
exec "nc #{host} #{port} < /dev/null", (err, stdout, stderr) ->
return callback null, true unless err
return callback null, false if err.code is 1
callback err
###
`merge([inverse], obj1, obj2, ...]`: Recursively merge objects
--------------------------------------------------------------
On matching keys, the last object take precedence over previous ones
unless the inverse arguments is provided as true. Only objects are
merge, arrays are overwritten.
Enrich an existing object with a second one:
obj1 = { a_key: 'a value', b_key: 'b value'}
obj2 = { b_key: 'new b value'}
result = misc.merge obj1, obj2
assert.eql result, obj1
assert.eql obj1.b_key, 'new b value'
Create a new object from two objects:
obj1 = { a_key: 'a value', b_key: 'b value'}
obj2 = { b_key: 'new b value'}
result = misc.merge {}, obj1, obj2
assert.eql result.b_key, 'new b value'
Using inverse:
obj1 = { b_key: 'b value'}
obj2 = { a_key: 'a value', b_key: 'new b value'}
misc.merge true, obj1, obj2
assert.eql obj1.a_key, 'a value'
assert.eql obj1.b_key, 'b value'
###
merge: () ->
target = arguments[0]
from = 1
to = arguments.length
if typeof target is 'boolean'
inverse = !! target
target = arguments[1]
from = 2
# Handle case when target is a string or something (possible in deep copy)
if typeof target isnt "object" and typeof target isnt 'function'
target = {}
for i in [from ... to]
# Only deal with non-null/undefined values
if (options = arguments[ i ]) isnt null
# Extend the base object
for name of options
src = target[ name ]
copy = options[ name ]
# Prevent never-ending loop
continue if target is copy
# Recurse if we're merging plain objects
if copy? and typeof copy is 'object' and not Array.isArray(copy) and copy not instanceof RegExp
clone = src and ( if src and typeof src is 'object' then src else {} )
# Never move original objects, clone them
target[ name ] = misc.merge false, clone, copy
# Don't bring in undefined values
else if copy isnt undefined
target[ name ] = copy unless inverse and typeof target[ name ] isnt 'undefined'
# Return the modified object
target
kadmin: (options, cmd) ->
realm = if options.realm then "-r #{options.realm}" else ''
if options.kadmin_principal
then "kadmin #{realm} -p #{options.kadmin_principal} -s #{options.kadmin_server} -w #{options.kadmin_password} -q '#{cmd}'"
else "kadmin.local #{realm} -q '#{cmd}'"
ini:
safe: (val) ->
if ( typeof val isnt "string" or val.match(/[\r\n]/) or val.match(/^\[/) or (val.length > 1 and val.charAt(0) is "\"" and val.slice(-1) is "\"") or val isnt val.trim() )
then JSON.stringify(val)
else val.replace(/;/g, '\\;')
dotSplit: `function (str) {
return str.replace(/\1/g, '\2LITERAL\\1LITERAL\2')
.replace(/\\\./g, '\1')
.split(/\./).map(function (part) {
return part.replace(/\1/g, '\\.')
.replace(/\2LITERAL\\1LITERAL\2/g, '\1')
})
}`
parse: (content, options) ->
ini.parse content
###
Each category is surrounded by one or several square brackets. The number of brackets indicates
the depth of the category.
Options are
* `comment` Default to ";"
###
parse_multi_brackets: (str, options={}) ->
lines = str.split /[\r\n]+/g
current = data = {}
stack = [current]
comment = options.comment or ';'
lines.forEach (line, _, __) ->
return if not line or line.match(/^\s*$/)
# Category
if match = line.match /^\s*(\[+)(.+?)(\]+)\s*$/
depth = match[1].length
# Add a child
if depth is stack.length
parent = stack[depth - 1]
parent[match[2]] = current = {}
stack.push current
# Invalid child hierarchy
if depth > stack.length
throw new Error "Invalid child #{match[2]}"
# Move up or at the same level
if depth < stack.length
stack.splice depth, stack.length - depth
parent = stack[depth - 1]
parent[match[2]] = current = {}
stack.push current
# comment
else if comment and match = line.match ///^\s*(#{comment}.*)$///
current[match[1]] = null
# key value
else if match = line.match /^\s*(.+?)\s*=\s*(.+)\s*$/
current[match[1]] = match[2]
# else
else if match = line.match /^\s*(.+?)\s*$/
current[match[1]] = null
data
stringify: (obj, section, options) ->
if arguments.length is 2
options = section
section = undefined
options.separator ?= ' = '
eol = if process.platform is "win32" then "\r\n" else "\n"
safe = misc.ini.safe
dotSplit = misc.ini.dotSplit
children = []
out = ""
Object.keys(obj).forEach (k, _, __) ->
val = obj[k]
if val and Array.isArray val
val.forEach (item) ->
out += safe("#{k}[]") + options.separator + safe(item) + "\n"
else if val and typeof val is "object"
children.push k
else
out += safe(k) + options.separator + safe(val) + eol
if section and out.length
out = "[" + safe(section) + "]" + eol + out
children.forEach (k, _, __) ->
nk = dotSplit(k).join '\\.'
child = misc.ini.stringify(obj[k], (if section then section + "." else "") + nk, options)
if out.length and child.length
out += eol
out += child
out
stringify_square_then_curly: (content, depth=0, options={}) ->
if arguments.length is 2
options = depth
depth = 0
options.separator ?= ' = '
out = ''
indent = ' '
prefix = ''
for i in [0...depth]
prefix += indent
for k, v of content
isUndefined = typeof v is 'undefined'
isBoolean = typeof v is 'boolean'
isNull = v is null
isArray = Array.isArray v
isObj = typeof v is 'object' and not isNull and not isArray
if isObj
if depth is 0
out += "#{prefix}[#{k}]\n"
out += misc.ini.stringify_square_then_curly v, depth + 1, options
out += "\n"
else
out += "#{prefix}#{k}#{options.separator}{\n"
out += misc.ini.stringify_square_then_curly v, depth + 1, options
out += "#{prefix}}\n"
else
if isArray
out = []
for element in v
out.push "#{prefix}#{k}#{options.separator}#{element}"
out = out.join '\n'
else if isNull
out += "#{prefix}#{k}#{options.separator}null"
else if isBoolean
out += "#{prefix}#{k}#{options.separator}#{if v then 'true' else 'false'}"
else
out += "#{prefix}#{k}#{options.separator}#{v}"
out += '\n'
out
###
Each category is surrounded by one or several square brackets. The number of brackets indicates
the depth of the category.
###
stringify_multi_brackets: (content, depth=0, options={}) ->
if arguments.length is 2
options = depth
depth = 0
options.separator ?= ' = '
out = ''
indent = ' '
prefix = ''
for i in [0...depth]
prefix += indent
for k, v of content
isUndefined = typeof v is 'undefined'
isBoolean = typeof v is 'boolean'
isNull = v is null
isObj = typeof v is 'object' and not isNull
continue if isObj
if isNull
out += "#{prefix}#{k}"
else if isBoolean
out += "#{prefix}#{k}#{options.separator}#{if v then 'true' else 'false'}"
else
out += "#{prefix}#{k}#{options.separator}#{v}"
out += '\n'
for k, v of content
isNull = v is null
isObj = typeof v is 'object' and not isNull
continue unless isObj
out += "#{prefix}#{misc.string.repeat '[', depth+1}#{k}#{misc.string.repeat ']', depth+1}\n"
out += misc.ini.stringify_multi_brackets v, depth + 1, options
out
args: (args, overwrite_goptions={}) ->
# [goptions, options, callback] = args
if args.length is 2 and typeof args[1] is 'function'
args[2] = args[1]
args[1] = args[0]
args[0] = null
else if args.length is 1
args[1] = args[0]
args[0] = null
args[0] ?= misc.merge parallel: true, overwrite_goptions
args
###
`options(options, callback)`
----------------------------
Normalize options. An ssh connection is needed if the key "ssh"
hold a configuration object. The 'uid' and 'gid' fields will
be converted to integer if they match a username or a group.
`callback` Received parameters are:
* `err` Error object if any.
* `options` Sanitized options.
###
options: (options, callback) ->
options = [options] unless Array.isArray options
each(options)
.on 'item', (options, next) ->
options.if = [options.if] if options.if? and not Array.isArray options.if
# options.if_exists = options.destination if options.if_exists is true and options.destination
options.if_exists = [options.if_exists] if options.if_exists? and not Array.isArray options.if_exists
# options.not_if_exists = options.destination if options.not_if_exists is true and options.destination
options.not_if_exists = [options.not_if_exists] if options.not_if_exists? and not Array.isArray options.not_if_exists
if options.if_exists then for el, i in options.if_exists
options.if_exists[i] = options.destination if el is true and options.destination
if options.not_if_exists then for v, i in options.not_if_exists
options.not_if_exists[i] = options.destination if v is true and options.destination
options.mode ?= options.chmod if options.chmod
connection = ->
return source() unless options.ssh
return source() if options.ssh._host
connect options.ssh, (err, ssh) ->
return next err if err
options.ssh = ssh
source()
source = ->
return destination() unless options.source?
return destination() if /^\w+:/.test options.source # skip url
tilde options.source, (source) ->
options.source = source
destination()
destination = ->
return mode() unless options.destination?
return mode() unless typeof options.destination is 'string' # destination is a function
return mode() if /^\w+:/.test options.source # skip url
tilde options.destination, (destination) ->
options.destination = destination
mode()
mode = ->
options.mode = parseInt(options.mode, 8) if typeof options.mode is 'string'
uid()
uid = ->
# uid=`id -u $USER`,
return gid() unless options.uid
return gid() if typeof options.uid is 'number' or /\d+/.test options.uid
misc.ssh.passwd options.ssh, options.uid, (err, user) ->
return next err if err
if user
options.uid = user.uid
options.gid ?= user.gid
gid()
gid = ->
# gid=`getent group $GROUP | cut -d: -f3`
return next() unless options.gid
return next() if typeof options.gid is 'number' or /\d+/.test options.gid
misc.ssh.group options.ssh, options.gid, (err, group) ->
return next err if err
options.gid = group.gid if group
next()
connection()
.on 'both', (err) ->
callback err, options