├── .gitignore ├── examples └── mirror.js ├── index.js ├── package.json ├── readme.md ├── stat.js └── test ├── hello.txt └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store -------------------------------------------------------------------------------- /examples/mirror.js: -------------------------------------------------------------------------------- 1 | var mirror = require('mirror-folder') 2 | var ftpfs = require('../') 3 | var ftp = ftpfs({ host: process.argv[2], debug: function (x) { } }) 4 | 5 | var progress = mirror({name: '/', fs: ftp}, __dirname + '/mirror', { dryRun: true }, function (err) { 6 | if (err) throw err 7 | console.error('all done') 8 | ftp.end() 9 | }) 10 | 11 | progress.on('put', function (src, dest) { 12 | var meta = {name: src.name, stat: src.stat, metadata: src.stat._ftpmetadata} 13 | delete meta.stat._ftpmetadata 14 | console.log(JSON.stringify(meta)) 15 | }) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var FTP = require('ftp') 3 | var thunky = require('thunky') 4 | var duplexify = require('duplexify') 5 | var pump = require('pump') 6 | var through = require('through2') 7 | var extend = require('xtend') 8 | var debug = require('debug')('ftpfs') 9 | var Stat = require('./stat.js') 10 | 11 | module.exports = FTPFS 12 | 13 | function FTPFS (opts) { 14 | if (!(this instanceof FTPFS)) return new FTPFS(opts) 15 | var ftp = new FTP() 16 | this.ftp = ftp 17 | this.cache = {} 18 | 19 | this.connect = thunky(function (cb) { 20 | function onErr (err) { 21 | cb(err) 22 | } 23 | ftp.once('error', onErr) 24 | ftp.once('ready', function () { 25 | ftp.removeListener('error', onErr) 26 | debug('ftp connected') 27 | cb() 28 | }) 29 | ftp.connect(opts) 30 | }) 31 | 32 | this.end = ftp.end.bind(ftp) 33 | } 34 | 35 | FTPFS.prototype.readdir = function (file, cb) { 36 | var self = this 37 | this.connect(function (err) { 38 | if (err) return cb(err) 39 | debug('readdir', file, {cached: !!self.cache[file]}) 40 | if (self.cache[file]) { 41 | return cb(null, self.cache[file].map(function (i) { 42 | return i.name 43 | })) 44 | } 45 | self.ftp.list(file, function (err, list) { 46 | if (err) return cb(err) 47 | self.cache[file] = list 48 | cb(null, list.map(function (i) { return i.name })) 49 | }) 50 | }) 51 | } 52 | 53 | FTPFS.prototype.createReadStream = function (file) { 54 | var self = this 55 | var duplex = duplexify() 56 | 57 | this.connect(function (err) { 58 | if (err) return duplex.destroy(err) 59 | debug('createReadStream', file) 60 | self.ftp.get(file, function (err, reader) { 61 | if (err) return duplex.destroy(err) 62 | 63 | // i without proxy, reader doesnt work, weird stream impl i guess 64 | var proxy = through() 65 | pump(reader, proxy, function (err) { 66 | if (err) return duplex.destroy(err) 67 | }) 68 | 69 | duplex.setReadable(proxy) 70 | }) 71 | }) 72 | 73 | return duplex 74 | } 75 | 76 | FTPFS.prototype.stat = FTPFS.prototype.lstat = function (file, cb) { 77 | var self = this 78 | this.connect(function (err) { 79 | if (err) return cb(err) 80 | var dir = path.dirname(file) 81 | var basename = path.basename(file) 82 | 83 | var isFolder = false 84 | if (!basename) { 85 | isFolder = true 86 | if (dir === '/') return cb(null, mkstat({ 87 | size: 136, 88 | type: 'directory', 89 | mode: 16877, 90 | date: new Date(0) 91 | })) 92 | basename = path.split('/').pop() 93 | dir = path.resolve(dir, '..') 94 | } 95 | 96 | debug('stat', file, dir, {cached: !!self.cache[dir]}) 97 | if (self.cache[dir]) return createStat(self.cache[dir]) 98 | 99 | self.ftp.list(dir, function (err, list) { 100 | if (err) return cb(err) 101 | self.cache[dir] = list 102 | createStat(list) 103 | }) 104 | 105 | function createStat (list) { 106 | var match = list.find(function (l) { return l.name === basename }) 107 | info = extend({}, match) // clone 108 | if (!info) { 109 | debug('file not found', list) 110 | return cb(new Error('file not found')) 111 | } 112 | if (info.type === 'd') { 113 | info.size = 136 114 | info.mode = 16877 115 | info.type = 'directory' 116 | } else { 117 | info.type = 'file' 118 | } 119 | var stat = mkstat(info) 120 | stat._ftpmetadata = match 121 | cb(null, stat) 122 | } 123 | }) 124 | 125 | function mkstat (info) { 126 | debug('mkstat', info) 127 | return new Stat({ 128 | mode: info.mode || 33188, 129 | type: info.type, 130 | size: info.size, 131 | atime: info.date, 132 | mtime: info.date, 133 | ctime: info.date, 134 | birthtime: info.date 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ftpfs", 3 | "version": "1.1.0", 4 | "description": "an ftp client that expose the node fs API", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node test/test.js" 8 | }, 9 | "keywords": [], 10 | "author": "max ogden", 11 | "license": "ISC", 12 | "dependencies": { 13 | "debug": "^2.6.6", 14 | "duplexify": "^3.5.0", 15 | "ftp": "^0.3.10", 16 | "mirror-folder": "^2.1.0", 17 | "pump": "^1.0.2", 18 | "through2": "^2.0.3", 19 | "thunky": "^1.0.2", 20 | "xtend": "^4.0.1" 21 | }, 22 | "devDependencies": { 23 | "connections": "^1.4.2", 24 | "ftpd": "^0.2.15", 25 | "simple-ftp": "^1.0.3", 26 | "tape": "^4.6.3" 27 | }, 28 | "directories": { 29 | "test": "test" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/maxogden/ftpfs.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/maxogden/ftpfs/issues" 37 | }, 38 | "homepage": "https://github.com/maxogden/ftpfs#readme" 39 | } 40 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ftpfs 2 | 3 | an ftp client that expose the node fs API 4 | 5 | only supports `createReadStream`, `readDir`, `stat` and `lstat` right now 6 | 7 | ## usage 8 | 9 | ```js 10 | var ftp = ftpfs({ port: 2121 }) 11 | 12 | var readStream = ftp.createReadStream('hello.txt') 13 | 14 | ftp.readdir('/', function (err, list) { 15 | 16 | }) 17 | 18 | ftp.stat('hello.txt', function (err, stat) { 19 | 20 | }) 21 | 22 | ftp.end() 23 | ``` -------------------------------------------------------------------------------- /stat.js: -------------------------------------------------------------------------------- 1 | var toDate = function(date) { 2 | if (!date) return new Date(); 3 | if (typeof date === 'string') return new Date(date); 4 | return date; 5 | }; 6 | 7 | var Stat = function(opts) { 8 | this.uid = opts.uid || 0; 9 | this.gid = opts.gid || 0; 10 | this.mode = opts.mode || 0; 11 | this.size = opts.size || 0; 12 | this.mtime = toDate(opts.mtime); 13 | this.atime = toDate(opts.atime); 14 | this.ctime = toDate(opts.ctime); 15 | this.type = opts.type; 16 | this.target = opts.target; 17 | this.link = opts.link; 18 | this.blob = opts.blob; 19 | }; 20 | 21 | Stat.prototype.isDirectory = function() { 22 | return this.type === 'directory'; 23 | }; 24 | 25 | Stat.prototype.isFile = function() { 26 | return this.type === 'file'; 27 | }; 28 | 29 | Stat.prototype.isBlockDevice = function() { 30 | return false; 31 | }; 32 | 33 | Stat.prototype.isCharacterDevice = function() { 34 | return false; 35 | }; 36 | 37 | Stat.prototype.isSymbolicLink = function() { 38 | return this.type === 'symlink'; 39 | }; 40 | 41 | Stat.prototype.isFIFO = function() { 42 | return false; 43 | }; 44 | 45 | Stat.prototype.isSocket = function() { 46 | return false; 47 | }; 48 | 49 | module.exports = function(opts) { 50 | return new Stat(opts); 51 | }; -------------------------------------------------------------------------------- /test/hello.txt: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var child = require('child_process') 3 | var concat = require('concat-stream') 4 | var ftpd = require('ftpd') 5 | var connections = require('connections') 6 | var ftpfs = require('../') 7 | 8 | var server 9 | var sockets = [] 10 | var ftp = ftpfs({ port: 2121 }) 11 | 12 | test('start test server', function (t) { 13 | server = new ftpd.FtpServer('127.0.0.1', { 14 | getInitialCwd: function () { return './' }, 15 | getRoot: function() { return __dirname } 16 | }) 17 | 18 | server.connections = connections(server.server) 19 | server.debugging = 2 20 | 21 | server.on('client:connected', function (connection) { 22 | sockets.push(connection.socket) 23 | connection.on('command:user', function (user, success, failure) { 24 | success() 25 | }) 26 | connection.on('command:pass', function (user, success, failure) { 27 | success('anonymous') 28 | }) 29 | }) 30 | 31 | server.listen(2121, function () { 32 | t.ok(true, 'server listening') 33 | t.end() 34 | }) 35 | }) 36 | 37 | test('readdir', function (t) { 38 | ftp.readdir('/', function (err, list) { 39 | t.ifErr(err, 'no err') 40 | t.deepEqual(list, ['hello.txt', 'test.js']) 41 | t.end() 42 | }) 43 | }) 44 | 45 | test('createReadStream', function (t) { 46 | var rs = ftp.createReadStream('hello.txt') 47 | rs.pipe(concat(function (data) { 48 | t.equals(data.toString(), 'hello') 49 | t.end() 50 | })) 51 | }) 52 | 53 | test('stat', function (t) { 54 | ftp.stat('hello.txt', function (err, stat) { 55 | t.ifErr(err, 'no err') 56 | t.equals(stat.mode, 33188) 57 | t.equals(stat.size, 5) 58 | t.end() 59 | }) 60 | }) 61 | 62 | test('teardown', function (t) { 63 | server.close() 64 | ftp.end() 65 | t.ok(true, 'tearing down') 66 | t.end() 67 | process.exit(0) // hack, open socket somewhere in server (couldnt figure it out) 68 | }) 69 | --------------------------------------------------------------------------------