1 |
|
2 | fs = require 'fs'
|
3 | path = require 'path'
|
4 | fs.exists ?= path.exists
|
5 | util = require 'util'
|
6 | each = require 'each'
|
7 | eco = require 'eco'
|
8 | rimraf = require 'rimraf'
|
9 | exec = require('child_process').exec
|
10 | open = require 'open-uri'
|
11 |
|
12 | conditions = require './conditions'
|
13 | misc = require './misc'
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | mecano = module.exports =
|
22 | |
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 |
|
45 |
|
46 |
|
47 |
|
48 | copy: (options, callback) ->
|
49 | options = misc.options options
|
50 | copied = 0
|
51 | each( options )
|
52 | .on 'item', (options, next) ->
|
53 | return next new Error 'Missing source' unless options.source
|
54 | return next new Error 'Missing destination' unless options.destination
|
55 | options.not_if_exists = options.destination if options.not_if_exists is true
|
56 | dstStat = null
|
57 | source = ->
|
58 | fs.stat options.source, (err, stat) ->
|
59 |
|
60 | return next err if err
|
61 | return next new Error 'Source is a directory' if stat.isDirectory()
|
62 | copy()
|
63 | copy = (destination = options.destination) ->
|
64 | fs.stat destination, (err, stat) ->
|
65 | dstStat = stat
|
66 | return next err if err and err.code isnt 'ENOENT'
|
67 |
|
68 | dirExists = not err and stat.isDirectory()
|
69 | fileExists = not err and stat.isFile()
|
70 | return next null, 0 if fileExists and not options.force
|
71 |
|
72 | return copy path.resolve options.destination, path.basename options.source if dirExists
|
73 |
|
74 | input = fs.createReadStream options.source
|
75 | output = fs.createWriteStream destination
|
76 | util.pump input, output, (err) ->
|
77 | return next err if err
|
78 | chmod()
|
79 | chmod = ->
|
80 | return finish() if not options.chmod or options.chmod is dstStat.mode
|
81 | fs.chmod options.destination, options.chmod, (err) ->
|
82 | return next err if err
|
83 | finish()
|
84 | finish = ->
|
85 | copied++
|
86 | next()
|
87 | conditions.all(options, next, copy)
|
88 | .on 'both', (err) ->
|
89 | callback err, copied
|
90 | |
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 | download: (options, callback) ->
|
122 | options = misc.options options
|
123 | downloaded = 0
|
124 | each( options )
|
125 | .on 'item', (options, next) ->
|
126 | return next new Error "Missing source: #{options.source}" unless options.source
|
127 | return next new Error "Missing destination: #{options.destination}" unless options.destination
|
128 | options.force ?= false
|
129 | download = () ->
|
130 | destination = fs.createWriteStream(options.destination)
|
131 | open(options.source, destination)
|
132 | destination.on 'close', () ->
|
133 | downloaded++
|
134 | next()
|
135 | destination.on 'error', (err) ->
|
136 | next err
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 | fs.exists options.destination, (exists) ->
|
148 |
|
149 | if exists and not options.force
|
150 | next()
|
151 |
|
152 | else if exists
|
153 | rimraf options.destination, (err) ->
|
154 | return next err if err
|
155 | download()
|
156 | else download()
|
157 | .on 'both', (err) ->
|
158 | callback err, downloaded
|
159 | |
160 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 | execute: (goptions, options, callback) ->
|
195 | if arguments.length is 2
|
196 | callback = options
|
197 | options = goptions
|
198 | isArray = Array.isArray options
|
199 | options = misc.options options
|
200 | executed = 0
|
201 | stdouts = []
|
202 | stderrs = []
|
203 | escape = (cmd) ->
|
204 | esccmd = ''
|
205 | for char in cmd
|
206 | if char is '$'
|
207 | esccmd += '\\'
|
208 | esccmd += char
|
209 | esccmd
|
210 | each( options )
|
211 | .parallel( goptions.parallel )
|
212 | .on 'item', (option, i, next) ->
|
213 | option = { cmd: option } if typeof option is 'string'
|
214 | misc.merge true, option, goptions
|
215 | return next new Error "Missing cmd: #{option.cmd}" unless option.cmd?
|
216 | option.code ?= [0]
|
217 | option.code = [option.code] unless Array.isArray option.code
|
218 | cmdOption = {}
|
219 | cmdOption.env = option.env or process.env
|
220 | cmdOption.cwd = option.cwd or null
|
221 | cmdOption.uid = option.uid if options.uid
|
222 | cmdOption.gid = option.gid if options.gid
|
223 | cmd = () ->
|
224 | if option.host
|
225 | option.cmd = escape option.cmd
|
226 | option.cmd = option.host + ' "' + option.cmd + '"'
|
227 | if option.username
|
228 | option.cmd = option.username + '@' + option.cmd
|
229 | option.cmd = 'ssh -o StrictHostKeyChecking=no ' + option.cmd
|
230 | run = exec option.cmd, cmdOption
|
231 | stdout = stderr = ''
|
232 | if option.stdout
|
233 | then run.stdout.pipe option.stdout
|
234 | else run.stdout.on 'data', (data) ->
|
235 | stdout += data
|
236 | if option.stderr
|
237 | then run.stderr.pipe option.stderr
|
238 | else run.stderr.on 'data', (data) -> stderr += data
|
239 | run.on "exit", (code) ->
|
240 |
|
241 |
|
242 |
|
243 | setTimeout ->
|
244 | executed++
|
245 | stdouts.push if option.stdout then undefined else stdout
|
246 | stderrs.push if option.stderr then undefined else stderr
|
247 | if option.code.indexOf(code) is -1
|
248 | err = new Error "Invalid exec code #{code}"
|
249 | err.code = code
|
250 | return next err
|
251 | next()
|
252 | , 1
|
253 |
|
254 |
|
255 |
|
256 |
|
257 |
|
258 | conditions.all(option, next, cmd)
|
259 | .on 'both', (err) ->
|
260 | stdouts = stdouts[0] unless isArray
|
261 | stderrs = stderrs[0] unless isArray
|
262 | callback err, executed, stdouts, stderrs
|
263 | |
264 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 |
|
283 |
|
284 |
|
285 |
|
286 | extract: (options, callback) ->
|
287 | options = misc.options options
|
288 | extracted = 0
|
289 | each( options )
|
290 | .on 'item', (options, next) ->
|
291 | return next new Error "Missing source: #{options.source}" unless options.source
|
292 | destination = options.destination ? path.dirname options.source
|
293 |
|
294 | if options.format?
|
295 | format = options.format
|
296 | else
|
297 | if /\.(tar\.gz|tgz)$/.test options.source
|
298 | format = 'tgz'
|
299 | else if /\.zip$/.test options.source
|
300 | format = 'zip'
|
301 | else
|
302 | ext = path.extname options.source
|
303 | return next new Error "Unsupported extension, got #{JSON.stringify(ext)}"
|
304 |
|
305 | extract = () ->
|
306 | cmd = null
|
307 | switch format
|
308 | when 'tgz' then cmd = "tar xzf #{options.source} -C #{destination}"
|
309 | when 'zip' then cmd = "unzip -u #{options.source} -d #{destination}"
|
310 | exec cmd, (err, stdout, stderr) ->
|
311 | return next err if err
|
312 | creates()
|
313 |
|
314 | creates = () ->
|
315 | return success() unless options.creates?
|
316 | fs.exists options.creates, (exists) ->
|
317 | return next new Error "Failed to create '#{path.basename options.creates}'" unless exists
|
318 | success()
|
319 |
|
320 | success = () ->
|
321 | extracted++
|
322 | next()
|
323 | conditions.all(options, next, extract)
|
324 | .on 'both', (err) ->
|
325 | callback err, extracted
|
326 | |
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 |
|
338 | git: (options, callback) ->
|
339 | options = misc.options options
|
340 | updated = 0
|
341 | each( options )
|
342 | .on 'item', (options, next) ->
|
343 | options.revision ?= 'HEAD'
|
344 | rev = null
|
345 | prepare = ->
|
346 | fs.stat options.destination, (err, stat) ->
|
347 | return clone() if err and err.code is 'ENOENT'
|
348 | return next new Error "Destination not a directory, got #{options.destination}" unless stat.isDirectory()
|
349 | gitDir = "#{options.destination}/.git"
|
350 | fs.stat gitDir, (err, stat) ->
|
351 | return next err if err or not stat.isDirectory()
|
352 | log()
|
353 | clone = ->
|
354 | mecano.exec
|
355 | cmd: "git clone #{options.source} #{path.basename options.destination}"
|
356 | cwd: path.dirname options.destination
|
357 | , (err, executed, stdout, stderr) ->
|
358 | return next err if err
|
359 | checkout()
|
360 | log = ->
|
361 | mecano.exec
|
362 | cmd: "git log --pretty=format:'%H' -n 1"
|
363 | cwd: options.destination
|
364 | , (err, executed, stdout, stderr) ->
|
365 | return next err if err
|
366 | current = stdout.trim()
|
367 | mecano.exec
|
368 | cmd: "git rev-list --max-count=1 #{options.revision}"
|
369 | cwd: options.destination
|
370 | , (err, executed, stdout, stderr) ->
|
371 | return next err if err
|
372 | if stdout.trim() isnt current
|
373 | then checkout()
|
374 | else next()
|
375 | checkout = ->
|
376 | mecano.exec
|
377 | cmd: "git checkout #{options.revision}"
|
378 | cwd: options.destination
|
379 | , (err) ->
|
380 | return next err if err
|
381 | updated++
|
382 | next()
|
383 | conditions.all options, next, prepare
|
384 | .on 'both', (err) ->
|
385 | callback err, updated
|
386 |
|
387 | |
388 |
|
389 |
|
390 |
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 |
|
401 |
|
402 |
|
403 |
|
404 |
|
405 |
|
406 |
|
407 | link: (options, callback) ->
|
408 | options = misc.options options
|
409 | linked = 0
|
410 |
|
411 | sym_exists = (option, callback) ->
|
412 | fs.exists option.destination, (exists) ->
|
413 | return callback null, false unless exists
|
414 | fs.readlink option.destination, (err, resolvedPath) ->
|
415 | return callback err if err
|
416 | return callback null, true if resolvedPath is option.source
|
417 | fs.unlink option.destination, (err) ->
|
418 | return callback err if err
|
419 | callback null, false
|
420 | sym_create = (option, callback) ->
|
421 | fs.symlink option.source, option.destination, (err) ->
|
422 | return callback err if err
|
423 | linked++
|
424 | callback()
|
425 | exec_exists = (option, callback) ->
|
426 | fs.exists option.destination, (exists) ->
|
427 | return callback null, false unless exists
|
428 | fs.readFile option.destination, 'ascii', (err, content) ->
|
429 | return callback err if err
|
430 | exec_cmd = /exec (.*) \$@/.exec(content)[1]
|
431 | callback null, exec_cmd and exec_cmd is option.source
|
432 | exec_create = (option, callback) ->
|
433 | content = """
|
434 | #!/bin/bash
|
435 | exec #{option.source} $@
|
436 | """
|
437 | fs.writeFile option.destination, content, (err) ->
|
438 | return callback err if err
|
439 | fs.chmod option.destination, option.chmod, (err) ->
|
440 | return callback err if err
|
441 | linked++
|
442 | callback()
|
443 | parents = for option in options then path.normalize path.dirname option.destination
|
444 | mecano.mkdir parents, (err, created) ->
|
445 | return callback err if err
|
446 | each( options )
|
447 | .parallel( true )
|
448 | .on 'item', (option, next) ->
|
449 | return next new Error "Missing source, got #{JSON.stringify(option.source)}" unless option.source
|
450 | return next new Error "Missing destination, got #{JSON.stringify(option.destination)}" unless option.destination
|
451 | option.chmod ?= 0o0755
|
452 | dispatch = ->
|
453 | if option.exec
|
454 | exec_exists option, (err, exists) ->
|
455 | return next() if exists
|
456 | exec_create option, next
|
457 | else
|
458 | sym_exists option, (err, exists) ->
|
459 | return next() if exists
|
460 | sym_create option, next
|
461 | dispatch()
|
462 | .on 'both', (err) ->
|
463 | callback err, linked
|
464 | |
465 |
|
466 |
|
467 |
|
468 |
|
469 |
|
470 |
|
471 |
|
472 |
|
473 |
|
474 |
|
475 |
|
476 |
|
477 |
|
478 |
|
479 |
|
480 |
|
481 |
|
482 |
|
483 |
|
484 |
|
485 |
|
486 |
|
487 |
|
488 |
|
489 |
|
490 |
|
491 |
|
492 | mkdir: (options, callback) ->
|
493 | options = misc.options options
|
494 | created = 0
|
495 | each( options )
|
496 | .on 'item', (option, next) ->
|
497 | option = { source: option } if typeof option is 'string'
|
498 | option.source = option.directory if not option.source? and option.directory?
|
499 | cwd = option.cwd ? process.cwd()
|
500 | option.source = path.resolve cwd, option.source
|
501 | return next new Error 'Missing source option' unless option.source?
|
502 | check = () ->
|
503 |
|
504 |
|
505 | fs.stat option.source, (err, stat) ->
|
506 | return create() if err and err.code is 'ENOENT'
|
507 | return next err if err
|
508 | return next() if stat.isDirectory()
|
509 | next err 'Invalid source, got #{JSON.encode(option.source)}'
|
510 | create = () ->
|
511 | option.chmod ?= 0o0755
|
512 | current = ''
|
513 | dirCreated = false
|
514 | dirs = option.source.split '/'
|
515 | each( dirs )
|
516 | .on 'item', (dir, next) ->
|
517 |
|
518 |
|
519 | if option.exclude? and option.exclude instanceof RegExp
|
520 | return next() if option.exclude.test dir
|
521 |
|
522 |
|
523 |
|
524 | current += "/#{dir}"
|
525 | fs.exists current, (exists) ->
|
526 | return next() if exists
|
527 | fs.mkdir current, option.chmod, (err) ->
|
528 | return next err if err
|
529 | dirCreated = true
|
530 | next()
|
531 | .on 'both', (err) ->
|
532 | created++ if dirCreated
|
533 | next err
|
534 | check()
|
535 | .on 'both', (err) ->
|
536 | callback err, created
|
537 | |
538 |
|
539 |
|
540 |
|
541 |
|
542 |
|
543 |
|
544 |
|
545 |
|
546 |
|
547 |
|
548 |
|
549 |
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 |
|
576 | remove: (options, callback) ->
|
577 | options = misc.options options
|
578 | deleted = 0
|
579 | each( options )
|
580 | .on 'item', (options, next) ->
|
581 | options = source: options if typeof options is 'string'
|
582 | return next new Error 'Missing source: #{option.source}' unless options.source?
|
583 |
|
584 | fs.lstat options.source, (err, stat) ->
|
585 | return next() if err
|
586 | options.options ?= {}
|
587 | rimraf options.source, (err) ->
|
588 | return next err if err
|
589 | deleted++
|
590 | next()
|
591 | .on 'both', (err) ->
|
592 | callback err, deleted
|
593 | |
594 |
|
595 |
|
596 |
|
597 |
|
598 |
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 |
|
611 |
|
612 |
|
613 |
|
614 |
|
615 | render: (options, callback) ->
|
616 | options = misc.options options
|
617 | rendered = 0
|
618 | each( options )
|
619 | .on 'item', (option, next) ->
|
620 | return next new Error 'Missing source or content' unless option.source or option.content
|
621 | return next new Error 'Missing destination' unless option.destination
|
622 | readSource = ->
|
623 | return writeContent() unless option.source
|
624 | fs.exists option.source, (exists) ->
|
625 | return next new Error "Invalid source, got #{JSON.stringify(option.source)}" unless exists
|
626 | fs.readFile option.source, (err, content) ->
|
627 | return next err if err
|
628 | option.content = content
|
629 | writeContent()
|
630 | writeContent = ->
|
631 | try content = eco.render option.content.toString(), option.context or {}
|
632 | catch err then return next err
|
633 | fs.writeFile option.destination, content, (err) ->
|
634 | return next err if err
|
635 | rendered++
|
636 | next()
|
637 | readSource()
|
638 | .on 'both', (err) ->
|
639 | callback err, rendered
|
640 |
|
641 |
|
642 |
|
643 | mecano.cp = mecano.copy
|
644 | mecano.exec = mecano.execute
|
645 | mecano.ln = mecano.link
|
646 | mecano.rm = mecano.remove
|
647 |
|
648 |
|