#2227 Implement content scaffolding

This commit is contained in:
Cohee
2024-05-17 02:43:14 +03:00
parent 782f85e05d
commit c661fea07d
3 changed files with 72 additions and 7 deletions

1
.gitignore vendored
View File

@ -47,3 +47,4 @@ access.log
public/css/user.css public/css/user.css
/plugins/ /plugins/
/data /data
/default/scaffold

View File

@ -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"
}
]
```

View File

@ -7,7 +7,9 @@ const { getConfigValue, color } = require('../util');
const { jsonParser } = require('../express-common'); const { jsonParser } = require('../express-common');
const writeFileAtomicSync = require('write-file-atomic').sync; const writeFileAtomicSync = require('write-file-atomic').sync;
const contentDirectory = path.join(process.cwd(), 'default/content'); const contentDirectory = path.join(process.cwd(), 'default/content');
const scaffoldDirectory = path.join(process.cwd(), 'default/scaffold');
const contentIndexPath = path.join(contentDirectory, 'index.json'); const contentIndexPath = path.join(contentDirectory, 'index.json');
const scaffoldIndexPath = path.join(scaffoldDirectory, 'index.json');
const characterCardParser = require('../character-card-parser.js'); const characterCardParser = require('../character-card-parser.js');
const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []); const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDomains', []);
@ -16,6 +18,8 @@ const WHITELIST_GENERIC_URL_DOWNLOAD_SOURCES = getConfigValue('whitelistImportDo
* @typedef {Object} ContentItem * @typedef {Object} ContentItem
* @property {string} filename * @property {string} filename
* @property {string} type * @property {string} type
* @property {string} [name]
* @property {string|null} [folder]
*/ */
/** /**
@ -48,9 +52,7 @@ const CONTENT_TYPES = {
*/ */
function getDefaultPresets(directories) { function getDefaultPresets(directories) {
try { try {
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); const contentIndex = getContentIndex();
const contentIndex = JSON.parse(contentIndexText);
const presets = []; const presets = [];
for (const contentItem of contentIndex) { for (const contentItem of contentIndex) {
@ -112,8 +114,12 @@ async function seedContentForUser(contentIndex, directories, forceCategories) {
continue; continue;
} }
contentLog.push(contentItem.filename); if (!contentItem.folder) {
const contentPath = path.join(contentDirectory, contentItem.filename); console.log(`Content file ${contentItem.filename} has no parent folder`);
continue;
}
const contentPath = path.join(contentItem.folder, contentItem.filename);
if (!fs.existsSync(contentPath)) { if (!fs.existsSync(contentPath)) {
console.log(`Content file ${contentItem.filename} is missing`); 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 basePath = path.parse(contentItem.filename).base;
const targetPath = path.join(contentTarget, basePath); const targetPath = path.join(contentTarget, basePath);
contentLog.push(contentItem.filename);
if (fs.existsSync(targetPath)) { if (fs.existsSync(targetPath)) {
console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`); console.log(`Content file ${contentItem.filename} already exists in ${contentTarget}`);
@ -157,8 +164,7 @@ async function checkForNewContent(directoriesList, forceCategories = []) {
return; return;
} }
const contentIndexText = fs.readFileSync(contentIndexPath, 'utf8'); const contentIndex = getContentIndex();
const contentIndex = JSON.parse(contentIndexText);
let anyContentAdded = false; let anyContentAdded = false;
for (const directories of directoriesList) { 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. * Gets the target directory for the specified asset type.
* @param {ContentType} type Asset type * @param {ContentType} type Asset type