assets.js 6.9 KB

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