UNPKG

bitdrive-cli

Version:

A Bitspace service that for managing Bitdrives over FUSE.

141 lines (123 loc) 4.21 kB
const fs = require('fs').promises const p = require('path') const { EventEmitter } = require('events') const mirrorFolder = require('mirror-folder') const low = require('last-one-wins') const streamx = require('streamx') const pump = require('pump') const { EXPORT_KEY_FILE_PATH } = require('./constants') class DriveWatcher extends EventEmitter { constructor (client, drive, opts = {}) { super() this.client = client this.drive = drive this.recursive = !!opts.recursive this.drivesByPath = new Map([['/', drive]]) this.versionsByPath = new Map() this.unwatchesByPath = new Map() this.watchers = [] this.emittingStats = false } _createDiffer (path, drive) { // Last-one-wins in case the watch is triggered many times in quick succession. const self = this return low(onupdate) async function onupdate (_, cb) { const lastVersion = self.versionsByPath.get(path) try { const diffStream = drive.createDiffStream(lastVersion) const currentVersion = drive.version self.versionsByPath.set(path, currentVersion) return pump(diffStream, new streamx.Transform({ transform: (data, cb) => { for (const watcher of self.watchers) { watcher(p.join(path, data.name)) } return cb(null) } }), err => { if (err) return cb(err) return cb(null) }) } catch (err) { return cb(err) } } } async start () { // TODO: Handle dynamic (un)mounting. this.versionsByPath.set('/', this.drive.version) this.unwatchesByPath.set('/', this.drive.watch('/', this._createDiffer('/', this.drive))) const allMounts = await this.drive.getAllMounts({ memory: false, recursive: this.recursive }) for (const [path, { metadata }] of allMounts) { if (path === '/') continue const childDrive = await this.client._createDrive({ key: metadata.key }) this.drivesByPath.set(path, childDrive) this.versionsByPath.set(path, childDrive.version) this.unwatchesByPath.set(path, childDrive.watch('/', this._createDiffer(path, childDrive))) } } watch (_, onwatch) { // The watch path is ignored for drives. this.watchers.push(onwatch) return () => { this.watchers.splice(this.watchers.indexOf(onwatch), 1) } } async close () { for (const [, unwatch] of this.unwatchesByPath) { await unwatch.destroy() } } } module.exports = async function exportDrive (client, key, dir, opts = {}) { var loadedKey = false var closed = false if (!dir) { // If the directory was not specified, we're in one of two cases: // 1) We're in a directory that we previously downloaded into, in which case reuse that key. // 2) This is a new download, in which case create a new target dir. dir = process.cwd() const savedKey = await loadKeyFromFile(dir) if (savedKey) { loadedKey = true key = key || savedKey } else { if (!key) throw new Error('If you are not resuming a previous download, a key must be specified.') dir = p.join(dir, key.toString('hex')) } } const drive = await client._createDrive({ key }) if (!loadedKey) await saveKeyToFile(dir, drive.key) const driveWatcher = new DriveWatcher(client, drive, { recursive: opts.recursive }) await driveWatcher.start() const progress = mirrorFolder({ fs: drive.drive, name: '/' }, dir, { watch: opts.watch ? driveWatcher.watch.bind(driveWatcher) : false, keepExisting: true, ensureParents: true }) return { progress, drive, cleanup, dir } async function cleanup () { if (closed) return closed = true await driveWatcher.close() await drive.close() } } async function loadKeyFromFile (dir) { const keyPath = p.join(dir, EXPORT_KEY_FILE_PATH) try { const key = await fs.readFile(keyPath) return key } catch (err) { if (err && err.code !== 'ENOENT') throw err return null } } async function saveKeyToFile (dir, key) { const keyPath = p.join(dir, EXPORT_KEY_FILE_PATH) await fs.mkdir(dir, { recursive: true }) return fs.writeFile(keyPath, key) }