OverviewFilesCommitsIssuesDocsReleasesPeers

mirall-cli / node_modules / hyperdrive / lib / download.js

131 lines · javascript ref:
const ReadyResource = require('ready-resource')

module.exports = class Download extends ReadyResource {
  constructor(drive, folder, options) {
    super()

    this.drive = drive
    this.folder = folder
    this.options = options || {}
    this.downloads = this.options.downloads || []
    this._cancelled = new Promise((resolve) => {
      this._cancel = resolve
    })
    this.ready().catch(noop)
  }

  async _open() {
    const drive = this.drive
    const blobs = await drive.getBlobs()

    const entry =
      !this.folder || this.folder.endsWith('/')
        ? null
        : await drive.entry(this.folder, this.options)

    if (entry) {
      const blob = entry.value.blob
      if (!blob) return

      if (blob.blockMap) {
        const mapDownload = await this._cancellableDownload(
          this.constructor.downloadEntryMap(blobs, blob)
        )
        if (!mapDownload) return
      }

      const download = await this.constructor.downloadEntry(blobs, blob)
      this.downloads.push(download)
      return
    }

    if (this.folder !== null) {
      // first preload the list so we can use the full power afterwards to actually preload everything
      // eslint-disable-next-line
      for await (const _ of drive.list(this.folder, this.options)) {
        // ignore
      }

      const maps = []
      const entries = []
      for await (const entry of drive.list(this.folder, this.options)) {
        const blob = entry.value.blob
        if (!blob) continue
        if (blob.blockMap && blob.blockLength) {
          maps.push(blobs.core.download({ start: blob.blockOffset, length: blob.blockLength }))
        }
        entries.push(entry) // cache entries
      }

      const results = await Promise.all(maps.map((m) => this._cancellableDownload(m)))
      if (results.includes(false)) return

      for (const entry of entries) {
        const blob = entry.value.blob
        const download = await this.constructor.downloadEntry(blobs, blob)
        this.downloads.push(download)
      }
    }
  }

  static downloadEntryMap(blobs, blob) {
    return blobs.core.download({ start: blob.blockOffset, length: blob.blockLength })
  }

  static async downloadEntry(blobs, blob) {
    if (blob.blockMap) {
      const map = await blobs.getBlockMap(blob)
      const blocks = []
      for (const block of map.blocks) blocks.push(block.index)

      const download = blobs.core.download({ blocks })
      await download.ready()
      return download
    } else {
      const download = blobs.core.download({
        start: blob.blockOffset,
        length: blob.blockLength
      })
      await download.ready()
      return download
    }
  }

  async _cancellableDownload(download) {
    return Promise.race([
      download.done().then(() => true),
      this._cancelled.then(() => {
        download.destroy()
        return false
      })
    ])
  }

  _close() {
    for (const d of this.downloads) {
      d.destroy()
    }
  }

  destroy() {
    this.destroyed = true
    this._cancel()
    this._safeBackgroundDestroy()
  }

  async _safeBackgroundDestroy() {
    try {
      await this.ready()
    } catch {}

    await this.close()
  }

  async done() {
    await this.ready()
    await Promise.allSettled(this.downloads.map((d) => d.done()))
  }
}

function noop() {}