Skip to content

Commit

Permalink
feat: asset rename + asset delete dialogs + linting fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
NGPixel committed Jan 21, 2024
1 parent f8bc9e8 commit 291fe26
Show file tree
Hide file tree
Showing 42 changed files with 597 additions and 152 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Expand Up @@ -8,7 +8,7 @@
"vue"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
"source.fixAll.eslint": "explicit"
},
"i18n-ally.localesPaths": [
"server/locales",
Expand Down
139 changes: 74 additions & 65 deletions server/graph/resolvers/asset.mjs
@@ -1,6 +1,7 @@
import _ from 'lodash-es'
import sanitize from 'sanitize-filename'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
import { decodeFolderPath, decodeTreePath, generateHash } from '../../helpers/common.mjs'
import path from 'node:path'
import fs from 'fs-extra'
import { v4 as uuid } from 'uuid'
Expand All @@ -9,7 +10,12 @@ import { pipeline } from 'node:stream/promises'
export default {
Query: {
async assetById(obj, args, context) {
return null
const asset = await WIKI.db.assets.query().findById(args.id)
if (asset) {
return asset
} else {
throw new Error('ERR_ASSET_NOT_FOUND')
}
}
},
Mutation: {
Expand All @@ -18,75 +24,75 @@ export default {
*/
async renameAsset(obj, args, context) {
try {
const filename = sanitize(args.filename).toLowerCase()
const filename = sanitize(args.fileName).toLowerCase()

const asset = await WIKI.db.assets.query().findById(args.id)
if (asset) {
const treeItem = await WIKI.db.tree.query().findById(args.id)
if (asset && treeItem) {
// Check for extension mismatch
if (!_.endsWith(filename, asset.ext)) {
throw new WIKI.Error.AssetRenameInvalidExt()
if (!_.endsWith(filename, asset.fileExt)) {
throw new Error('ERR_ASSET_EXT_MISMATCH')
}

// Check for non-dot files changing to dotfile
if (asset.ext.length > 0 && filename.length - asset.ext.length < 1) {
throw new WIKI.Error.AssetRenameInvalid()
if (asset.fileExt.length > 0 && filename.length - asset.fileExt.length < 1) {
throw new Error('ERR_ASSET_INVALID_DOTFILE')
}

// Check for collision
const assetCollision = await WIKI.db.assets.query().where({
filename,
folderId: asset.folderId
const assetCollision = await WIKI.db.tree.query().where({
folderPath: treeItem.folderPath,
fileName: filename
}).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)
throw new Error('ERR_ASSET_ALREADY_EXISTS')
}

// Check source asset permissions
const assetSourcePath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${asset.filename}` : asset.filename
const assetSourcePath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${treeItem.fileName}` : treeItem.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetSourcePath })) {
throw new WIKI.Error.AssetRenameForbidden()
throw new Error('ERR_FORBIDDEN')
}

// Check target asset permissions
const assetTargetPath = (asset.folderId) ? hierarchy.map(h => h.slug).join('/') + `/${filename}` : filename
const assetTargetPath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${filename}` : filename
if (!WIKI.auth.checkAccess(context.req.user, ['write:assets'], { path: assetTargetPath })) {
throw new WIKI.Error.AssetRenameTargetForbidden()
throw new Error('ERR_TARGET_FORBIDDEN')
}

// Update filename + hash
const fileHash = '' // assetHelper.generateHash(assetTargetPath)
const itemHash = 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
}
})
fileName: filename
}).findById(asset.id)

await WIKI.db.tree.query().patch({
fileName: filename,
title: filename,
hash: itemHash
}).findById(treeItem.id)

// TODO: Delete old asset cache
WIKI.events.outbound.emit('purgeItemCache', itemHash)

// TODO: 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: generateSuccess('Asset has been renamed successfully.')
operation: generateSuccess('Asset has been renamed successfully.')
}
} else {
throw new WIKI.Error.AssetInvalid()
throw new Error('ERR_INVALID_ASSET')
}
} catch (err) {
return generateError(err)
Expand All @@ -97,35 +103,38 @@ export default {
*/
async deleteAsset(obj, args, context) {
try {
const asset = await WIKI.db.assets.query().findById(args.id)
if (asset) {
const treeItem = await WIKI.db.tree.query().findById(args.id)
if (treeItem) {
// Check permissions
const assetPath = await asset.getAssetPath()
const assetPath = (treeItem.folderPath) ? decodeTreePath(decodeFolderPath(treeItem.folderPath)) + `/${treeItem.fileName}` : treeItem.fileName
if (!WIKI.auth.checkAccess(context.req.user, ['manage:assets'], { path: assetPath })) {
throw new WIKI.Error.AssetDeleteForbidden()
throw new Error('ERR_FORBIDDEN')
}

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
}
})
// Delete from DB
await WIKI.db.assets.query().deleteById(treeItem.id)
await WIKI.db.tree.query().deleteById(treeItem.id)

// TODO: Delete asset cache
WIKI.events.outbound.emit('purgeItemCache', treeItem.hash)

// TODO: 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: generateSuccess('Asset has been deleted successfully.')
operation: generateSuccess('Asset has been deleted successfully.')
}
} else {
throw new WIKI.Error.AssetInvalid()
throw new Error('ERR_INVALID_ASSET')
}
} catch (err) {
return generateError(err)
Expand Down Expand Up @@ -373,7 +382,7 @@ export default {
try {
await WIKI.db.assets.flushTempUploads()
return {
responseResult: generateSuccess('Temporary Uploads have been flushed successfully.')
operation: generateSuccess('Temporary Uploads have been flushed successfully.')
}
} catch (err) {
return generateError(err)
Expand Down
6 changes: 3 additions & 3 deletions server/graph/schemas/asset.graphql
Expand Up @@ -5,13 +5,13 @@
extend type Query {
assetById(
id: UUID!
): [AssetItem]
): AssetItem
}

extend type Mutation {
renameAsset(
id: UUID!
filename: String!
fileName: String!
): DefaultResponse

deleteAsset(
Expand Down Expand Up @@ -39,7 +39,7 @@ extend type Mutation {

type AssetItem {
id: UUID
filename: String
fileName: String
ext: String
kind: AssetKind
mime: String
Expand Down
9 changes: 9 additions & 0 deletions server/locales/en.json
Expand Up @@ -1643,6 +1643,13 @@
"fileman.aiFileType": "Adobe Illustrator Document",
"fileman.aifFileType": "AIF Audio File",
"fileman.apkFileType": "Android Package",
"fileman.assetDelete": "Delete Asset",
"fileman.assetDeleteConfirm": "Are you sure you want to delete {name}?",
"fileman.assetDeleteId": "Asset ID {id}",
"fileman.assetDeleteSuccess": "Asset deleted successfully.",
"fileman.assetFileName": "Asset Name",
"fileman.assetFileNameHint": "Filename of the asset, including the file extension.",
"fileman.assetRename": "Rename Asset",
"fileman.aviFileType": "AVI Video File",
"fileman.binFileType": "Binary File",
"fileman.bz2FileType": "BZIP2 Archive",
Expand Down Expand Up @@ -1698,6 +1705,8 @@
"fileman.pptxFileType": "Microsoft Powerpoint Presentation",
"fileman.psdFileType": "Adobe Photoshop Document",
"fileman.rarFileType": "RAR Archive",
"fileman.renameAssetInvalid": "Asset name is invalid.",
"fileman.renameAssetSuccess": "Asset renamed successfully",
"fileman.renameFolderInvalidData": "One or more fields are invalid.",
"fileman.renameFolderSuccess": "Folder renamed successfully.",
"fileman.searchFolder": "Search folder...",
Expand Down
6 changes: 3 additions & 3 deletions server/models/assets.mjs
Expand Up @@ -47,13 +47,13 @@ export class Asset extends Model {
async $beforeUpdate(opt, context) {
await super.$beforeUpdate(opt, context)

this.updatedAt = moment.utc().toISOString()
this.updatedAt = new Date().toISOString()
}
async $beforeInsert(context) {
await super.$beforeInsert(context)

this.createdAt = moment.utc().toISOString()
this.updatedAt = moment.utc().toISOString()
this.createdAt = new Date().toISOString()
this.updatedAt = new Date().toISOString()
}

async getAssetPath() {
Expand Down
34 changes: 11 additions & 23 deletions server/models/pages.mjs
Expand Up @@ -13,19 +13,16 @@ import CleanCSS from 'clean-css'
import TurndownService from 'turndown'
import { gfm as turndownPluginGfm } from '@joplin/turndown-plugin-gfm'
import cheerio from 'cheerio'
import matter from 'gray-matter'

import { Locale } from './locales.mjs'
import { PageLink } from './pageLinks.mjs'
import { Tag } from './tags.mjs'
import { User } from './users.mjs'

const pageRegex = /^[a-zA-Z0-9-_/]*$/
const aliasRegex = /^[a-zA-Z0-9-_]*$/

const frontmatterRegex = {
html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/,
legacy: /^(<!-- TITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)?(<!-- SUBTITLE: ?([\w\W]+?) ?-{2}>)?(?:\n|\r)*([\w\W]*)*/i,
markdown: /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{3})?(?:\n|\r)*([\w\W]*)*/
html: /^(<!-{2}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{2}>)?(?:\n|\r)*([\w\W]*)*/
}

/**
Expand Down Expand Up @@ -178,37 +175,28 @@ export class Page extends Model {
* @returns {Object} Parsed Page Metadata with Raw Content
*/
static parseMetadata (raw, contentType) {
let result
try {
switch (contentType) {
case 'markdown':
result = frontmatterRegex.markdown.exec(raw)
if (result[2]) {
case 'markdown': {
const result = matter(raw)
if (!result?.isEmpty) {
return {
...yaml.safeLoad(result[2]),
content: result[3]
}
} else {
// Attempt legacy v1 format
result = frontmatterRegex.legacy.exec(raw)
if (result[2]) {
return {
title: result[2],
description: result[4],
content: result[5]
}
content: result.content,
...result.data
}
}
break
case 'html':
result = frontmatterRegex.html.exec(raw)
}
case 'html': {
const result = frontmatterRegex.html.exec(raw)
if (result[2]) {
return {
...yaml.safeLoad(result[2]),
content: result[3]
}
}
break
}
}
} catch (err) {
WIKI.logger.warn('Failed to parse page metadata. Invalid syntax.')
Expand Down
1 change: 1 addition & 0 deletions server/package.json
Expand Up @@ -85,6 +85,7 @@
"graphql-rate-limit-directive": "2.0.4",
"graphql-tools": "9.0.0",
"graphql-upload": "16.0.2",
"gray-matter": "4.0.3",
"he": "1.2.0",
"highlight.js": "11.9.0",
"image-size": "1.0.2",
Expand Down

0 comments on commit 291fe26

Please sign in to comment.