From c661fea07d06d18e093ef03f22949a36804114fd Mon Sep 17 00:00:00 2001 From: Cohee <18619528+Cohee1207@users.noreply.github.com> Date: Fri, 17 May 2024 02:43:14 +0300 Subject: [PATCH] #2227 Implement content scaffolding --- .gitignore | 1 + default/scaffold/README.md | 26 ++++++++++++++++ src/endpoints/content-manager.js | 52 +++++++++++++++++++++++++++----- 3 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 default/scaffold/README.md diff --git a/.gitignore b/.gitignore index d6a5061bb..fbef3d33f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ access.log public/css/user.css /plugins/ /data +/default/scaffold diff --git a/default/scaffold/README.md b/default/scaffold/README.md new file mode 100644 index 000000000..b1272cf8f --- /dev/null +++ b/default/scaffold/README.md @@ -0,0 +1,26 @@ +# Content Scaffolding + +Content files in this folder will be copied for all users (old and new) on the server startup. + +1. You **must** create an `index.json` file in `/default/scaffold` for it to work. The syntax is the same as for default content. +2. All file paths should be relative to `/default/scaffold`, the use of subdirectories is allowed. +3. Scaffolded files are copied first, so they override any of the default files (presets/settings/etc.) that have the same file name. + +## Example + +```json +[ + { + "filename": "themes/Midnight.json", + "type": "theme" + }, + { + "filename": "backgrounds/city.png", + "type": "background" + }, + { + "filename": "characters/Charlie.png", + "type": "character" + } +] +``` diff --git a/src/endpoints/content-manager.js b/src/endpoints/content-manager.js index d91319051..c282dba36 100644 --- a/src/endpoints/content-manager.js +++ b/src/endpoints/content-manager.js @@ -7,7 +7,9 @@ const { getConfigValue, color } = require('../util'); const { jsonParser } = require('../express-common'); const writeFileAtomicSync = require('write-file-atomic').sync; const contentDirectory = path.join(process.cwd(), 'default/content'); +const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold'); const contentIndexPath = path.join(contentDirectory, 'index.json'); +const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json'); const characterCardParser = require('../character-card-parser.js'); const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []); @@ -16,6 +18,8 @@ const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDo * @typedef {Object} ContentItem * @property {string} filename * @property {string} type + * @property {string} [name] + * @property {string|null} [folder] */ /** @@ -48,9 +52,7 @@ const CONTENT_TYPES = { */ function getDefaultPresets(directories) { try { - const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); - const contentIndex = JSON.parse(contentIndexText); - + const contentIndex = getContentIndex(); const presets = []; for (const contentItem of contentIndex) { @@ -112,8 +114,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) { continue; } - contentLog.push(contentItem.filename); - const contentPath = path.join(contentDirectory, contentItem.filename); + if (!contentItem.folder) { + console.log(`Content file ${contentItem.filename} has no parent folder`); + continue; + } + + const contentPath = path.join(contentItem.folder, contentItem.filename); if (!fs.existsSync(contentPath)) { console.log(`Content file ${contentItem.filename} is missing`); @@ -129,6 +135,7 @@ async function seedContentForUser(contentIndex, directories, forceCategories) { const basePath = path.parse(contentItem.filename).base; const targetPath = path.join(contentTarget, basePath); + contentLog.push(contentItem.filename); if (fs.existsSync(targetPath)) { console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`); @@ -157,8 +164,7 @@ async function checkForNewContent(directoriesList, forceCategories = []) { return; } - const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); - const contentIndex = JSON.parse(contentIndexText); + const contentIndex = getContentIndex(); let anyContentAdded = false; for (const directories of directoriesList) { @@ -179,6 +185,38 @@ async function checkForNewContent(directoriesList, forceCategories = []) { } } +/** + * Gets combined content index from the content and scaffold directories. + * @returns {ContentItem[]} Array of content index + */ +function getContentIndex() { + const result = []; + + if (fs.existsSync(scaffoldIndexPath)) { + const scaffoldIndexText = fs.readFileSync(scaffoldIndexPath, 'utf8'); + const scaffoldIndex = JSON.parse(scaffoldIndexText); + if (Array.isArray(scaffoldIndex)) { + scaffoldIndex.forEach((item) => { + item.folder = scaffoldDirectory; + }); + result.push(...scaffoldIndex); + } + } + + if (fs.existsSync(contentIndexPath)) { + const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); + const contentIndex = JSON.parse(contentIndexText); + if (Array.isArray(contentIndex)) { + contentIndex.forEach((item) => { + item.folder = contentDirectory; + }); + result.push(...contentIndex); + } + } + + return result; +} + /** * Gets the target directory for the specified asset type. * @param {ContentType} type Asset type