diff --git a/lib/FtpConnection.js b/lib/FtpConnection.js index 828e1a8..e373d3a 100644 --- a/lib/FtpConnection.js +++ b/lib/FtpConnection.js @@ -486,46 +486,46 @@ FtpConnection.prototype._LIST = function(commandArg, detailed, cmd) { // We're not doing a detailed listing, so we don't need to get username // and group name. fileInfos = files; - return finished(); + return finish(); } // Now we need to get username and group name for each file from user/group ids. fileInfos = []; var CONC = self.server.options.maxStatsAtOnce; - var j = 0; - for (var i = 0; i < files.length && i < CONC; ++i) { - handleFile(i); + var total = files.length; + for (var i = 0; i < CONC; ++i) { + handleFile(); } - j = --i; - function handleFile(ii) { - if (i >= files.length) { - return i === files.length + j ? finished() : null; + function handleFile() { + if (fileInfos.length === total) { + return finish(); } - self.server.getUsernameFromUid(files[ii].stats.uid, function(e1, uname) { - self.server.getGroupFromGid(files[ii].stats.gid, function(e2, gname) { + if (files.length === 0) { + return; + } + + var file = files.shift(); + self.server.getUsernameFromUid(file.stats.uid, function(e1, uname) { + self.server.getGroupFromGid(file.stats.gid, function(e2, gname) { if (e1 || e2) { self._logIf(LOG.WARN, 'Error getting user/group name for file: ' + util.inspect(e1 || e2)); - fileInfos.push({ - file: files[ii], - uname: null, - gname: null, - }); - } else { - fileInfos.push({ - file: files[ii], - uname: uname, - gname: gname, - }); + uname = null; + gname = null; } - handleFile(++i); + fileInfos.push({ + file: file, + uname: uname, + gname: gname, + }); + handleFile(); }); }); } - function finished() { + function finish() { // Sort file names. if (!self.server.options.dontSortFilenames) { if (self.server.options.filenameSortMap !== false) { diff --git a/lib/FtpServer.js b/lib/FtpServer.js index 2ff206e..aa21455 100644 --- a/lib/FtpServer.js +++ b/lib/FtpServer.js @@ -31,10 +31,10 @@ function FtpServer(host, options) { self.getRoot = options.getRoot; self.getUsernameFromUid = options.getUsernameFromUid || function(uid, c) { - c(null, 'ftp'); + process.nextTick(c, null, 'ftp'); }; self.getGroupFromGid = options.getGroupFromGid || function(gid, c) { - c(null, 'ftp'); + process.nextTick(c, null, 'ftp'); }; self.debugging = options.logLevel || 0; self.useWriteFile = options.useWriteFile; diff --git a/lib/glob.js b/lib/glob.js index e6805aa..3fb6fdb 100644 --- a/lib/glob.js +++ b/lib/glob.js @@ -10,46 +10,43 @@ function setMaxStatsAtOnce(n) { // unique directory to be listed. So this can be pretty simple. function statList(fsm, list, callback) { - if (list.length === 0) { - return callback(null, []); - } - - var stats = []; var total = list.length; + var finished = false; + var stats = []; + for (var i = 0; i < CONC; ++i) { handleFile(); } - var erroredOut = false; - function handleFile() { - if (erroredOut) { - return; + if (stats.length === total) { + return finish(null); } + if (list.length === 0) { - if (stats.length === total) { - finished(); - } return; } var path = list.shift(); fsm.stat(path, function(err, st) { if (err) { - erroredOut = true; - callback(err); - } else { - stats.push({ - name: PathModule.basename(path), - stats: st, - }); - handleFile(); + return finish(err); } + + stats.push({ + name: PathModule.basename(path), + stats: st, + }); + handleFile(); }); } - function finished() { - callback(null, stats); + function finish(err) { + if (finished) { + return; + } + finished = true; + callback(err, stats); } } diff --git a/test/lib/common.js b/test/lib/common.js index e41847b..3edb0b2 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -73,7 +73,7 @@ var common = module.exports = { }); connection.on('command:pass', function(pass, success, failure) { if (pass === customOptions.pass) { - success(username); + success(username, customOptions.fs); } else { failure(); } diff --git a/test/list.js b/test/list.js index 31e0773..0a624b9 100644 --- a/test/list.js +++ b/test/list.js @@ -1,4 +1,5 @@ var common = require('./lib/common'); +var fs = require('fs'); describe('LIST command', function() { 'use strict'; @@ -6,81 +7,149 @@ describe('LIST command', function() { var client; var server; - beforeEach(function(done) { - server = common.server(); - client = common.client(done); - }); + describe('regular cases', function() { - function unslashRgx(rgx) { - return String(rgx).replace(/^\/|\/$/g, ''); - } - - it('should return "-" as first character for files', function(done) { - client.list('/', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing, / data\d*\.txt$/); - listing.should.have.lengthOf(6); - listing[0].should.startWith('-'); - done(); + beforeEach(function(done) { + server = common.server(); + client = common.client(done); }); - }); - it('should return "d" as first character for directories', function(done) { - client.list('/', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing, / usr$/); - listing.should.have.lengthOf(1); - listing[0].should.startWith('d'); - done(); + function unslashRgx(rgx) { + return String(rgx).replace(/^\/|\/$/g, ''); + } + + it('should return "-" as first character for files', function(done) { + client.list('/', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing, / data\d*\.txt$/); + listing.should.have.lengthOf(6); + listing[0].should.startWith('-'); + done(); + }); }); - }); - it('should list files similar to ls -l', function(done) { - client.list('/usr', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing); - listing.should.have.lengthOf(1); - var lsLongRgx = [ - /($# file modes: ___|)[d-]([r-][w-][x-]){3}/, - /($# ?¿?¿? inodes?: |)\d+/, - /($# owner name: ___|)\S+/, - /($# owner group: __|)\S+/, - /($# size in bytes: |)\d+/, - /($# month: ________|)[A-Z][a-z]{2}/, - /($# day of month: _|)\d{1,2}/, - /($# time or year: _|)([\d ]\d:|19|[2-9]\d)\d{2}/, - /($# file name: ____|)[\S\s]+/, - ].map(unslashRgx).join('\\s+'); - lsLongRgx = new RegExp(lsLongRgx, ''); - var match = (lsLongRgx.exec(listing[0]) || [false]); - match[0].should.equal(listing[0]); - done(); + it('should return "d" as first character for directories', function(done) { + client.list('/', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing, / usr$/); + listing.should.have.lengthOf(1); + listing[0].should.startWith('d'); + done(); + }); }); - }); - it('should list a single file', function(done) { - var filename = 'data.txt'; - client.list('/' + filename, function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing, ' ' + filename); - listing.should.have.lengthOf(1); - listing[0].should.startWith('-'); - done(); + it('should list files similar to ls -l', function(done) { + client.list('/usr', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(1); + var lsLongRgx = [ + /($# file modes: ___|)[d-]([r-][w-][x-]){3}/, + /($# ?¿?¿? inodes?: |)\d+/, + /($# owner name: ___|)\S+/, + /($# owner group: __|)\S+/, + /($# size in bytes: |)\d+/, + /($# month: ________|)[A-Z][a-z]{2}/, + /($# day of month: _|)\d{1,2}/, + /($# time or year: _|)([\d ]\d:|19|[2-9]\d)\d{2}/, + /($# file name: ____|)[\S\s]+/, + ].map(unslashRgx).join('\\s+'); + lsLongRgx = new RegExp(lsLongRgx, ''); + var match = (lsLongRgx.exec(listing[0]) || [false]); + match[0].should.equal(listing[0]); + done(); + }); }); - }); - it('should list a subdirectory', function(done) { - client.list('/usr', function(error, listing) { - error.should.equal(false); - listing = common.splitResponseLines(listing); - listing.should.have.lengthOf(1); - listing[0].should.startWith('d'); - listing[0].should.endWith(' local'); - done(); + it('should list a single file', function(done) { + var filename = 'data.txt'; + client.list('/' + filename, function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing, ' ' + filename); + listing.should.have.lengthOf(1); + listing[0].should.startWith('-'); + done(); + }); + }); + + it('should list a subdirectory', function(done) { + client.list('/usr', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(1); + listing[0].should.startWith('d'); + listing[0].should.endWith(' local'); + done(); + }); + }); + + afterEach(function() { + server.close(); }); }); - afterEach(function() { - server.close(); + describe('corner case', function() { + 'use strict'; + + var files; + + beforeEach(function(done) { + server = common.server({ + fs: { + stat: function(path, callback) { + process.nextTick( + callback, + undefined /* err */, + new fs.Stats(0,32768 /* file mode */,0,0,0,0,0,0,0,43 /* size */,0,0,0,0) + ); + }, + readdir: function(path, callback) { + process.nextTick(callback, undefined, files); + }, + }, + }); + client = common.client(done); + }); + + it('supports directories without files', function(done) { + files = []; + client.list('/', function(error, listing) { + error.should.equal(false); + listing.should.equal(''); + done(); + }); + }); + + it('supports directories with only a few files', function(done) { + files = ['a']; + client.list('/', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(1); + done(); + }); + }); + + it('supports directories with many files', function(done) { + function ArrayWithStrings(n) { + return Array.apply(null, Array(n)).map(function(x, i) { + return i.toString(); + }); + } + files = ArrayWithStrings(6000); + client.list('/', function(error, listing) { + error.should.equal(false); + listing = common.splitResponseLines(listing); + listing.should.have.lengthOf(files.length); + done(); + }); + }); + + afterEach(function() { + server.close(); + }); }); + + }); +