assets.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. /* global WIKI */
  2. const Model = require('objection').Model
  3. const moment = require('moment')
  4. const path = require('path')
  5. const fs = require('fs-extra')
  6. const _ = require('lodash')
  7. const assetHelper = require('../helpers/asset')
  8. const Promise = require('bluebird')
  9. /**
  10. * Users model
  11. */
  12. module.exports = class Asset extends Model {
  13. static get tableName() { return 'assets' }
  14. static get jsonSchema () {
  15. return {
  16. type: 'object',
  17. properties: {
  18. id: {type: 'integer'},
  19. filename: {type: 'string'},
  20. hash: {type: 'string'},
  21. ext: {type: 'string'},
  22. kind: {type: 'string'},
  23. mime: {type: 'string'},
  24. fileSize: {type: 'integer'},
  25. metadata: {type: 'object'},
  26. createdAt: {type: 'string'},
  27. updatedAt: {type: 'string'}
  28. }
  29. }
  30. }
  31. static get relationMappings() {
  32. return {
  33. author: {
  34. relation: Model.BelongsToOneRelation,
  35. modelClass: require('./users'),
  36. join: {
  37. from: 'assets.authorId',
  38. to: 'users.id'
  39. }
  40. },
  41. folder: {
  42. relation: Model.BelongsToOneRelation,
  43. modelClass: require('./assetFolders'),
  44. join: {
  45. from: 'assets.folderId',
  46. to: 'assetFolders.id'
  47. }
  48. }
  49. }
  50. }
  51. async $beforeUpdate(opt, context) {
  52. await super.$beforeUpdate(opt, context)
  53. this.updatedAt = moment.utc().toISOString()
  54. }
  55. async $beforeInsert(context) {
  56. await super.$beforeInsert(context)
  57. this.createdAt = moment.utc().toISOString()
  58. this.updatedAt = moment.utc().toISOString()
  59. }
  60. async getAssetPath() {
  61. let hierarchy = []
  62. if (this.folderId) {
  63. hierarchy = await WIKI.models.assetFolders.getHierarchy(this.folderId)
  64. }
  65. return (this.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${this.filename}` : this.filename
  66. }
  67. async deleteAssetCache() {
  68. await fs.remove(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${this.hash}.dat`))
  69. }
  70. static async upload(opts) {
  71. const fileInfo = path.parse(opts.originalname)
  72. const fileHash = assetHelper.generateHash(opts.assetPath)
  73. // Check for existing asset
  74. let asset = await WIKI.models.assets.query().where({
  75. hash: fileHash,
  76. folderId: opts.folderId
  77. }).first()
  78. // Build Object
  79. let assetRow = {
  80. filename: opts.originalname,
  81. hash: fileHash,
  82. ext: fileInfo.ext,
  83. kind: _.startsWith(opts.mimetype, 'image/') ? 'image' : 'binary',
  84. mime: opts.mimetype,
  85. fileSize: opts.size,
  86. folderId: opts.folderId
  87. }
  88. // Sanitize SVG contents
  89. if (WIKI.config.uploads.scanSVG && opts.mimetype === 'image/svg+xml') {
  90. const svgSanitizeJob = await WIKI.scheduler.registerJob({
  91. name: 'sanitize-svg',
  92. immediate: true,
  93. worker: true
  94. }, opts.path)
  95. await svgSanitizeJob.finished
  96. }
  97. // Save asset data
  98. try {
  99. const fileBuffer = await fs.readFile(opts.path)
  100. if (asset) {
  101. // Patch existing asset
  102. if (opts.mode === 'upload') {
  103. assetRow.authorId = opts.user.id
  104. }
  105. await WIKI.models.assets.query().patch(assetRow).findById(asset.id)
  106. await WIKI.models.knex('assetData').where({
  107. id: asset.id
  108. }).update({
  109. data: fileBuffer
  110. })
  111. } else {
  112. // Create asset entry
  113. assetRow.authorId = opts.user.id
  114. asset = await WIKI.models.assets.query().insert(assetRow)
  115. await WIKI.models.knex('assetData').insert({
  116. id: asset.id,
  117. data: fileBuffer
  118. })
  119. }
  120. // Move temp upload to cache
  121. if (opts.mode === 'upload') {
  122. await fs.move(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
  123. } else {
  124. await fs.copy(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
  125. }
  126. // Add to Storage
  127. if (!opts.skipStorage) {
  128. await WIKI.models.storage.assetEvent({
  129. event: 'uploaded',
  130. asset: {
  131. ...asset,
  132. path: await asset.getAssetPath(),
  133. data: fileBuffer,
  134. authorId: opts.user.id,
  135. authorName: opts.user.name,
  136. authorEmail: opts.user.email
  137. }
  138. })
  139. }
  140. } catch (err) {
  141. WIKI.logger.warn(err)
  142. }
  143. }
  144. static async getAsset(assetPath, res) {
  145. try {
  146. const fileHash = assetHelper.generateHash(assetPath)
  147. const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`)
  148. if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) {
  149. return
  150. }
  151. if (await WIKI.models.assets.getAssetFromStorage(assetPath, res)) {
  152. return
  153. }
  154. await WIKI.models.assets.getAssetFromDb(assetPath, fileHash, cachePath, res)
  155. } catch (err) {
  156. if (err.code === `ECONNABORTED` || err.code === `EPIPE`) {
  157. return
  158. }
  159. WIKI.logger.error(err)
  160. res.sendStatus(500)
  161. }
  162. }
  163. static async getAssetFromCache(assetPath, cachePath, res) {
  164. try {
  165. await fs.access(cachePath, fs.constants.R_OK)
  166. } catch (err) {
  167. return false
  168. }
  169. const sendFile = Promise.promisify(res.sendFile, {context: res})
  170. res.type(path.extname(assetPath))
  171. await sendFile(cachePath, { dotfiles: 'deny' })
  172. return true
  173. }
  174. static async getAssetFromStorage(assetPath, res) {
  175. const localLocations = await WIKI.models.storage.getLocalLocations({
  176. asset: {
  177. path: assetPath
  178. }
  179. })
  180. for (let location of _.filter(localLocations, location => Boolean(location.path))) {
  181. const assetExists = await WIKI.models.assets.getAssetFromCache(assetPath, location.path, res)
  182. if (assetExists) {
  183. return true
  184. }
  185. }
  186. return false
  187. }
  188. static async getAssetFromDb(assetPath, fileHash, cachePath, res) {
  189. const asset = await WIKI.models.assets.query().where('hash', fileHash).first()
  190. if (asset) {
  191. const assetData = await WIKI.models.knex('assetData').where('id', asset.id).first()
  192. res.type(asset.ext)
  193. res.send(assetData.data)
  194. await fs.outputFile(cachePath, assetData.data)
  195. } else {
  196. res.sendStatus(404)
  197. }
  198. }
  199. static async flushTempUploads() {
  200. return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
  201. }
  202. }