asset.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. const _ = require('lodash')
  2. const sanitize = require('sanitize-filename')
  3. const graphHelper = require('../../helpers/graph')
  4. const assetHelper = require('../../helpers/asset')
  5. const path = require('node:path')
  6. const fs = require('fs-extra')
  7. const { v4: uuid } = require('uuid')
  8. module.exports = {
  9. Query: {
  10. async assets(obj, args, context) {
  11. let cond = {
  12. folderId: args.folderId === 0 ? null : args.folderId
  13. }
  14. if (args.kind !== 'ALL') {
  15. cond.kind = args.kind.toLowerCase()
  16. }
  17. const folderHierarchy = await WIKI.db.assetFolders.getHierarchy(args.folderId)
  18. const folderPath = folderHierarchy.map(h => h.slug).join('/')
  19. const results = await WIKI.db.assets.query().where(cond)
  20. return _.filter(results, r => {
  21. const path = folderPath ? `${folderPath}/${r.filename}` : r.filename
  22. return WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path })
  23. }).map(a => ({
  24. ...a,
  25. kind: a.kind.toUpperCase()
  26. }))
  27. },
  28. async assetsFolders(obj, args, context) {
  29. const results = await WIKI.db.assetFolders.query().where({
  30. parentId: args.parentFolderId === 0 ? null : args.parentFolderId
  31. })
  32. const parentHierarchy = await WIKI.db.assetFolders.getHierarchy(args.parentFolderId)
  33. const parentPath = parentHierarchy.map(h => h.slug).join('/')
  34. return _.filter(results, r => {
  35. const path = parentPath ? `${parentPath}/${r.slug}` : r.slug
  36. return WIKI.auth.checkAccess(context.req.user, ['read:assets'], { path })
  37. })
  38. }
  39. },
  40. Mutation: {
  41. /**
  42. * Create New Asset Folder
  43. */
  44. async createAssetsFolder(obj, args, context) {
  45. try {
  46. const folderSlug = sanitize(args.slug).toLowerCase()
  47. const parentFolderId = args.parentFolderId === 0 ? null : args.parentFolderId
  48. const result = await WIKI.db.assetFolders.query().where({
  49. parentId: parentFolderId,
  50. slug: folderSlug
  51. }).first()
  52. if (!result) {
  53. await WIKI.db.assetFolders.query().insert({
  54. slug: folderSlug,
  55. name: folderSlug,
  56. parentId: parentFolderId
  57. })
  58. return {
  59. responseResult: graphHelper.generateSuccess('Asset Folder has been created successfully.')
  60. }
  61. } else {
  62. throw new WIKI.Error.AssetFolderExists()
  63. }
  64. } catch (err) {
  65. return graphHelper.generateError(err)
  66. }
  67. },
  68. /**
  69. * Rename an Asset
  70. */
  71. async renameAsset(obj, args, context) {
  72. try {
  73. const filename = sanitize(args.filename).toLowerCase()
  74. const asset = await WIKI.db.assets.query().findById(args.id)
  75. if (asset) {
  76. // Check for extension mismatch
  77. if (!_.endsWith(filename, asset.ext)) {
  78. throw new WIKI.Error.AssetRenameInvalidExt()
  79. }
  80. // Check for non-dot files changing to dotfile
  81. if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) {
  82. throw new WIKI.Error.AssetRenameInvalid()
  83. }
  84. // Check for collision
  85. const assetCollision = await WIKI.db.assets.query().where({
  86. filename,
  87. folderId: asset.folderId
  88. }).first()
  89. if (assetCollision) {
  90. throw new WIKI.Error.AssetRenameCollision()
  91. }
  92. // Get asset folder path
  93. let hierarchy = []
  94. if (asset.folderId) {
  95. hierarchy = await WIKI.db.assetFolders.getHierarchy(asset.folderId)
  96. }
  97. // Check source asset permissions
  98. const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${asset.filename}` : asset.filename
  99. if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {
  100. throw new WIKI.Error.AssetRenameForbidden()
  101. }
  102. // Check target asset permissions
  103. const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename
  104. if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {
  105. throw new WIKI.Error.AssetRenameTargetForbidden()
  106. }
  107. // Update filename + hash
  108. const fileHash = assetHelper.generateHash(assetTargetPath)
  109. await WIKI.db.assets.query().patch({
  110. filename: filename,
  111. hash: fileHash
  112. }).findById(args.id)
  113. // Delete old asset cache
  114. await asset.deleteAssetCache()
  115. // Rename in Storage
  116. await WIKI.db.storage.assetEvent({
  117. event: 'renamed',
  118. asset: {
  119. ...asset,
  120. path: assetSourcePath,
  121. destinationPath: assetTargetPath,
  122. moveAuthorId: context.req.user.id,
  123. moveAuthorName: context.req.user.name,
  124. moveAuthorEmail: context.req.user.email
  125. }
  126. })
  127. return {
  128. responseResult: graphHelper.generateSuccess('Asset has been renamed successfully.')
  129. }
  130. } else {
  131. throw new WIKI.Error.AssetInvalid()
  132. }
  133. } catch (err) {
  134. return graphHelper.generateError(err)
  135. }
  136. },
  137. /**
  138. * Delete an Asset
  139. */
  140. async deleteAsset(obj, args, context) {
  141. try {
  142. const asset = await WIKI.db.assets.query().findById(args.id)
  143. if (asset) {
  144. // Check permissions
  145. const assetPath = await asset.getAssetPath()
  146. if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {
  147. throw new WIKI.Error.AssetDeleteForbidden()
  148. }
  149. await WIKI.db.knex('assetData').where('id', args.id).del()
  150. await WIKI.db.assets.query().deleteById(args.id)
  151. await asset.deleteAssetCache()
  152. // Delete from Storage
  153. await WIKI.db.storage.assetEvent({
  154. event: 'deleted',
  155. asset: {
  156. ...asset,
  157. path: assetPath,
  158. authorId: context.req.user.id,
  159. authorName: context.req.user.name,
  160. authorEmail: context.req.user.email
  161. }
  162. })
  163. return {
  164. responseResult: graphHelper.generateSuccess('Asset has been deleted successfully.')
  165. }
  166. } else {
  167. throw new WIKI.Error.AssetInvalid()
  168. }
  169. } catch (err) {
  170. return graphHelper.generateError(err)
  171. }
  172. },
  173. /**
  174. * Upload Assets
  175. */
  176. async uploadAssets(obj, args, context) {
  177. try {
  178. const results = await Promise.allSettled(args.files.map(async fl => {
  179. const { filename, mimetype, createReadStream } = await fl
  180. WIKI.logger.debug(`Processing asset upload ${filename} of type ${mimetype}...`)
  181. if (!WIKI.extensions.ext.sharp.isInstalled) {
  182. throw new Error('This feature requires the Sharp extension but it is not installed.')
  183. }
  184. if (!['.png', '.jpg', 'webp', '.gif'].some(s => filename.endsWith(s))) {
  185. throw new Error('Invalid File Extension. Must be svg, png, jpg, webp or gif.')
  186. }
  187. const destFormat = mimetype.startsWith('image/svg') ? 'svg' : 'png'
  188. const destFolder = path.resolve(
  189. process.cwd(),
  190. WIKI.config.dataPath,
  191. `assets`
  192. )
  193. const destPath = path.join(destFolder, `logo-${args.id}.${destFormat}`)
  194. await fs.ensureDir(destFolder)
  195. // -> Resize
  196. await WIKI.extensions.ext.sharp.resize({
  197. format: destFormat,
  198. inputStream: createReadStream(),
  199. outputPath: destPath,
  200. height: 72
  201. })
  202. // -> Save logo meta to DB
  203. const site = await WIKI.db.sites.query().findById(args.id)
  204. if (!site.config.assets.logo) {
  205. site.config.assets.logo = uuid()
  206. }
  207. site.config.assets.logoExt = destFormat
  208. await WIKI.db.sites.query().findById(args.id).patch({ config: site.config })
  209. await WIKI.db.sites.reloadCache()
  210. // -> Save image data to DB
  211. const imgBuffer = await fs.readFile(destPath)
  212. await WIKI.db.knex('assetData').insert({
  213. id: site.config.assets.logo,
  214. data: imgBuffer
  215. }).onConflict('id').merge()
  216. }))
  217. WIKI.logger.debug('Asset(s) uploaded successfully.')
  218. return {
  219. operation: graphHelper.generateSuccess('Asset(s) uploaded successfully')
  220. }
  221. } catch (err) {
  222. WIKI.logger.warn(err)
  223. return graphHelper.generateError(err)
  224. }
  225. },
  226. /**
  227. * Flush Temporary Uploads
  228. */
  229. async flushTempUploads(obj, args, context) {
  230. try {
  231. await WIKI.db.assets.flushTempUploads()
  232. return {
  233. responseResult: graphHelper.generateSuccess('Temporary Uploads have been flushed successfully.')
  234. }
  235. } catch (err) {
  236. return graphHelper.generateError(err)
  237. }
  238. }
  239. }
  240. // File: {
  241. // folder(fl) {
  242. // return fl.getFolder()
  243. // }
  244. // }
  245. }