hyperdrive-daemon-client
Version:
A client library and CLI tool for interacting with the Hyperdrive daemon.
595 lines (495 loc) • 16.6 kB
JavaScript
const grpc = require('@grpc/grpc-js')
const maybe = require('call-me-maybe')
const collectStream = require('stream-collector')
const pumpify = require('pumpify')
const map = require('through2-map')
const codecs = require('codecs')
const { Writable } = require('streamx')
const { Stat } = require('hyperdrive-schemas')
const { drive: { services,messages } } = require('../rpc.js')
const {
toHyperdriveOptions,
fromHyperdriveOptions,
toStat,
fromStat,
toMount,
fromMount,
toChunks,
toNetworkConfiguration,
fromDiffEntry,
fromDriveStats,
fromDownloadProgress,
fromFileStats,
fromNetworkConfiguration,
setMetadata,
fromMetadata,
fromDriveInfo,
toRPCMetadata: toMetadata
} = require('../common')
module.exports = class DriveClient {
constructor (endpoint, token) {
this.endpoint = endpoint
this.token = token
this._client = new services.DriveClient(this.endpoint, grpc.credentials.createInsecure())
}
closeClient () {
const channel = this._client.getChannel()
channel.close()
}
get (opts, cb) {
if (typeof opts === 'function') return this.get(null, opts)
const req = new messages.GetDriveRequest()
req.setOpts(toHyperdriveOptions(opts))
return maybe(cb, new Promise((resolve, reject) => {
this._client.get(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const drive = new RemoteHyperdrive(this, this._client, this.token, rsp.getId(), fromHyperdriveOptions(rsp.getOpts()))
return resolve(drive)
})
}))
}
allStats (opts, cb) {
if (typeof opts === 'function') return this.stats(null, opts)
const req = new messages.StatsRequest()
if (opts && opts.networkingOnly) req.setNetworkingonly(true)
return maybe(cb, new Promise((resolve, reject) => {
this._client.allStats(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const statsList = rsp.getStatsList().map(stat => fromDriveStats(stat))
return resolve(statsList)
})
}))
}
allNetworkConfigurations (cb) {
const req = new messages.NetworkConfigurationsRequest()
return maybe(cb, new Promise((resolve, reject) => {
this._client.allNetworkConfigurations(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const configMap = new Map(rsp.getConfigurationsList().map(rawConfig => {
const configAndKey = fromNetworkConfiguration(rawConfig)
const { key, ...networkConfig } = configAndKey
return [configAndKey.key.toString('hex'), networkConfig]
}))
return resolve(configMap)
})
}))
}
peerCounts (keys, cb) {
const req = new messages.PeerCountsRequest()
return maybe(cb, new Promise((resolve, reject) => {
if (!keys || !keys.length) return reject(new Error('peerCounts must be given a list of drive keys.'))
req.setKeysList(keys)
this._client.peerCounts(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve(rsp.getPeercountsList())
})
}))
}
}
class RemoteHyperdrive {
constructor (drives, client, token, id, opts) {
this._client = client
this._drives = drives
this.token = token
this.id = id
this.opts = opts
this.key = opts.key
this.discoveryKey = opts.discoveryKey
this.hash = opts.hash
this.writable = opts.writable
}
version (cb) {
const req = new messages.DriveVersionRequest()
req.setId(this.id)
return maybe(cb, new Promise((resolve, reject) => {
this._client.version(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve(rsp.getVersion())
})
}))
}
checkout (version) {
return this._drives.get({
...this.opts,
version,
writable: false
})
}
configureNetwork (opts = {}, cb) {
const req = new messages.ConfigureNetworkRequest()
req.setId(this.id)
req.setNetwork(toNetworkConfiguration({
announce: !!opts.announce,
lookup: !!opts.lookup,
remember: opts.remember !== undefined ? opts.remember : true
}))
return maybe(cb, new Promise((resolve, reject) => {
this._client.configureNetwork(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
stats (opts, cb) {
if (typeof opts === 'function') return this.stats(null, opts)
const req = new messages.DriveStatsRequest()
req.setId(this.id)
if (opts && opts.recursive) req.setRecursive(true)
if (opts && opts.networkingOnly) req.setNetworkingonly(true)
return maybe(cb, new Promise((resolve, reject) => {
this._client.stats(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const stats = fromDriveStats(rsp.getStats())
const network = fromNetworkConfiguration(rsp.getNetwork())
return resolve({ stats, network })
})
}))
}
download (path, opts, cb) {
if (typeof opts === 'function') return this.download(path, null, opts)
const req = new messages.DownloadRequest()
req.setId(this.id)
if (path) req.setPath(path)
var downloadId = null
const handler = {
destroy: (cb) => {
return maybe(cb, new Promise((resolve, reject) => {
if (!downloadId) return reject(new Error('Destroy must be called after the download event has been received.'))
this.undownload(downloadId, err => {
if (err) return reject(err)
return resolve()
})
}))
}
}
return maybe(cb, new Promise((resolve, reject) => {
this._client.download(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
downloadId = rsp.getDownloadid()
return resolve(handler)
})
}))
}
undownload (downloadId, cb) {
const req = new messages.UndownloadRequest()
req.setId(this.id)
req.setDownloadid(downloadId)
return maybe(cb, new Promise((resolve, reject) => {
this._client.undownload(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
async createDiffStream (other, prefix) {
if (typeof other === 'string') {
return this.createDiffStream(0, other)
} else if (typeof other === 'object') {
const version = await other.version()
return this.createDiffStream(version, prefix)
}
if (other === undefined) other = 0
const req = new messages.DiffStreamRequest()
req.setId(this.id)
req.setOther(other)
if (prefix) req.setPrefix(prefix)
const call = this._client.createDiffStream(req, toMetadata({ token: this.token }))
return pumpify.obj(
call,
map.obj(rsp => {
return {
type: rsp.getType(),
name: rsp.getName(),
value: fromDiffEntry(rsp.getValue())
}
})
)
}
createReadStream (path, opts = {}) {
const req = new messages.ReadStreamRequest()
req.setId(this.id)
req.setPath(path)
if (opts.start) req.setStart(opts.start)
if (opts.length) req.setLength(opts.length)
if (opts.end) req.setEnd(opts.end)
const call = this._client.createReadStream(req, toMetadata({ token: this.token }))
return pumpify(
call,
map.obj(rsp => Buffer.from(rsp.getChunk()))
)
}
readFile (path, opts = {}, cb) {
if (typeof opts === 'function') return this.readFile(path, null, opts)
const req = new messages.ReadFileRequest()
req.setId(this.id)
req.setPath(path)
var codec = null
if (opts.encoding) {
codec = typeof opts.encoding === 'object' ? opts.encoding: codecs(opts.encoding)
}
return maybe(cb, new Promise((resolve, reject) => {
const call = this._client.readFile(req, toMetadata({ token: this.token }))
collectStream(call, (err, rsps) => {
if (err) return reject(err)
const bufs = rsps.map(rsp => Buffer.from(rsp.getChunk()))
var decoded = Buffer.concat(bufs)
if (codec) {
try {
decoded = codec.decode(decoded)
} catch (err) {
return reject(err)
}
}
return resolve(decoded)
})
}))
}
createWriteStream (path, opts = {}) {
const req = new messages.WriteStreamRequest()
req.setId(this.id)
req.setPath(path)
req.setOpts(toStat(opts))
var flushed = false
var callback = null
const call = this._client.createWriteStream(toMetadata({ token: this.token }), err => {
if (err && stream && !stream.destroyed) return stream.destroy(err)
flushed = true
if (callback) callback(null)
})
call.write(req)
const stream = new Writable({
write: (data, cb) => {
return call.write(data, cb)
},
final: (cb) => {
call.end()
if (flushed) return process.nextTick(cb, null)
callback = cb
return null
},
map: chunk => {
const req = new messages.WriteStreamRequest()
req.setChunk(Buffer.from(chunk))
return req
}
})
return stream
}
writeFile (path, content, opts = {}, cb) {
if (typeof opts === 'function') return this.writeFile(path, content, null, opts)
if (!(content instanceof Buffer)) content = Buffer.from(content)
const req = new messages.WriteFileRequest()
req.setId(this.id)
req.setPath(path)
req.setOpts(toStat(opts))
return maybe(cb, new Promise((resolve, reject) => {
const call = this._client.writeFile(toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
call.write(req)
const chunks = toChunks(content)
for (const chunk of chunks) {
const req = new messages.WriteFileRequest()
req.setChunk(chunk)
call.write(req)
}
call.end()
}))
}
updateMetadata (path, metadata, cb) {
const req = new messages.UpdateMetadataRequest()
req.setId(this.id)
req.setPath(path)
setMetadata(req.getMetadataMap(), metadata)
return maybe(cb, new Promise((resolve, reject) => {
this._client.updateMetadata(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
deleteMetadata (path, keys, cb) {
const req = new messages.DeleteMetadataRequest()
req.setId(this.id)
req.setPath(path)
req.setKeysList(keys)
return maybe(cb, new Promise((resolve, reject) => {
this._client.deleteMetadata(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
stat (path, opts, cb) {
if (typeof opts === 'function') return this.stat(path, {}, opts)
const req = new messages.StatRequest()
opts = opts || {}
req.setId(this.id)
req.setPath(path)
if (opts.lstat) req.setLstat(opts.lstat)
return maybe(cb, new Promise((resolve, reject) => {
this._client.stat(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const rawStat = fromStat(rsp.getStat())
return resolve(new Stat(rawStat))
})
}))
}
lstat (path, opts, cb) {
if (typeof opts === 'function') return this.stat(path, {}, opts)
const req = new messages.StatRequest()
opts = opts || {}
req.setId(this.id)
req.setPath(path)
req.setLstat(true)
return maybe(cb, new Promise((resolve, reject) => {
this._client.stat(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const rawStat = fromStat(rsp.getStat())
return resolve(new Stat(rawStat))
})
}))
}
unlink (path, cb) {
const req = new messages.UnlinkRequest()
req.setId(this.id)
req.setPath(path)
return maybe(cb, new Promise((resolve, reject) => {
this._client.unlink(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err && err.errno !== 2) return reject(err)
return resolve()
})
}))
}
mkdir (path, opts, cb) {
if (typeof opts === 'function') return this.mkdir(path, {}, opts)
const req = new messages.MkdirRequest()
opts = opts || {}
req.setId(this.id)
req.setPath(path)
req.setOpts(toStat(opts))
return maybe(cb, new Promise((resolve, reject) => {
this._client.mkdir(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
rmdir (path, cb) {
const req = new messages.RmdirRequest()
req.setId(this.id)
req.setPath(path)
return maybe(cb, new Promise((resolve, reject) => {
this._client.rmdir(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
readdir (path, opts, cb) {
if (typeof opts === 'function') return this.readdir(path, {}, opts)
const req = new messages.ReadDirectoryRequest()
opts = opts || {}
path = path || '/'
req.setId(this.id)
req.setPath(path)
if (opts.recursive) req.setRecursive(opts.recursive)
if (opts.noMounts) req.setNomounts(opts.noMounts)
if (opts.includeStats) req.setIncludestats(opts.includeStats)
return maybe(cb, new Promise((resolve, reject) => {
this._client.readdir(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const names = rsp.getFilesList()
if (!opts.includeStats) return resolve(names)
const statsList = rsp.getStatsList()
const mountsList = rsp.getMountsList()
const innerPathsList = rsp.getInnerpathsList()
return resolve(names.map((name, i) => {
return {
name,
stat: new Stat(fromStat(statsList[i])),
mount: fromMount(mountsList[i]),
innerPath: innerPathsList[i]
}
}))
})
}))
}
mount (path, opts, cb) {
const req = new messages.MountDriveRequest()
path = path || '/'
const mountInfo = new messages.MountInfo()
mountInfo.setPath(path)
mountInfo.setOpts(toMount(opts))
req.setId(this.id)
req.setInfo(mountInfo)
return maybe(cb, new Promise((resolve, reject) => {
this._client.mount(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
unmount (path, cb) {
const req = new messages.UnmountDriveRequest()
path = path || '/'
req.setId(this.id)
req.setPath(path)
return maybe(cb, new Promise((resolve, reject) => {
this._client.unmount(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
watch (path, cb) {
const req = new messages.WatchRequest()
req.setId(this.id)
req.setPath(path)
const call = this._client.watch(toMetadata({ token: this.token }))
call.write(req)
call.on('data', () => cb())
return function (cb) {
return maybe(cb, new Promise(resolve => {
const req = new messages.WatchRequest()
call.write(req)
return resolve()
}))
}
}
symlink (target, linkname, cb) {
const req = new messages.SymlinkRequest()
req.setId(this.id)
req.setTarget(target)
req.setLinkname(linkname)
return maybe(cb, new Promise((resolve, reject) => {
this._client.symlink(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
close (cb) {
const req = new messages.CloseSessionRequest()
req.setId(this.id)
return maybe(cb, new Promise((resolve, reject) => {
this._client.close(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
return resolve()
})
}))
}
fileStats (name, cb) {
const req = new messages.FileStatsRequest()
req.setId(this.id)
req.setPath(name)
return maybe(cb, new Promise((resolve, reject) => {
this._client.fileStats(req, toMetadata({ token: this.token }), (err, rsp) => {
if (err) return reject(err)
const stats = fromFileStats(rsp.getStatsMap())
return resolve(stats)
})
}))
}
}