UNPKG

node-dat-archive

Version:

A nodejs API for Dat which is compatible with Beaker's DatArchive API

387 lines (341 loc) 11.2 kB
const path = require('path') const fs = require('fs') const parseDatURL = require('parse-dat-url') const pda = require('pauls-dat-api') const concat = require('concat-stream') const Dat = require('dat-node') const ram = require('random-access-memory') const {datDns, timer, toEventTarget} = require('./lib/util') const { DAT_MANIFEST_FILENAME, DAT_VALID_PATH_REGEX, DEFAULT_DAT_API_TIMEOUT } = require('./lib/const') const { ArchiveNotWritableError, ProtectedFileNotWritableError, InvalidPathError } = require('beaker-error-constants') // exported api // = const to = (opts) => (opts && typeof opts.timeout !== 'undefined') ? opts.timeout : DEFAULT_DAT_API_TIMEOUT class DatArchive { constructor (url, {localPath, datOptions, netOptions} = {}) { // parse URL const urlp = url ? parseDatURL(url) : null this.url = urlp ? `dat://${urlp.hostname}` : null // load the archive this._archive = null this._checkout = null this._version = urlp && urlp.version ? +urlp.version : null this._localPath = localPath this._loadPromise = new Promise((resolve, reject) => { // TODO resolve DNS const temp = !localPath let options = urlp ? {key: urlp.hostname, sparse: true, temp} : {indexing: false, temp} if (datOptions) { Object.keys(datOptions).forEach((key) => { options[key] = datOptions[key] }) } if (typeof options.latest === 'undefined') { options.latest = false } Dat(localPath || ram, options, async (err, dat) => { if (err) { return reject(err) } dat.joinNetwork(netOptions) this.url = this.url || `dat://${dat.archive.key.toString('hex')}` this._archive = dat.archive this._checkout = (this._version) ? dat.archive.checkout(this._version) : dat.archive this._close = async () => { await new Promise((resolve, reject) => { dat.close(err => { if (err) reject(err) else resolve() }) }) } // await initial metadata sync if not the owner if (!dat.archive.writable && !dat.archive.metadata.length) { // wait to receive a first update await new Promise((resolve, reject) => { dat.archive.metadata.update(err => { if (err) reject(err) else resolve() }) }) } resolve() }) }) } static async create ({localPath, datOptions, netOptions, title, description, type, author}) { // make sure the directory DNE or is empty if (localPath) { let st = await new Promise(resolve => fs.stat(localPath, (err, st) => resolve(st))) if (st) { if (!st.isDirectory()) { throw new Error('Cannot create Dat archive. (A file exists at the target location.)') } let listing = await new Promise(resolve => fs.readdir(localPath, (err, listing) => resolve(listing))) if (listing && listing.length > 0) { throw new Error('Cannot create Dat archive. (The target folder is not empty.)') } } } // create the dat var archive = new DatArchive(null, {localPath, datOptions, netOptions}) await archive._loadPromise await pda.writeManifest(archive._archive, {url: archive.url, title, description, type, author}) return archive } static async load ({localPath, datOptions, netOptions}) { if (!localPath) { throw new Error('Must provide {localPath}.') } // make sure the directory exists var st = await new Promise(resolve => fs.stat(localPath, (err, st) => resolve(st))) if (!st || !st.isDirectory()) { throw new Error('Cannot load Dat archive. (No folder exists at the given location.)') } // load the dat var archive = new DatArchive(null, {localPath, datOptions, netOptions}) await archive._loadPromise return archive } async configure (settings) { await this._loadPromise if (!settings || typeof settings !== 'object') throw new Error('Invalid argument') const knownProps = [ 'author', 'description', 'fallback_page', 'links', 'title', 'type', 'web_root' ] if (knownProps.filter(prop => prop in settings).length > 0) { await pda.updateManifest(this._archive, settings) } if ('networked' in settings) { // TODO } } async getInfo (url, opts = {}) { return timer(to(opts), async () => { await this._loadPromise // read manifest var manifest try { manifest = await pda.readManifest(this._checkout) } catch (e) { manifest = {} } // return return { key: this._archive.key.toString('hex'), url: this.url, isOwner: this._archive.writable, // state version: this._checkout.version, peers: this._archive.metadata.peers.length, mtime: 0, size: 0, // manifest title: manifest.title, description: manifest.description, type: manifest.type, author: manifest.author, links: manifest.links } }) } async diff () { // noop return [] } async commit () { // noop return [] } async revert () { // noop return [] } async history (opts = {}) { return timer(to(opts), async () => { await this._loadPromise var reverse = opts.reverse === true var {start, end} = opts // if reversing the output, modify start/end start = start || 0 end = end || this._checkout.metadata.length if (reverse) { // swap values let t = start start = end end = t // start from the end start = this._checkout.metadata.length - start end = this._checkout.metadata.length - end } return new Promise((resolve, reject) => { var stream = this._checkout.history({live: false, start, end}) stream.pipe(concat({encoding: 'object'}, values => { values = values.map(massageHistoryObj) if (reverse) values.reverse() resolve(values) })) stream.on('error', reject) }) }) } async stat (filepath, opts = {}) { filepath = massageFilepath(filepath) return timer(to(opts), async () => { await this._loadPromise return pda.stat(this._checkout, filepath) }) } async readFile (filepath, opts = {}) { filepath = massageFilepath(filepath) return timer(to(opts), async () => { await this._loadPromise return pda.readFile(this._checkout, filepath, opts) }) } async writeFile (filepath, data, opts = {}) { filepath = massageFilepath(filepath) return timer(to(opts), async () => { await this._loadPromise if (this._version) throw new ArchiveNotWritableError('Cannot modify a historic version') await assertWritePermission(this._archive) await assertValidFilePath(filepath) await assertUnprotectedFilePath(filepath) return pda.writeFile(this._archive, filepath, data, opts) }) } async unlink (filepath) { filepath = massageFilepath(filepath) return timer(to(), async () => { await this._loadPromise if (this._version) throw new ArchiveNotWritableError('Cannot modify a historic version') await assertWritePermission(this._archive) await assertUnprotectedFilePath(filepath) return pda.unlink(this._archive, filepath) }) } async download (filepath, opts = {}) { filepath = massageFilepath(filepath) return timer(to(opts), async (checkin) => { await this._loadPromise if (this._version) throw new Error('Not yet supported: can\'t download() old versions yet. Sorry!') // TODO if (this._archive.writable) { return // no need to download } return pda.download(this._archive, filepath) }) } async readdir (filepath, opts = {}) { filepath = massageFilepath(filepath) return timer(to(opts), async () => { await this._loadPromise var names = await pda.readdir(this._checkout, filepath, opts) if (opts.stat) { for (let i = 0; i < names.length; i++) { names[i] = { name: names[i], stat: await pda.stat(this._checkout, path.join(filepath, names[i])) } } } return names }) } async mkdir (filepath) { filepath = massageFilepath(filepath) return timer(to(), async () => { await this._loadPromise if (this._version) throw new ArchiveNotWritableError('Cannot modify a historic version') await assertWritePermission(this._archive) await assertValidPath(filepath) await assertUnprotectedFilePath(filepath) return pda.mkdir(this._archive, filepath) }) } async rmdir (filepath, opts = {}) { return timer(to(opts), async () => { filepath = massageFilepath(filepath) await this._loadPromise if (this._version) throw new ArchiveNotWritableError('Cannot modify a historic version') await assertUnprotectedFilePath(filepath) return pda.rmdir(this._archive, filepath, opts) }) } watch (pathSpec = null, onInvalidated = null) { // usage: (onInvalidated) if (typeof pathSpec === 'function') { onInvalidated = pathSpec pathSpec = null } var evts = toEventTarget(pda.watch(this._archive, pathSpec)) if (onInvalidated) { evts.addEventListener('invalidated', onInvalidated) } return evts } createFileActivityStream (pathPattern) { console.warn('node-dat-archive: The DatArchive createFileActivityStream() API has been deprecated, use watch() instead.') return this.watch(pathPattern) } createNetworkActivityStream () { return toEventTarget(pda.createNetworkActivityStream(this._archive)) } static async resolveName (name) { return datDns.resolveName(name) } } module.exports = DatArchive // internal helpers // = // helper to check if filepath refers to a file that userland is not allowed to edit directly function assertUnprotectedFilePath (filepath) { if (filepath === '/' + DAT_MANIFEST_FILENAME) { throw new ProtectedFileNotWritableError() } } async function assertWritePermission (archive) { // ensure we have the archive's private key if (!archive.writable) { throw new ArchiveNotWritableError() } return true } async function assertValidFilePath (filepath) { if (filepath.slice(-1) === '/') { throw new InvalidPathError('Files can not have a trailing slash') } await assertValidPath(filepath) } async function assertValidPath (fileOrFolderPath) { if (!DAT_VALID_PATH_REGEX.test(fileOrFolderPath)) { throw new InvalidPathError('Path contains invalid characters') } } function massageHistoryObj ({name, version, type}) { return {path: name, version, type} } function massageFilepath (filepath) { filepath = filepath || '' filepath = decodeURIComponent(filepath) if (!filepath.startsWith('/')) { filepath = '/' + filepath } return filepath }