hyperdrive-daemon-client
Version:
A client library and CLI tool for interacting with the Hyperdrive daemon.
249 lines (222 loc) • 7.28 kB
JavaScript
const maybe = require('call-me-maybe')
const pump = require('pump')
const hyperdrive = require('hyperdrive')
const applyHeuristics = require('hyperdrive-network-heuristics')
const { Transform } = require('streamx')
module.exports = class DriveClient {
constructor (client) {
this._client = client
this._drives = new Set()
}
_addCompatMethods (drive) {
drive.configureNetwork = (opts = {}, cb) => {
return maybe(cb, this._client.network.configure(drive.metadata.discoveryKey, opts))
}
Object.defineProperty(drive, 'version', {
enumerable: true,
value: () => drive.drive.version
})
// TODO: This needs to be fixed to not use _update (but it needs to be atomic).
drive.updateMetadata = (path, metadata, cb) => {
return maybe(cb, new Promise((resolve, reject) => {
return drive.drive._update(path, { metadata }, err => {
if (err) return reject(err)
return resolve(null)
})
}))
}
// TODO: This needs to be fixed to not use _update (but it needs to be atomic).
drive.deleteMetadata = (path, keys, cb) => {
return maybe(cb, new Promise((resolve, reject) => {
const metadata = {}
for (const key of keys) {
metadata[key] = null
}
return drive.drive._update(path, { metadata }, err => {
if (err) return reject(err)
return resolve(null)
})
}))
}
drive.mount = (path, opts, cb) => {
return maybe(cb, new Promise((resolve, reject) => {
return drive.drive.mount(path, opts && opts.key, opts, err => {
if (err) return reject(err)
return resolve(null)
})
}))
}
drive.createDiffStream = (other, prefix) => {
const diffStream = drive.drive.createDiffStream(other, prefix)
return pump(diffStream, new Transform({
transform (msg, cb) {
if (msg.type === 'put') {
msg.value = { stat: msg.value }
} else if (msg.type === 'mount') {
msg.value = { mount: msg.value }
}
return cb(null, msg)
}
}))
}
const promiseCheckout = drive.checkout.bind(drive)
drive.checkout = (version, opts) => {
const checkout = promiseCheckout(version, opts)
this._addCompatMethods(checkout)
return checkout
}
drive.watch = (path, onwatch) => {
const watcher = drive.drive.watch(path, onwatch)
return watcher.destroy
}
// The client did not previously expose a way to wait for the download to complete. This is for compat.
const downloadPromise = drive.download.bind(drive)
drive.download = (path, opts, cb) => {
const prom = downloadPromise(path, opts)
const handleProm = Promise.resolve({ destroy: prom.destroy })
return maybe(cb, handleProm)
}
drive.stats = async (opts, cb) => {
if (typeof opts === 'function') {
opts = null
cb = opts
}
opts = opts || {}
const network = await this._client.network.status(drive.discoveryKey)
const mounts = await drive.getAllMounts({ memory: true, recursive: !!opts.recursive })
const stats = []
for (const [path, { metadata, content }] of mounts) {
stats.push({
path,
metadata: await getCoreStats(metadata),
content: await getCoreStats(content)
})
}
return { stats, network }
async function getCoreStats (core) {
if (!core) return {}
const stats = core.stats
const networkingStats = {
key: core.key,
discoveryKey: core.discoveryKey,
peers: core.peers.length
}
return {
...networkingStats,
uploadedBytes: (stats && stats.totals.uploadedBytes) || 0,
uploadedBlocks: (stats && stats.totals.uploadedBlocks) || 0,
downloadedBytes: (stats && stats.totals.downloadedBytes) || 0,
downloadedBlocks: opts.networkingOnly ? 0 : await core.downloaded(),
totalBlocks: core.length
}
}
}
drive.fileStats = async (name, cb) => {
return maybe(cb, new Promise((resolve, reject) => {
drive.drive.stats(name, (err, stats) => {
if (err) return reject(err)
if (!(stats instanceof Map)) {
const fileStats = stats
stats = new Map()
stats.set(name, fileStats)
}
return resolve(stats)
})
}))
}
}
// hyperdrive-daemon-client works both with Promises and callbacks. hyperdrive.promises is exclusively promises.
// This adds callback support to the promises API.
_makeCallbackCompat (drive) {
const methodNames = [
'readFile',
'writeFile',
'stats',
'mirror',
'updateMetadata',
'deleteMetadata',
'stat',
'lstat',
'unlink',
'mkdir',
'rmdir',
'readdir',
'mount',
'unmount',
'symlink',
'close',
'fileStats',
'mounts'
]
for (const methodName of methodNames) {
if (!drive[methodName]) continue
const oldMethod = drive[methodName].bind(drive)
drive[methodName] = (...args) => {
var cb = null
if (typeof args[args.length - 1] === 'function') {
cb = args.pop()
}
return maybe(cb, oldMethod(...args))
}
}
}
async _createDrive (opts) {
const store = this._client.corestore()
var drive = hyperdrive(store, opts && opts.key, {
...opts,
extension: false
}).promises
await drive.ready()
await this._makeCallbackCompat(drive)
await applyHeuristics(drive, this._client.network)
await this._addCompatMethods(drive)
this._drives.add(drive)
drive.drive.once('close', () => {
this._drives.delete(drive)
})
return drive
}
get (opts, cb) {
if (typeof opts === 'function') return this.get(null, opts)
return maybe(cb, this._createDrive(opts))
}
// TODO: This cannot be backwards compat because network configurations are indexed by discovery key.
async _allNetworkConfigurations () {
const configs = await this._client.network.allStatuses()
const configMap = new Map()
for (const config of configs) {
configMap.set(config.discoveryKey.toString('hex'), config)
}
return configMap
}
allNetworkConfigurations (cb) {
return maybe(cb, this._allNetworkConfigurations())
}
async _allStats (opts) {
const stats = []
for (const drive of this._drives) {
const driveStats = await drive.stats(opts)
stats.push(driveStats.stats)
}
return stats
}
allStats (opts, cb) {
if (typeof opts === 'function') return this.allStats(null, opts)
return maybe(cb, this._allStats(opts))
}
async _peerCounts (keys) {
if (!keys || !keys.length) throw new Error('peerCounts must be given a list of drive keys.')
const peerCounts = []
const store = this._client.corestore()
for (const key of keys) {
const feed = store.get(key)
await feed.ready()
peerCounts.push(feed.peers.length)
}
await store.close()
return peerCounts
}
async peerCounts (keys, cb) {
return maybe(cb, this._peerCounts(keys))
}
}