Unverified Commit 7fde587a authored by NGPixel's avatar NGPixel

fix: file manager rename + ltree path encode/decode + various improvements

parent b8a44d99
import { get, has, isEmpty, isPlainObject } from 'lodash-es'
import { create, get, has, isEmpty, isPlainObject } from 'lodash-es'
import path from 'node:path'
import knex from 'knex'
import fs from 'node:fs/promises'
import Objection from 'objection'
import PGPubSub from 'pg-pubsub'
import semver from 'semver'
import { createDeferred } from '../helpers/common.mjs'
import migrationSource from '../db/migrator-source.mjs'
// const migrateFromLegacy = require('../db/legacy')
import { setTimeout } from 'node:timers/promises'
......@@ -17,12 +19,14 @@ export default {
knex: null,
listener: null,
config: null,
VERSION: null,
LEGACY: false,
onReady: createDeferred(),
connectAttempts: 0,
/**
* Initialize DB
*/
async init (workerMode = false) {
let self = this
WIKI.logger.info('Checking DB configuration...')
// Fetch DB Config
......@@ -101,58 +105,28 @@ export default {
Objection.Model.knex(this.knex)
// Load DB Models
WIKI.logger.info('Loading DB models...')
const models = (await import(path.join(WIKI.SERVERPATH, 'models/index.mjs'))).default
// Set init tasks
let conAttempts = 0
const initTasks = {
// -> Attempt initial connection
async connect () {
try {
WIKI.logger.info('Connecting to database...')
await self.knex.raw('SELECT 1 + 1;')
WIKI.logger.info('Database Connection Successful [ OK ]')
} catch (err) {
if (conAttempts < 10) {
if (err.code) {
WIKI.logger.error(`Database Connection Error: ${err.code} ${err.address}:${err.port}`)
} else {
WIKI.logger.error(`Database Connection Error: ${err.message}`)
}
WIKI.logger.warn(`Will retry in 3 seconds... [Attempt ${++conAttempts} of 10]`)
await setTimeout(3000)
await initTasks.connect()
} else {
throw err
}
}
},
// -> Migrate DB Schemas
async syncSchemas () {
WIKI.logger.info('Ensuring DB schema exists...')
await self.knex.raw(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schemas.wiki}`)
WIKI.logger.info('Ensuring DB migrations have been applied...')
return self.knex.migrate.latest({
tableName: 'migrations',
migrationSource,
schemaName: WIKI.config.db.schemas.wiki
})
},
// -> Migrate DB Schemas from 2.x
async migrateFromLegacy () {
// return migrateFromLegacy.migrate(self.knex)
}
}
// Connect
await this.connect()
// Perform init tasks
// Check DB Version
const resVersion = await this.knex.raw('SHOW server_version;')
const dbVersion = semver.coerce(resVersion.rows[0].server_version, { loose: true })
this.VERSION = dbVersion.version
this.LEGACY = dbVersion.major < 16
if (dbVersion.major < 11) {
WIKI.logger.error('Your PostgreSQL database version is too old and unsupported by Wiki.js. Exiting...')
process.exit(1)
}
WIKI.logger.info(`PostgreSQL ${dbVersion.version} [ ${this.LEGACY ? 'LEGACY MODE' : 'OK'} ]`)
this.onReady = workerMode ? Promise.resolve() : (async () => {
await initTasks.connect()
await initTasks.migrateFromLegacy()
await initTasks.syncSchemas()
})()
// Run Migrations
if (!workerMode) {
await this.migrateFromLegacy()
await this.syncSchemas()
}
return {
...this,
......@@ -220,5 +194,47 @@ export default {
event,
value
})
},
/**
* Attempt initial connection
*/
async connect () {
try {
WIKI.logger.info('Connecting to database...')
await this.knex.raw('SELECT 1 + 1;')
WIKI.logger.info('Database Connection Successful [ OK ]')
} catch (err) {
if (this.connectAttempts < 10) {
if (err.code) {
WIKI.logger.error(`Database Connection Error: ${err.code} ${err.address}:${err.port}`)
} else {
WIKI.logger.error(`Database Connection Error: ${err.message}`)
}
WIKI.logger.warn(`Will retry in 3 seconds... [Attempt ${++this.connectAttempts} of 10]`)
await setTimeout(3000)
await this.connect()
} else {
throw err
}
}
},
/**
* Migrate DB Schemas
*/
async syncSchemas () {
WIKI.logger.info('Ensuring DB schema exists...')
await this.knex.raw(`CREATE SCHEMA IF NOT EXISTS ${WIKI.config.db.schemas.wiki}`)
WIKI.logger.info('Ensuring DB migrations have been applied...')
return this.knex.migrate.latest({
tableName: 'migrations',
migrationSource,
schemaName: WIKI.config.db.schemas.wiki
})
},
/**
* Migrate DB Schemas from 2.x
*/
async migrateFromLegacy () {
// return migrateFromLegacy.migrate(self.knex)
}
}
......@@ -21,7 +21,6 @@ export default {
WIKI.db = await db.init()
try {
await WIKI.db.onReady
await WIKI.configSvc.loadFromDb()
await WIKI.configSvc.applyFlags()
} catch (err) {
......
......@@ -202,7 +202,7 @@ export default {
return WIKI.config.db.host
},
dbVersion () {
return _.get(WIKI.db, 'knex.client.version', 'Unknown Version')
return WIKI.db.VERSION
},
hostname () {
return os.hostname()
......
import _ from 'lodash-es'
import {
decodeFolderPath,
encodeFolderPath,
decodeTreePath,
encodeTreePath
} from '../../helpers/common.mjs'
import { generateError, generateSuccess } from '../../helpers/graph.mjs'
const typeResolvers = {
......@@ -44,12 +50,12 @@ export default {
if (args.parentId) {
const parent = await WIKI.db.knex('tree').where('id', args.parentId).first()
if (parent) {
parentPath = (parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName).replaceAll('-', '_')
parentPath = (parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName)
}
} else if (args.parentPath) {
parentPath = args.parentPath.replaceAll('/', '.').replaceAll('-', '_').toLowerCase()
parentPath = encodeTreePath(args.parentPath)
}
const folderPathCondition = parentPath ? `${parentPath}.${depthCondition}` : depthCondition
const folderPathCondition = parentPath ? `${encodeFolderPath(parentPath)}.${depthCondition}` : depthCondition
// Fetch Items
const items = await WIKI.db.knex('tree')
......@@ -59,9 +65,14 @@ export default {
// -> Include ancestors
if (args.includeAncestors) {
const parentPathParts = parentPath.split('.')
for (let i = 1; i <= parentPathParts.length; i++) {
for (let i = 0; i <= parentPathParts.length; i++) {
console.info({
folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
fileName: _.nth(parentPathParts, i * -1),
type: 'folder'
})
builder.orWhere({
folderPath: _.dropRight(parentPathParts, i).join('.'),
folderPath: encodeFolderPath(_.dropRight(parentPathParts, i).join('.')),
fileName: _.nth(parentPathParts, i * -1),
type: 'folder'
})
......@@ -92,14 +103,14 @@ export default {
id: item.id,
depth: item.depth,
type: item.type,
folderPath: item.folderPath.replaceAll('.', '/').replaceAll('_', '-'),
folderPath: decodeTreePath(decodeFolderPath(item.folderPath)),
fileName: item.fileName,
title: item.title,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
...(item.type === 'folder') && {
childrenCount: item.meta?.children || 0,
isAncestor: item.folderPath.length < parentPath.length
isAncestor: item.folderPath.length < parentPath.length || (parentPath !== '' && item.folderPath === parentPath)
},
...(item.type === 'asset') && {
fileSize: item.meta?.fileSize || 0,
......
......@@ -37,7 +37,7 @@ export function createDeferred () {
* @returns Decoded tree path
*/
export function decodeTreePath (str) {
return str.replaceAll('_', '-').replaceAll('.', '/')
return str?.replaceAll('.', '/')
}
/**
......@@ -47,7 +47,27 @@ export function decodeTreePath (str) {
* @returns Encoded tree path
*/
export function encodeTreePath (str) {
return str?.toLowerCase()?.replaceAll('-', '_')?.replaceAll('/', '.') || ''
return str?.toLowerCase()?.replaceAll('/', '.') || ''
}
/**
* Encode a folder path (to support legacy PostgresSQL ltree)
*
* @param {string} val String to encode
* @returns Encoded folder path
*/
export function encodeFolderPath (val) {
return WIKI.db.LEGACY ? val?.replaceAll('-', '_') : val
}
/**
* Decode a folder path (to support legacy PostgresSQL ltree)
*
* @param {string} val String to decode
* @returns Decoded folder path
*/
export function decodeFolderPath (val) {
return WIKI.db.LEGACY ? val?.replaceAll('_', '-') : val
}
/**
......
import { Model } from 'objection'
import { differenceWith, dropRight, last, nth } from 'lodash-es'
import { decodeTreePath, encodeTreePath, generateHash } from '../helpers/common.mjs'
import {
decodeFolderPath,
decodeTreePath,
encodeFolderPath,
encodeTreePath,
generateHash
} from '../helpers/common.mjs'
import { Locale } from './locales.mjs'
import { Site } from './sites.mjs'
......@@ -87,7 +93,7 @@ export class Tree extends Model {
const parentPath = encodeTreePath(path)
const parentPathParts = parentPath.split('.')
const parentFilter = {
folderPath: dropRight(parentPathParts).join('.'),
folderPath: encodeFolderPath(dropRight(parentPathParts).join('.')),
fileName: last(parentPathParts)
}
const parent = await WIKI.db.knex('tree').where({
......@@ -135,14 +141,14 @@ export class Tree extends Model {
folderPath: '',
fileName: ''
}
const folderPath = decodeTreePath(folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName)
const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
const fullPath = folderPath ? `${folderPath}/${fileName}` : fileName
WIKI.logger.debug(`Adding page ${fullPath} to tree...`)
const pageEntry = await WIKI.db.knex('tree').insert({
id,
folderPath,
folderPath: encodeFolderPath(folderPath),
fileName,
type: 'page',
title: title,
......@@ -179,14 +185,13 @@ export class Tree extends Model {
fileName: ''
}
const folderPath = folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName
const decodedFolderPath = decodeTreePath(folderPath)
const fullPath = decodedFolderPath ? `${decodedFolderPath}/${fileName}` : fileName
const fullPath = folderPath ? `${folderPath}/${fileName}` : fileName
WIKI.logger.debug(`Adding asset ${fullPath} to tree...`)
const assetEntry = await WIKI.db.knex('tree').insert({
id,
folderPath,
folderPath: encodeFolderPath(folderPath),
fileName,
type: 'asset',
title: title,
......@@ -225,7 +230,7 @@ export class Tree extends Model {
WIKI.logger.debug(`Creating new folder ${pathName}...`)
const parentPathParts = parentPath.split('.')
const parentFilter = {
folderPath: dropRight(parentPathParts).join('.'),
folderPath: encodeFolderPath(dropRight(parentPathParts).join('.')),
fileName: last(parentPathParts)
}
......@@ -236,7 +241,7 @@ export class Tree extends Model {
if (!parent) {
throw new Error('ERR_NONEXISTING_PARENT_ID')
}
parentPath = parent.folderPath ? `${parent.folderPath}.${parent.fileName}` : parent.fileName
parentPath = parent.folderPath ? `${decodeFolderPath(parent.folderPath)}.${parent.fileName}` : parent.fileName
} else if (parentPath) {
parent = await WIKI.db.knex('tree').where(parentFilter).first()
} else {
......@@ -247,7 +252,7 @@ export class Tree extends Model {
const existingFolder = await WIKI.db.knex('tree').select('id').where({
siteId: siteId,
localeCode: locale,
folderPath: encodeTreePath(parentPath),
folderPath: encodeFolderPath(parentPath),
fileName: pathName
}).first()
if (existingFolder) {
......@@ -261,7 +266,7 @@ export class Tree extends Model {
const parentPathParts = parentPath.split('.')
for (let i = 1; i <= parentPathParts.length; i++) {
const ancestor = {
folderPath: encodeTreePath(dropRight(parentPathParts, i).join('.')),
folderPath: encodeFolderPath(dropRight(parentPathParts, i).join('.')),
fileName: nth(parentPathParts, i * -1)
}
expectedAncestors.push(ancestor)
......@@ -296,7 +301,7 @@ export class Tree extends Model {
// Create folder
const fullPath = parentPath ? `${decodeTreePath(parentPath)}/${pathName}` : pathName
const folder = await WIKI.db.knex('tree').insert({
folderPath: encodeTreePath(parentPath),
folderPath: encodeFolderPath(parentPath),
fileName: pathName,
type: 'folder',
title: title,
......@@ -364,8 +369,8 @@ export class Tree extends Model {
}
// Build new paths
const oldFolderPath = (folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName).replaceAll('-', '_')
const newFolderPath = (folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName).replaceAll('-', '_')
const oldFolderPath = encodeFolderPath(folder.folderPath ? `${folder.folderPath}.${folder.fileName}` : folder.fileName)
const newFolderPath = encodeFolderPath(folder.folderPath ? `${folder.folderPath}.${pathName}` : pathName)
// Update children nodes
WIKI.logger.debug(`Updating parent path of children nodes from ${oldFolderPath} to ${newFolderPath} ...`)
......@@ -377,7 +382,7 @@ export class Tree extends Model {
})
// Rename the folder itself
const fullPath = folder.folderPath ? `${decodeTreePath(folder.folderPath)}/${pathName}` : pathName
const fullPath = folder.folderPath ? `${decodeFolderPath(folder.folderPath)}/${pathName}` : pathName
await WIKI.db.knex('tree').where('id', folder.id).update({
fileName: pathName,
title: title,
......@@ -408,7 +413,7 @@ export class Tree extends Model {
WIKI.logger.debug(`Deleting folder ${folder.id} at path ${folderPath}...`)
// Delete all children
const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', folderPath).del().returning(['id', 'type'])
const deletedNodes = await WIKI.db.knex('tree').where('folderPath', '<@', encodeFolderPath(folderPath)).del().returning(['id', 'type'])
// Delete folders
const deletedFolders = deletedNodes.filter(n => n.type === 'folder').map(n => n.id)
......@@ -439,7 +444,7 @@ export class Tree extends Model {
if (folder.folderPath) {
const parentPathParts = folder.folderPath.split('.')
const parent = await WIKI.db.knex('tree').where({
folderPath: dropRight(parentPathParts).join('.'),
folderPath: encodeFolderPath(dropRight(parentPathParts).join('.')),
fileName: last(parentPathParts)
}).first()
await WIKI.db.knex('tree').where('id', parent.id).update({
......
......@@ -34,18 +34,18 @@
"node": ">=18.0"
},
"dependencies": {
"@apollo/server": "4.7.1",
"@apollo/server": "4.7.3",
"@azure/storage-blob": "12.14.0",
"@exlinc/keycloak-passport": "1.0.2",
"@graphql-tools/schema": "10.0.0",
"@graphql-tools/utils": "10.0.0",
"@graphql-tools/utils": "10.0.1",
"@joplin/turndown-plugin-gfm": "1.0.47",
"@root/csr": "0.8.1",
"@root/keypairs": "0.10.3",
"@root/pem": "1.0.4",
"acme": "3.0.3",
"akismet-api": "6.0.0",
"aws-sdk": "2.1386.0",
"aws-sdk": "2.1395.0",
"bcryptjs": "2.4.3",
"body-parser": "1.20.2",
"chalk": "5.2.0",
......@@ -71,7 +71,7 @@
"express": "4.18.2",
"express-brute": "1.0.1",
"express-session": "1.17.3",
"file-type": "18.4.0",
"file-type": "18.5.0",
"filesize": "10.0.7",
"fs-extra": "11.1.1",
"getos": "3.2.1",
......@@ -82,7 +82,7 @@
"graphql-upload": "16.0.2",
"he": "1.2.0",
"highlight.js": "11.8.0",
"i18next": "22.5.0",
"i18next": "22.5.1",
"i18next-node-fs-backend": "2.1.3",
"image-size": "1.0.2",
"js-base64": "3.7.5",
......@@ -116,7 +116,7 @@
"nanoid": "4.0.2",
"node-2fa": "2.0.3",
"node-cache": "5.1.2",
"nodemailer": "6.9.2",
"nodemailer": "6.9.3",
"objection": "3.0.1",
"passport": "0.6.0",
"passport-auth0": "1.4.3",
......@@ -136,7 +136,7 @@
"passport-okta-oauth": "0.0.1",
"passport-openidconnect": "0.1.1",
"passport-saml": "3.2.1",
"passport-slack-oauth2": "1.1.1",
"passport-slack-oauth2": "1.2.0",
"passport-twitch-strategy": "2.2.0",
"pem-jwk": "2.0.0",
"pg": "8.11.0",
......@@ -144,9 +144,9 @@
"pg-pubsub": "0.8.1",
"pg-query-stream": "4.5.0",
"pg-tsquery": "8.4.1",
"poolifier": "2.4.14",
"poolifier": "2.6.2",
"punycode": "2.3.0",
"puppeteer-core": "20.4.0",
"puppeteer-core": "20.5.0",
"qr-image": "3.2.0",
"rate-limiter-flexible": "2.4.1",
"remove-markdown": "0.5.0",
......@@ -159,7 +159,7 @@
"serve-favicon": "2.5.0",
"sharp": "0.32.1",
"simple-git": "3.19.0",
"socket.io": "4.6.1",
"socket.io": "4.6.2",
"striptags": "3.2.0",
"tar-fs": "2.1.1",
"turndown": "7.1.2",
......@@ -171,9 +171,9 @@
"yargs": "17.7.2"
},
"devDependencies": {
"eslint": "8.41.0",
"eslint": "8.42.0",
"eslint-config-requarks": "1.0.7",
"eslint-config-standard": "17.0.0",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "6.1.1",
......@@ -181,7 +181,7 @@
"nodemon": "2.0.22"
},
"overrides": {
"@graphql-tools/utils": "10.0.0"
"@graphql-tools/utils": "10.0.1"
},
"collective": {
"type": "opencollective",
......
......@@ -10,11 +10,12 @@ export async function task (payload) {
version: strictVersion,
versionDate: resp.published_at
}
await WIKI.config.saveToDb(['update'])
await WIKI.configSvc.saveToDb(['update'])
WIKI.logger.info('Checked for latest version: [ COMPLETED ]')
} catch (err) {
WIKI.logger.error('Checking for latest version: [ FAILED ]')
WIKI.logger.error(err.message)
throw err
}
}
......@@ -13,5 +13,6 @@ export async function task (payload) {
} catch (err) {
WIKI.logger.error('Cleaning scheduler job history: [ FAILED ]')
WIKI.logger.error(err.message)
throw err
}
}
......@@ -48,5 +48,6 @@ export async function task (payload) {
} catch (err) {
WIKI.logger.error('Fetching latest localization data: [ FAILED ]')
WIKI.logger.error(err.message)
throw err
}
}
......@@ -13,8 +13,8 @@
"ncu-u": "ncu -u -x codemirror,codemirror-asciidoc"
},
"dependencies": {
"@apollo/client": "3.7.14",
"@lezer/common": "1.0.2",
"@apollo/client": "3.7.15",
"@lezer/common": "1.0.3",
"@mdi/font": "7.2.96",
"@quasar/extras": "1.16.4",
"@tiptap/core": "2.0.3",
......@@ -74,21 +74,21 @@
"markdown-it-sup": "1.0.0",
"markdown-it-task-lists": "2.1.1",
"mitt": "3.0.0",
"monaco-editor": "0.38.0",
"monaco-editor": "0.39.0",
"pako": "2.1.0",
"pinia": "2.1.3",
"prosemirror-commands": "1.5.2",
"prosemirror-history": "1.3.2",
"prosemirror-keymap": "1.2.2",
"prosemirror-model": "1.19.2",
"prosemirror-schema-list": "1.2.3",
"prosemirror-schema-list": "1.3.0",
"prosemirror-state": "1.4.3",
"prosemirror-transform": "1.7.2",
"prosemirror-view": "1.31.3",
"prosemirror-transform": "1.7.3",
"prosemirror-view": "1.31.4",
"pug": "3.0.2",
"quasar": "2.12.0",
"slugify": "1.6.6",
"socket.io-client": "4.6.1",
"socket.io-client": "4.6.2",
"tabulator-tables": "5.5.0",
"tippy.js": "6.3.7",
"twemoji": "14.0.2",
......@@ -96,10 +96,10 @@
"v-network-graph": "0.9.3",
"vue": "3.3.4",
"vue-i18n": "9.2.2",
"vue-router": "4.2.1",
"vue-router": "4.2.2",
"vue3-otp-input": "0.4.1",
"vuedraggable": "4.1.0",
"xterm": "5.1.0",
"xterm": "5.2.1",
"zxcvbn": "4.4.2"
},
"devDependencies": {
......@@ -109,10 +109,10 @@
"@volar/vue-language-plugin-pug": "1.6.5",
"autoprefixer": "10.4.14",
"browserlist": "latest",
"eslint": "8.41.0",
"eslint-config-standard": "17.0.0",
"eslint": "8.42.0",
"eslint-config-standard": "17.1.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.7.0",
"eslint-plugin-n": "16.0.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.14.1"
},
......
......@@ -615,7 +615,7 @@ async function loadTree ({ parentId = null, parentPath = null, types, initLoad =
// -> Set Ancestors / Tree Roots
if (item.folderPath) {
if (!state.treeNodes[parentId].children.includes(item.id)) {
if (item.id !== parentId && !state.treeNodes[parentId].children.includes(item.id)) {
state.treeNodes[parentId].children.push(item.id)
}
} else {
......@@ -725,9 +725,20 @@ function renameFolder (folderId) {
componentProps: {
folderId
}
}).onOk(() => {
}).onOk(async () => {
treeComp.value.resetLoaded()
loadTree({ parentId: folderId, initLoad: true })
// // -> Delete current folder and children from cache
// const fPath = [state.treeNodes[folderId].folderPath, state.treeNodes[folderId].fileName].filter(p => !!p).join('/')
// delete state.treeNodes[folderId]
// for (const [nodeId, node] of Object.entries(state.treeNodes)) {
// if (node.folderPath.startsWith(fPath)) {
// delete state.treeNodes[nodeId]
// }
// }
// -> Reload tree
await loadTree({ parentId: folderId, types: ['folder'], initLoad: true }) // Update tree
// -> Reload current view (in case current folder is included)
await loadTree({ parentId: state.currentFolderId })
})
}
......
......@@ -204,6 +204,7 @@ onMounted(async () => {
}
}
`,
fetchPolicy: 'network-only',
variables: {
id: props.folderId
}
......
<template lang="pug">
q-menu.translucent-menu(
auto-close
anchor='top right'
anchor='bottom middle'
self='top left'
)
q-list(padding, style='min-width: 200px;')
......
......@@ -249,6 +249,11 @@
border-color: $green-5;
}
}
.codeblock > code {
background-color: inherit;
color: inherit;
}
}
// ---------------------------------
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment