hyperdrive-daemon
Version:
A FUSE-mountable distributed filesystem, built on Hyperdrive
1,078 lines (902 loc) • 35.5 kB
JavaScript
const crypto = require('crypto')
const { EventEmitter } = require('events')
const hyperdrive = require('hyperdrive')
const collectStream = require('stream-collector')
const sub = require('subleveldown')
const bjson = require('buffer-json-encoding')
const datEncoding = require('dat-encoding')
const pump = require('pump')
const map = require('through2-map')
const {
fromHyperdriveOptions,
fromStat,
fromMount,
fromMetadata,
fromNetworkConfiguration,
toHyperdriveOptions,
toStat,
toMount,
toMountInfo,
toDriveStats,
toDiffEntry,
toNetworkConfiguration,
setFileStats,
toChunks
} = require('hyperdrive-daemon-client/lib/common')
const { rpc } = require('hyperdrive-daemon-client')
const ArrayIndex = require('./array-index.js')
const log = require('../log').child({ component: 'drive-manager' })
class DriveManager extends EventEmitter {
constructor (corestore, networking, db, opts = {}) {
super()
this.corestore = corestore
this.networking = networking
this.db = db
this.opts = opts
this.watchLimit = opts.watchLimit
this.noAnnounce = !!opts.noAnnounce
this.memoryOnly = !!opts.memoryOnly
this._driveIndex = sub(this.db, 'drives', { valueEncoding: bjson })
this._seedIndex = sub(this.db, 'seeding', { valueEncoding: 'json' })
this._namespaceIndex = sub(this.db, 'namespaces', { valueEncoding: 'utf8' })
this._drives = new Map()
this._checkouts = new Map()
this._watchers = new Map()
this._sessionsByKey = new Map()
this._transientSeedIndex = new Map()
this._configuredMounts = new Set()
this._sessions = new ArrayIndex()
this._downloads = new ArrayIndex()
this._watchCount = 0
this._readyPromise = null
this.ready = () => {
if (this._readyPromise) return this._readyPromise
this._readyPromise = Promise.all([
this._rejoin()
])
return this._readyPromise
}
}
async _rejoin () {
if (this.noAnnounce) return
const driveList = await collect(this._seedIndex)
for (const { key: discoveryKey, value: networkOpts } of driveList) {
const opts = networkOpts && networkOpts.opts
if (!opts || !opts.announce) continue
this.networking.join(discoveryKey, { ...networkOpts.opts, loadForLength: true })
}
}
_generateKeyString (key, opts) {
var keyString = (key instanceof Buffer) ? key.toString('hex') : key
if (opts && opts.version) keyString = keyString + '+' + opts.version
if (opts && opts.hash) keyString = keyString + '+' + opts.hash
return keyString
}
async _getNamespace (keyString) {
if (!keyString) return null
try {
const namespace = await this._namespaceIndex.get('by-drive/' + keyString)
return namespace
} catch (err) {
if (!err.notFound) throw err
return null
}
}
async _createNamespace (keyString) {
const namespace = crypto.randomBytes(32).toString('hex')
try {
var existing = await this._namespaceIndex.get('by-namespace/' + namespace)
} catch (err) {
if (!err.notFound) throw err
existing = null
}
if (existing) return this._createNamespace(keyString)
return namespace
}
driveForSession (sessionId) {
const drive = this._sessions.get(sessionId)
if (!drive) throw new Error('Session does not exist.')
return drive
}
async createSession (drive, key, opts) {
if (!drive) drive = await this.get(key, opts)
key = drive.key.toString('hex')
const sessionId = this._sessions.insert(drive)
var driveSessions = this._sessionsByKey.get(key)
if (!driveSessions) {
driveSessions = []
this._sessionsByKey.set(key, driveSessions)
}
driveSessions.push(sessionId)
return { drive, session: sessionId }
}
closeSession (id) {
const drive = this._sessions.get(id)
if (!drive) return null
const driveKey = drive.key.toString('hex')
const driveSessions = this._sessionsByKey.get(driveKey)
this._sessions.delete(id)
driveSessions.splice(driveSessions.indexOf(id), 1)
if (!driveSessions.length) {
log.debug({ id, key: driveKey }, 'closing drive because all associated sessions have closed')
this._sessionsByKey.delete(driveKey)
// If a drive is closed in memory-only mode, its storage will be deleted.
if (this.memoryOnly) {
log.debug({ id, key: driveKey }, 'aborting drive close because we\'re in memory-only mode')
return null
}
const watchers = this._watchers.get(driveKey)
if (watchers && watchers.length) {
for (const watcher of watchers) {
watcher.destroy()
}
}
this._watchers.delete(driveKey)
return new Promise((resolve, reject) => {
drive.close(err => {
if (err) return reject(err)
this._drives.delete(driveKey)
const checkouts = this._checkouts.get(driveKey)
if (checkouts && checkouts.length) {
for (const keyString of checkouts) {
this._drives.delete(keyString)
}
}
this._checkouts.delete(driveKey)
log.debug({ id, key: driveKey }, 'closed drive and cleaned up any remaining watchers')
return resolve()
})
})
}
return null
}
async getAllStats (opts) {
const allStats = []
for (const [, drive] of this._drives) {
const driveStats = await this.getDriveStats(drive, opts)
allStats.push(driveStats)
}
return allStats
}
async getDriveStats (drive, opts = {}) {
const mounts = await new Promise((resolve, reject) => {
drive.getAllMounts({ memory: true, recursive: !!opts.recursive }, (err, mounts) => {
if (err) return reject(err)
return resolve(mounts)
})
})
const stats = []
for (const [path, { metadata, content }] of mounts) {
stats.push({
path,
metadata: await getCoreStats(metadata),
content: await getCoreStats(content)
})
}
return stats
async function getCoreStats (core) {
if (!core) return {}
const stats = core.stats
const networkingStats = {
key: core.key,
discoveryKey: core.discoveryKey,
peerCount: core.peers.length,
peers: core.peers.map(p => {
return {
...p.stats,
remoteAddress: p.remoteAddress
}
})
}
if (opts.networkingOnly) return networkingStats
return {
...networkingStats,
uploadedBytes: stats.totals.uploadedBytes,
uploadedBlocks: stats.totals.uploadedBlocks,
downloadedBytes: stats.totals.downloadedBytes,
downloadedBlocks: core.downloaded(),
totalBlocks: core.length
}
}
}
listDrives () {
return collect(this._driveIndex)
}
async getAllNetworkConfigurations () {
const storedConfigurations = (await collect(this._seedIndex)).map(({ key, value }) => [key, value])
const transientConfigurations = [...this._transientSeedIndex]
return new Map([...storedConfigurations, ...transientConfigurations])
}
async get (key, opts = {}) {
key = (key instanceof Buffer) ? datEncoding.decode(key) : key
var keyString = this._generateKeyString(key, opts)
const version = opts.version
if (key) {
// TODO: cache checkouts
const existing = this._drives.get(keyString)
if (existing) return existing
}
const driveOpts = {
...opts,
version: null,
key: null,
sparse: opts.sparse !== false,
sparseMetadata: opts.sparseMetadata !== false
}
var drive = this._drives.get(key)
var checkout = null
if (!drive) {
var namespace = await this._getNamespace(keyString)
if (!namespace) namespace = await this._createNamespace(keyString)
drive = hyperdrive(this.corestore, key, {
...driveOpts,
namespace
})
drive.on('metadata-feed', feed => {
// Periodically update the trie.
// TODO: This is to give the writer a bit of time between update requests, but we should do deferred HAVEs instead.
let updateTimeout = null
const loop = () => {
updateTimeout = setTimeout(() => {
feed.update(loop)
}, 5000)
}
loop()
feed.once('close', () => clearTimeout(updateTimeout))
})
}
await new Promise((resolve, reject) => {
drive.ready(err => {
if (err) return reject(err)
return resolve()
})
})
if (version || (version === 0)) checkout = drive.checkout(version)
key = datEncoding.encode(drive.key)
keyString = this._generateKeyString(key, opts)
if (namespace) {
await this._namespaceIndex.batch([
{ type: 'put', key: 'by-namespace/' + namespace, value: keyString },
{ type: 'put', key: 'by-drive/' + keyString, value: namespace }
])
}
var initialConfig
// TODO: Need to fully work through all the default networking behaviors.
if (opts.fuseNetwork) {
// TODO: The Network drive does not announce or remember any settings for now.
initialConfig = { lookup: true, announce: false, remember: false }
await this.configureNetwork(drive.metadata, initialConfig)
} else if (!drive.writable || opts.seed) {
initialConfig = { lookup: true, announce: false, remember: true }
await this.configureNetwork(drive.metadata, initialConfig)
}
// Make sure that any inner mounts are recorded in the drive index.
drive.on('mount', async (trie) => {
const feed = trie.feed
const mountInfo = { version: trie.version }
const mountKey = feed.key.toString('hex')
log.info({ key }, 'registering mountpoint in drive index')
const parentConfig = (await this.getNetworkConfiguration(drive)) || initialConfig || {}
const existingMountConfig = (await this.getNetworkConfiguration(feed)) || {}
const mountConfig = {
lookup: (existingMountConfig.lookup !== false) && (parentConfig.lookup !== false),
announce: !!(existingMountConfig.announce || parentConfig.announce),
remember: true
}
if (mountConfig) await this.configureNetwork(feed, mountConfig)
this.emit('configured-mount', feed.key)
this._configuredMounts.add(mountKey)
try {
await this._driveIndex.get(mountKey)
} catch (err) {
if (err && !err.notFound) log.error({ error: err }, 'error registering mountpoint in drive index')
try {
await this._driveIndex.put(mountKey, mountInfo)
} catch (err) {
log.error({ error: err }, 'could not register mountpoint in drive index')
}
}
})
// TODO: This should all be in one batch.
await Promise.all([
this._driveIndex.put(key, driveOpts)
])
this._drives.set(key, drive)
if (checkout) {
var checkouts = this._checkouts.get(key)
if (!checkouts) {
checkouts = []
this._checkouts.set(key, checkouts)
}
checkouts.push(keyString)
this._drives.set(keyString, checkout)
}
return checkout || drive
}
async configureNetwork (feed, opts = {}) {
const self = this
const encodedKey = datEncoding.encode(feed.discoveryKey)
const networkOpts = {
lookup: !!opts.lookup,
announce: !!opts.announce,
remember: !!opts.remember
}
const seeding = opts.lookup || opts.announce
var networkingPromise
const sameConfig = sameNetworkConfig(feed.discoveryKey, opts)
// If all the networking options are the same, exit early.
if (sameConfig) return
const networkConfig = { key: datEncoding.encode(feed.key), opts: networkOpts }
if (opts.remember) {
if (seeding) await this._seedIndex.put(encodedKey, networkConfig)
else await this._seedIndex.del(encodedKey)
} else {
this._transientSeedIndex.set(encodedKey, networkConfig)
}
// Failsafe
if (networkOpts.announce && this.noAnnounce) networkOpts.announce = false
try {
if (seeding) {
networkingPromise = this.networking.join(feed.discoveryKey, networkOpts)
} else {
networkingPromise = this.networking.leave(feed.discoveryKey)
}
networkingPromise.then(configurationSuccess)
networkingPromise.catch(configurationError)
} catch (err) {
configurationError(err)
}
function sameNetworkConfig (discoveryKey, opts = {}) {
const swarmStatus = self.networking.status(discoveryKey)
if (!swarmStatus) return opts.lookup === false && opts.announce === false
return swarmStatus.announce === opts.announce && swarmStatus.lookup === opts.lookup
}
function configurationError (err) {
log.error({ err, encodedKey }, 'network configuration error')
}
function configurationSuccess () {
log.debug({ encodedKey }, 'network configuration succeeded')
}
}
async getNetworkConfiguration (drive) {
const encodedKey = datEncoding.encode(drive.discoveryKey)
const networkOpts = this._transientSeedIndex.get(encodedKey)
if (networkOpts) return networkOpts.opts
try {
const persistentOpts = await this._seedIndex.get(encodedKey)
return persistentOpts.opts
} catch (err) {
return null
}
}
download (drive, path) {
const dl = drive.download(path)
return this._downloads.insert(dl)
}
getHandlers () {
return {
version: async (call) => {
const id = call.request.getId()
if (!id) throw new Error('A version request must specify a session ID.')
const drive = this.driveForSession(id)
const rsp = new rpc.drive.messages.DriveVersionResponse()
rsp.setVersion(drive.version)
return rsp
},
get: async (call) => {
var driveOpts = fromHyperdriveOptions(call.request.getOpts())
const { drive, session } = await this.createSession(null, driveOpts.key, driveOpts)
driveOpts.key = drive.key
driveOpts.discoveryKey = drive.discoveryKey
driveOpts.version = drive.version
driveOpts.writable = drive.writable
const rsp = new rpc.drive.messages.GetDriveResponse()
rsp.setId(session)
rsp.setOpts(toHyperdriveOptions(driveOpts))
return rsp
},
allStats: async (call) => {
const networkingOnly = call.request.getNetworkingonly()
var stats = await this.getAllStats({ networkingOnly })
stats = stats.map(driveStats => toDriveStats(driveStats))
const rsp = new rpc.drive.messages.StatsResponse()
rsp.setStatsList(stats)
return rsp
},
allNetworkConfigurations: async (call) => {
const networkConfigurations = await this.getAllNetworkConfigurations()
const rsp = new rpc.drive.messages.NetworkConfigurationsResponse()
rsp.setConfigurationsList([...networkConfigurations].map(([, value]) => toNetworkConfiguration({
...value.opts,
key: Buffer.from(value.key, 'hex')
})))
return rsp
},
peerCounts: async (call) => {
const rsp = new rpc.drive.messages.PeerCountsResponse()
const keys = call.request.getKeysList()
if (!keys) return rsp
const counts = []
for (let key of keys) {
key = Buffer.from(key)
if (this.corestore.isLoaded(key)) {
const core = this.corestore.get(key)
counts.push(core.peers.length)
} else {
counts.push(0)
}
}
rsp.setPeercountsList(counts)
return rsp
},
configureNetwork: async (call) => {
const id = call.request.getId()
if (!id) throw new Error('A network configuration request must specify a session ID.')
const drive = this.driveForSession(id)
const opts = fromNetworkConfiguration(call.request.getNetwork())
await this.configureNetwork(drive.metadata, { ...opts })
const rsp = new rpc.drive.messages.ConfigureNetworkResponse()
return rsp
},
stats: async (call) => {
const id = call.request.getId()
if (!id) throw new Error('A stats request must specify a session ID.')
const drive = this.driveForSession(id)
const recursive = call.request.getRecursive()
const networkingOnly = call.request.getNetworkingonly()
const driveStats = await this.getDriveStats(drive, { recursive, networkingOnly })
const networkConfig = await this.getNetworkConfiguration(drive)
const rsp = new rpc.drive.messages.DriveStatsResponse()
rsp.setStats(toDriveStats(driveStats))
if (networkConfig) rsp.setNetwork(toNetworkConfiguration(networkConfig))
return rsp
},
download: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
if (!id) throw new Error('A download request must specify a session ID.')
const drive = this.driveForSession(id)
const downloadId = this.download(drive, path)
const rsp = new rpc.drive.messages.DownloadResponse()
rsp.setDownloadid(downloadId)
return rsp
},
undownload: async (call) => {
const id = call.request.getId()
const downloadId = call.request.getDownloadid()
if (!id) throw new Error('An undownload request must specify a session ID.')
if (!downloadId) throw new Error('An undownload request must specify a download ID.')
const dl = this._downloads.get(downloadId)
if (dl) dl.destroy()
this._downloads.delete(downloadId)
return new rpc.drive.messages.UndownloadResponse()
},
createDiffStream: async (call) => {
const id = call.request.getId()
const prefix = call.request.getPrefix()
const otherVersion = call.request.getOther()
if (!id) throw new Error('A diff stream request must specify a session ID.')
const drive = this.driveForSession(id)
const stream = drive.createDiffStream(otherVersion, prefix)
const rspMapper = map.obj(chunk => {
const rsp = new rpc.drive.messages.DiffStreamResponse()
if (!chunk) return rsp
const { name, type, value } = chunk
rsp.setType(type)
rsp.setName(name)
if (type === 'put') {
rsp.setValue(toDiffEntry({ stat: value }))
} else {
rsp.setValue(toDiffEntry({ mount: value }))
}
return rsp
})
pump(stream, rspMapper, call, err => {
if (err) {
log.error({ id, err }, 'createDiffStream error')
call.destroy(err)
}
})
},
createReadStream: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
const start = call.request.getStart()
var end = call.request.getEnd()
const length = call.request.getLength()
if (!id) throw new Error('A readFile request must specify a session ID.')
if (!path) throw new Error('A readFile request must specify a path.')
const drive = this.driveForSession(id)
const streamOpts = {}
if (end !== 0) streamOpts.end = end
if (length !== 0) streamOpts.length = length
streamOpts.start = start
const stream = drive.createReadStream(path, streamOpts)
const rspMapper = map.obj(chunk => {
const rsp = new rpc.drive.messages.ReadStreamResponse()
rsp.setChunk(chunk)
return rsp
})
pump(stream, rspMapper, call, err => {
if (err) {
log.error({ id, err }, 'createReadStream error')
call.destroy(err)
}
})
},
readFile: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
if (!id) throw new Error('A readFile request must specify a session ID.')
if (!path) throw new Error('A readFile request must specify a path.')
const drive = this.driveForSession(id)
const content = await new Promise((resolve, reject) => {
drive.readFile(path, (err, content) => {
if (err) return reject(err)
return resolve(content)
})
})
const chunks = toChunks(content)
for (const chunk of chunks) {
const rsp = new rpc.drive.messages.ReadFileResponse()
rsp.setChunk(chunk)
call.write(rsp)
}
call.end()
},
createWriteStream: async (call) => {
return new Promise((resolve, reject) => {
call.once('data', req => {
const id = req.getId()
const path = req.getPath()
const opts = fromStat(req.getOpts())
if (!id) throw new Error('A readFile request must specify a session ID.')
if (!path) throw new Error('A readFile request must specify a path.')
const drive = this.driveForSession(id)
const stream = drive.createWriteStream(path, { mode: opts.mode, uid: opts.uid, gid: opts.gid, metadata: opts.metadata })
return onstream(resolve, reject, stream)
})
})
function onstream (resolve, reject, stream) {
pump(call, map.obj(chunk => Buffer.from(chunk.getChunk())), stream, err => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.WriteStreamResponse()
return resolve(rsp)
})
}
},
writeFile: async (call) => {
return new Promise((resolve, reject) => {
call.once('data', req => {
const id = req.getId()
const path = req.getPath()
const opts = fromStat(req.getOpts())
if (!id) throw new Error('A writeFile request must specify a session ID.')
if (!path) throw new Error('A writeFile request must specify a path.')
const drive = this.driveForSession(id)
return loadContent(resolve, reject, path, drive, opts)
})
})
function loadContent (resolve, reject, path, drive, opts) {
return collectStream(call, (err, reqs) => {
if (err) return reject(err)
const chunks = reqs.map(req => Buffer.from(req.getChunk()))
return drive.writeFile(path, Buffer.concat(chunks), opts, err => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.WriteFileResponse()
return resolve(rsp)
})
})
}
},
updateMetadata: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
const metadata = fromMetadata(call.request.getMetadataMap())
if (!id) throw new Error('A metadata update request must specify a session ID.')
if (!path) throw new Error('A metadata update request must specify a path.')
if (!metadata) throw new Error('A metadata update request must specify metadata.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive._update(path, { metadata }, err => {
if (err) return reject(err)
return resolve(new rpc.drive.messages.UpdateMetadataResponse())
})
})
},
deleteMetadata: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
const keys = call.request.getKeysList()
if (!id) throw new Error('A metadata update request must specify a session ID.')
if (!path) throw new Error('A metadata update request must specify a path.')
if (!keys) throw new Error('A metadata update request must specify metadata keys.')
const drive = this.driveForSession(id)
const metadata = {}
for (const key of keys) {
metadata[key] = null
}
return new Promise((resolve, reject) => {
drive._update(path, { metadata }, err => {
if (err) return reject(err)
return resolve(new rpc.drive.messages.DeleteMetadataResponse())
})
})
},
stat: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
const lstat = call.request.getLstat()
if (!id) throw new Error('A stat request must specify a session ID.')
if (!path) throw new Error('A stat request must specify a path.')
const drive = this.driveForSession(id)
const method = lstat ? drive.lstat.bind(drive) : drive.stat.bind(drive)
return new Promise((resolve, reject) => {
method(path, (err, stat) => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.StatResponse()
rsp.setStat(toStat(stat))
return resolve(rsp)
})
})
},
unlink: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
if (!id) throw new Error('An unlink request must specify a session ID.')
if (!path) throw new Error('An unlink request must specify a path. ')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive.unlink(path, err => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.UnlinkResponse()
return resolve(rsp)
})
})
},
readdir: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
const recursive = call.request.getRecursive()
const noMounts = call.request.getNomounts()
const includeStats = call.request.getIncludestats()
if (!id) throw new Error('A readdir request must specify a session ID.')
if (!path) throw new Error('A readdir request must specify a path.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive.readdir(path, { recursive, noMounts, includeStats }, (err, files) => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.ReadDirectoryResponse()
if (!includeStats) {
rsp.setFilesList(files)
} else {
const names = []
const stats = []
const mounts = []
const innerPaths = []
for (const { name, stat, mount, innerPath } of files) {
names.push(name)
stats.push(toStat(stat))
mounts.push(toMount(mount))
innerPaths.push(innerPath)
}
rsp.setFilesList(names)
rsp.setStatsList(stats)
rsp.setMountsList(mounts)
rsp.setInnerpathsList(innerPaths)
}
return resolve(rsp)
})
})
},
mkdir: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
const opts = fromStat(call.request.getOpts())
if (!id) throw new Error('A mkdir request must specify a session ID.')
if (!path) throw new Error('A mkdir request must specify a directory path.')
const drive = this.driveForSession(id)
const mkdirOpts = {}
if (opts.uid) mkdirOpts.uid = opts.uid
if (opts.gid) mkdirOpts.gid = opts.gid
if (opts.mode) mkdirOpts.mode = opts.mode
return new Promise((resolve, reject) => {
drive.mkdir(path, mkdirOpts, err => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.MkdirResponse()
return resolve(rsp)
})
})
},
rmdir: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
if (!id) throw new Error('A rmdir request must specify a session ID.')
if (!path) throw new Error('A rmdir request must specify a directory path.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive.rmdir(path, err => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.RmdirResponse()
return resolve(rsp)
})
})
},
mount: async (call) => {
const id = call.request.getId()
const mountInfo = call.request.getInfo()
const path = mountInfo.getPath()
const opts = fromMount(mountInfo.getOpts())
if (!id) throw new Error('A mount request must specify a session ID.')
if (!path) throw new Error('A mount request must specify a path.')
if (!opts) throw new Error('A mount request must specify mount options.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
let error = null
const mountListener = key => {
if (!opts.key || key.equals(opts.key)) {
this.removeListener('configured-mount', mountListener)
if (error) return
const rsp = new rpc.drive.messages.MountDriveResponse()
return resolve(rsp)
}
}
this.on('configured-mount', mountListener)
drive.mount(path, opts.key, opts, err => {
if (err) {
error = err
return reject(err)
}
if (opts.key && this._configuredMounts.has(opts.key.toString('hex'))) {
return mountListener(opts.key)
}
})
})
},
unmount: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
if (!id) throw new Error('An unmount request must specify a session ID.')
if (!path) throw new Error('An unmount request must specify a path.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive.unmount(path, err => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.UnmountDriveResponse()
return resolve(rsp)
})
})
},
watch: async (call) => {
const self = this
var watcher = null
var closed = false
var driveWatchers = null
var keyString = null
call.once('data', req => {
const id = req.getId()
var path = req.getPath()
if (!id) throw new Error('A watch request must specify a session ID.')
if (!path) path = '/'
const drive = this.driveForSession(id)
keyString = drive.key.toString('hex')
driveWatchers = this._watchers.get(keyString)
if (!driveWatchers) {
driveWatchers = []
this._watchers.set(keyString, driveWatchers)
}
watcher = drive.watch(path, () => {
const rsp = new rpc.drive.messages.WatchResponse()
call.write(rsp)
})
const close = onclose.bind(null, id, path, driveWatchers)
watcher.once('ready', subWatchers => {
// Add one in order to include the root watcher.
this._watchCount += subWatchers.length + 1
if (this._watchCount > this.watchLimit) {
return close('Watch limit reached. Please close watch connections then try again.')
}
driveWatchers.push(watcher)
// Any subsequent messages are considered cancellations.
call.on('data', close)
call.on('close', close)
call.on('finish', close)
call.on('error', close)
call.on('end', close)
})
})
function onclose (id, path, driveWatchers, err) {
if (closed) return
closed = true
log.debug({ id, path }, 'unregistering watcher')
if (watcher) {
watcher.destroy()
if (watcher.watchers) self._watchCount -= (watcher.watchers.length + 1)
driveWatchers.splice(driveWatchers.indexOf(watcher), 1)
if (!driveWatchers.length) self._watchers.delete(keyString)
}
if (err) log.error({ id, path, err }, 'watch stream errored')
call.end()
}
},
symlink: async (call) => {
const id = call.request.getId()
const target = call.request.getTarget()
const linkname = call.request.getLinkname()
if (!id) throw new Error('A symlink request must specify a session ID.')
if (!target) throw new Error('A symlink request must specify a target.')
if (!linkname) throw new Error('A symlink request must specify a linkname.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive.symlink(target, linkname, err => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.SymlinkResponse()
return resolve(rsp)
})
})
},
close: async (call) => {
const id = call.request.getId()
this.driveForSession(id)
await this.closeSession(id)
const rsp = new rpc.drive.messages.CloseSessionResponse()
return rsp
},
fileStats: async (call) => {
const id = call.request.getId()
const path = call.request.getPath()
if (!id) throw new Error('A fileStats request must specify a session ID.')
if (!path) throw new Error('A fileStats request must specify a path.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive.stats(path, (err, stats) => {
if (err) return reject(err)
if (!(stats instanceof Map)) {
const fileStats = stats
stats = new Map()
stats.set(path, fileStats)
}
const rsp = new rpc.drive.messages.FileStatsResponse()
setFileStats(rsp.getStatsMap(), stats)
return resolve(rsp)
})
})
},
mounts: async (call) => {
const id = call.request.getId()
const memory = call.request.getMemory()
const recursive = call.request.getRecursive()
if (!id) throw new Error('A mounts request must specify a session ID.')
const drive = this.driveForSession(id)
return new Promise((resolve, reject) => {
drive.getAllMounts({ memory, recursive }, (err, mounts) => {
if (err) return reject(err)
const rsp = new rpc.drive.messages.DriveMountsResponse()
if (!mounts) return resolve(rsp)
const mountsList = []
for (const [path, { metadata }] of mounts) {
mountsList.push(toMountInfo({
path,
opts: {
key: metadata.key,
version: metadata.version
}
}))
}
rsp.setMountsList(mountsList)
return resolve(rsp)
})
})
}
}
}
}
function collect (index, opts) {
return new Promise((resolve, reject) => {
collectStream(index.createReadStream(opts), (err, list) => {
if (err) return reject(err)
return resolve(list)
})
})
}
module.exports = DriveManager