asset.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. const _ = require('lodash')
  2. const sanitize = require('sanitize-filename')
  3. const graphHelper = require('../../helpers/graph')
  4. const path = require('node:path')
  5. const fs = require('fs-extra')
  6. const { v4: uuid } = require('uuid')
  7. const { pipeline } = require('node:stream/promises')
  8. module.exports = {
  9. Query: {
  10. async assetById(obj, args, context) {
  11. return null
  12. }
  13. },
  14. Mutation: {
  15. /**
  16. * Rename an Asset
  17. */
  18. async renameAsset(obj, args, context) {
  19. try {
  20. const filename = sanitize(args.filename).toLowerCase()
  21. const asset = await WIKI.db.assets.query().findById(args.id)
  22. if (asset) {
  23. // Check for extension mismatch
  24. if (!_.endsWith(filename, asset.ext)) {
  25. throw new WIKI.Error.AssetRenameInvalidExt()
  26. }
  27. // Check for non-dot files changing to dotfile
  28. if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) {
  29. throw new WIKI.Error.AssetRenameInvalid()
  30. }
  31. // Check for collision
  32. const assetCollision = await WIKI.db.assets.query().where({
  33. filename,
  34. folderId: asset.folderId
  35. }).first()
  36. if (assetCollision) {
  37. throw new WIKI.Error.AssetRenameCollision()
  38. }
  39. // Get asset folder path
  40. let hierarchy = []
  41. if (asset.folderId) {
  42. hierarchy = await WIKI.db.assetFolders.getHierarchy(asset.folderId)
  43. }
  44. // Check source asset permissions
  45. const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${asset.filename}` : asset.filename
  46. if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {
  47. throw new WIKI.Error.AssetRenameForbidden()
  48. }
  49. // Check target asset permissions
  50. const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename
  51. if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {
  52. throw new WIKI.Error.AssetRenameTargetForbidden()
  53. }
  54. // Update filename + hash
  55. const fileHash = '' // assetHelper.generateHash(assetTargetPath)
  56. await WIKI.db.assets.query().patch({
  57. filename: filename,
  58. hash: fileHash
  59. }).findById(args.id)
  60. // Delete old asset cache
  61. await asset.deleteAssetCache()
  62. // Rename in Storage
  63. await WIKI.db.storage.assetEvent({
  64. event: 'renamed',
  65. asset: {
  66. ...asset,
  67. path: assetSourcePath,
  68. destinationPath: assetTargetPath,
  69. moveAuthorId: context.req.user.id,
  70. moveAuthorName: context.req.user.name,
  71. moveAuthorEmail: context.req.user.email
  72. }
  73. })
  74. return {
  75. responseResult: graphHelper.generateSuccess('Asset has been renamed successfully.')
  76. }
  77. } else {
  78. throw new WIKI.Error.AssetInvalid()
  79. }
  80. } catch (err) {
  81. return graphHelper.generateError(err)
  82. }
  83. },
  84. /**
  85. * Delete an Asset
  86. */
  87. async deleteAsset(obj, args, context) {
  88. try {
  89. const asset = await WIKI.db.assets.query().findById(args.id)
  90. if (asset) {
  91. // Check permissions
  92. const assetPath = await asset.getAssetPath()
  93. if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {
  94. throw new WIKI.Error.AssetDeleteForbidden()
  95. }
  96. await WIKI.db.knex('assetData').where('id', args.id).del()
  97. await WIKI.db.assets.query().deleteById(args.id)
  98. await asset.deleteAssetCache()
  99. // Delete from Storage
  100. await WIKI.db.storage.assetEvent({
  101. event: 'deleted',
  102. asset: {
  103. ...asset,
  104. path: assetPath,
  105. authorId: context.req.user.id,
  106. authorName: context.req.user.name,
  107. authorEmail: context.req.user.email
  108. }
  109. })
  110. return {
  111. responseResult: graphHelper.generateSuccess('Asset has been deleted successfully.')
  112. }
  113. } else {
  114. throw new WIKI.Error.AssetInvalid()
  115. }
  116. } catch (err) {
  117. return graphHelper.generateError(err)
  118. }
  119. },
  120. /**
  121. * Upload Assets
  122. */
  123. async uploadAssets(obj, args, context) {
  124. try {
  125. // -> Get Folder
  126. let folder = {}
  127. if (args.folderId || args.folderPath) {
  128. // Get Folder by ID
  129. folder = await WIKI.db.tree.getFolder({ id: args.folderId })
  130. if (!folder) {
  131. throw new Error('ERR_INVALID_FOLDER_ID')
  132. }
  133. } else if (args.folderPath) {
  134. // Get Folder by Path
  135. if (!args.locale) {
  136. throw new Error('ERR_MISSING_LOCALE')
  137. } else if (!args.siteId) {
  138. throw new Error('ERR_MISSING_SITE_ID')
  139. }
  140. folder = await WIKI.db.tree.getFolder({
  141. path: args.folderPath,
  142. locale: args.locale,
  143. siteId: args.siteId,
  144. createIfMissing: true
  145. })
  146. if (!folder) {
  147. throw new Error('ERR_INVALID_FOLDER_PATH')
  148. }
  149. } else {
  150. // Use Root Folder
  151. if (!args.locale) {
  152. throw new Error('ERR_MISSING_LOCALE')
  153. } else if (!args.siteId) {
  154. throw new Error('ERR_MISSING_SITE_ID')
  155. }
  156. folder = {
  157. folderPath: '',
  158. fileName: '',
  159. localeCode: args.locale,
  160. siteId: args.siteId
  161. }
  162. }
  163. // -> Get Site
  164. const site = await WIKI.db.sites.query().findById(folder.siteId)
  165. if (!site) {
  166. throw new Error('ERR_INVALID_SITE_ID')
  167. }
  168. // -> Get Storage Targets
  169. const storageTargets = await WIKI.db.storage.getTargets({ siteId: folder.siteId, enabledOnly: true })
  170. // -> Process Assets
  171. const results = await Promise.allSettled(args.files.map(async fl => {
  172. const { filename, mimetype, createReadStream } = await fl
  173. const sanitizedFilename = sanitize(filename).toLowerCase().trim()
  174. WIKI.logger.debug(`Processing asset upload ${sanitizedFilename} of type ${mimetype}...`)
  175. // Parse file extension
  176. if (sanitizedFilename.indexOf('.') <= 0) {
  177. throw new Error('ERR_ASSET_DOTFILE_NOTALLOWED')
  178. }
  179. const fileExt = _.last(sanitizedFilename.split('.')).toLowerCase()
  180. // Determine asset kind
  181. let fileKind = 'other'
  182. switch (fileExt) {
  183. case 'jpg':
  184. case 'jpeg':
  185. case 'png':
  186. case 'webp':
  187. case 'gif':
  188. case 'tiff':
  189. case 'svg':
  190. fileKind = 'image'
  191. break
  192. case 'pdf':
  193. case 'docx':
  194. case 'xlsx':
  195. case 'pptx':
  196. case 'odt':
  197. case 'epub':
  198. case 'csv':
  199. case 'md':
  200. case 'txt':
  201. case 'adoc':
  202. case 'rtf':
  203. case 'wdp':
  204. case 'xps':
  205. case 'ods':
  206. fileKind = 'document'
  207. break
  208. }
  209. // Save to temp disk
  210. const tempFileId = uuid()
  211. const tempFilePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads/${tempFileId}.dat`)
  212. WIKI.logger.debug(`Writing asset upload ${sanitizedFilename} to temp disk...`)
  213. await pipeline(
  214. createReadStream(),
  215. fs.createWriteStream(tempFilePath)
  216. )
  217. WIKI.logger.debug(`Querying asset ${sanitizedFilename} file size...`)
  218. const tempFileStat = await fs.stat(tempFilePath)
  219. // Format filename
  220. const formattedFilename = site.config.uploads.normalizeFilename ? sanitizedFilename.replaceAll(' ', '-') : sanitizedFilename
  221. // Save asset to DB
  222. WIKI.logger.debug(`Saving asset ${sanitizedFilename} metadata to DB...`)
  223. const assetRaw = await WIKI.db.knex('assets').insert({
  224. fileName: formattedFilename,
  225. fileExt,
  226. kind: fileKind,
  227. mimeType: mimetype,
  228. fileSize: Math.round(tempFileStat.size),
  229. meta: {},
  230. previewState: fileKind === 'image' ? 'pending' : 'none',
  231. authorId: context.req.user.id,
  232. siteId: folder.siteId
  233. }).returning('*')
  234. const asset = assetRaw[0]
  235. // Add to tree
  236. await WIKI.db.tree.addAsset({
  237. id: asset.id,
  238. parentPath: folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName,
  239. fileName: formattedFilename,
  240. title: formattedFilename,
  241. locale: folder.localeCode,
  242. siteId: folder.siteId,
  243. meta: {
  244. authorId: asset.authorId,
  245. creatorId: asset.creatorId,
  246. fileSize: asset.fileSize,
  247. fileExt,
  248. mimeType: mimetype,
  249. ownerId: asset.ownerId
  250. }
  251. })
  252. // Save to storage targets
  253. const storageInfo = {}
  254. const failedStorage = []
  255. await Promise.allSettled(storageTargets.map(async storageTarget => {
  256. WIKI.logger.debug(`Saving asset ${sanitizedFilename} to storage target ${storageTarget.module} (${storageTarget.id})...`)
  257. try {
  258. const strInfo = await WIKI.storage.modules[storageTarget.module].assetUploaded({
  259. asset,
  260. createReadStream,
  261. storageTarget,
  262. tempFilePath
  263. })
  264. storageInfo[storageTarget.id] = strInfo ?? true
  265. } catch (err) {
  266. WIKI.logger.warn(`Failed to save asset ${sanitizedFilename} to storage target ${storageTarget.module} (${storageTarget.id}):`)
  267. WIKI.logger.warn(err)
  268. failedStorage.push({
  269. storageId: storageTarget.id,
  270. storageModule: storageTarget.module,
  271. fileId: asset.id,
  272. fileName: formattedFilename
  273. })
  274. }
  275. }))
  276. // Save Storage Info to DB
  277. await WIKI.db.knex('assets').where({ id: asset.id }).update({ storageInfo })
  278. // Create thumbnail
  279. if (fileKind === 'image') {
  280. if (!WIKI.extensions.ext.sharp.isInstalled) {
  281. WIKI.logger.warn('Cannot generate asset thumbnail because the Sharp extension is not installed.')
  282. } else {
  283. WIKI.logger.debug(`Generating thumbnail of asset ${sanitizedFilename}...`)
  284. const previewDestPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads/${tempFileId}-thumb.webp`)
  285. // -> Resize
  286. await WIKI.extensions.ext.sharp.resize({
  287. format: 'webp',
  288. inputStream: createReadStream(),
  289. outputPath: previewDestPath,
  290. width: 320,
  291. height: 200,
  292. fit: 'inside'
  293. })
  294. // -> Save to DB
  295. await WIKI.db.knex('assets').where({
  296. id: asset.id
  297. }).update({
  298. preview: await fs.readFile(previewDestPath),
  299. previewState: 'ready'
  300. })
  301. // -> Delete
  302. await fs.remove(previewDestPath)
  303. }
  304. }
  305. WIKI.logger.debug(`Removing asset ${sanitizedFilename} temp file...`)
  306. await fs.remove(tempFilePath)
  307. WIKI.logger.debug(`Processed asset ${sanitizedFilename} successfully.`)
  308. return failedStorage
  309. }))
  310. // Return results
  311. const failedResults = results.filter(r => r.status === 'rejected')
  312. if (failedResults.length > 0) {
  313. // -> One or more thrown errors
  314. WIKI.logger.warn(`Failed to upload one or more assets:`)
  315. for (const failedResult of failedResults) {
  316. WIKI.logger.warn(failedResult.reason)
  317. }
  318. throw new Error('ERR_UPLOAD_FAILED')
  319. } else {
  320. const failedSaveTargets = results.map(r => r.value).filter(r => r.length > 0)
  321. if (failedSaveTargets.length > 0) {
  322. // -> One or more storage target save errors
  323. WIKI.logger.warn('Failed to save one or more assets to storage targets.')
  324. throw new Error('ERR_UPLOAD_TARGET_FAILED')
  325. } else {
  326. WIKI.logger.debug('Asset(s) uploaded successfully.')
  327. return {
  328. operation: graphHelper.generateSuccess('Asset(s) uploaded successfully')
  329. }
  330. }
  331. }
  332. } catch (err) {
  333. WIKI.logger.warn(err)
  334. return graphHelper.generateError(err)
  335. }
  336. },
  337. /**
  338. * Flush Temporary Uploads
  339. */
  340. async flushTempUploads(obj, args, context) {
  341. try {
  342. await WIKI.db.assets.flushTempUploads()
  343. return {
  344. responseResult: graphHelper.generateSuccess('Temporary Uploads have been flushed successfully.')
  345. }
  346. } catch (err) {
  347. return graphHelper.generateError(err)
  348. }
  349. }
  350. }
  351. }