123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501 |
- var fs = require('fs'),
- EventEmitter = require('events').EventEmitter,
- inherits = require('util').inherits,
- FTP = require('ftp'),
- _ = require('lodash'),
- glob = require('glob'),
- async = require('async'),
- Client,
- MAX_CONNECTIONS = 10,
- logging = 'basic',
- loggingLevels = ['none', 'basic', 'debug'],
- log = function (msg, lvl) {
- if (loggingLevels.indexOf(lvl) <= logging) {
- console.log(msg);
- }
- };
-
- Client = module.exports = function (config, options) {
- if (!(this instanceof Client))
- return new Client();
-
- this.config = _.defaults(config || {}, {
- host: 'localhost',
- port: 21,
- user: 'anonymous',
- password: 'anonymous@'
- });
-
- this.options = _.defaults(options || {}, {
- overwrite: 'older' // | 'all' | 'none'
- });
-
- if (this.options.logging) {
- logging = this.options.logging;
- logging = loggingLevels.indexOf(logging);
- }
-
- this.ftp = new FTP();
- this.ftp.on('error', function (err) {
- throw new Error(err);
- });
- };
-
- inherits(Client, EventEmitter);
-
- Client.prototype.connect = function (callback) {
- this.ftp.on('ready', function () {
- log('Connected to ' + this.config.host, 'debug');
- log('Checking server local time...', 'debug');
- this._checkTimezone(function () {
- this.emit('ready');
- if (typeof callback !== 'undefined') {
- callback();
- }
- }.bind(this));
- }.bind(this));
-
- this.ftp.connect(this.config || {});
- };
-
- Client.prototype.upload = function (patterns, dest, options, uploadCallback) {
- options = _.defaults(options || {}, this.options);
-
- var paths, files, dirs, toDelete = [], ftp = this.ftp;
-
- paths = this._glob(patterns);
- paths = this._clean(paths, options.baseDir);
- paths = this._stat(paths);
-
- files = paths[1];
- dirs = paths[0];
-
- var sources = function (array) {
- array.forEach(function (file) {
- log(file.src, 'debug');
- });
- }
-
- log('FILES TO UPLOAD', 'debug');
- sources(files);
-
- log('DIRS TO UPLOAD', 'debug');
- sources(dirs);
-
- var deleteFiles = function (cb) {
- async.eachLimit(toDelete, MAX_CONNECTIONS, function (file, callback) {
- var destPath = (file.src.indexOf(options.baseDir) === 0 ?
- file.src.substring(options.baseDir.length + 1) : file.src);
-
- log('Deleting ' + destPath, 'debug');
-
- if (file.isDirectory()) {
- ftp.rmdir(destPath, function (err) {
- if (err) log(err, 'debug');
- callback();
- }.bind(file));
- } else {
- ftp.delete(destPath, function (err) {
- if (err) log(err, 'debug');
- callback();
- }.bind(file));
- }
-
- }, cb);
- },
- uploadFiles = function (cb) {
- async.eachLimit(files, MAX_CONNECTIONS, function (file, callback) {
- var destPath = (file.src.indexOf(options.baseDir) === 0 ?
- file.src.substring(options.baseDir.length + 1) : file.src);
-
- log('Uploading file ' + destPath, 'debug');
-
- ftp.put(file.src, destPath, function (err) {
- if (err) {
- log('Error uploading file ' + destPath + ': ' + err, 'basic');
- this.uploaded = false;
- this.error = err;
- } else {
- log('Finished uploading file ' + destPath, 'basic');
- this.uploaded = true;
- }
- callback();
- }.bind(file));
- }, cb);
- },
- uploadDirs = function (cb) {
- async.eachLimit(dirs, MAX_CONNECTIONS, function (dir, callback) {
- var destPath = (dir.src.indexOf(options.baseDir) === 0 ?
- dir.src.substring(options.baseDir.length + 1) : dir.src);
-
- log('Uploading directory ' + destPath, 'debug');
-
- ftp.mkdir(destPath, function (err) {
- if (err) {
- log('Error uploading directory ' + destPath + ': ' + err, 'basic');
- this.uploaded = false;
- this.error = err;
- } else {
- log('Finished uploading directory ' + destPath, 'basic');
- this.uploaded = true;
- }
- callback();
- }.bind(dir))
- }, cb);
- },
- compare = function (cb) {
- var timeDif = this.serverTimeDif;
- if (options.overwrite === 'all') {
- toDelete = files.concat(dirs);
- cb();
- } else {
- async.eachLimit(files.concat(dirs), MAX_CONNECTIONS, function (file, callback) {
- var destPath = (file.src.indexOf(options.baseDir) === 0 ?
- file.src.substring(options.baseDir.length + 1) : file.src);
-
- ftp.list(destPath, function (err, list) {
- if (err) log(err, 'debug');
- log('Comparing file' + this.src, 'debug');
- if (list && list[0]) {
- if (options.overwrite === 'older' && list[0].date && new Date(list[0].date.getTime() + timeDif) < this.mtime) {
- toDelete.push(this);
- } else {
- if (this.isDirectory()) {
- dirs.forEach(function (dir, i) {
- if (dir.src === this.src) {
- dirs.splice(i, 1);
- }
- }.bind(this))
- } else {
- files.forEach(function (file, i) {
- if (file.src === this.src) {
- files.splice(i, 1);
- }
- }.bind(this))
- }
- }
- }
- callback();
- }.bind(file))
- }, cb);
- }
- }.bind(this)
-
-
- this._cwd(dest, function () {
- log('Moved to directory ' + dest, 'debug');
-
- var tasks = [];
-
- // collect files and dirs to be deleted
- tasks.push(function (callback) {
- log('1. Compare files', 'debug');
- return compare(function (err) {
- if (err) log(err, 'debug');
- log('FILES TO DELETE', 'debug');
- sources(toDelete);
- log('Found ' + files.length + ' files and ' + dirs.length + ' directories to upload.', 'basic');
- callback();
- }.bind(this));
- }.bind(this));
-
- // delete files and dirs
- tasks.push(function (callback) {
- log('2. Delete files', 'debug');
- return deleteFiles(function (err) {
- if (err) log(err, 'debug');
- callback();
- }.bind(this));
- }.bind(this));
-
- // upload dirs
- tasks.push(function (callback) {
- log('3. Upload dirs', 'debug');
- return uploadDirs(function (err) {
- if (err) log(err, 'debug');
- else log('Uploaded dirs', 'debug');
- callback();
- }.bind(this));
- }.bind(this));
-
- // upload files
- tasks.push(function (callback) {
- log('4. Upload files', 'debug');
- return uploadFiles(function (err) {
- if (err) log(err, 'debug');
- else log('Uploaded files', 'debug');
- callback();
- }.bind(this));
- }.bind(this));
-
- async.series(tasks, function (err) {
- if (err) throw err;
- ftp.end();
- log('Upload done', 'debug');
- var result = {
- uploadedFiles: [],
- uploadedDirs: [],
- errors: {}
- }
- dirs.forEach(function (dir) {
- if (dir.uploaded) {
- result.uploadedDirs.push(dir.src);
- } else {
- result.errors[dir.src] = dir.error;
- }
- });
- files.forEach(function (file) {
- if (file.uploaded) {
- result.uploadedFiles.push(file.src);
- } else {
- result.errors[file.src] = file.error;
- }
- })
- log('Finished uploading ' + result.uploadedFiles.length + ' of ' + files.length + ' files.', 'basic');
- uploadCallback(result);
- });
- }.bind(this));
- }
-
- Client.prototype.download = function (source, dest, options, downloadCallback) {
- options = _.defaults(options || {}, this.options);
-
- if (!fs.existsSync(dest)) {
- this.ftp.end();
- throw new Error('The download destination directory ' + dest + ' does not exist.');
- }
-
- var ftp = this.ftp;
- var timeDif = this.serverTimeDif;
-
- var files = {}, dirs = [];
- var queue = async.queue(function (task, callback) {
- log('Queue worker started for ' + task.src, 'debug');
- ftp.list(task.src, function (err, list) {
- if (err || typeof list === 'undefined' || typeof list[0] === 'undefined') {
- throw new Error('The source directory on the server ' + task.src + ' does not exist.');
- }
-
- if (list && list.length > 1) {
- _.each(list.splice(1, list.length - 1), function (file) {
- if (file.name !== '.' && file.name !== '..') {
- var filename = task.src + '/' + file.name;
- if (file.type === 'd') {
- dirs.push(filename);
- queue.push({src: filename}, function (err) {
- if (err) log(err, 'debug');
- });
- } else if (file.type === '-') {
- files[filename] = {
- date: file.date
- };
- }
- }
- });
- }
-
- callback();
- });
- }, MAX_CONNECTIONS);
-
- queue.drain = function () {
- log([dirs, files], 'debug');
-
- dirs.forEach(function (dir) {
- var dirName = dest + '/' + (dir.indexOf(source) === 0 ? dir.substring(source.length + 1) : dir);
- if (!fs.existsSync(dirName)) {
- fs.mkdirSync(dirName);
- log('Created directory ' + dirName, 'debug');
- }
- });
-
- var toDelete = [], result = {
- downloadedFiles: [],
- errors: {}
- };
-
- if (options.overwrite === 'all') {
- toDelete = _.keys(files);
- }
-
- if (options.overwrite === 'older') {
- var skip = [];
-
- _.each(files, function (details, file) {
- var fileName = file.replace(source, dest);
- log('Comparing file ' + fileName, 'debug');
-
- if (fs.existsSync(fileName)) {
- var stat = fs.statSync(fileName);
-
- if (stat.mtime.getTime() < details.date.getTime() + timeDif) {
- toDelete.push(fileName);
- } else {
- skip.push(file);
- }
- }
- });
-
- skip.forEach(function (file) {
- delete files[file];
- });
- }
-
- if (options.overwrite === 'none') {
- var skip = [];
- _.each(files, function (details, file) {
- var fileName = file.replace(source, dest);
-
- if (fs.existsSync(fileName)) {
- skip.push(file);
- }
- });
-
- skip.forEach(function (file) {
- delete files[file];
- });
- }
-
- toDelete.forEach(function (file) {
- try {
- fs.unlinkSync(file.replace(source, dest));
- } catch (e) {
-
- }
- });
-
- log('Found ' + _.keys(files).length + ' files to download.', 'basic');
-
- async.forEachLimit(_.keys(files), MAX_CONNECTIONS, function (file, callback) {
- log('Downloading file ' + file, 'debug');
-
- ftp.get(file, function (err, stream) {
- if (err && err.message !== 'Unable to make data connection') {
- log('Error downloading file ' + file, 'basic');
- result['errors'][file] = err;
- }
- if (stream) {
- stream.once('close', function () {
- log('Finished downloading file ' + file, 'basic');
- result['downloadedFiles'].push(file);
- callback();
- });
- stream.pipe(fs.createWriteStream(file.replace(source, dest)));
- }
- });
- }, function (err) {
- if (err) return next(err);
- if (downloadCallback) {
- downloadCallback(result);
- }
- log('Finished downloading ' + result.downloadedFiles.length + ' of ' + _.keys(files).length + ' files', 'basic');
- ftp.end();
- });
-
- log(['To delete: ', toDelete], 'debug');
- log(['To download: ', files], 'debug');
- }
-
- queue.push({src: source}, function (err) {
- if (err) log(err, 'debug');
- });
-
- // 1. check if directory exists
- // 2. if not throw an error
- // 3. if it does - build a list of directories and files using async.queue
- // 4. download all the files from the list
-
- }
-
- Client.prototype._cwd = function (path, callback) {
- this.ftp.mkdir(path, true, function (err) {
- if (err) log(err, 'debug');
- this.ftp.cwd(path, function (err) {
- if (err) log(err, 'debug');
- callback();
- });
- }.bind(this));
- }
-
- Client.prototype._checkTimezone = function (cb) {
- var localTime = new Date().getTime(),
- serverTime,
- ftp = this.ftp;
-
- async.series([
- function (next) {
- return ftp.put(new Buffer(''), '.timestamp', function (err) {
- if (err) log(err, 'debug');
- next();
- });
- },
- function (next) {
- return ftp.list('.timestamp', function (err, list) {
- if (err) log(err, 'debug');
- if (list && list[0] && list[0].date) {
- serverTime = list[0].date.getTime();
- }
- next();
- });
- },
- function (next) {
- return ftp.delete('.timestamp', function (err) {
- if (err) log(err, 'debug');
- next();
- });
- }
- ], function () {
- this.serverTimeDif = localTime - serverTime;
- log('Server time is ' + new Date(new Date().getTime() - this.serverTimeDif), 'debug');
- cb();
- }.bind(this));
- }
-
- Client.prototype._glob = function (patterns) {
- var include = [],
- exclude = [];
-
- if (!_.isArray(patterns)) {
- patterns = [patterns];
- }
-
- patterns.forEach(function (pattern) {
- if (pattern.indexOf('!') === 0) {
- exclude = exclude.concat(glob.sync(pattern.substring(1), {nonull: false}) || []);
- } else {
- include = include.concat(glob.sync(pattern, {nonull: false}) || []);
- }
- });
-
- return _.difference(include, exclude);
- }
-
- Client.prototype._stat = function (files) {
- var result = [
- [],
- []
- ];
- _.each(files, function (file) {
- file = _.extend(fs.statSync(file), {src: file});
- if (file.isDirectory()) {
- result[0].push(file);
- } else {
- result[1].push(file);
- }
- });
- return result;
- }
-
- Client.prototype._clean = function (files, baseDir) {
- if (!baseDir) {
- return files;
- }
-
- return _.compact(_.map(files, function (file) {
- if (file.replace(baseDir, '')) {
- return file;
- } else {
- return null;
- }
- }));
- }
|