assets.js 6.5 KB

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