recursive walk folder
@param {String} dir directory
@param {Object} opts walk options - include/exclude file extensions
@param {Function} done callback(err, array)
@param {String} rootDir used when walking recursively, do not use this argument
@param {Integer} level actual level in tree
@returns {String} resolved dir
function walk(dir, opts, done, rootDir, level) { // done(err, result)
    if(arguments.length === 2) {
        done = arguments[1];
        opts = { levels: null };
    }
    
    if(typeof done !== 'function') throw new Error('Wrong arguments');
    if(!rootDir) dir = resolve(dir).fullPath;
    level = level || 1;
    
    var onError = opts.onError; // if onerror callback defined, just emit error and go ahead
    var maxRepeat = opts.maxRepeat || 5;
    var repeatInterval = opts.repeatInterval || 200;
    var skipSymlinks = opts.skipSymlinks === false ? false : true;
    
    //if(dir.substring(dir.length-1, dir.length) === '/')
    //    dir = dir.substring(0, dir.length-1); // remove last '/'
    
    var include = opts.include || [];
    //var exclude = opts.exclude || []; - not implemented
    
    var results = {};
    var pending = 0;
    
    fs.readdir(dir, function(err, list) {
        if(err && err.code === 'ENOENT') return done(null, {}); // directory removed, just return empty result 
        else if(err) {
            err = new Error('fsExt.walkdirRecursive: readdir failed').cause(err);
            if(onError) {
                onError(err);
                return done(null, results);
            }
            else {
                pending = 0;
                return done(err);
            }
        }
        
        if(!rootDir && opts.includeRootDir) { // undefined rootDir means this is parent dir
            list.unshift(dir);
        }
        pending = list.length;
        if(!pending) return done(null, results);
        
        rootDir = rootDir || dir;
        list.forEach(function(file) {
            if(file!==rootDir){
                file = dir + '/' + file;
                file = file.replace(/\\/g, '/');
            }
            
            var baseDir = rootDir;
            if(opts.includeRootDir) { 
                baseDir = baseDir.split('/');
                baseDir.pop();
                baseDir = baseDir.join('/');
            }
            var fileId = file.replace(baseDir + '/', '');
            getStats(file, fileId, done);
        });
    });
    
    function getStats(file, fileId, done, repeated){
        repeated = repeated || 0;
        fs.lstat(file, function(err, stat) {
            if(pending <= 0) return;
            
            if(err && err.code === 'ENOENT') { // directory or file removed, just skip it
                if(pending===1) return done(null, results);
            }
            else if(err && repeated >= maxRepeat) {
                if(onError) {
                    onError(err);
                    if(!--pending) return done(null, results);
                }
                else {
                    pending = 0;
                    return done(err);
                }
            }
            else if(err) {
                return setTimeout(function(){
                    getStats(file, fileId, done, repeated+1);
                }, repeatInterval);
            }
            
            if(skipSymlinks && stat && stat.isSymbolicLink()) {
                if(!--pending) done(null, results);
                return;
            }
            
            if(!stat){
                if(!--pending) done(null, results);
            }
            else if(stat.isDirectory()) {
                results[fileId] = getFileItem(fileId, stat, file);
                if(file!==rootDir && (!opts.levels || opts.levels>=level+1)) walk(file, opts, function(err, res) {
                    if(err) {
                        res = {};
                        if(onError) onError(err);
                    }
                    
                    for(var key in res) {
                        results[key] = res[key];
                    }
                    if (!--pending) done(null, results);
                }, rootDir, level+1);
                else if(!--pending) done(null, results); 
            }
            else {
                var fileExt = getExt(fileId);
                if(include.length > 0 && include.indexOf(fileExt) !== -1)
                    results[fileId] = getFileItem(fileId, stat, file);
                else if(include.length === 0)
                    results[fileId] = getFileItem(fileId, stat, file);
                if(!--pending) done(null, results);
            }
            
        });
    }
    
    return dir;
}