123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- const _ = require('lodash')
- const sanitize = require('sanitize-filename')
- const graphHelper = require('../../helpers/graph')
- const path = require('node:path')
- const fs = require('fs-extra')
- const { v4: uuid } = require('uuid')
- const { pipeline } = require('node:stream/promises')
- module.exports = {
- Query: {
- async assetById(obj, args, context) {
- return null
- }
- },
- Mutation: {
- /**
- * Rename an Asset
- */
- async renameAsset(obj, args, context) {
- try {
- const filename = sanitize(args.filename).toLowerCase()
- const asset = await WIKI.db.assets.query().findById(args.id)
- if (asset) {
- // Check for extension mismatch
- if (!_.endsWith(filename, asset.ext)) {
- throw new WIKI.Error.AssetRenameInvalidExt()
- }
- // Check for non-dot files changing to dotfile
- if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) {
- throw new WIKI.Error.AssetRenameInvalid()
- }
- // Check for collision
- const assetCollision = await WIKI.db.assets.query().where({
- filename,
- folderId: asset.folderId
- }).first()
- if (assetCollision) {
- throw new WIKI.Error.AssetRenameCollision()
- }
- // Get asset folder path
- let hierarchy = []
- if (asset.folderId) {
- hierarchy = await WIKI.db.assetFolders.getHierarchy(asset.folderId)
- }
- // Check source asset permissions
- const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${asset.filename}` : asset.filename
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {
- throw new WIKI.Error.AssetRenameForbidden()
- }
- // Check target asset permissions
- const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename
- if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {
- throw new WIKI.Error.AssetRenameTargetForbidden()
- }
- // Update filename + hash
- const fileHash = '' // assetHelper.generateHash(assetTargetPath)
- await WIKI.db.assets.query().patch({
- filename: filename,
- hash: fileHash
- }).findById(args.id)
- // Delete old asset cache
- await asset.deleteAssetCache()
- // Rename in Storage
- await WIKI.db.storage.assetEvent({
- event: 'renamed',
- asset: {
- ...asset,
- path: assetSourcePath,
- destinationPath: assetTargetPath,
- moveAuthorId: context.req.user.id,
- moveAuthorName: context.req.user.name,
- moveAuthorEmail: context.req.user.email
- }
- })
- return {
- responseResult: graphHelper.generateSuccess('Asset has been renamed successfully.')
- }
- } else {
- throw new WIKI.Error.AssetInvalid()
- }
- } catch (err) {
- return graphHelper.generateError(err)
- }
- },
- /**
- * Delete an Asset
- */
- async deleteAsset(obj, args, context) {
- try {
- const asset = await WIKI.db.assets.query().findById(args.id)
- if (asset) {
- // Check permissions
- const assetPath = await asset.getAssetPath()
- if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {
- throw new WIKI.Error.AssetDeleteForbidden()
- }
- await WIKI.db.knex('assetData').where('id', args.id).del()
- await WIKI.db.assets.query().deleteById(args.id)
- await asset.deleteAssetCache()
- // Delete from Storage
- await WIKI.db.storage.assetEvent({
- event: 'deleted',
- asset: {
- ...asset,
- path: assetPath,
- authorId: context.req.user.id,
- authorName: context.req.user.name,
- authorEmail: context.req.user.email
- }
- })
- return {
- responseResult: graphHelper.generateSuccess('Asset has been deleted successfully.')
- }
- } else {
- throw new WIKI.Error.AssetInvalid()
- }
- } catch (err) {
- return graphHelper.generateError(err)
- }
- },
- /**
- * Upload Assets
- */
- async uploadAssets(obj, args, context) {
- try {
- // -> Get Folder
- let folder = {}
- if (args.folderId || args.folderPath) {
- // Get Folder by ID
- folder = await WIKI.db.tree.getFolder({ id: args.folderId })
- if (!folder) {
- throw new Error('ERR_INVALID_FOLDER_ID')
- }
- } else if (args.folderPath) {
- // Get Folder by Path
- if (!args.locale) {
- throw new Error('ERR_MISSING_LOCALE')
- } else if (!args.siteId) {
- throw new Error('ERR_MISSING_SITE_ID')
- }
- folder = await WIKI.db.tree.getFolder({
- path: args.folderPath,
- locale: args.locale,
- siteId: args.siteId,
- createIfMissing: true
- })
- if (!folder) {
- throw new Error('ERR_INVALID_FOLDER_PATH')
- }
- } else {
- // Use Root Folder
- if (!args.locale) {
- throw new Error('ERR_MISSING_LOCALE')
- } else if (!args.siteId) {
- throw new Error('ERR_MISSING_SITE_ID')
- }
- folder = {
- folderPath: '',
- fileName: '',
- localeCode: args.locale,
- siteId: args.siteId
- }
- }
- // -> Get Site
- const site = await WIKI.db.sites.query().findById(folder.siteId)
- if (!site) {
- throw new Error('ERR_INVALID_SITE_ID')
- }
- // -> Get Storage Targets
- const storageTargets = await WIKI.db.storage.getTargets({ siteId: folder.siteId, enabledOnly: true })
- // -> Process Assets
- const results = await Promise.allSettled(args.files.map(async fl => {
- const { filename, mimetype, createReadStream } = await fl
- const sanitizedFilename = sanitize(filename).toLowerCase().trim()
- WIKI.logger.debug(`Processing asset upload ${sanitizedFilename} of type ${mimetype}...`)
- // Parse file extension
- if (sanitizedFilename.indexOf('.') <= 0) {
- throw new Error('ERR_ASSET_DOTFILE_NOTALLOWED')
- }
- const fileExt = _.last(sanitizedFilename.split('.')).toLowerCase()
- // Determine asset kind
- let fileKind = 'other'
- switch (fileExt) {
- case 'jpg':
- case 'jpeg':
- case 'png':
- case 'webp':
- case 'gif':
- case 'tiff':
- case 'svg':
- fileKind = 'image'
- break
- case 'pdf':
- case 'docx':
- case 'xlsx':
- case 'pptx':
- case 'odt':
- case 'epub':
- case 'csv':
- case 'md':
- case 'txt':
- case 'adoc':
- case 'rtf':
- case 'wdp':
- case 'xps':
- case 'ods':
- fileKind = 'document'
- break
- }
- // Save to temp disk
- const tempFileId = uuid()
- const tempFilePath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads/${tempFileId}.dat`)
- WIKI.logger.debug(`Writing asset upload ${sanitizedFilename} to temp disk...`)
- await pipeline(
- createReadStream(),
- fs.createWriteStream(tempFilePath)
- )
- WIKI.logger.debug(`Querying asset ${sanitizedFilename} file size...`)
- const tempFileStat = await fs.stat(tempFilePath)
- // Format filename
- const formattedFilename = site.config.uploads.normalizeFilename ? sanitizedFilename.replaceAll(' ', '-') : sanitizedFilename
- // Save asset to DB
- WIKI.logger.debug(`Saving asset ${sanitizedFilename} metadata to DB...`)
- const assetRaw = await WIKI.db.knex('assets').insert({
- fileName: formattedFilename,
- fileExt,
- kind: fileKind,
- mimeType: mimetype,
- fileSize: Math.round(tempFileStat.size),
- meta: {},
- previewState: fileKind === 'image' ? 'pending' : 'none',
- authorId: context.req.user.id,
- siteId: folder.siteId
- }).returning('*')
- const asset = assetRaw[0]
- // Add to tree
- await WIKI.db.tree.addAsset({
- id: asset.id,
- parentPath: folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName,
- fileName: formattedFilename,
- title: formattedFilename,
- locale: folder.localeCode,
- siteId: folder.siteId,
- meta: {
- authorId: asset.authorId,
- creatorId: asset.creatorId,
- fileSize: asset.fileSize,
- fileExt,
- mimeType: mimetype,
- ownerId: asset.ownerId
- }
- })
- // Save to storage targets
- const storageInfo = {}
- const failedStorage = []
- await Promise.allSettled(storageTargets.map(async storageTarget => {
- WIKI.logger.debug(`Saving asset ${sanitizedFilename} to storage target ${storageTarget.module} (${storageTarget.id})...`)
- try {
- const strInfo = await WIKI.storage.modules[storageTarget.module].assetUploaded({
- asset,
- createReadStream,
- storageTarget,
- tempFilePath
- })
- storageInfo[storageTarget.id] = strInfo ?? true
- } catch (err) {
- WIKI.logger.warn(`Failed to save asset ${sanitizedFilename} to storage target ${storageTarget.module} (${storageTarget.id}):`)
- WIKI.logger.warn(err)
- failedStorage.push({
- storageId: storageTarget.id,
- storageModule: storageTarget.module,
- fileId: asset.id,
- fileName: formattedFilename
- })
- }
- }))
- // Save Storage Info to DB
- await WIKI.db.knex('assets').where({ id: asset.id }).update({ storageInfo })
- // Create thumbnail
- if (fileKind === 'image') {
- if (!WIKI.extensions.ext.sharp.isInstalled) {
- WIKI.logger.warn('Cannot generate asset thumbnail because the Sharp extension is not installed.')
- } else {
- WIKI.logger.debug(`Generating thumbnail of asset ${sanitizedFilename}...`)
- const previewDestPath = path.resolve(WIKI.ROOTPATH, WIKI.config.dataPath, `uploads/${tempFileId}-thumb.webp`)
- // -> Resize
- await WIKI.extensions.ext.sharp.resize({
- format: 'webp',
- inputStream: createReadStream(),
- outputPath: previewDestPath,
- width: 320,
- height: 200,
- fit: 'inside'
- })
- // -> Save to DB
- await WIKI.db.knex('assets').where({
- id: asset.id
- }).update({
- preview: await fs.readFile(previewDestPath),
- previewState: 'ready'
- })
- // -> Delete
- await fs.remove(previewDestPath)
- }
- }
- WIKI.logger.debug(`Removing asset ${sanitizedFilename} temp file...`)
- await fs.remove(tempFilePath)
- WIKI.logger.debug(`Processed asset ${sanitizedFilename} successfully.`)
- return failedStorage
- }))
- // Return results
- const failedResults = results.filter(r => r.status === 'rejected')
- if (failedResults.length > 0) {
- // -> One or more thrown errors
- WIKI.logger.warn(`Failed to upload one or more assets:`)
- for (const failedResult of failedResults) {
- WIKI.logger.warn(failedResult.reason)
- }
- throw new Error('ERR_UPLOAD_FAILED')
- } else {
- const failedSaveTargets = results.map(r => r.value).filter(r => r.length > 0)
- if (failedSaveTargets.length > 0) {
- // -> One or more storage target save errors
- WIKI.logger.warn('Failed to save one or more assets to storage targets.')
- throw new Error('ERR_UPLOAD_TARGET_FAILED')
- } else {
- WIKI.logger.debug('Asset(s) uploaded successfully.')
- return {
- operation: graphHelper.generateSuccess('Asset(s) uploaded successfully')
- }
- }
- }
- } catch (err) {
- WIKI.logger.warn(err)
- return graphHelper.generateError(err)
- }
- },
- /**
- * Flush Temporary Uploads
- */
- async flushTempUploads(obj, args, context) {
- try {
- await WIKI.db.assets.flushTempUploads()
- return {
- responseResult: graphHelper.generateSuccess('Temporary Uploads have been flushed successfully.')
- }
- } catch (err) {
- return graphHelper.generateError(err)
- }
- }
- }
- }
|