You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

238 lines
6.7 KiB

  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 (
  90. WIKI.config.uploads.scanSVG &&
  91. (
  92. opts.mimetype.toLowerCase().startsWith('image/svg') ||
  93. fileInfo.ext.toLowerCase() === '.svg'
  94. )
  95. ) {
  96. const svgSanitizeJob = await WIKI.scheduler.registerJob({
  97. name: 'sanitize-svg',
  98. immediate: true,
  99. worker: true
  100. }, opts.path)
  101. await svgSanitizeJob.finished
  102. }
  103. // Save asset data
  104. try {
  105. const fileBuffer = await fs.readFile(opts.path)
  106. if (asset) {
  107. // Patch existing asset
  108. if (opts.mode === 'upload') {
  109. assetRow.authorId = opts.user.id
  110. }
  111. await WIKI.models.assets.query().patch(assetRow).findById(asset.id)
  112. await WIKI.models.knex('assetData').where({
  113. id: asset.id
  114. }).update({
  115. data: fileBuffer
  116. })
  117. } else {
  118. // Create asset entry
  119. assetRow.authorId = opts.user.id
  120. asset = await WIKI.models.assets.query().insert(assetRow)
  121. await WIKI.models.knex('assetData').insert({
  122. id: asset.id,
  123. data: fileBuffer
  124. })
  125. }
  126. // Move temp upload to cache
  127. if (opts.mode === 'upload') {
  128. await fs.move(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
  129. } else {
  130. await fs.copy(opts.path, path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`), { overwrite: true })
  131. }
  132. // Add to Storage
  133. if (!opts.skipStorage) {
  134. await WIKI.models.storage.assetEvent({
  135. event: 'uploaded',
  136. asset: {
  137. ...asset,
  138. path: await asset.getAssetPath(),
  139. data: fileBuffer,
  140. authorId: opts.user.id,
  141. authorName: opts.user.name,
  142. authorEmail: opts.user.email
  143. }
  144. })
  145. }
  146. } catch (err) {
  147. WIKI.logger.warn(err)
  148. }
  149. }
  150. static async getAsset(assetPath, res) {
  151. try {
  152. const fileInfo = assetHelper.getPathInfo(assetPath)
  153. const fileHash = assetHelper.generateHash(assetPath)
  154. const cachePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `cache/${fileHash}.dat`)
  155. // Force unsafe extensions to download
  156. if (WIKI.config.uploads.forceDownload && !['.png', '.apng', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg'].includes(fileInfo.ext)) {
  157. res.set('Content-disposition', 'attachment; filename=' + encodeURIComponent(fileInfo.base))
  158. }
  159. if (await WIKI.models.assets.getAssetFromCache(assetPath, cachePath, res)) {
  160. return
  161. }
  162. if (await WIKI.models.assets.getAssetFromStorage(assetPath, res)) {
  163. return
  164. }
  165. await WIKI.models.assets.getAssetFromDb(assetPath, fileHash, cachePath, res)
  166. } catch (err) {
  167. if (err.code === `ECONNABORTED` || err.code === `EPIPE`) {
  168. return
  169. }
  170. WIKI.logger.error(err)
  171. res.sendStatus(500)
  172. }
  173. }
  174. static async getAssetFromCache(assetPath, cachePath, res) {
  175. try {
  176. await fs.access(cachePath, fs.constants.R_OK)
  177. } catch (err) {
  178. return false
  179. }
  180. const sendFile = Promise.promisify(res.sendFile, {context: res})
  181. res.type(path.extname(assetPath))
  182. await sendFile(cachePath, { dotfiles: 'deny' })
  183. return true
  184. }
  185. static async getAssetFromStorage(assetPath, res) {
  186. const localLocations = await WIKI.models.storage.getLocalLocations({
  187. asset: {
  188. path: assetPath
  189. }
  190. })
  191. for (let location of _.filter(localLocations, location => Boolean(location.path))) {
  192. const assetExists = await WIKI.models.assets.getAssetFromCache(assetPath, location.path, res)
  193. if (assetExists) {
  194. return true
  195. }
  196. }
  197. return false
  198. }
  199. static async getAssetFromDb(assetPath, fileHash, cachePath, res) {
  200. const asset = await WIKI.models.assets.query().where('hash', fileHash).first()
  201. if (asset) {
  202. const assetData = await WIKI.models.knex('assetData').where('id', asset.id).first()
  203. res.type(asset.ext)
  204. res.send(assetData.data)
  205. await fs.outputFile(cachePath, assetData.data)
  206. } else {
  207. res.sendStatus(404)
  208. }
  209. }
  210. static async flushTempUploads() {
  211. return fs.emptyDir(path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads`))
  212. }
  213. }