├── test ├── fixtures │ ├── folder │ │ └── lib │ │ │ ├── file1 │ │ │ ├── file3.png │ │ │ └── file2.txt │ ├── folder.zip │ ├── folder.tar.gz │ ├── folder.tar.xz │ ├── folder.tar.bz2 │ ├── folder.a │ └── folder.tar ├── vfs │ ├── zip.test.js │ ├── ar.test.js.DISABLED.js │ ├── file.test.js │ ├── tar.test.js │ └── sftp.test.js ├── dispatcher.test.js ├── plugin.test.js ├── package.json ├── z.smoke.test.js └── lib │ └── vfs-test.js ├── circle.yml ├── .gitignore ├── lerna.json ├── vfs ├── index.js ├── README.md ├── package.json ├── dispatcher.js ├── node.js ├── base.js └── api.js ├── package.json ├── .travis.yml ├── vfs-server ├── bin │ └── vfs-server.js ├── server.test.js ├── package.json ├── server.js └── route │ └── webdav.js ├── vfs-util-path ├── package.json └── util-path.js ├── vfs-util-errors ├── util-errors.js └── package.json ├── vfs-sftp ├── sftp.example.js ├── package.json └── vfs-sftp.js ├── vfs-util-stream ├── package.json └── util-stream.js ├── vfs-file ├── package.json └── vfs-file.js ├── vfs-plugin-mimetype ├── package.json └── vfs-plugin-mimetype.js ├── vfs-zip ├── package.json └── vfs-zip.js ├── vfs-util-compression ├── package.json └── util-compression.js ├── vfs-ar ├── package.json └── vfs-ar.js ├── vfs-tar ├── package.json └── vfs-tar.js ├── Makefile └── README.md /test/fixtures/folder/lib/file1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/folder/lib/file3.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/folder/lib/file2.txt: -------------------------------------------------------------------------------- 1 | ÜÄ✓✗ 2 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: v8.2.0 4 | -------------------------------------------------------------------------------- /test/fixtures/folder.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/vfs/master/test/fixtures/folder.zip -------------------------------------------------------------------------------- /test/fixtures/folder.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/vfs/master/test/fixtures/folder.tar.gz -------------------------------------------------------------------------------- /test/fixtures/folder.tar.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/vfs/master/test/fixtures/folder.tar.xz -------------------------------------------------------------------------------- /test/fixtures/folder.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HelloZeroNet/vfs/master/test/fixtures/folder.tar.bz2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | docs 3 | lerna-debug.log 4 | */node_modules 5 | npm-debug.log 6 | vfs-cli 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.0.0-beta.38", 3 | "packages": [ 4 | "vfs*", 5 | "test" 6 | ], 7 | "version": "0.0.5" 8 | } 9 | -------------------------------------------------------------------------------- /vfs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = new(require('./dispatcher'))() 2 | module.exports.base = require('./base') 3 | module.exports.Node = require('./node') 4 | -------------------------------------------------------------------------------- /test/vfs/zip.test.js: -------------------------------------------------------------------------------- 1 | const {testVfs} = require('../lib/vfs-test') 2 | 3 | testVfs('zip', [ 4 | [{location: 'folder.zip'}, ['vfsReadTest']] 5 | ]) 6 | -------------------------------------------------------------------------------- /test/vfs/ar.test.js.DISABLED.js: -------------------------------------------------------------------------------- 1 | const {testVfs} = require('../lib/vfs-test') 2 | 3 | testVfs('ar', [ 4 | [{location: 'folder.a'}, ['vfsReadTest']] 5 | ]) 6 | 7 | -------------------------------------------------------------------------------- /test/vfs/file.test.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const {testVfs} = require('../lib/vfs-test') 3 | 4 | testVfs('file', [ 5 | [{chroot: Path.resolve(__dirname+'/../fixtures/folder')}, ['vfsReadTest']] 6 | ]) 7 | 8 | -------------------------------------------------------------------------------- /test/fixtures/folder.a: -------------------------------------------------------------------------------- 1 | ! 2 | file2.txt/ 0 0 0 644 11 ` 3 | ÜÄ✓✗ 4 | 5 | file3.png/ 0 0 0 644 0 ` 6 | file1/ 0 0 0 644 0 ` 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "bootstrap": "lerna bootstrap", 4 | "postinstall": "npm run bootstrap", 5 | "test": "make test" 6 | }, 7 | "devDependencies": { 8 | "lerna": "2.0.0-beta.38", 9 | "supertest": "^3.0.0", 10 | "tap": "^10.7.0", 11 | "tape": "^4.6.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | - "8" 5 | # https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Node.js-v4-(or-io.js-v3)-compiler-requirements 6 | env: 7 | - CXX=g++-4.8 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | packages: 13 | - g++-4.8 14 | -------------------------------------------------------------------------------- /test/dispatcher.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | 3 | tap.test('byScheme', t => { 4 | const dispatcher = require('@kba/vfs') 5 | const filevfs = require('@kba/vfs-file') 6 | const zipvfs = require('@kba/vfs-zip') 7 | dispatcher.enable(filevfs) 8 | t.equals(dispatcher.get('file'), filevfs, 'get(file)') 9 | t.end() 10 | }) 11 | -------------------------------------------------------------------------------- /vfs-server/bin/vfs-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const dispatcher = require('@kba/vfs') 3 | dispatcher.enable(require('@kba/vfs-file')) 4 | const port = process.env.VFS_PORT || 3001 5 | 6 | const server = new(require('express'))() 7 | server.use(require('../server')({dispatcher})) 8 | console.log(`Listening on port ${port} (${new Date()})`) 9 | server.listen(port) 10 | -------------------------------------------------------------------------------- /test/vfs/tar.test.js: -------------------------------------------------------------------------------- 1 | const {testVfs} = require('../lib/vfs-test') 2 | 3 | testVfs('tar', [ 4 | [{location: 'folder.tar'}, ['vfsReadTest']], 5 | // [{location: 'folder.tar.gz', compression: 'gzip'}, ['vfsReadTest']], 6 | // [{location: 'folder.tar.bz2', compression: 'bzip2'}, ['vfsReadTest']], 7 | // [{location: 'folder.tar.xz', compression: 'xz'}, ['vfsReadTest']], 8 | ]) 9 | -------------------------------------------------------------------------------- /vfs-util-path/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-util-path", 3 | "version": "0.0.4", 4 | "description": "Virtual File Systems with a node fs-like API - Utilities", 5 | "main": "util-path.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /vfs-util-errors/util-errors.js: -------------------------------------------------------------------------------- 1 | module.exports = class Errors { 2 | static NoSuchFileError(path) { return new Error(`NoSuchFileError: ${path}`) } 3 | static NotImplementedError(fn) { return new Error(`NotImplementedError: ${fn}`) } 4 | static PathNotAbsoluteError(path) { return new Error(`PathNotAbsoluteError: ${path}`) } 5 | static UnsupportedFormatError(format) { return new Error(`UnsupportedFormatError: ${format}`) } 6 | } 7 | -------------------------------------------------------------------------------- /vfs-util-errors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-util-errors", 3 | "version": "0.0.4", 4 | "description": "Virtual File Systems with a node fs-like API - Utilities", 5 | "main": "util-errors.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">=7" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vfs-sftp/sftp.example.js: -------------------------------------------------------------------------------- 1 | const fs = new(require('.'))({ 2 | sshOptions: { 3 | host: '127.0.0.1', 4 | port: '22', 5 | username: 'kb', 6 | privateKey: require('fs').readFileSync(`${process.env.HOME}/.ssh/id_rsa`) 7 | } 8 | }) 9 | fs.on('error', err => console.log("ERROR", err)) 10 | fs.once('sync', () => { 11 | console.log('connected') 12 | fs.getdir(__dirname, (err, list) => { 13 | console.log({err, list}) 14 | fs.end() 15 | }) 16 | }) 17 | fs.init() 18 | -------------------------------------------------------------------------------- /vfs-util-stream/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-util-stream", 3 | "version": "0.0.4", 4 | "description": "Virtual File Systems with a node fs-like API - Utilities", 5 | "main": "util-stream.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">= 7" 14 | }, 15 | "dependencies": { 16 | "async": "^2.1.4" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vfs-file/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-file", 3 | "version": "0.0.5", 4 | "description": "Virtual File Systems with a node fs-like API - fs adapter", 5 | "main": "vfs-file.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">= 7" 14 | }, 15 | "dependencies": { 16 | "@kba/vfs": "^0.0.5", 17 | "async": "^2.4.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vfs/README.md: -------------------------------------------------------------------------------- 1 | # vfs 2 | A virtual filesystem that works like [fs](http://nodejs.org/api/fs.html) 3 | 4 | [![Build Status](https://travis-ci.org/kba/fsvfs.svg?branch=master)](https://travis-ci.org/kba/fsvfs) 5 | 6 | ## Currently implemented 7 | 8 | * `file` - a VFS that mirrors the local filesystem 9 | * `zip` - a VFS on top of ZIP content 10 | * `tar` - a VFS on top of tarball content (compressions: gzip, bzip2, xz) 11 | 12 | ## Creating a new VFS 13 | 14 | * Subclass `vfs.base` 15 | * Override 16 | * `_stat` 17 | * `_readdir` 18 | -------------------------------------------------------------------------------- /vfs-plugin-mimetype/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-plugin-mimetype", 3 | "version": "0.0.4", 4 | "description": "Virtual File Systems with a node fs-like API - MIME type plugin", 5 | "main": "vfs-plugin-mimetype.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">=7" 14 | }, 15 | "dependencies": { 16 | "mime-types": "^2.1.14" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vfs-plugin-mimetype/vfs-plugin-mimetype.js: -------------------------------------------------------------------------------- 1 | const mimeTypes = require('mime-types') 2 | 3 | class MimetypePlugin { 4 | 5 | constructor(options={}) { 6 | this.options = options 7 | } 8 | 9 | stat(node, options, cb) { 10 | if (typeof options === 'function') [cb, options] = [options, {}] 11 | node.mimetype = node.isDirectory 12 | ? 'inode/directory' 13 | : mimeTypes.lookup(node.path) || 'application/octet-stream' 14 | return cb(null, node) 15 | } 16 | 17 | } 18 | module.exports = MimetypePlugin 19 | -------------------------------------------------------------------------------- /vfs-zip/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-zip", 3 | "version": "0.0.5", 4 | "description": "Virtual File Systems with a node fs-like API - ZIP adapter", 5 | "main": "vfs-zip.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">= 7" 14 | }, 15 | "dependencies": { 16 | "@kba/vfs": "^0.0.5", 17 | "@kba/vfs-util-path": "^0.0.4", 18 | "async": "^2.4.0", 19 | "jszip": "^3.1.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /vfs-util-compression/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-util-compression", 3 | "version": "0.0.4", 4 | "description": "Node.JS Virtual File Systems - Compression Utilities", 5 | "main": "util-compression.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">=7" 14 | }, 15 | "dependencies": { 16 | "@kba/vfs-util-errors": "^0.0.4", 17 | "unbzip2-stream": "^1.0.11", 18 | "xz": "^1.2.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /vfs-server/server.test.js: -------------------------------------------------------------------------------- 1 | /* globals __filename */ 2 | const supertest = require('supertest') 3 | const tap = require('tape') 4 | 5 | const dispatcher = require('@kba/vfs') 6 | dispatcher.enable(require('@kba/vfs-file')) 7 | const app = require('./server')({dispatcher}) 8 | 9 | tap.test(`/stat?url=${__filename}`, t => { 10 | supertest(app) 11 | .get(`/stat?url=${__filename}`) 12 | .end((err, res) => { 13 | if (err) throw err 14 | t.equals(res.status, 200, '200') 15 | t.ok(res.body.stat, "JSON, contains 'stat'") 16 | t.end() 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /vfs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs", 3 | "version": "0.0.5", 4 | "description": "Virtual File Systems with a node fs-like API", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": { 10 | "url": "https://github.com/kba/vfs", 11 | "type": "git" 12 | }, 13 | "author": "Konstantin Baierer ", 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">=7" 17 | }, 18 | "dependencies": { 19 | "@kba/vfs-util-errors": "^0.0.4", 20 | "async": "^2.1.4", 21 | "mime-types": "^2.1.15" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const vfsFile = require('@kba/vfs-file') 3 | 4 | class TestPlugin { 5 | 6 | stat(node, cb) { 7 | node.foo = '42' 8 | cb() 9 | } 10 | 11 | } 12 | 13 | tap.test('plugin loading', t => { 14 | const vfs = new vfsFile({chroot: __dirname + '/fixtures'}) 15 | vfs.use(TestPlugin, {}) 16 | t.equals(vfs.plugins.length, 1, 'one plugin loaded') 17 | vfs.stat('/', (err, node) => { 18 | t.deepEquals(err, undefined, 'no error') 19 | t.equals(node.foo, '42', 'test plugin worked') 20 | t.end() 21 | }) 22 | }) 23 | 24 | -------------------------------------------------------------------------------- /vfs-ar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-ar", 3 | "version": "0.0.5", 4 | "description": "Virtual File Systems with a node fs-like API - ZIP adapter", 5 | "main": "vfs-ar.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">= 7" 14 | }, 15 | "dependencies": { 16 | "@kba/vfs": "^0.0.5", 17 | "@kba/vfs-util-compression": "^0.0.4", 18 | "@kba/vfs-util-errors": "^0.0.4", 19 | "ar-async": "^0.1.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/vfs/sftp.test.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const {testVfs} = require('../lib/vfs-test') 3 | 4 | if (process.env.ENABLE_SFTP_TESTS !== 'true' && process.env.USER !== 'kba') { 5 | console.warn("Skipping SFTP test since ENABLE_SFTP_TESTS is not set in env and USER is not 'kba'") 6 | } else { 7 | testVfs('sftp', [ 8 | [{ 9 | chroot: Path.resolve(__dirname+'/../fixtures/folder'), 10 | sshOptions: { 11 | host: '127.0.0.1', 12 | port: '22', 13 | username: process.env.USER, 14 | privateKey: require('fs').readFileSync(`${process.env.HOME}/.ssh/id_rsa`) 15 | } 16 | }, ['vfsReadTest']] 17 | ]) 18 | } 19 | -------------------------------------------------------------------------------- /vfs-tar/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-tar", 3 | "version": "0.0.5", 4 | "description": "Virtual File Systems with a node fs-like API - ZIP adapter", 5 | "main": "vfs-tar.js", 6 | "scripts": { 7 | "test": "tap -Rspec *.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">=7" 14 | }, 15 | "dependencies": { 16 | "@kba/vfs": "^0.0.5", 17 | "@kba/vfs-util-errors": "^0.0.4", 18 | "@kba/vfs-util-stream": "^0.0.4", 19 | "@kba/vfs-util-compression": "^0.0.4", 20 | "tar-stream": "^1.5.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /vfs-sftp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-sftp", 3 | "version": "0.0.5", 4 | "description": "Virtual File Systems with a node fs-like API - SFTP adapter", 5 | "main": "vfs-sftp.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "license": "MIT", 12 | "engines": { 13 | "node": ">=7" 14 | }, 15 | "dependencies": { 16 | "@kba/vfs": "^0.0.5", 17 | "@kba/vfs-util-path": "^0.0.4", 18 | "@kba/vfs-util-errors": "^0.0.4", 19 | "@kba/vfs-util-stream": "^0.0.4", 20 | "ssh2-sftp-client": "^1.1.0", 21 | "async": "^2.4.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "@kba/vfs-test", 4 | "version": "0.0.5", 5 | "description": "Virtual File Systems with a node fs-like API - Tests", 6 | "scripts": { 7 | "test": "tap -Rspec *.test.js" 8 | }, 9 | "repository": "github:kba/vfs", 10 | "author": "Konstantin Baierer ", 11 | "engines": { 12 | "node": ">=7" 13 | }, 14 | "license": "MIT", 15 | "dependencies": { 16 | "@kba/vfs": "^0.0.5", 17 | "@kba/vfs-ar": "^0.0.5", 18 | "@kba/vfs-file": "^0.0.5", 19 | "@kba/vfs-zip": "^0.0.5", 20 | "@kba/vfs-tar": "^0.0.5", 21 | "@kba/vfs-sftp": "^0.0.5", 22 | "async": "^2.4.0", 23 | "tap": "^10.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vfs-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kba/vfs-server", 3 | "version": "0.0.5", 4 | "description": "Virtual File Systems with a node fs-like API - Server", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "tap -Rspec test/*.test.js", 8 | "watch": "nodemon -w . bin/vfs-server.js" 9 | }, 10 | "bin": { 11 | "vfs-server": "./bin/vfs-server.js" 12 | }, 13 | "repository": "github:kba/vfs", 14 | "author": "Konstantin Baierer ", 15 | "license": "MIT", 16 | "engines": { 17 | "node": ">=7" 18 | }, 19 | "dependencies": { 20 | "@kba/vfs": "^0.0.5", 21 | "@kba/vfs-file": "^0.0.5", 22 | "@kba/vfs-util-path": "^0.0.4", 23 | "async": "^2.4.0", 24 | "body-parser": "^1.17.2", 25 | "express": "^4.15.3", 26 | "morgan": "^1.8.2", 27 | "send": "^0.15.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vfs-util-compression/util-compression.js: -------------------------------------------------------------------------------- 1 | const {UnsupportedFormatError} = require('@kba/vfs-util-errors') 2 | 3 | const decompress = { 4 | gzip: require('zlib').createGunzip, 5 | bzip2: require('unbzip2-stream'), 6 | xz: () => new(require('xz').Decompressor)(), 7 | } 8 | 9 | /** 10 | * ### CompressionUtils 11 | * 12 | */ 13 | 14 | /** 15 | * #### `(static) hasDecompressor(format)` 16 | * 17 | * Whether a decompression format is supported 18 | */ 19 | function hasDecompressor(format) { return format in decompress } 20 | 21 | /** 22 | * #### `(static) getDecompressor(format)` 23 | * 24 | * Instantiate a decompression stream 25 | * @memberof util 26 | */ 27 | function getDecompressor(format, ...args) { 28 | if (!(hasDecompressor(format))) throw UnsupportedFormatError(format) 29 | return decompress[format](...args) 30 | } 31 | 32 | module.exports = { 33 | hasDecompressor, 34 | getDecompressor, 35 | } 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS += --no-print-directory --silent 2 | PATH := ./node_modules/.bin:$(PATH) 3 | PATH := ./test/node_modules/.bin:$(PATH) 4 | REPORTER = tap 5 | TAP = tap -R$(REPORTER) 6 | VFS = file 7 | 8 | help: 9 | @echo "Targets" 10 | @echo "" 11 | @echo "test Run all tests" 12 | @echo "test-one Run only the test given by TEST variable" 13 | @echo "doc Regenerate the API doc in README (requires shinclude)" 14 | 15 | bootstrap: 16 | lerna bootstrap --loglevel info 17 | touch -m test/fixtures/folder/lib/file3.png 18 | 19 | .PHONY: test 20 | test: 21 | $(MAKE) bootstrap 22 | $(TAP) test/*.test.js test/*/*.test.js 23 | 24 | test-one: 25 | $(MAKE) bootstrap 26 | tap -R$(REPORTER) $(TEST) 27 | 28 | test-vfs: 29 | $(MAKE) bootstrap 30 | tap -R$(REPORTER) test/vfs/$(VFS).test.js 31 | 32 | .PHONY: doc doc/watch 33 | doc: 34 | shinclude -c xml -i README.md 35 | 36 | doc/watch: 37 | nodemon -w . -e 'js,md' -x make doc 38 | 39 | doc/serve: 40 | grip 41 | -------------------------------------------------------------------------------- /vfs-util-stream/util-stream.js: -------------------------------------------------------------------------------- 1 | const Readable = require('stream').Readable; 2 | /** 3 | * ### StreamUtils 4 | * 5 | * #### `(static) createReadableWrapper()` 6 | * 7 | * Wraps another ReadableStream to allow synchronously returning a stream 8 | * that will become readable only later. 9 | * 10 | * ```js 11 | * const {createReadableWrapper} = require('@kba/vfs-util-stream') 12 | * const readable = createReadableWrapper() 13 | * // TODO, see vfs-tar 14 | * ``` 15 | * 16 | */ 17 | function createReadableWrapper() { 18 | const ret = new Readable({ 19 | read(...args) { 20 | if (this._wrapped) this._wrapped.read(...args) 21 | } 22 | }) 23 | /** 24 | * #### `ReadableWrapper` 25 | * 26 | * TODO 27 | * 28 | * ##### `wrapStream(stream)` 29 | * 30 | * TODO 31 | */ 32 | ret.wrapStream = (stream) => { 33 | ret._wrapped = stream 34 | ;['close', 'data', 'end', 'error'].forEach(event => { 35 | stream.on(event, (...args) => ret.emit(event, ...args)) 36 | }) 37 | ret.emit('readable') 38 | } 39 | return ret 40 | } 41 | 42 | module.exports = { 43 | createReadableWrapper, 44 | } 45 | -------------------------------------------------------------------------------- /vfs-util-path/util-path.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | /** 3 | * ### PathUtils 4 | * 5 | * Enhancing [path](https://nodejs.org/api/path.html) 6 | * 7 | * ```js 8 | * const PathUtils = require('@kba/vfs-util-path') 9 | * PathUtils.removeTrailingSep('/foo/') // '/foo' 10 | * // or 11 | * const {removeTrailingSep} = require('@kba/vfs-util-path') 12 | * removeTrailingSep('/foo/') // '/foo' 13 | * ``` 14 | */ 15 | class PathUtils { 16 | 17 | /** 18 | * #### `(static) removeTrailingSep(path)` 19 | * 20 | * Remove trailing separators (slashes) from `path`. 21 | * 22 | * @param {boolean} keepRoot Whether to remove or keep a single root slash 23 | */ 24 | static removeTrailingSep(path, keepRoot=true) { 25 | path = path.replace(/\/$/, '') 26 | if (path === '' && keepRoot) path = '/' 27 | return path 28 | 29 | } 30 | 31 | /** 32 | * #### `(static) removeLeadingSep(path)` 33 | * 34 | * Remove leading separators (slashes) from `path`. 35 | */ 36 | static removeLeadingSep(path) { 37 | return path.replace(/^\//, '') 38 | } 39 | 40 | static chrootPath(path, chroot) { 41 | // console.log({path, chroot}) 42 | if (path.indexOf(chroot) === 0) return path 43 | return Path.join(chroot, path) 44 | } 45 | } 46 | 47 | module.exports = PathUtils 48 | 49 | // vim: sw=4 ts=4 50 | -------------------------------------------------------------------------------- /test/z.smoke.test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const vfs = require('@kba/vfs') 3 | const api = require('@kba/vfs/api') 4 | const base = require('@kba/vfs/base') 5 | 6 | const RESET = '\x1b[0m' 7 | const OK = '\x1b[32mX' 8 | const NOT_OK = '\x1b[31m-' 9 | const WIDTH_CAP = 20 10 | const WIDTH_VFS = 5 11 | 12 | function rightPad(str, pad) { 13 | return (str + ' '.repeat(100)).substr(0, pad) 14 | } 15 | 16 | tap.test('summarize capabilities', t => { 17 | t.plan(0) 18 | const vfsNames = ['zip', 'file', 'tar', 'ar', 'sftp'] 19 | console.log('\t' + ' '.repeat(WIDTH_CAP) + vfsNames.map( 20 | str => rightPad(str, WIDTH_VFS)).join('')) 21 | const props = Object.getOwnPropertyNames(api.prototype) 22 | props.forEach(prop => { 23 | vfsNames.forEach(vfsName => { 24 | const vfsClass = require(`@kba/vfs-${vfsName}`) 25 | if (prop.indexOf('constructor') === -1 && vfsClass.prototype.hasOwnProperty(prop)) { 26 | throw new Error(`vfs-${vfsName} should not override ${prop}`) 27 | } 28 | }) 29 | if (['constructor', 'use', 'sync', 'init', 'end'].indexOf(prop) > -1) return 30 | let row = [RESET, rightPad(prop, WIDTH_CAP)] 31 | vfsNames.forEach(vfsName => { 32 | const vfsClass = require(`@kba/vfs-${vfsName}`) 33 | const sign = (vfsClass.capabilities.has(prop)) ? OK : NOT_OK 34 | row.push(' ' + sign + ' '.repeat(WIDTH_VFS - 2)) 35 | row.push(RESET) 36 | }) 37 | console.log('\t' + row.join('')) 38 | }) 39 | t.end() 40 | }) 41 | -------------------------------------------------------------------------------- /vfs/dispatcher.js: -------------------------------------------------------------------------------- 1 | const urlParse = require('url').parse 2 | const {UnsupportedFormatError} = require('@kba/vfs-util-errors') 3 | 4 | class VfsDispatcher { 5 | 6 | constructor() { 7 | this.byScheme = {} 8 | this.instanceCache = {} 9 | } 10 | 11 | enable(vfs, options) { 12 | this.byScheme[vfs.scheme] = vfs 13 | } 14 | 15 | get(scheme='') { 16 | return this.byScheme[scheme] 17 | } 18 | 19 | parseUrl(url, options={}) { 20 | if (!('parseQueryString' in options)) 21 | options.parseQueryString = false 22 | if (!('slashDenoteHost' in options)) 23 | options.slashDenoteHost = false 24 | var parts = urlParse(url, options.parseQueryString, options.slashDenoteHost) 25 | if (!(parts.protocol)) { 26 | url = 'file://' + url 27 | parts = urlParse(url, options.parseQueryString, options.slashDenoteHost) 28 | } 29 | parts.protocol = parts.protocol.replace(/:$/, '') 30 | if (parts.path) parts.path = decodeURIComponent(parts.path) 31 | if (!(parts.protocol in this.byScheme)) { 32 | throw UnsupportedFormatError(`${parts.protocol} not available. Did you run 33 | vfs.enable(require('@kba/vfs-${parts.protocol}') ?`) 34 | } 35 | return parts 36 | } 37 | 38 | vfsByUrl(url, options={}) { 39 | const {protocol} = this.parseUrl(url, options) 40 | return this.byScheme[protocol] 41 | } 42 | 43 | instantiate(vfs, options={}) { 44 | if (typeof vfs === 'string') { 45 | vfs = this.vfsByUrl(vfs) 46 | } 47 | const key = `${vfs.scheme}__${JSON.stringify(options)}` 48 | if (!(key in this.instanceCache)) { 49 | const instance = new vfs(options) 50 | this.instanceCache[key] = instance 51 | } 52 | return this.instanceCache[key] 53 | } 54 | } 55 | 56 | module.exports = VfsDispatcher 57 | -------------------------------------------------------------------------------- /vfs/node.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const mimeTypes = require('mime-types') 3 | 4 | /** 5 | * 6 | * ### vfs.Node 7 | * 8 | * ```js 9 | * new fsvfs.Node({path: "/...", vfs: vfsInstance}) 10 | * ``` 11 | * 12 | * Class representing file metadata 13 | * #### Constructor 14 | * 15 | * - `@param {object} options` Options that will be passed 16 | * - `@param {string} options.path` Absolute path to the node 17 | * - `@param {fsvfs} options.vfs` Instance of a {@link fsvfs} 18 | * 19 | * #### Properties 20 | * ##### `vfs` 21 | * Parent vfs instance, e.g. a [file](./vfs-file) 22 | * ##### `path` 23 | * Absolute, normalized path of the node within the vfs 24 | * ##### `mtime` 25 | * Date of last modification 26 | * ##### `mode` 27 | * ##### `mimetype` 28 | * MIME type of this node 29 | * ##### `%root` 30 | * See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 31 | * ##### `%dir` 32 | * See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 33 | * ##### `%base` 34 | * See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 35 | * ##### `%ext` 36 | * See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 37 | * ##### `%name` 38 | * See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 39 | * 40 | */ 41 | class Node { 42 | constructor(options) { 43 | if (!(options.path)) throw new Error("Must set 'path'") 44 | if (!Path.isAbsolute(options.path)) throw new Error(`'path' must be absolute: ${options.path}`) 45 | if (!(options.vfs)) throw new Error("Must set 'vfs'") 46 | Object.keys(options).forEach(k => this[k] = options[k]) 47 | const pathParsed = Path.parse(options.path) 48 | for (let k in pathParsed) this['%' + k] = pathParsed[k] 49 | this.mimetype = this.isDirectory 50 | ? 'inode/directory' 51 | : mimeTypes.lookup(this.path) || 'application/octet-stream' 52 | } 53 | 54 | } 55 | 56 | module.exports = Node 57 | -------------------------------------------------------------------------------- /vfs-file/vfs-file.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const Path = require('path') 3 | const {base, Node} = require('@kba/vfs') 4 | 5 | /** 6 | * A VFS over the local filesystem. 7 | * @implements api 8 | * @implements base 9 | * @alias file 10 | */ 11 | class filevfs extends base { 12 | 13 | static get scheme() {return 'file'} 14 | 15 | /** 16 | * 17 | * @param {object} options 18 | * @param {object} options.chroot A root path to restrict access of the vfs 19 | * to a certain area of the underlying fs 20 | */ 21 | constructor(options={}) { 22 | options.chroot = (options.chroot || '') 23 | if (options.chroot !== '' && !Path.isAbsolute(options.chroot)) 24 | throw new TypeError(`options.chroot must be absolute, '${options.chroot}' is not`) 25 | super(options) 26 | } 27 | 28 | _fsStatsToAttr(path, stats) { 29 | base.NODE_TYPES.forEach(type => stats[`is${type}`] = stats[`is${type}`]()) 30 | stats.path = path.substr(this.options.chroot.length) || '/' 31 | stats.vfs = this 32 | return new Node(stats) 33 | } 34 | 35 | _resolvePath(path) { 36 | return Path.resolve(Path.join(this.options.chroot, path)) 37 | } 38 | 39 | _sync() {this.emit('sync')} 40 | 41 | _stat(path, opts, cb) { 42 | if (!cb && typeof opts == 'function') [cb, opts] = [opts, {}] 43 | path = this._resolvePath(path) 44 | fs.lstat(path, (err, stat) => { 45 | if (err) return cb(err) 46 | return cb(null, this._fsStatsToAttr(path, stat)) 47 | }) 48 | } 49 | 50 | _unlink(path, options, ...args) {return fs.unlink(this._resolvePath(path), ...args)} 51 | _rmdir(path, options, ...args) {return fs.rmdir(this._resolvePath(path), ...args)} 52 | _mkdir(path, ...args) {return fs.mkdir(this._resolvePath(path), ...args)} 53 | _createReadStream(path, ...args) {return fs.createReadStream(this._resolvePath(path), ...args)} 54 | _createWriteStream(path, ...args) {return fs.createWriteStream(this._resolvePath(path), ...args)} 55 | _readdir(path, ...args) {return fs.readdir(this._resolvePath(path), ...args)} 56 | _readFile(path, ...args) {return fs.readFile(this._resolvePath(path), ...args)} 57 | _writeFile(path, ...args) {return fs.writeFile(this._resolvePath(path), ...args)} 58 | } 59 | 60 | module.exports = filevfs 61 | -------------------------------------------------------------------------------- /vfs-sftp/vfs-sftp.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const PathUtils = require('@kba/vfs-util-path') 3 | const sftpClient = require('ssh2-sftp-client') 4 | 5 | const {base, Node} = require('@kba/vfs') 6 | const errors = require('@kba/vfs-util-errors') 7 | const {createReadableWrapper} = require('@kba/vfs-util-stream') 8 | 9 | /** 10 | * A VFS over SFTP connection 11 | * @implements api 12 | * @implements base 13 | * @alias sftp 14 | */ 15 | class sftpvfs extends base { 16 | 17 | static get scheme() { return 'sftp' } 18 | 19 | constructor(options={}) { 20 | if (!options.sshOptions) throw new Error("Must set 'sshOptions'") 21 | super(options) 22 | } 23 | 24 | _sync() { 25 | const sftp = new sftpClient() 26 | sftp.connect(this.options.sshOptions) 27 | .then(() => { 28 | this.sftp = sftp 29 | this.emit('sync') 30 | }) 31 | .catch(err => { 32 | this.emit('error', err) 33 | }) 34 | } 35 | 36 | _end() { 37 | this.sftp.client.end() 38 | this.emit('end') 39 | } 40 | 41 | _sftpEntryToNode(path, e) { 42 | const _attr = {path} 43 | _attr.vfs = this 44 | if (e.type === 'd') { 45 | _attr.isDirectory = true 46 | } else if (e.type === 'l') { 47 | _attr.isDirectory = false 48 | _attr.isSymbolicLink = true 49 | } else { 50 | _attr.isDirectory = false 51 | _attr.isSymbolicLink = false 52 | } 53 | _attr.mtime = new Date(e.modifyTime) 54 | // TODO permissions 55 | _attr.size = e.size 56 | // console.log(e) 57 | return new Node(_attr) 58 | } 59 | 60 | _stat(path, options, cb) { 61 | if (!(Path.isAbsolute(path))) return cb(errors.PathNotAbsoluteError(path)) 62 | const abspath = PathUtils.chrootPath(path, this.options.chroot) 63 | const dirname = Path.dirname(abspath) 64 | const basename = Path.basename(abspath) 65 | // console.log('_stat', {abspath, dirname, basename}) 66 | // XXX inefficient 67 | this.sftp.list(dirname) 68 | .then(list => { 69 | const e = list.find(e => e.name === basename) 70 | if (!e) return cb(errors.NoSuchFileError(path)) 71 | return cb(null, this._sftpEntryToNode(path, e)) 72 | }) 73 | .catch(err => { 74 | console.warn(err) 75 | cb(err) 76 | }) 77 | } 78 | 79 | _readdir(dir, options, cb) { 80 | if (!(Path.isAbsolute(dir))) return cb(errors.PathNotAbsoluteError(dir)) 81 | dir = PathUtils.chrootPath(dir, this.options.chroot) 82 | dir = PathUtils.removeTrailingSep(dir) 83 | // console.log('_readdir', {dir}) 84 | this.sftp.list(dir) 85 | .then(list => { 86 | const ret = list.map(e => e.name) 87 | return cb(null, ret) 88 | }) 89 | .catch(err => cb(err)) 90 | } 91 | 92 | _createReadStream(path, options={}) { 93 | if (!(Path.isAbsolute(path))) throw errors.PathNotAbsoluteError(path) 94 | const abspath = (this.options.chroot) ? Path.join(this.options.chroot, path) : path 95 | const wrapper = createReadableWrapper() 96 | this.sftp.get( 97 | abspath, 98 | this.options.sshOptions.useCompression, 99 | options.encoding 100 | ).then(stream => { 101 | wrapper.wrapStream(stream) 102 | }).catch(err => { 103 | throw err 104 | }) 105 | return wrapper 106 | } 107 | 108 | } 109 | module.exports = sftpvfs 110 | 111 | // vim: sw=4 ts=4 112 | -------------------------------------------------------------------------------- /vfs-tar/vfs-tar.js: -------------------------------------------------------------------------------- 1 | const Path = require('path') 2 | const tar = require("tar-stream"); 3 | 4 | const {base, Node} = require('@kba/vfs') 5 | const { 6 | UnsupportedFormatError, 7 | PathNotAbsoluteError, 8 | NoSuchFileError, 9 | } = require('@kba/vfs-util-errors') 10 | const {hasDecompressor, getDecompressor} = require('@kba/vfs-util-compression') 11 | const {createReadableWrapper} = require('@kba/vfs-util-stream') 12 | 13 | /** 14 | * A VFS over tarballs 15 | * @implements api 16 | * @implements base 17 | * @alias tar 18 | */ 19 | class tarvfs extends base { 20 | 21 | static get scheme() { return 'tar' } 22 | 23 | constructor(options) { 24 | if (!options.location) 25 | throw new Error("Must set 'location'") 26 | if (!(options.location instanceof Node)) 27 | throw new Error("'location' must be a vfs.Node") 28 | if (options.compression && !hasDecompressor(options.compression)) 29 | throw UnsupportedFormatError(options.compression) 30 | super(options) 31 | this._files = new Map() 32 | } 33 | 34 | _tarEntryToVfsNode(entry) { 35 | const node = {} 36 | node.vfs = this 37 | node.path = entry.name.substr(1).replace(/\/$/, '') || '/' 38 | node.isDirectory = entry.type == 'directory' 39 | // TODO symlink fifo etc. 40 | ;['size', 'mtime', 'mode', 'uid', 'gid', 'uname', 'gname'].forEach( 41 | k => node[k] = entry[k]) 42 | // console.log(node) 43 | return new Node(node) 44 | } 45 | 46 | // TODO handle compression 47 | _extract(handlers) { 48 | const location = this.options.location 49 | var inStream = location.vfs.createReadStream(location.path) 50 | const extract = tar.extract() 51 | Object.keys(handlers).forEach(event => { 52 | extract.on(event, handlers[event]) 53 | }) 54 | if (this.options.compression !== undefined) { 55 | inStream.pipe(getDecompressor(this.options.compression)).pipe(extract) 56 | } else { 57 | inStream.pipe(extract) 58 | } 59 | } 60 | 61 | _sync() { 62 | this._extract({ 63 | entry: (header, stream, next) => { 64 | const node = this._tarEntryToVfsNode(header) 65 | this._files.set(node.path, node) 66 | stream.on('end', next) 67 | stream.resume() // just auto drain the stream 68 | }, 69 | finish: () => { 70 | this.emit('sync') 71 | } 72 | }) 73 | } 74 | 75 | _createReadStream(path, options) { 76 | if (!Path.isAbsolute(path)) throw PathNotAbsoluteError(path) 77 | if (!this._files.has(path)) throw NoSuchFileError(path) 78 | const wrapper = createReadableWrapper() 79 | this._extract({ 80 | entry: (header, stream, next) => { 81 | if (header.name.substr(1) === path) { 82 | wrapper.wrapStream(stream) 83 | next() 84 | } else { 85 | stream.on('end', next) 86 | stream.resume() // just auto drain the stream 87 | } 88 | } 89 | }) 90 | return wrapper 91 | } 92 | 93 | _stat(path, options, cb) { 94 | if (!(Path.isAbsolute(path))) return cb(PathNotAbsoluteError(path)) 95 | if (!this._files.has(path)) return cb(NoSuchFileError(path)) 96 | return cb(null, this._files.get(path)) 97 | } 98 | 99 | _readdir(path, options, cb) { 100 | if (!(Path.isAbsolute(path))) return cb(PathNotAbsoluteError(path)) 101 | return cb(null, Array.from(this._files.keys()) 102 | .filter(filePath => filePath.indexOf(path) === 0) 103 | .map(filePath => filePath.replace(path, '').replace(/^\//, '')) 104 | .filter(filePath => filePath !== '' && filePath.indexOf('/') === -1)) 105 | } 106 | 107 | } 108 | module.exports = tarvfs 109 | -------------------------------------------------------------------------------- /vfs-server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const bodyParser = require('body-parser') 3 | const morgan = require('morgan') 4 | 5 | function vfsMiddleware({dispatcher}) { 6 | return (req, res, next) => { 7 | const options = {} 8 | var urlParsed = {} 9 | var url = null 10 | if (req.header['x-vfs-options']) 11 | Object.assign(options, JSON.parse(req.header['x-vfs-options'])) 12 | if (req.query.options) 13 | Object.assign(options, JSON.parse(req.query.options)) 14 | Object.keys(req.query) 15 | .filter(k => k.match(/^opt\./)) 16 | .forEach(k => { 17 | try { 18 | options[k.substr(4)] = JSON.parse(req.query[k]) 19 | } catch(e) { 20 | options[k.substr(4)] = req.query[k] 21 | } 22 | }) 23 | if (req.header['x-vfs-url']) 24 | url = req.header['x-vfs-url'] 25 | if (req.query.url) { 26 | url = req.query.url 27 | } 28 | if (url) { 29 | urlParsed = dispatcher.parseUrl(decodeURIComponent(url), options) 30 | url = urlParsed.href 31 | } 32 | console.log('vfsMiddleware', {url, options}) 33 | req.vfs = {url, urlParsed, options} 34 | next() 35 | } 36 | } 37 | 38 | function createServer({dispatcher, port}) { 39 | const app = new express.Router() 40 | app.use(morgan('dev')) 41 | app.use(vfsMiddleware({dispatcher})) 42 | app.use('/dav', [ 43 | bodyParser.text({type: 'application/xml'}), 44 | require('./route/webdav')({ 45 | dispatcher, 46 | basepath: '/dav' 47 | }) 48 | ]) 49 | app.use(bodyParser.json()) 50 | 51 | app.get('/stat', (req, res, next) => { 52 | const {url, urlParsed, options} = req.vfs 53 | const vfs = dispatcher.instantiate(url, options) 54 | const {path} = req.vfs.urlParsed 55 | vfs.stat(path, options, (err, stat) => { 56 | if (err) return next(err) 57 | if (! stat.isDirectory) { 58 | return res.send({stat}) 59 | } else { 60 | vfs.getdir(path, options, (err, ls) => { 61 | if (err) return next(err) 62 | return res.send(ls) 63 | }) 64 | } 65 | }) 66 | }) 67 | 68 | app.get('/stream', (req, resp, next) => { 69 | const {url, options} = req.vfs 70 | const {path} = req.vfs.urlParsed 71 | const vfs = dispatcher.instantiate(url, options) 72 | vfs.stat(path, options, (err, stat) => { 73 | if (err) return next(err) 74 | const total = stat.size 75 | var status = 206 76 | var data = null 77 | const headers = { 78 | 'Content-Type': stat.mimetype, 79 | 'Accept-Ranges': 'bytes', 80 | } 81 | if (req.headers.range) { 82 | var range = req.headers.range; 83 | var parts = range.replace(/bytes=/, "").split("-"); 84 | var partialstart = parts[0]; 85 | var partialend = parts[1]; 86 | 87 | var start = parseInt(partialstart, 10); 88 | var end = partialend ? parseInt(partialend, 10) : total-1; 89 | var chunksize = end - start + 1; 90 | console.log('RANGE: ' + start + ' - ' + end + ' = ' + chunksize); 91 | data = vfs.createReadStream(path, {start: start, end: end}); 92 | status = 206 93 | headers['Content-Range'] = `bytes ${start}-${end}/${total}` 94 | headers['Content-Length'] = chunksize 95 | } else { 96 | headers['Content-Length'] = total 97 | data = vfs.createReadStream(path) 98 | } 99 | resp.writeHead(status, headers) 100 | data.pipe(resp); 101 | }) 102 | }) 103 | 104 | return app 105 | } 106 | 107 | module.exports = createServer 108 | -------------------------------------------------------------------------------- /vfs-ar/vfs-ar.js: -------------------------------------------------------------------------------- 1 | const Readable = require('stream').Readable; 2 | const Path = require('path') 3 | const ar = require("ar-async"); 4 | const {getDecompressor} = require('@kba/vfs-util-compression') 5 | 6 | const {base, Node} = require('@kba/vfs') 7 | const errors = require('@kba/vfs-util-errors') 8 | 9 | /** 10 | * A VFS over UNIX archives, e.g. Debian packages 11 | * @implements api 12 | * @implements base 13 | * @alias tar 14 | */ 15 | class arvfs extends base { 16 | 17 | static get scheme() { return 'ar' } 18 | static get supportedCompression() { return new Set(['xz']) } 19 | 20 | constructor(options) { 21 | if (!options.location) 22 | throw new Error("Must set 'location'") 23 | if (!(options.location instanceof Node)) 24 | throw new Error("'location' must be a vfs.Node") 25 | if (options.compression && !arvfs.supportedCompression.has(options.compression)) 26 | throw new Error(`Compression not supported: ${options.compression}`) 27 | super(options) 28 | this._files = new Map() 29 | } 30 | 31 | _tarEntryToVfsNode(entry) { 32 | const node = {} 33 | node.vfs = this 34 | node.path = entry.name.substr(1).replace(/\/$/, '') || '/' 35 | node.isDirectory = entry.type == 'directory' 36 | ;['size', 'mtime', 'mode', 'uid', 'gid', 'uname', 'gname'].forEach( 37 | k => node[k] = entry[k]) 38 | // console.log(node) 39 | return new Node(node) 40 | } 41 | 42 | // TODO handle compression 43 | _extract(handlers) { 44 | const location = this.options.location 45 | var inStream = location.vfs.createReadStream(location.path) 46 | const extract = ar.extract() 47 | Object.keys(handlers).forEach(event => { 48 | extract.on(event, handlers[event]) 49 | }) 50 | if (this.options.compression !== undefined) { 51 | const decompress = getDecompressor(this.options.compression)() 52 | inStream.pipe(decompress).pipe(extract) 53 | } else { 54 | inStream.pipe(extract) 55 | } 56 | } 57 | 58 | _sync() { 59 | this._extract({ 60 | entry: (header, stream, next) => { 61 | const node = this._tarEntryToVfsNode(header) 62 | this._files.set(node.path, node) 63 | console.log("FILES", this._files) 64 | stream.on('end', next) 65 | stream.resume() // just auto drain the stream 66 | }, 67 | finish: () => { 68 | // all entries read 69 | // console.log(this._files) 70 | // this.emit('sync') 71 | } 72 | }) 73 | } 74 | 75 | _createReadStream(path, options) { 76 | if (!Path.isAbsolute(path)) throw errors.PathNotAbsoluteError(path) 77 | if (!this._files.has(path)) throw errors.NoSuchFileError(path) 78 | const ret = new Readable({ 79 | read(...args) { 80 | if (this._wrapped) this._wrapped.read(...args) 81 | } 82 | }) 83 | this._extract({ 84 | entry: (header, stream, next) => { 85 | if (header.name.substr(1) === path) { 86 | ret._wrapped = stream 87 | ;['close', 'data', 'end', 'error'].forEach(event => { 88 | stream.on(event, (...args) => ret.emit(event, ...args)) 89 | }) 90 | ret.emit('readable') 91 | next() 92 | } else { 93 | stream.on('end', next) 94 | stream.resume() // just auto drain the stream 95 | } 96 | } 97 | }) 98 | return ret 99 | } 100 | 101 | _stat(path, options, cb) { 102 | if (!(Path.isAbsolute(path))) return cb(errors.PathNotAbsoluteError(path)) 103 | if (!this._files.has(path)) return cb(errors.NoSuchFileError(path)) 104 | return cb(null, this._files.get(path)) 105 | } 106 | 107 | _readdir(path, cb) { 108 | if (!(Path.isAbsolute(path))) return cb(errors.PathNotAbsoluteError(path)) 109 | return cb(null, Array.from(this._files.keys()) 110 | .filter(filePath => filePath.indexOf(path) === 0) 111 | .map(filePath => filePath.replace(path, '').replace(/^\//, '')) 112 | .filter(filePath => filePath !== '' && filePath.indexOf('/') === -1)) 113 | } 114 | 115 | } 116 | module.exports = arvfs 117 | -------------------------------------------------------------------------------- /vfs-zip/vfs-zip.js: -------------------------------------------------------------------------------- 1 | const async = require('async') 2 | const fs = require('fs') 3 | const Path = require('path') 4 | const PathUtils = require('@kba/vfs-util-path') 5 | const JSZip = require("jszip"); 6 | const {Readable} = require('stream') 7 | 8 | const {base, errors, Node} = require('@kba/vfs') 9 | 10 | /** 11 | * A VFS over ZIP content 12 | * @implements api 13 | * @implements base 14 | * @alias zip 15 | */ 16 | class zipvfs extends base { 17 | 18 | static get scheme() { return 'zip' } 19 | 20 | constructor(options={}) { 21 | if (!options.location) throw new Error("Must set 'location'") 22 | if (!(options.location instanceof Node)) throw new Error("'location' must be a vfs.Node") 23 | super(options) 24 | } 25 | 26 | _zipEntryToVfsNode(entry) { 27 | const _attr = {} 28 | // log.debug("ZIP entry", entry) 29 | // log.debug("ZIP entry _data", entry.data) 30 | _attr.vfs = this 31 | _attr.path = '/' + entry.name 32 | if (entry.dir) { 33 | _attr.isDirectory = true 34 | _attr.size = 4096 35 | _attr.mode = 16832 36 | } else { 37 | _attr.isDirectory = false 38 | _attr.compressedSize = entry._data.compressedSize 39 | _attr.size = entry._data.uncompressedSize 40 | _attr.crc32 = entry.crc32 41 | _attr.mode = entry.unixPermissions 42 | } 43 | _attr.mtime = entry.date 44 | _attr.ctime = entry.date 45 | _attr.atime = entry.date 46 | return new Node(_attr) 47 | } 48 | 49 | _sync() { 50 | const new_zip = new JSZip(); 51 | const location = this.options.location 52 | location.vfs.readFile(location.path, (err, content) => { 53 | if (err) return this.emit('error', err) 54 | new_zip.loadAsync(content).then((zip) => { 55 | this.zipRoot = zip 56 | this.emit('sync') 57 | }).catch(err => this.emit('error', err)) 58 | }) 59 | } 60 | 61 | _stat(path, options, cb) { 62 | if (!(Path.isAbsolute(path))) return cb(errors.PathNotAbsoluteError(path)) 63 | path = Path.normalize(path.substr(1)) 64 | if (!(path in this.zipRoot.files)) path += '/' 65 | if (!(path in this.zipRoot.files)) return cb(errors.NoSuchFileError(path)) 66 | const entry = (path.match(/\/$/)) 67 | ? this.zipRoot.files[path] 68 | : this.zipRoot.file(path) 69 | // log.debug("ZIP entry", entry) 70 | return cb(null, this._zipEntryToVfsNode(entry)) 71 | } 72 | 73 | _readdir(dir, options, cb) { 74 | if (!(Path.isAbsolute(dir))) return cb(errors.PathNotAbsoluteError(dir)) 75 | dir = PathUtils.removeTrailingSep(dir, false) 76 | let ret = Object.keys(this.zipRoot.files) 77 | .map(filename => PathUtils.removeTrailingSep('/' + filename)) 78 | .filter(filename => filename !== dir && filename.indexOf(dir) === 0) 79 | .map(filename => filename.replace(dir + '/', '')) 80 | .filter(filename => ! filename.match('/')) 81 | // .map(filename => { console.log(filename); return filename }) 82 | return cb(null, ret) 83 | } 84 | 85 | _createReadStream(path, options={}) { 86 | if (!(Path.isAbsolute(path))) throw errors.PathNotAbsoluteError(path) 87 | const relpath = Path.normalize(path.substr(1)) 88 | if (!(relpath in this.zipRoot.files)) throw errors.NoSuchFileError(path) 89 | var self = this 90 | var read = 0 91 | return new Readable({ 92 | read(size) { 93 | if (read > 0) return 94 | self.readFile(path, options, (err, buf) => { 95 | read = buf.length 96 | this.push(buf) 97 | this.push(null) 98 | }) 99 | } 100 | }) 101 | } 102 | 103 | _readFile(path, options, cb) { 104 | if (typeof options === 'function') [cb, options] = [options, {}] 105 | if (!(Path.isAbsolute(path))) return cb(errors.PathNotAbsoluteError(path)) 106 | path = path.substr(1) 107 | const format = options.encoding ? 'string' : 'arraybuffer' 108 | this.zipRoot.file(path).async(format) 109 | .then(data => { 110 | cb(null, format === 'string' ? data : new Buffer(data)) 111 | }) 112 | .catch(cb) 113 | } 114 | 115 | _writeFile(path, data, options, cb) { 116 | if (!(Path.isAbsolute(path))) return cb(errors.PathNotAbsoluteError(path)) 117 | path = path.substr(1) 118 | if (typeof options === 'function') [cb, options] = [options, {}] 119 | this.zipRoot.file(path, data) 120 | // TODO how to catch errors since file is chainable?? 121 | return cb(null) 122 | } 123 | 124 | _unlink(path, options, cb) { 125 | this.stat(path, (err, file) => { 126 | if (err) return cb(null) 127 | if (file.isDirectory) return cb(new Error("Cannot unlink directory, use 'rmdir'")) 128 | this.zipRoot.remove(path.substr(1)) 129 | return cb(null) 130 | }) 131 | } 132 | 133 | } 134 | module.exports = zipvfs 135 | -------------------------------------------------------------------------------- /vfs-server/route/webdav.js: -------------------------------------------------------------------------------- 1 | const {Router} = require('express') 2 | const bodyParser = require('body-parser') 3 | 4 | /** 5 | * WebDAV route 6 | */ 7 | class DavRoute { 8 | 9 | constructor({dispatcher, basepath}) { 10 | this.dispatcher = dispatcher 11 | this.basepath = basepath 12 | } 13 | 14 | _nodeToDavResponse(node) { 15 | const resourcetype = node.isDirectory 16 | ? '' 17 | : '' 18 | const displayname = node.path 19 | const getlastmodified = node.mtime.toUTCString() 20 | const creationdate = node.ctime.toUTCString() 21 | const href = this.basepath + node.path 22 | const getcontenttype = node.mimetype 23 | // const href = node.path 24 | const getcontentlength = node.size 25 | return ` 26 | 27 | ${href} 28 | 29 | 30 | ${creationdate} 31 | ${getlastmodified} 32 | ${getcontentlength} 33 | ${getcontenttype} 34 | ${node["%base"]} 35 | ${resourcetype} 36 | 37 | HTTP/1.1 200 OK 38 | 39 | ` 40 | } 41 | 42 | _sendMultistatus(resp, inner) { 43 | const ret = ` 44 | ${inner} 45 | 46 | ` 47 | console.log("MULTISTATUS", ret) 48 | resp.status(207) 49 | resp.header('Content-Type', 'application/xml') 50 | resp.send(ret) 51 | } 52 | 53 | _respond404(path) { 54 | const ret =` 55 | 56 | ${this.basepath}${path} 57 | 58 | HTTP/1.1 404 Not found 59 | 61 | ` 62 | return ret 63 | } 64 | 65 | move(req, resp, next) { 66 | const source = '/' + req.params.path 67 | const dest = req.headers.destination 68 | .replace(/^https?:\/\//, '') 69 | .replace(/^[^\/]+/, '') 70 | .replace(this.basepath, '') 71 | console.log(`${source} -> ${dest}`) 72 | resp.status(404) 73 | return resp.end() 74 | } 75 | 76 | delete(req, resp, next) { 77 | const path = '/' + req.params.path 78 | const vfs = this.dispatcher.instantiate(path, req.vfsOptions) 79 | vfs.stat(path, (err, stat) => { 80 | if (err) { 81 | this._sendMultistatus(resp, this._respond404(path)) 82 | return 83 | } 84 | console.log(stat.isDirectory ? 'rmdir' : 'unlink') 85 | vfs[stat.isDirectory ? 'rmdir' : 'unlink'](path, req.vfsOptions, err => { 86 | if (err) { 87 | console.log("ERROR", err) 88 | resp.status(403) 89 | resp.end() 90 | } else { 91 | resp.status(200) 92 | resp.end() 93 | } 94 | }) 95 | }) 96 | } 97 | 98 | put(req, resp, next) { 99 | const path = '/' + req.params.path 100 | const vfs = this.dispatcher.instantiate(path, req.vfsOptions) 101 | const outstream = vfs.createWriteStream(path, req.vfsOptions) 102 | outstream.on('finish', () => { 103 | resp.status(200) 104 | resp.end() 105 | }) 106 | outstream.write(req.body) 107 | outstream.end() 108 | resp.end() 109 | } 110 | 111 | mkcol(req, resp, next) { 112 | const path = '/' + req.params.path 113 | const vfs = this.dispatcher.instantiate(path, req.vfsOptions) 114 | vfs.mkdir(path, (err) => { 115 | if (err) { 116 | console.log("ERROR", err) 117 | resp.status(401) 118 | resp.end() 119 | } else { 120 | resp.status(201) 121 | resp.end() 122 | } 123 | }) 124 | } 125 | 126 | propfind(req, resp, next) { 127 | const {body} = req 128 | const path = '/' + req.params.path 129 | console.log("PATH", path) 130 | console.log("BODY", body) 131 | const vfs = this.dispatcher.instantiate(path, req.vfsOptions) 132 | vfs.stat(path, (err, pathNode) => { 133 | if (err) { 134 | this._sendMultistatus(resp, this._respond404(path)) 135 | return 136 | } 137 | let out = this._nodeToDavResponse(pathNode) 138 | if (pathNode.isDirectory) { 139 | vfs.getdir(pathNode.path, (err, files) => { 140 | if (err) return next(err) 141 | files.forEach(node => { 142 | out += this._nodeToDavResponse(node) 143 | }) 144 | this._sendMultistatus(resp, out) 145 | }) 146 | } else { 147 | this._sendMultistatus(resp, out) 148 | } 149 | }) 150 | } 151 | 152 | create() { 153 | const route = new Router() 154 | route.get('/:path(*)', [ 155 | bodyParser.text({type: 'application/xml'}), 156 | this.propfind.bind(this) 157 | ]) 158 | route.mkcol('/:path(*)', [ 159 | bodyParser.text({type: 'application/xml'}), 160 | this.mkcol.bind(this) 161 | ]) 162 | route.put('/:path(*)', [ 163 | bodyParser.raw(), 164 | this.put.bind(this) 165 | ]) 166 | route.propfind('/:path(*)', [ 167 | bodyParser.text({type: 'application/xml'}), 168 | this.propfind.bind(this) 169 | ]) 170 | route.delete('/:path(*)', [ 171 | this.delete.bind(this) 172 | ]) 173 | route.move('/:path(*)', [ 174 | this.move.bind(this) 175 | ]) 176 | route.options('/:path(*)', (req, resp, next) => { 177 | resp.header('DAV', '1,2,3') 178 | resp.end() 179 | }) 180 | return route 181 | } 182 | } 183 | 184 | 185 | module.exports = (options) => { 186 | return new DavRoute(options).create() 187 | } 188 | // function webdavRoute(options, cb) { 189 | // } 190 | -------------------------------------------------------------------------------- /vfs/base.js: -------------------------------------------------------------------------------- 1 | const async = require('async') 2 | const Node = require('./node') 3 | const Path = require('path') 4 | const api = require('./api') 5 | 6 | const errors = require('@kba/vfs-util-errors') 7 | 8 | const NODE_TYPES = [ 9 | // 'File', 10 | 'Directory', 11 | // 'BlockDevice', 12 | // 'CharacterDevice', 13 | 'SymbolicLink', 14 | // 'FIFO', 15 | // 'Socket', 16 | ] 17 | 18 | /** 19 | * ### vfs.base 20 | * 21 | * Base class of all vfs 22 | * 23 | * Provides default implementations for [some api methods](#vfsapi). 24 | * 25 | */ 26 | class base extends api { 27 | 28 | static get Node() {return Node} 29 | 30 | /** 31 | * #### `(static) NODE_TYPES` 32 | * 33 | * Types a [vfs.Node](#vfsnode) can have. 34 | * 35 | * Currently: 36 | * - `Directory` 37 | * - `SymbolicLink` 38 | * 39 | */ 40 | static get NODE_TYPES() {return NODE_TYPES} 41 | 42 | /** 43 | * #### `(static) capabilities` 44 | * 45 | * Lists the capabilities of a VFS, i.e. which methods are available 46 | * 47 | * - `@return {Set}` set of available methods 48 | */ 49 | static get capabilities() { 50 | const toSkip = new Set(['constructor']) 51 | const ret = new Set() 52 | Object.getOwnPropertyNames(this.prototype) 53 | .filter(prop => !(toSkip.has(prop))) 54 | .map(prop => ret.add(prop.replace(/^_/, ''))) 55 | const deps = { 56 | 'getdir': ['stat', 'readdir'], 57 | 'find': ['stat', 'readdir'], 58 | 'mkdirRecursive': ['stat', 'mkdir'], 59 | 'du': ['stat', 'readdir'], 60 | 'readFile': ['stat', 'createReadStream'], 61 | 'writeFile': ['stat', 'writeFile'], 62 | 'copyFile': ['stat', 'readFile', 'writeFile'], 63 | 'nextFile': ['stat', 'readdir'], 64 | } 65 | const props = Object.getOwnPropertyNames(api.prototype) 66 | props.filter(prop => !(toSkip.has(prop)) 67 | && deps[prop] && deps[prop].every(prop => ret.has(prop))) 68 | .map(prop => ret.add(prop)) 69 | return ret 70 | } 71 | 72 | constructor(...args) {super(...args)} 73 | 74 | _applyPlugins(fn, args, cb) { 75 | const plugins = this.plugins.filter(plugin => plugin[fn]) 76 | if (plugins.length === 0) return cb() 77 | return async.each(plugins, (plugin, done) => {return plugin[fn](...args, done)}, cb) 78 | } 79 | 80 | /* readFile default implementation */ 81 | _readFile(path, options, cb) { 82 | if (!Path.isAbsolute(path)) return cb(errors.PathNotAbsoluteError(path)) 83 | try { 84 | let inStream = this.createReadStream(path, options) 85 | const bufs = [] 86 | inStream.on('data', data => { 87 | if (typeof data === 'string') 88 | data = Buffer.from(data) 89 | bufs.push(data) 90 | }) 91 | inStream.on('error', err => cb(err)) 92 | inStream.on('end', () => { 93 | const buf = Buffer.concat(bufs) 94 | if (options.encoding) 95 | return cb(null, buf.toString(options.encoding)) 96 | return cb(null, buf) 97 | }) 98 | } catch (err) { 99 | return cb(err) 100 | } 101 | } 102 | 103 | /* getdir default implementation */ 104 | _getdir(dir, options, cb) { 105 | if (typeof options === 'function') [cb, options] = [options, {}] 106 | this.readdir(dir, (err, filenames) => { 107 | if (err) return cb(err) 108 | async.map(filenames, (filename, done) => { 109 | filename = Path.join(dir, filename) 110 | this.stat(filename, (err, node) => { 111 | if (err) return done(err) 112 | return done(null, node) 113 | }) 114 | }, (err, ret) => { 115 | if (err) return cb(err) 116 | if (options.sortBy) { 117 | let {sortBy, sortDir} = options 118 | sortDir = sortDir || + 1 119 | ret = ret.sort((objA, objB) => { 120 | const [a, b] = [objA, objB].map(x => { 121 | x = x[sortBy] 122 | if (x instanceof Date) x = x.getTime() 123 | return x 124 | }) 125 | return sortDir * (a == b ? 0 : a < b ? -1 : +1) 126 | }) 127 | } 128 | if (! options.parent) 129 | return cb(null, ret) 130 | options.parent.vfs.stat(Path.join(options.parent.path, '..'), (err, parent) => { 131 | if (parent) { 132 | // const parent = options.parent 133 | parent.pathLabel = '..' 134 | ret.unshift(parent) 135 | } 136 | return cb(null, ret) 137 | }) 138 | }) 139 | }) 140 | } 141 | 142 | /* copyFile default implementation */ 143 | _copyFile(from, to, options, cb) { 144 | [from, to] = [from, to].map(arg => { 145 | if (typeof arg === 'string') {arg = {vfs: this, path: arg}} 146 | if (typeof arg === 'object') { 147 | if (!(arg.vfs instanceof api)) throw new Error("'arg.vfs' must be a vfs") 148 | if (typeof arg.path !== 'string') throw new Error("'arg.path' must be a string") 149 | } 150 | return arg 151 | }) 152 | from.vfs.readFile(from.path, (err, data) => { 153 | if (err) return cb(err) 154 | if (data instanceof ArrayBuffer) { 155 | data = new Buffer(data) 156 | } 157 | to.vfs.writeFile(to.path, data, cb) 158 | }) 159 | } 160 | 161 | _nextFile(path, options, cb) { 162 | // TODO normalize path 163 | this.stat(path, (err, node) => { 164 | if (err) return cb(err) 165 | if (node['%dir'] === path) return cb(new Error("Already at root")) 166 | this.getdir(node['%dir'], (err, _files) => { 167 | if (err) return cb(err) 168 | const files = _files 169 | .filter(f => options.whitelistFn(f)) 170 | .filter(f => ! options.blacklistFn(f)) 171 | // console.log(filtered.map(p => p.path)) 172 | const idx = files.findIndex(f => f.path == path) 173 | let nextIdx = idx + options.delta 174 | if (nextIdx >= files.length || nextIdx < 0) { 175 | if (options.wrapStrategy === 'wrap') { 176 | nextIdx %= files.length 177 | if (nextIdx < 0) nextIdx = files.length + nextIdx 178 | } else { 179 | cb(errors.NotImplementedError(`wrapStrategy ${options.wrapStrategy}`)) 180 | } 181 | } 182 | const nextFile = files[nextIdx] 183 | return cb(null, nextFile) 184 | }) 185 | }) 186 | } 187 | 188 | } 189 | module.exports = base 190 | -------------------------------------------------------------------------------- /test/lib/vfs-test.js: -------------------------------------------------------------------------------- 1 | const tap = require('tap') 2 | const Path = require('path') 3 | const async = require('async') 4 | const vfsFile = require('@kba/vfs-file') 5 | 6 | const testFunctions = module.exports = { 7 | vfsReadTest(t, fs, cb) { 8 | const testFileContents = 'ÜÄ✓✗\n' 9 | const testFilePath = '/lib/file2.txt' 10 | fs.on('error', err => { 11 | console.error("ERROR", err) 12 | fs.end() 13 | }) 14 | fs.once('sync', () => async.waterfall([ 15 | cb => t.test('stat', t => { 16 | t.equals(fs.constructor.capabilities.has('stat'), true, 'implements stat') 17 | fs.stat(testFilePath, (err, node) => { 18 | t.deepEquals(err, undefined, 'no error') 19 | t.equals(node.size, 11, '11 bytes long') 20 | t.equals(node.isDirectory, false, 'not a Directory') 21 | return cb(t.end()) 22 | }) 23 | }), 24 | cb => t.test('readdir', (t) => { 25 | fs.readdir('/lib', (err, files) => { 26 | t.equals(files.length, 3, '3 files in /lib') 27 | return cb(t.end()) 28 | }) 29 | }), 30 | cb => t.test('getdir', (t) => { 31 | fs.getdir('/lib', (err, files) => { 32 | t.equals(files.length, 3, '3 files in /lib') 33 | return cb(t.end()) 34 | }) 35 | }), 36 | cb => t.test('getdir {sortBy: "mtime", sortDir: 1}', (t) => { 37 | fs.getdir('/lib', {sortBy: 'mtime', sortDir: 1}, (err, files) => { 38 | t.equals(files[0].mtime.getTime() < files[2].mtime.getTime(), true, 'mtime: 0 < 2') 39 | return cb(t.end()) 40 | }) 41 | }), 42 | cb => t.test('getdir {sortBy:mtime, sortDir: -1}', (t) => { 43 | fs.getdir('/lib', {sortBy: 'mtime', sortDir: -1}, (err, files) => { 44 | t.equals(files[0].mtime.getTime() > files[2].mtime.getTime(), true, 'mtime: 0 > 2') 45 | return cb(t.end()) 46 | }) 47 | }), 48 | cb => t.test('nextFile', t => { 49 | fs.nextFile('/lib/file1', (err, nextFile) => { 50 | t.equals(nextFile.path, '/lib/file2.txt', 'file1 -> file2.txt') 51 | return cb(t.end()) 52 | }) 53 | }), 54 | cb => t.test('nextFile', t => { 55 | fs.nextFile('/lib/file3.png', (err, nextFile) => { 56 | t.equals(nextFile.path, '/lib/file1', 'file3.png -> file1') 57 | return cb(t.end()) 58 | }) 59 | }), 60 | cb => t.test('nextFile {delta: -1}', t => { 61 | fs.nextFile('/lib/file1', {delta: -1}, (err, nextFile) => { 62 | t.equals(nextFile.path, '/lib/file3.png', 'file1 -> file3.png') 63 | return cb(t.end()) 64 | }) 65 | }), 66 | cb => t.test('nextFile {blacklistFn}', t => { 67 | fs.nextFile('/lib/file2.txt', {blacklistFn: f => f['%base'] === 'file3.png'}, (err, nextFile) => { 68 | t.equals(nextFile.path, '/lib/file1', 'file2.txt -> file1') 69 | return cb(t.end()) 70 | }) 71 | }), 72 | cb => t.test('find', (t) => { 73 | fs.find('/', (err, files) => { 74 | t.notOk(err, 'no error') 75 | t.equals(files.length, 4, '4 files in the fs') 76 | return cb(t.end()) 77 | }) 78 | }), 79 | cb => t.test('createReadStream', t =>{ 80 | if (!(fs.constructor.capabilities.has('createReadStream'))) { 81 | t.comment('Not implemented') 82 | return cb(t.end()) 83 | } 84 | const stream = fs.createReadStream(testFilePath) 85 | stream.on('data', (data) => t.equals(data.toString(), testFileContents)) 86 | stream.on('end', () => { 87 | return cb(t.end()) 88 | }) 89 | }), 90 | cb => t.test('readFile/string', t => { 91 | fs.readFile(testFilePath, {encoding:'utf8'}, (err, buf) => { 92 | t.deepEquals(err, undefined, 'no error') 93 | t.equals(buf, testFileContents) 94 | t.end() 95 | return cb() 96 | }) 97 | }), 98 | cb => t.test('readFile/Buffer', t => { 99 | fs.readFile(testFilePath, (err, buf) => { 100 | t.deepEquals(err, undefined, 'no error') 101 | t.deepEquals(buf, new Buffer(testFileContents)) 102 | return cb(t.end()) 103 | }) 104 | }), 105 | cb => { 106 | fs.end() 107 | cb() 108 | }, 109 | // cb => t.test('writeFile', t => { 110 | // vfs.writeFile(dummyPath, dummyData, (err) => { 111 | // t.deepEquals(err, undefined, 'writeFile/String: no error') 112 | // return cb(t.end()) 113 | // }) 114 | // }), 115 | // cb => t.test('readFile/string', t => { 116 | // vfs.readFile(dummyPath, {encoding: 'utf8'}, (err, data) => { 117 | // t.deepEquals(err, undefined, 'readFile/string: no error') 118 | // t.equals(typeof data, 'string', 'is a string') 119 | // t.equals(data.length, dummyData.length, `${dummyData.length} characters long`) 120 | // return cb(t.end()) 121 | // }) 122 | // }), 123 | // cb => t.test('copyFile(string, {vfs:fs, path: /tmp/foo})', t => { 124 | // vfs.copyFile(dummyPath, {vfs: fs, path: '/tmp/foo'}, (err) => { 125 | // t.notOk(err, 'no error') 126 | // return cb(t.end()) 127 | // }) 128 | // }), 129 | // cb => t.test('unlink', t => { 130 | // vfs.unlink(dummyPath, (err) => { 131 | // t.deepEquals(err, undefined, 'unlink: no error') 132 | // return cb(t.end()) 133 | // }) 134 | // }), 135 | // cb => t.test('stat after unlink', t => { 136 | // vfs.stat(dummyPath, (err, x) => { 137 | // t.ok(err.message.match('NoSuchFileError'), 'stat fails after delete') 138 | // return cb(t.end()) 139 | // }) 140 | // }), 141 | ], cb)) 142 | fs.init() 143 | } 144 | } 145 | 146 | testFunctions.testVfs = function(vfsName, tests) { 147 | const fileVfs = new vfsFile() 148 | tap.test(`${vfsName} vfs`, t => { 149 | const vfsClass = require(`@kba/vfs-${vfsName}`) 150 | t.equals(vfsClass.scheme, vfsName, `scheme is ${vfsName}`) 151 | const runTests = (options, fns, done) => { 152 | fns.forEach(fn => { 153 | testFunctions[fn](t, new(vfsClass)(options), err => { 154 | if (err) return done(err) 155 | return done() 156 | }) 157 | }) 158 | } 159 | async.eachSeries(tests, ([options, fns], done) => { 160 | if ('location' in options) { 161 | const fixtureName = Path.join(__dirname, '..', 'fixtures', options.location) 162 | fileVfs.stat(fixtureName, (err, location) => { 163 | if (err) throw err 164 | t.notOk(err, `read ${fixtureName}`) 165 | options.location = location 166 | runTests(options, fns, done) 167 | }) 168 | } else { 169 | runTests(options, fns, done) 170 | } 171 | }, (err) => { 172 | if (err) t.fail(":-(") 173 | t.end() 174 | }) 175 | }) 176 | } 177 | 178 | -------------------------------------------------------------------------------- /test/fixtures/folder.tar: -------------------------------------------------------------------------------- 1 | ./0000755000175000017500000000000013044324250006762 5ustar kbkb./lib/0000755000175000017500000000000013044323751007535 5ustar kbkb./lib/file2.txt0000644000175000017500000000001313044323751011271 0ustar kbkbÜÄ✓✗ 2 | ./lib/file3.png0000644000175000017500000000000013044323740011231 0ustar kbkb./lib/file10000644000175000017500000000000013044323724010446 0ustar kbkb -------------------------------------------------------------------------------- /vfs/api.js: -------------------------------------------------------------------------------- 1 | const async = require('async') 2 | const {EventEmitter} = require('events') 3 | const Path = require('path') 4 | 5 | const _EVENT = Symbol('event') 6 | 7 | /** 8 | * ### vfs.api 9 | * Interface of all vfs 10 | * 11 | */ 12 | class api { 13 | 14 | /** 15 | * #### Constructor 16 | */ 17 | constructor(options={}) { 18 | this.options = options 19 | 20 | this[_EVENT] = new EventEmitter() 21 | ;['on', 'once', 'emit'].forEach(k => { 22 | Object.defineProperty(this, k, { 23 | enumerable: false, 24 | value: this[_EVENT][k].bind(this[_EVENT]), 25 | }) 26 | }) 27 | 28 | this.plugins = [] 29 | if ('plugins' in options) { 30 | options.plugins.forEach(([pluginClass, pluginOptions]) => { 31 | this.use(pluginClass, pluginOptions) 32 | }) 33 | } 34 | } 35 | 36 | /** 37 | * #### `use(pluginClass, pluginOptions)` 38 | * 39 | * Enable a plugin 40 | * 41 | */ 42 | use(pluginClass, pluginOptions) { 43 | const plugin = new pluginClass(pluginOptions) 44 | this.plugins.push(plugin) 45 | } 46 | 47 | /** 48 | * #### `stat(path, options, callback)` 49 | * 50 | * Get metadata about a node in the vfs. 51 | * 52 | * - `@param {String} path` absolute path to the file 53 | * - `@param {Function} callback` error or {@link Node} 54 | * 55 | */ 56 | stat(path, options, cb) { 57 | if (typeof options === 'function') [cb, options] = [options, {}] 58 | this._stat(path, options, (err, node) => { 59 | if (err) return cb(err) 60 | this._applyPlugins('stat', [node], (err) => { 61 | return cb(err, node) 62 | }) 63 | }) 64 | } 65 | 66 | /** 67 | * #### `mkdir(path, mode, callback)` 68 | * 69 | * Create a directory 70 | * 71 | * - `@param {string} path` absolute path to the folder 72 | * - `@param {errorCallback} cb` 73 | * - @see [fs#mkdir](https://nodejs.org/api/fs.html#fs_fs_mkdir_path_mode_callback) 74 | */ 75 | mkdir(path, mode, cb) { 76 | if (typeof mode === 'function') [cb, mode] = [mode, {}] 77 | return this._mkdir(path, mode, cb) 78 | } 79 | 80 | /** 81 | * #### `init()` 82 | * 83 | * Initialize the filesystem. 84 | * 85 | * By default only calls #sync and emits [`ready`](#events-ready) on [`sync`](#events-sync)} 86 | */ 87 | init() { 88 | this.once('sync', () => this.emit('ready')) 89 | this.sync() 90 | } 91 | 92 | /** 93 | * #### `end()` 94 | * 95 | * Un-initialize the filesystem, e.g. disconnect a client. 96 | * 97 | */ 98 | end() { 99 | if (this._end) this._end() 100 | else this.emit('end') 101 | } 102 | 103 | /** 104 | * #### `sync(options)` 105 | * 106 | * Sync the filesystem. 107 | * 108 | */ 109 | sync(options={}) { 110 | return this._sync(options) 111 | } 112 | 113 | /** 114 | * #### `createReadStream(path, options)` 115 | * 116 | * See [fs.createReadStream](https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options) 117 | * 118 | * Create a ReadableStream from a file 119 | * 120 | * @param {string} path absolute path to the file 121 | */ 122 | createReadStream(path, ...args) { 123 | return this._createReadStream(path, ...args) 124 | } 125 | 126 | /** 127 | * #### `createWriteStream(path, options)` 128 | * 129 | * Create a WritableStream to a file 130 | * 131 | * See [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options). 132 | * 133 | * @param {string} path absolute path to the file 134 | */ 135 | createWriteStream(path, ...args) { 136 | return this._createWriteStream(path, ...args) 137 | } 138 | 139 | /** 140 | * @callback readFileCallback 141 | * @param {Error} err 142 | * @param {Buffer|String} data the file data as a buffer or stream 143 | */ 144 | /** 145 | * #### `readFile(path, options, callback)` 146 | * 147 | * @see {@link https://nodejs.org/api/fs.html#fs_fs_readfile_file_options_callback fs#readFile} 148 | * 149 | * - `@param {string} path` absolute path to the file 150 | * - `@param {object} options` 151 | * - `@param {object} options.encoding=undefined` Encoding of the data. Setting this will return a String 152 | * - `@param {readFileCallback} cb` 153 | */ 154 | readFile(path, options, cb) { 155 | if (typeof options === 'function') [cb, options] = [options, {}] 156 | return this._readFile(path, options, cb) 157 | } 158 | 159 | /** 160 | * #### `writeFile(path, data, options, callback)` 161 | * 162 | * @see {@link https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback fs#writeFile} 163 | * 164 | * - `@param {string} path` absolute path to the file 165 | * - `@param {object} options` 166 | * - `@param {function(err)}` cb 167 | */ 168 | writeFile(path, data, options, cb) { 169 | if (typeof options === 'function') [cb, options] = [options, {}] 170 | return this._writeFile(path, data, options, cb) 171 | } 172 | 173 | /** 174 | * #### `unlink(path, options, cb)` 175 | * 176 | * @param {string} path absolute path to the folder 177 | * @param {errorCallback} cb 178 | * @see {@link https://nodejs.org/api/fs.html#fs_fs_unlink_path_callback fs#unlink} 179 | */ 180 | unlink(path, options, cb) { 181 | if (typeof options === 'function') [cb, options] = [options, {}] 182 | return this._unlink(path, options, cb) 183 | } 184 | 185 | /** 186 | * #### `mkdirRecursive(path, cb)` 187 | * 188 | * mkdir -p 189 | * 190 | * @param {string} path absolute path to the folder to create 191 | * @param {errorCallback} cb 192 | */ 193 | mkdirRecursive(path, cb) { 194 | if (!Path.isAbsolute(path)) throw new Error(`'path' must be absolute: ${path}`) 195 | path = path.substr(1) 196 | let fullDir = '' 197 | async.eachSeries(path.split('/'), (dir, done) => { 198 | fullDir += '/' + Path.join(fullDir, dir) 199 | this.mkdir(fullDir, done) 200 | }, cb) 201 | } 202 | 203 | /** 204 | * #### `copyFile(from, to, options, cb)` 205 | * 206 | * Copy file, possibly across different VFS. 207 | * 208 | * @param {string|Node} from 209 | * @param {string|Node} to 210 | * @param {errorCallback} cb 211 | */ 212 | copyFile(from, to, options, cb) { 213 | if (typeof options === 'function') [cb, options] = [options, {}] 214 | this._copyFile(from, to, options, cb) 215 | } 216 | 217 | /** 218 | * #### `getdir(dir, options, callback)` 219 | * 220 | * Get directory contents as {@link Node} objects. 221 | * 222 | * Essentially a shortcut for {@link api#stat} applied to {@link api#getdir}. 223 | * 224 | * - @param {string} dir 225 | * - @param {object} options 226 | * - @param {Node} options.parent=null 227 | * - @param {string} options.sortBy=null 228 | * - @param {number} options.sortDir=-1 229 | * - @return {function(err, nodes)} cb 230 | */ 231 | getdir(dir, options, cb) { 232 | if (typeof options === 'function') [cb, options] = [options, {}] 233 | return this._getdir(dir, options, cb) 234 | } 235 | 236 | // TODO should accept options 237 | /** 238 | * #### `find(path, callback)` 239 | * 240 | * List recursive folder contents 241 | * 242 | * @param path string path 243 | * @param cb function (err, files) 244 | */ 245 | find(path, cb) { 246 | const ret = [] 247 | this.getdir(path, (err, files) => { 248 | async.each(files, (file, done) => { 249 | if (err) return done(err) 250 | if (file.isDirectory) 251 | this.find(file.path, (err, subfiles) => { 252 | if (err) return done(err) 253 | ret.push(file) 254 | subfiles.forEach(subfile => ret.push(subfile)) 255 | return done() 256 | }) 257 | else { 258 | ret.push(file) 259 | return done() 260 | } 261 | }, (err) => { 262 | return cb(err, ret) 263 | }) 264 | }) 265 | } 266 | 267 | 268 | // TODO should accept options 269 | /** 270 | * #### `du(path, callback)` 271 | * 272 | * Recursive size of a node. 273 | * 274 | * @param {string} path absolute path to the file 275 | */ 276 | du(path, cb) { 277 | let totalSize = 0 278 | this.find(path, (err, files) => { 279 | if (err) return cb(err) 280 | files.forEach(file => totalSize += file.size) 281 | return cb(null, totalSize) 282 | }) 283 | } 284 | 285 | /** 286 | * #### `readdir(path, options, callback)` 287 | * 288 | * List the nodes in a folder. 289 | * 290 | * @see [fs#readdir](https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback). 291 | * 292 | * - `@param {string} path` absolute path to the folder 293 | * - `@param {function(err, filenames)} callback` 294 | * - `@param {Error} err` 295 | * - `@param {array} filenames` list of relative path names in this folder 296 | */ 297 | readdir(path, options, cb) { 298 | if (typeof options === 'function') [cb, options] = [options, {}] 299 | this._readdir(path, options, cb) 300 | } 301 | 302 | /** 303 | * #### `nextFile(path, options, callback)` 304 | * 305 | * Find the next file starting from path 306 | * 307 | * - `@param {string} path` absolute path to the file 308 | * - `@param {object} options` 309 | * - `@param {boolean} delta` Offset. Set to negative to get previous file. Default: +1 310 | * - `@param {function(path)} whitelistFn` Consider only paths for which this fn returns true 311 | * - `@param {function(path)} blacklistFn` Discard all paths for which this fn returns true 312 | * - `@param {String} wrapStrategy` What to do when hitting a directory boundary 313 | * - `throw` Throw an error when files are exhausted 314 | * - `wrap` Jump from beginning to end / vice versa (Default) 315 | * - `jump` Jump to first file in next folder / last file in previous folder 316 | * - `@param {function(err, nextPath)} callback` 317 | * - `@param {Error} err` 318 | * - `@param {array} filenames` list of relative path names in this folder 319 | */ 320 | nextFile(path, options, cb) { 321 | if (typeof options === 'function') [cb, options] = [options, {}] 322 | if (!(options.delta)) options.delta = +1 323 | if (!(options.wrapStrategy)) options.wrapStrategy = 'wrap' 324 | if (!(options.whitelistFn)) options.whitelistFn = p => true 325 | if (!(options.blacklistFn)) options.blacklistFn = p => false 326 | this._nextFile(path, options, cb) 327 | } 328 | 329 | rmdir(path, options, cb) { 330 | if (typeof options === 'function') [cb, options] = [options, {}] 331 | this._rmdir(path, options, cb) 332 | } 333 | 334 | /** 335 | * #### Events 336 | * 337 | * ##### Events: `ready` 338 | * ##### Events: `sync` 339 | * ##### Events: `error` 340 | * ##### Events: `end` 341 | */ 342 | 343 | 344 | } 345 | module.exports = api 346 | 347 | // vim: sw=4 ts=4 348 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vfs 2 | > A virtual filesystem that works like [fs](http://nodejs.org/api/fs.html) 3 | 4 | [![Build Status](https://travis-ci.org/kba/vfs.svg?branch=master)](https://travis-ci.org/kba/vfs) 5 | [![CircleCI](https://circleci.com/gh/kba/vfs.svg?style=svg)](https://circleci.com/gh/kba/vfs) 6 | 7 | 8 | * [Introduction](#introduction) 9 | * [Currently implemented](#currently-implemented) 10 | * [Creating a new VFS](#creating-a-new-vfs) 11 | * [API](#api) 12 | * [vfs.base](#vfsbase) 13 | * [`(static) NODE_TYPES`](#static-node_types) 14 | * [`(static) capabilities`](#static-capabilities) 15 | * [vfs.api](#vfsapi) 16 | * [Constructor](#constructor) 17 | * [`use(pluginClass, pluginOptions)`](#usepluginclass-pluginoptions) 18 | * [`stat(path, options, callback)`](#statpath-options-callback) 19 | * [`mkdir(path, mode, callback)`](#mkdirpath-mode-callback) 20 | * [`init()`](#init) 21 | * [`end()`](#end) 22 | * [`sync(options)`](#syncoptions) 23 | * [`createReadStream(path, options)`](#createreadstreampath-options) 24 | * [`createWriteStream(path, options)`](#createwritestreampath-options) 25 | * [`readFile(path, options, callback)`](#readfilepath-options-callback) 26 | * [`writeFile(path, data, options, callback)`](#writefilepath-data-options-callback) 27 | * [`unlink(path, options, cb)`](#unlinkpath-options-cb) 28 | * [`mkdirRecursive(path, cb)`](#mkdirrecursivepath-cb) 29 | * [`copyFile(from, to, options, cb)`](#copyfilefrom-to-options-cb) 30 | * [`getdir(dir, options, callback)`](#getdirdir-options-callback) 31 | * [`find(path, callback)`](#findpath-callback) 32 | * [`du(path, callback)`](#dupath-callback) 33 | * [`readdir(path, options, callback)`](#readdirpath-options-callback) 34 | * [`nextFile(path, options, callback)`](#nextfilepath-options-callback) 35 | * [Events](#events) 36 | * [Events: `ready`](#events-ready) 37 | * [Events: `sync`](#events-sync) 38 | * [Events: `error`](#events-error) 39 | * [Events: `end`](#events-end) 40 | * [vfs.Node](#vfsnode) 41 | * [Constructor](#constructor-1) 42 | * [Properties](#properties) 43 | * [`vfs`](#vfs) 44 | * [`path`](#path) 45 | * [`mtime`](#mtime) 46 | * [`mode`](#mode) 47 | * [`mimetype`](#mimetype) 48 | * [`%root`](#root) 49 | * [`%dir`](#dir) 50 | * [`%base`](#base) 51 | * [`%ext`](#ext) 52 | * [`%name`](#name) 53 | * [CompressionUtils](#compressionutils) 54 | * [`(static) hasDecompressor(format)`](#static-hasdecompressorformat) 55 | * [`(static) getDecompressor(format)`](#static-getdecompressorformat) 56 | * [PathUtils](#pathutils) 57 | * [`(static) removeTrailingSep(path)`](#static-removetrailingseppath) 58 | * [`(static) removeLeadingSep(path)`](#static-removeleadingseppath) 59 | * [StreamUtils](#streamutils) 60 | * [`(static) createReadableWrapper()`](#static-createreadablewrapper) 61 | * [`ReadableWrapper`](#readablewrapper) 62 | * [`wrapStream(stream)`](#wrapstreamstream) 63 | 64 | 65 | 66 | ## Introduction 67 | 68 | A virtual file system is an interface to some data with the semantics of a file 69 | system (directory hierarchy, files, metadata) and the mechanics of the [Node.JS 70 | fs module](http://nodejs.org/api/fs.html). 71 | 72 | ## Currently implemented 73 | 74 | * `file` - a VFS that mirrors the local filesystem 75 | * `zip` - a VFS on top of ZIP content 76 | * `tar` - a VFS on top of tarball content (compressions: gzip, bzip2, xz) 77 | 78 | 79 | zip file tar ar sftp 80 | stat X X X X X 81 | mkdir - X - - - 82 | createReadStream X X X X X 83 | createWriteStream - X - - - 84 | readFile X X X X X 85 | writeFile X X - - - 86 | unlink X X - - - 87 | mkdirRecursive - X - - - 88 | copyFile X X - - - 89 | getdir X X X X X 90 | find X X X X X 91 | du X X X X X 92 | readdir X X X X X 93 | nextFile - - - - - 94 | rmdir - X - - - 95 | 96 | 97 | 98 | ## Creating a new VFS 99 | 100 | * Subclass `vfs.base` 101 | * Override 102 | * `_stat` 103 | * `_readdir` 104 | 105 | ## API 106 | 107 | 108 | ### vfs.base 109 | 110 | Base class of all vfs 111 | 112 | Provides default implementations for [some api methods](#vfsapi). 113 | 114 | #### `(static) NODE_TYPES` 115 | 116 | Types a [vfs.Node](#vfsnode) can have. 117 | 118 | Currently: 119 | - `Directory` 120 | - `SymbolicLink` 121 | #### `(static) capabilities` 122 | 123 | Lists the capabilities of a VFS, i.e. which methods are available 124 | 125 | - `@return {Set}` set of available methods 126 | 127 | 128 | 129 | 130 | ### vfs.api 131 | Interface of all vfs 132 | #### Constructor 133 | #### `use(pluginClass, pluginOptions)` 134 | 135 | Enable a plugin 136 | #### `stat(path, options, callback)` 137 | 138 | Get metadata about a node in the vfs. 139 | - `@param {String} path` absolute path to the file 140 | - `@param {Function} callback` error or {@link Node} 141 | #### `mkdir(path, mode, callback)` 142 | 143 | Create a directory 144 | 145 | - `@param {string} path` absolute path to the folder 146 | - `@param {errorCallback} cb` 147 | - @see [fs#mkdir](https://nodejs.org/api/fs.html#fs_fs_mkdir_path_mode_callback) 148 | #### `init()` 149 | 150 | Initialize the filesystem. 151 | 152 | By default only calls #sync and emits [`ready`](#events-ready) on [`sync`](#events-sync)} 153 | #### `end()` 154 | Un-initialize the filesystem, e.g. disconnect a client. 155 | #### `sync(options)` 156 | 157 | Sync the filesystem. 158 | #### `createReadStream(path, options)` 159 | See [fs.createReadStream](https://nodejs.org/api/fs.html#fs_fs_createreadstream_path_options) 160 | Create a ReadableStream from a file 161 | @param {string} path absolute path to the file 162 | #### `createWriteStream(path, options)` 163 | 164 | Create a WritableStream to a file 165 | 166 | See [fs.createWriteStream](https://nodejs.org/api/fs.html#fs_fs_createwritestream_path_options). 167 | @param {string} path absolute path to the file 168 | @callback readFileCallback 169 | @param {Error} err 170 | @param {Buffer|String} data the file data as a buffer or stream 171 | #### `readFile(path, options, callback)` 172 | 173 | @see {@link https://nodejs.org/api/fs.html#fs_fs_readfile_file_options_callback fs#readFile} 174 | 175 | - `@param {string} path` absolute path to the file 176 | - `@param {object} options` 177 | - `@param {object} options.encoding=undefined` Encoding of the data. Setting this will return a String 178 | - `@param {readFileCallback} cb` 179 | #### `writeFile(path, data, options, callback)` 180 | 181 | @see {@link https://nodejs.org/api/fs.html#fs_fs_writefile_file_data_options_callback fs#writeFile} 182 | 183 | - `@param {string} path` absolute path to the file 184 | - `@param {object} options` 185 | - `@param {function(err)}` cb 186 | #### `unlink(path, options, cb)` 187 | 188 | @param {string} path absolute path to the folder 189 | @param {errorCallback} cb 190 | @see {@link https://nodejs.org/api/fs.html#fs_fs_unlink_path_callback fs#unlink} 191 | #### `mkdirRecursive(path, cb)` 192 | 193 | mkdir -p 194 | 195 | @param {string} path absolute path to the folder to create 196 | @param {errorCallback} cb 197 | #### `copyFile(from, to, options, cb)` 198 | 199 | Copy file, possibly across different VFS. 200 | 201 | @param {string|Node} from 202 | @param {string|Node} to 203 | @param {errorCallback} cb 204 | #### `getdir(dir, options, callback)` 205 | 206 | Get directory contents as {@link Node} objects. 207 | Essentially a shortcut for {@link api#stat} applied to {@link api#getdir}. 208 | - @param {string} dir 209 | - @param {object} options 210 | - @param {Node} options.parent=null 211 | - @param {string} options.sortBy=null 212 | - @param {number} options.sortDir=-1 213 | - @return {function(err, nodes)} cb 214 | #### `find(path, callback)` 215 | 216 | List recursive folder contents 217 | @param path string path 218 | @param cb function (err, files) 219 | #### `du(path, callback)` 220 | 221 | Recursive size of a node. 222 | @param {string} path absolute path to the file 223 | #### `readdir(path, options, callback)` 224 | 225 | List the nodes in a folder. 226 | @see [fs#readdir](https://nodejs.org/api/fs.html#fs_fs_readdir_path_options_callback). 227 | - `@param {string} path` absolute path to the folder 228 | - `@param {function(err, filenames)} callback` 229 | - `@param {Error} err` 230 | - `@param {array} filenames` list of relative path names in this folder 231 | #### `nextFile(path, options, callback)` 232 | 233 | Find the next file starting from path 234 | - `@param {string} path` absolute path to the file 235 | - `@param {object} options` 236 | - `@param {boolean} delta` Offset. Set to negative to get previous file. Default: +1 237 | - `@param {function(path)} whitelistFn` Consider only paths for which this fn returns true 238 | - `@param {function(path)} blacklistFn` Discard all paths for which this fn returns true 239 | - `@param {String} wrapStrategy` What to do when hitting a directory boundary 240 | - `throw` Throw an error when files are exhausted 241 | - `wrap` Jump from beginning to end / vice versa (Default) 242 | - `jump` Jump to first file in next folder / last file in previous folder 243 | - `@param {function(err, nextPath)} callback` 244 | - `@param {Error} err` 245 | - `@param {array} filenames` list of relative path names in this folder 246 | #### Events 247 | ##### Events: `ready` 248 | ##### Events: `sync` 249 | ##### Events: `error` 250 | ##### Events: `end` 251 | 252 | 253 | 254 | 255 | ### vfs.Node 256 | ```js 257 | new fsvfs.Node({path: "/...", vfs: vfsInstance}) 258 | ``` 259 | 260 | Class representing file metadata 261 | #### Constructor 262 | - `@param {object} options` Options that will be passed 263 | - `@param {string} options.path` Absolute path to the node 264 | - `@param {fsvfs} options.vfs` Instance of a {@link fsvfs} 265 | 266 | #### Properties 267 | ##### `vfs` 268 | Parent vfs instance, e.g. a [file](./vfs-file) 269 | ##### `path` 270 | Absolute, normalized path of the node within the vfs 271 | ##### `mtime` 272 | Date of last modification 273 | ##### `mode` 274 | ##### `mimetype` 275 | MIME type of this node 276 | ##### `%root` 277 | See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 278 | ##### `%dir` 279 | See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 280 | ##### `%base` 281 | See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 282 | ##### `%ext` 283 | See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 284 | ##### `%name` 285 | See [path.parse(path)](https://nodejs.org/api/path.html#path_path_parse_path) 286 | 287 | 288 | 289 | 290 | ### CompressionUtils 291 | #### `(static) hasDecompressor(format)` 292 | 293 | Whether a decompression format is supported 294 | #### `(static) getDecompressor(format)` 295 | 296 | Instantiate a decompression stream 297 | @memberof util 298 | 299 | 300 | 301 | 302 | ### PathUtils 303 | Enhancing [path](https://nodejs.org/api/path.html) 304 | ```js 305 | const PathUtils = require('@kba/vfs-util-path') 306 | PathUtils.removeTrailingSep('/foo/') // '/foo' 307 | // or 308 | const {removeTrailingSep} = require('@kba/vfs-util-path') 309 | removeTrailingSep('/foo/') // '/foo' 310 | ``` 311 | #### `(static) removeTrailingSep(path)` 312 | 313 | Remove trailing separators (slashes) from `path`. 314 | @param {boolean} keepRoot Whether to remove or keep a single root slash 315 | #### `(static) removeLeadingSep(path)` 316 | 317 | Remove leading separators (slashes) from `path`. 318 | 319 | 320 | 321 | 322 | ### StreamUtils 323 | #### `(static) createReadableWrapper()` 324 | Wraps another ReadableStream to allow synchronously returning a stream 325 | that will become readable only later. 326 | ```js 327 | const {createReadableWrapper} = require('@kba/vfs-util-stream') 328 | const readable = createReadableWrapper() 329 | // TODO, see vfs-tar 330 | ``` 331 | #### `ReadableWrapper` 332 | TODO 333 | ##### `wrapStream(stream)` 334 | TODO 335 | 336 | 337 | --------------------------------------------------------------------------------