'use strict'; const path = require('path') , fs = require('fs') , env = require('xtend')(process.env) , cp = require('child_process') , Machine = require('docker-machine') , unixify = require('unixify') , series = require('run-series') , debug = require('debug')('docker-share') if (env.VBOX_MSI_INSTALL_PATH) { env.PATH+= path.delimiter + env.VBOX_MSI_INSTALL_PATH } class VirtualBox { constructor(name) { this.name = name } command(args, done) { debug('vboxmanage', args) cp.execFile('vboxmanage', args, { env: env, encoding: 'utf8' }, done) } addShare(name, hostPath, opts, done) { if (typeof opts === 'function') done = opts, opts = {} else if (!opts) opts = {} const args = ['sharedfolder', 'add', this.name] args.push('--name', name) args.push('--hostpath', hostPath) if (opts.writable === false || opts.readonly) args.push('--readonly') if (opts.transient) args.push('--transient') this.command(args, done) } removeShare() { } info(done) { this.command(['showvminfo', this.name], done) } getShares(done) { this.info((err, info) => { if (err) return done(err) const re = /Name: '(.*)', Host path: '(.*)'(.*)/g const shares = {} let match; while(match = re.exec(info)) { const share = { name: match[1], hostPath: match[2] } ;(match[3] || '').split(',').forEach(prop => { // "(transient mapping)" => share.transient = true prop = prop.trim().replace(/^\(|\)$/g, '') if (prop === 'transient mapping') prop = 'transient' else if (prop === 'machine mapping') return share[prop] = true }) shares[share.name] = share } debug(shares) done(null, shares) }) } getShare(name, done) { this.getShares((err, shares) => { if (err) done(err) else done(null, shares[name] || null) }) } hasShare(name, done) { this.getShare(name, (err, share) => { if (err) done(err) else done(null, !!share) }) } getState(done) { this.info((err, info) => { if (err) return done(err) const m = /State:\s+(.+)/.exec(info) if (!m) return done(new Error('Could not find state')) done(null, m[1]) }) } } class Shares { constructor(opts) { this.machine = new Machine({ name: opts.machine }) this.vm = new VirtualBox(this.machine.name) } static unixify(path) { const unix = unixify(path) const m = path.match(/^([A-Z]):/i) return m ? '/' + m[1].toLowerCase() + unix : unix } mount(opts, done) { if (typeof opts === 'function') done = opts, opts = {} else if (!opts) opts = {} const hostPath = path.resolve(opts.hostPath || '.') const name = opts.name || path.basename(hostPath) const guestPath = Shares.unixify(opts.guestPath || hostPath) const state = {} debug('Mounting %s..', name) series([ (next) => { this.vm.getState((err, state) => { if (opts.transient && !/^running/.test(state)) { return next(new Error( 'VM must be running to create a transient shared folder' )) } // TODO: can't mount it if (!opts.transient && !/^powered off/.test(state)) { return next(new Error( 'VM must be powered off to create a permanent shared folder' )) } next() }) }, (next) => { this.vm.getShare(name, (err, share) => { if (err) return next(err) state.share = share next() }) }, (next) => { // TODO: compare properties if (state.share) next() else this.vm.addShare(name, hostPath, opts, next) }, (next) => { this.vm.command(['setextradata', 'default', 'VBoxInternal2/SharedFoldersEnableSymlinksCreate/' + name, '1'], next) }, (next) => { this.getFilesystem(name, (err, fs) => { if (err) return next(err) state.fs = fs next() }) }, (next) => { if (state.fs) { if (state.fs.type !== 'vboxsf') { return next(new Error( `Mount conflict: existing filesystem of type "${state.fs.type}"` )) } if (state.fs.path !== guestPath) { return next(new Error( `Mount conflict: existing filesystem at path "${state.fs.path}"` )) } return next() } this.mountFilesystem(name, guestPath, next) }, (next) => { process.stdout.write('Testing ' + guestPath + '.. ') this.machine.ssh('ls ' + guestPath, (err) => { if (err) process.stdout.write('FAIL\n') else process.stdout.write('OK\n') next(err) }) } ], done) } getFilesystems(done) { debug('mount') this.machine.ssh('mount', (err, result) => { if (err) return done(err) const re = /^([^ ]+) on ([^ ]+) type ([^ ]+)/igm const filesystems = {} let match; while(match = re.exec(result)) { const mp = { name: match[1], path: match[2], type: match[3] } filesystems[mp.name] = mp } debug(filesystems) done(null, filesystems) }) } getFilesystem(name, done) { this.getFilesystems((err, filesystems) => { if (err) done(err) else done(null, filesystems[name] || null) }) } mountFilesystem(name, guestPath, done) { const mkdir = `sudo mkdir -p "${guestPath}"` debug(mkdir) this.machine.ssh(mkdir, (err) => { if (err) return done(err) const opt = 'defaults,uid=`id -u docker`,gid=`id -g docker`' const mount = `sudo mount -t vboxsf -o ${opt} ${name} ${guestPath}` debug(mount) this.machine.ssh(mount, done) }) } } module.exports = Shares