UNPKG

grunt-ssh

Version:
427 lines (382 loc) 15.3 kB
/* * grunt-ssh * https://github.com/andrewrjones/grunt-ssh * * Copyright (c) 2013 Andrew Jones * Licensed under the MIT license. */ module.exports = function (grunt) { 'use strict'; grunt.util = grunt.util || grunt.utils; grunt.registerMultiTask('sftp', 'Copy files to a (remote) machine running an SSH daemon.', function () { var utillib = require('./lib/util').init(grunt); var fs = require('fs'); var async = require('async'); var Connection = require('ssh2'); var path = require('path'); var sftpHelper = require("./lib/sftpHelpers").init(grunt); var ProgressBar = require('progress'); var options = this.options({ path: '', host: false, username: false, password: false, agent: "", port: utillib.port, proxy: { port: utillib.port }, minimatch: {}, srcBasePath: "", destBasePath: "", createDirectories: false, directoryPermissions: parseInt(755, 8), showProgress: false, mode: 'upload' }); var tally = { dirs: 0, files: 0 }; grunt.verbose.writeflags(options, 'Raw Options'); function setOption(optionName) { var option; if ((!options[optionName]) && (option = grunt.option(optionName))) { options[optionName] = option; } } setOption('config'); if (options.config && grunt.util._(options.config).isString()) { this.requiresConfig(['sshconfig', options.config]); var configOptions = grunt.config.get(['sshconfig', options.config]); options = grunt.util._.extend(options, configOptions); } setOption('username'); setOption('password'); setOption('passphrase'); setOption('showProgress'); // add trailing slash to path if needed if (('' !== options.path) && !options.path.match(/(\/|\\)$/)) { options.path = options.path + '/'; } if (('' !== options.destBasePath) && !options.destBasePath.match(/(\/|\\)$/)) { options.destBasePath = options.destBasePath + '/'; } grunt.verbose.writeflags(options, 'Options'); var files = this.files; var c = new Connection(); var done = this.async(); c.on('keyboard-interactive', function(){ var prompts = arguments[3]; var reply = arguments[4]; prompts.forEach(function(question){ var msg = question.prompt.toLowerCase(); if (msg.indexOf('password') !== -1){ reply([options.password]); } }); }); c.on('connect', function () { grunt.verbose.writeln('Connection :: connect'); }).on('ready', function () { c.sftp(function (err, sftp) { if (err) { c.end(); grunt.log.error(err); return; } sftp.on('end', function () { grunt.verbose.writeln('SFTP :: session end'); }).on('close', function (had_error) { grunt.verbose.writeln('SFTP :: session close'); }); async.eachSeries(files, function (file, callback) { var srcFiles = options.mode === 'upload' ? grunt.file.expand(options.minimatch, file.src) : file.orig.src; var fileQueue = []; var functionQueue = []; var paths = []; if (options.mode === 'download') { if (srcFiles.length === 0) { return callback(new Error('Unable to copy; no valid remote files were found.')); } srcFiles.forEach(function (srcFile) { var downloadSrc = (options.srcBasePath ? options.srcBasePath : options.path) + srcFile; var downloadDest = options.destBasePath ? file.dest.replace(/^\//, '') : file.dest; var offsetDirectory = ''; if (options.srcBasePath.indexOf(options.path) === 0) { offsetDirectory = options.srcBasePath.replace(options.path, ""); } if (file.dest[file.dest.length - 1] === '/') { downloadDest += srcFile.replace(offsetDirectory, ""); } srcFiles.forEach(function (srcFile) { var destFile = options.path; if (srcFile.indexOf(options.srcBasePath) === 0) { destFile += srcFile.replace(options.srcBasePath, ""); } else { destFile += srcFile; } if (grunt.file.isDir(srcFile)) { if (paths.indexOf(destFile) === -1) { paths.push(destFile); } } else { fileQueue.push({ src: srcFile, dest: destFile }); } var pathName = path.dirname(destFile); if (paths.indexOf(pathName) === -1) { paths.push(pathName); } }); if (options.destBasePath) { downloadDest = options.destBasePath + downloadDest; } var count = 1; var downloadingRecursive = function (src, dest) { sftp.open(src, 'r', function (err, buffer) { if (err) { return callback(err); } sftp.fstat(buffer, function (err, stats) { if (err) { return callback(err); } var isDirectory = stats.isDirectory(); if (isDirectory) { grunt.verbose.writeln("Checking existence of path " + dest); fs.exists(dest, function (exists) { var recursiveCallback = function (directorySrc, directoryDest) { sftp.readdir(directorySrc, function (err, list) { if (err) { return callback(err); } count--; count += list.length; if (!count) { callback(); } list.forEach(function (item) { downloadingRecursive(path.join(directorySrc, item.filename), path.join(directoryDest, item.filename)); }); }); }; if (!exists) { if (!options.createDirectories) { callback(); return; } fs.mkdir(dest, options.directoryPermissions, function () { recursiveCallback(src, dest); }); } else { recursiveCallback(src, dest); } }); } else { var fpOptions = { chunkSize: options.chunkSize }; var downloadFile = function (fileSrc, fileDest) { grunt.verbose.writeln('downloading ' + fileSrc + ' to ' + fileDest); sftp.fastGet(fileSrc, fileDest, fpOptions, function (err) { if (err) { return callback(err); } grunt.verbose.writeln('download ' + fileSrc + ' to ' + fileDest); tally.files++; count--; if (!count) { callback(); } }); }; if (options.showProgress) { sftp.open(src, 'r', function (err, buffer) { sftp.fstat(buffer, function (err, stats) { if (err) { return callback(err); } var fileSize = stats.size; var barTemplate = src + ' [:bar] :percent of ' + utillib.fileSizeReadable(fileSize); var bar = new ProgressBar(barTemplate, { complete: '=', incomplete: ' ', width: 20, total: fileSize }); fpOptions.step = function (totalSent, lastSent, total) { bar.tick(lastSent); }; downloadFile(src, dest); }); }); } else { downloadFile(src, dest); } } }); }); }; var recursiveMkdir = function (dir, mode, callback) { var existsFunction = fs.exists || path.exists; existsFunction(dir, function (exists) { if (exists) { return callback(null); } var current = path.resolve(dir); var parent = path.dirname(current); recursiveMkdir(parent, mode, function (err) { if (err) { return callback(err); } fs.mkdir(current, mode, function (err) { if (err) { return callback(err); } callback(); }); }); }); }; if (options.createDirectories) { recursiveMkdir(path.dirname(downloadDest), options.directoryPermissions, function () { downloadingRecursive(downloadSrc, downloadDest); }); } else { downloadingRecursive(downloadSrc, downloadDest); } }); } else { if (options.mode === 'upload') { if (srcFiles.length === 0) { return callback(new Error('Unable to copy; no valid source files were found.')); } // TODO - before we start copying files ensure all // the directories we are copying into will exist, otherwise // the async thingie causes problems srcFiles.forEach(function (srcFile) { if (grunt.file.isDir(srcFile)) { return; } var destFile = options.path; if (srcFile.indexOf(options.srcBasePath) === 0) { destFile += srcFile.replace(options.srcBasePath, ""); } else { destFile += srcFile; } fileQueue.push({ src: srcFile, dest: destFile }); var pathName = path.dirname(destFile); if (paths.indexOf(pathName) === -1) { paths.push(pathName); } }); async.eachSeries(paths, function (path, callback) { if (!options.createDirectories) { callback(); return; } grunt.verbose.writeln("Checking existence of path " + path); sftpHelper.sftpRecursiveMkDir(sftp, path, { permissions: options.directoryPermissions }, function (result, msg) { if (!result) { callback(new Error(msg)); } else { callback(); tally.dirs++; } }); }, function (err) { if (err) { callback(new Error('Path creation failed: ' + err)); return; } async.eachSeries(fileQueue, function (file, callback) { var fpOptions = { chunkSize: options.chunkSize }; if (options.showProgress) { var fileSize = fs.statSync(file.src).size; var barTemplate = file.src + ' [:bar] :percent of ' + utillib.fileSizeReadable(fileSize); var bar = new ProgressBar(barTemplate, { complete: '=', incomplete: ' ', width: 20, total: fileSize }); fpOptions.step = function (totalSent, lastSent, total) { bar.tick(lastSent); }; } grunt.verbose.writeln('copying ' + file.src + ' to ' + file.dest); sftp.fastPut(file.src, file.dest, fpOptions, function (err) { if (err) { return callback(err); } grunt.verbose.writeln('copied ' + file.src + ' to ' + file.dest); tally.files++; callback(); }); }, function (err) { callback(err); }); }); } } }, function (err) { if (err) { grunt.fail.warn(err); } c.end(); }); }); }).on('error', function (err) { grunt.fail.warn('Connection :: error :: ' + err); }).on('debug', function (message) { grunt.log.debug('Connection :: debug :: ' + message); }).on('end', function () { grunt.verbose.writeln('Connection :: end'); }).on('close', function (had_error) { if (had_error) { grunt.log.error(had_error); } grunt.log.writeln(( tally.dirs ? 'Created ' + tally.dirs.toString().cyan + ' directories, copied ' : 'Copied ') + (tally.files ? tally.files.toString().cyan + ' files' : '')); grunt.verbose.writeln('Connection :: close'); done(); }); var connectionOptions = utillib.parseConnectionOptions(options); connectionOptions.tryKeyboard = true; if (options.proxy.host) { var proxyConnectionOptions = utillib.parseConnectionOptions(options.proxy); var proxyConnection = new Connection(); proxyConnection.on('connect', function () { grunt.verbose.writeln('Proxy connection :: connect'); }); proxyConnection.on('error', function (err) { grunt.fail.warn('Proxy connection :: error :: ' + err); }); proxyConnection.on('ready', function() { grunt.verbose.writeln('Proxy connection :: ready'); proxyConnection.exec('nc ' + connectionOptions.host + ' ' + connectionOptions.port, function(err, stream) { if (err) { proxyConnection.end(); throw err; } connectionOptions.sock = stream; c.connect(connectionOptions); }); }); proxyConnection.connect(proxyConnectionOptions); } else { c.connect(connectionOptions); } }); };