Source: version/FileChecksumList.js

import path from 'path'
import fs from 'fs-extra'
import {download, fileToMd5, unzip} from '../utils/files'

/**
 * Process checksum entries with limited concurrency to avoid spiking
 * CPU / mem usage too hard for large files. Returns array of checksums
 * with md5 hash of local file and a boolean for validity.
 * @param items
 * @param concurrencyLimit
 * @returns {Promise<object[]>}
 */
export const validateItems = async (items, concurrencyLimit = 5) => {
  const results = [];

  // Wrapper to process each file and resolve with the result
  async function processFile(item) {
    const file = item.absolutePath
    const exists = await fs.pathExists(file)
    if (!exists) {
      return {
        ...item,
        valid: false,
      }
    } else {
      const md5 = await fileToMd5(file)
      return {
        ...item,
        valid: md5 === item.md5,
      }
    }
  }

  const executing = []
  for (const item of items) {
    const executor = processFile(item).then(result => {
      // Remove resolved promise from executing array
      executing.splice(executing.indexOf(executor), 1)
      // Push the result to results array
      results.push(result)
    });

    executing.push(executor)

    if (executing.length >= concurrencyLimit) {
      // Wait for one of the promises to resolve before adding more
      await Promise.race(executing)
    }
  }

  // Wait for all promises to resolve
  await Promise.all(executing)
  return results
}

class ChecksumList {
  constructor ({checksums, filePath, workingDir, downloadsDir}) {
    this._checksums = checksums
    this._localChecksums = []
    this._filePath = filePath
    this._workingDir = workingDir
    this._downloadsDir = downloadsDir
  }

  /**
   * Read the json checksum array from the file (if any).
   * @returns {Promise<object[]>}
   */
  readFile = async () => {
    if (!this._checksums && this._filePath) {
      this._checksums = await fs.readJson(this._filePath)
    }
    return this._checksums
  }

  /**
   * Validate that the checksum files exist on disk and have a valid checksum.
   * @returns {Promise<object[]>}
   */
  validateFilesOnDisk = async () => {
    await this.readFile()
    this._localChecksums = await validateItems(this._checksums.map(item => {
      return {
        ...item,
        absolutePath: path.resolve(path.join(this._workingDir, item.relativePath))
      }
    }))
    return this._localChecksums
  }

  /**
   * Download any invalidated files.
   * @param {function} onProgress
   * @param {AbortSignal} abortSignal
   * @returns {Promise<void>}
   */
  downloadAndExtractInvalidatedFiles = async (onProgress = () => {}, abortSignal) => {
    if (!this._localChecksums.length) {
      await this.validateFilesOnDisk()
    }
    if (abortSignal?.aborted) {
      throw new Error('Downloads aborted before starting')
    }
    const invalidated = this._localChecksums.filter((item, index) => {
      return !item.valid && this._localChecksums.findIndex(i => i.downloadUrl === item.downloadUrl) === index
    })
    for (let item of invalidated) {
      if (abortSignal?.aborted) {
        throw new Error('Downloads aborted mid-download')
      }
      const downloadPath = path.join(this._downloadsDir, item.downloadUrl.split('/').pop())
      await download(item.downloadUrl, downloadPath, (props) => onProgress({...props, item}), abortSignal)
      if (abortSignal?.aborted) {
        throw new Error('Downloads aborted mid-download')
      }
      await unzip(downloadPath, this._workingDir)
    }
  }
}

export default ChecksumList