adapt-authoring-assets/lib/LocalAsset.js

import { App } from 'adapt-authoring-core'
import AbstractAsset from './AbstractAsset.js'
import fsCallbacks from 'fs'
import fsPromises from 'fs/promises'
import path from 'path'
/**
 * Class for handling assets stored in the local filesystem
 * @memberof assets
 * @extends {AbstractAsset}
 */
class LocalAsset extends AbstractAsset {
  /** @override */
  static get name () {
    return 'local'
  }

  /**
   * Returnes the file name of the asset
   * @return {String}
   */
  get filename () {
    return path.basename(this.path)
  }

  /**
   * Returnes the directory name of the asset
   * @return {String}
   */
  get dirname () {
    return path.dirname(this.path)
  }

  /** @override */
  get assetRoot () {
    return this.assets.getConfig('assetDir')
  }

  /** @override */
  resolvePath (filePath, basePath = this.root) {
    if (!basePath) throw App.instance.errors.INVALID_PARAMS.setData({ params: ['filePath'] })
    if (path.isAbsolute(filePath)) return filePath
    return path.resolve(basePath, filePath)
  }

  /** @override */
  async ensureDir (dir) {
    try {
      await fsPromises.mkdir(this.resolvePath(dir), { recursive: true })
    } catch (e) {
      if (e.code !== 'EEXIST') throw e
    }
  }

  /** @override */
  async ensureExists () {
    try {
      await fsPromises.stat(this.path)
    } catch (e) {
      if (e.code === 'ENOENT') throw App.instance.errors.NOT_FOUND.setData({ type: 'asset', id: this.path })
      throw e
    }
  }

  /** @override */
  async generateMetadata () {
    if (!this.hasThumb) {
      return {}
    }
    return new Promise(resolve => {
      this.ffprobe(this.path, (e, data) => {
        if (e) {
          this.assets.log('warn', 'METADATA_GEN_FAILED', this.path, e.errno, e.code, e.syscall)
          return resolve({})
        }
        const { width, height, duration } = data.streams[0]
        const metadata = {}
        if (width && height) metadata.resolution = `${width}x${height}`
        if (!this.isImage) metadata.duration = duration
        metadata.size = data.format.size
        resolve(metadata)
      })
    })
  }

  /** @override */
  async read () {
    await this.ensureExists(this.path)
    return fsCallbacks.createReadStream(this.path)
  }

  /** @override */
  async write (inputStream, outputPath) {
    const resolvedPath = this.resolvePath(outputPath)
    await this.ensureDir(path.dirname(resolvedPath))
    return new Promise((resolve, reject) => {
      const outputStream = fsCallbacks.createWriteStream(resolvedPath)
      outputStream.on('error', e => reject(e))
      inputStream.pipe(outputStream)
      inputStream.on('end', () => resolve())
    })
  }

  /** @override */
  async move (newPath) {
    const newResolvedPath = this.resolvePath(newPath)
    await this.ensureExists(this.path)
    await this.ensureDir(path.dirname(newResolvedPath))
    return fsPromises.rename(this.path, newResolvedPath)
  }

  /** @override */
  async delete () {
    if (!this.path) return
    try {
      await super.delete()
      await this.ensureExists(this.path)
      return fsPromises.rm(this.path)
    } catch (e) { // don't need to throw an error if the file doesn't exist
      if (e.code !== App.instance.errors.NOT_FOUND.code) throw e
    }
  }
}

export default LocalAsset