Adapt authoring tool UI documentation

v1.0.0-rc.4

adapt-authoring-assets/lib/AbstractAsset.js

  1. import { App } from 'adapt-authoring-core'
  2. import ffmpeg from 'fluent-ffmpeg'
  3. import ffmpegStatic from 'ffmpeg-static'
  4. import ffprobeStatic from 'ffprobe-static'
  5. import mime from 'mime-types'
  6. import path from 'path'
  7. ffmpeg.setFfprobePath(ffprobeStatic.path)
  8. ffmpeg.setFfmpegPath(ffmpegStatic)
  9. /**
  10. * Base class for handling an asset
  11. * @memberof assets
  12. */
  13. class AbstractAsset {
  14. /**
  15. * Name of the asset type
  16. * @type {string}
  17. */
  18. static get name () {
  19. return 'local'
  20. }
  21. constructor (data) {
  22. this.assets = App.instance.dependencyloader.instances['adapt-authoring-assets']
  23. this.root = this.assetRoot
  24. this.setData(data)
  25. }
  26. /**
  27. * Reference to the ffmpeg module
  28. * @type {*}
  29. */
  30. get ffmpeg () {
  31. return ffmpeg
  32. }
  33. /**
  34. * Reference to the ffprobe module
  35. * @type {*}
  36. */
  37. get ffprobe () {
  38. return ffmpeg.ffprobe
  39. }
  40. /**
  41. * The root location for this asset type
  42. * @type {string}
  43. */
  44. get assetRoot () {
  45. throw App.instance.errors.FUNC_NOT_OVERRIDDEN.setData({ name: `${this.constructor.name}#assetRoot` })
  46. }
  47. /**
  48. * The asset path
  49. * @type {string}
  50. */
  51. get path () {
  52. return this.data.path ? this.resolvePath(this.data.path) : undefined
  53. }
  54. /**
  55. * Whether the asset has a thumbnail
  56. */
  57. get hasThumb () {
  58. return this.data.hasThumb
  59. }
  60. /**
  61. * Whether the asset is an audio file
  62. */
  63. get isAudio () {
  64. return this.data.type === 'audio'
  65. }
  66. /**
  67. * Whether the asset is an image file
  68. */
  69. get isImage () {
  70. return this.data.type === 'image'
  71. }
  72. /**
  73. * Whether the asset is an video file
  74. */
  75. get isVideo () {
  76. return this.data.type === 'video'
  77. }
  78. /**
  79. * Access to the thumbnail asset
  80. * @return {LocalAsset} The thumb asset
  81. */
  82. get thumb () {
  83. if (!this._thumb) {
  84. const id = this.data?._id?.toString() ?? this.path.replace(path.extname(this.path), '')
  85. this._thumb = this.assets.createFsWrapper({
  86. repo: 'local',
  87. path: id + this.assets.getConfig('thumbnailExt'),
  88. root: this.assets.getConfig('thumbnailDir')
  89. })
  90. }
  91. return this._thumb
  92. }
  93. setData (data) {
  94. if (data.root) {
  95. this.root = data.root
  96. delete data.root
  97. }
  98. if (!this.data) this.data = {}
  99. Object.assign(this.data, JSON.parse(JSON.stringify(data)))
  100. return this.data
  101. }
  102. /**
  103. * Returns the expected file type from a MIME subtype
  104. * @param {FormidableFile} file File data
  105. * @returns {String}
  106. */
  107. getFileExtension (file) {
  108. return mime.extension(file.mimetype)
  109. }
  110. /**
  111. * Generate a thumbnail for an existing asset
  112. * @param {object} options Optional settings
  113. * @param {string} options.regenerate Will regenerate the thumbnail if one already exists
  114. */
  115. async generateThumb (options = { regenerate: false }) {
  116. if (!this.hasThumb) {
  117. return
  118. }
  119. await this.thumb.ensureDir(this.assets.getConfig('thumbnailDir'))
  120. try {
  121. await this.thumb.ensureExists()
  122. if (!options.regenerate) return
  123. } catch (e) {
  124. // thumb doesn't exist, continue
  125. }
  126. /**
  127. * ffmpeg doesn't work with streams in all cases, so we need to
  128. * temporarily download the asset locally before processing
  129. */
  130. const { default: LocalAsset } = await import('./LocalAsset.js')
  131. const tempAsset = new LocalAsset({ path: path.join(this.thumb.dirname, `TEMP_${this.filename}`) })
  132. await tempAsset.write(await this.read(), tempAsset.path)
  133. const ff = this.ffmpeg(tempAsset.path)
  134. const size = `${this.assets.getConfig('thumbnailWidth')}x?`
  135. try {
  136. await new Promise((resolve, reject) => {
  137. ff.on('end', () => resolve())
  138. ff.on('error', error => reject(App.instance.errors.GENERATE_THUMB_FAIL.setData({ file: this.path, error })))
  139. if (this.isVideo || this.data.subtype === 'gif') {
  140. const timemarks = [this.isVideo ? '25%' : '0']
  141. ff.screenshots({ size, timemarks, folder: this.thumb.dirname, filename: this.thumb.filename })
  142. } else if (this.isImage) {
  143. ff.size(size).save(this.thumb.path)
  144. }
  145. })
  146. } finally { // remove temp asset
  147. await tempAsset.delete()
  148. }
  149. }
  150. /**
  151. * Performs the required file operations when uploading/replacing an asset
  152. * @param {FormidableFile} file Uploaded file data
  153. * @returns {object} The update data
  154. */
  155. async updateFile (file) {
  156. if (!file.mimetype) file.mimetype = mime.lookup(file.originalFilename)
  157. const [type, subtype] = file.mimetype.split('/')
  158. // remove old file and set new path
  159. await this.delete()
  160. this.setData({
  161. path: `${this.data._id}.${this.getFileExtension(file)}`,
  162. repo: this.data.repo,
  163. size: file.size,
  164. subtype,
  165. type,
  166. hasThumb: (type === 'image' && subtype !== 'svg+xml') || type === 'video'
  167. })
  168. // perform filesystem operations
  169. const localAsset = this.assets.createFsWrapper({ repo: 'local', path: file.filepath })
  170. await this.write(await localAsset.read(), this.path)
  171. await localAsset.delete()
  172. await this.generateThumb({ regenerate: true })
  173. // generate metadata
  174. return this.setData(await this.generateMetadata(localAsset))
  175. }
  176. /**
  177. * Resolves a relative path to the root directory. Must be overridden by subclasses.
  178. * @param {string} filePath
  179. * @returns {string} The resolved path
  180. */
  181. resolvePath (filePath) {
  182. }
  183. /**
  184. * Ensures a directory exists, creating it if not. Must be overridden by subclasses.
  185. * @param {string} dir Directory to check
  186. * @return {Promise}
  187. */
  188. async ensureDir (dir) {
  189. }
  190. /**
  191. * Checks if a file exists. Must be overridden by subclasses.
  192. * @return {Promise} Rejects if not found
  193. */
  194. async ensureExists () {
  195. }
  196. /**
  197. * Sets metadata on an existing asset
  198. * @typedef
  199. * @return {AssetMetadata} The metadata
  200. */
  201. async generateMetadata () {
  202. }
  203. /**
  204. * Read a file. Must be overridden by subclasses.
  205. * @return {external:stream~Readable}
  206. */
  207. async read () {
  208. }
  209. /**
  210. * Write a file to the repository. Must be overridden by subclasses.
  211. * @param {external:stream~Readable} inputStream The file read stream
  212. * @param {string} outputPath Path at which to store the file
  213. * @return {Promise}
  214. */
  215. async write (inputStream, outputPath) {
  216. }
  217. /**
  218. *
  219. * @param {string} newPath New path for file
  220. * @return {Promise}
  221. */
  222. async move (newPath) {
  223. }
  224. /**
  225. * Removes a file from the repository
  226. * @return {Promise}
  227. */
  228. async delete () {
  229. if (this.hasThumb) await this.thumb.delete()
  230. }
  231. }
  232. export default AbstractAsset