assets.mjs 7.0 KB

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