Extract server endpoints for thumbnails and extensions into separate files
This commit is contained in:
parent
2d774f32b2
commit
6e562bd1ff
|
@ -8747,7 +8747,7 @@ jQuery(async function () {
|
||||||
/**
|
/**
|
||||||
* Handles the click event for the third-party extension import button.
|
* Handles the click event for the third-party extension import button.
|
||||||
* Prompts the user to enter the Git URL of the extension to import.
|
* Prompts the user to enter the Git URL of the extension to import.
|
||||||
* After obtaining the Git URL, makes a POST request to '/get_extension' to import the extension.
|
* After obtaining the Git URL, makes a POST request to '/api/extensions/install' to import the extension.
|
||||||
* If the extension is imported successfully, a success message is displayed.
|
* If the extension is imported successfully, a success message is displayed.
|
||||||
* If the extension import fails, an error message is displayed and the error is logged to the console.
|
* If the extension import fails, an error message is displayed and the error is logged to the console.
|
||||||
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
|
* After successfully importing the extension, the extension settings are reloaded and a 'EXTENSION_SETTINGS_LOADED' event is emitted.
|
||||||
|
@ -8770,7 +8770,7 @@ jQuery(async function () {
|
||||||
const url = input.trim();
|
const url = input.trim();
|
||||||
console.debug('Extension import started', url);
|
console.debug('Extension import started', url);
|
||||||
|
|
||||||
const request = await fetch('/get_extension', {
|
const request = await fetch('/api/extensions/install', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getRequestHeaders(),
|
headers: getRequestHeaders(),
|
||||||
body: JSON.stringify({ url }),
|
body: JSON.stringify({ url }),
|
||||||
|
|
|
@ -206,7 +206,7 @@ async function doExtrasFetch(endpoint, args) {
|
||||||
|
|
||||||
async function discoverExtensions() {
|
async function discoverExtensions() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/discover_extensions');
|
const response = await fetch('/api/extensions/discover');
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const extensions = await response.json();
|
const extensions = await response.json();
|
||||||
|
@ -631,7 +631,7 @@ async function onUpdateClick() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the click event for the delete button of an extension.
|
* Handles the click event for the delete button of an extension.
|
||||||
* This function makes a POST request to '/delete_extension' with the extension's name.
|
* This function makes a POST request to '/api/extensions/delete' with the extension's name.
|
||||||
* If the extension is deleted, it displays a success message.
|
* If the extension is deleted, it displays a success message.
|
||||||
* Creates a popup for the user to confirm before delete.
|
* Creates a popup for the user to confirm before delete.
|
||||||
*/
|
*/
|
||||||
|
@ -641,7 +641,7 @@ async function onDeleteClick() {
|
||||||
const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension');
|
const confirmation = await callPopup(`Are you sure you want to delete ${extensionName}?`, 'delete_extension');
|
||||||
if (confirmation) {
|
if (confirmation) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/delete_extension', {
|
const response = await fetch('/api/extensions/delete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getRequestHeaders(),
|
headers: getRequestHeaders(),
|
||||||
body: JSON.stringify({ extensionName })
|
body: JSON.stringify({ extensionName })
|
||||||
|
@ -668,7 +668,7 @@ async function onDeleteClick() {
|
||||||
*/
|
*/
|
||||||
async function getExtensionVersion(extensionName) {
|
async function getExtensionVersion(extensionName) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/get_extension_version', {
|
const response = await fetch('/api/extensions/version', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: getRequestHeaders(),
|
headers: getRequestHeaders(),
|
||||||
body: JSON.stringify({ extensionName })
|
body: JSON.stringify({ extensionName })
|
||||||
|
|
425
server.js
425
server.js
|
@ -15,7 +15,6 @@ const { TextDecoder } = require('util');
|
||||||
// cli/fs related library imports
|
// cli/fs related library imports
|
||||||
const open = require('open');
|
const open = require('open');
|
||||||
const sanitize = require('sanitize-filename');
|
const sanitize = require('sanitize-filename');
|
||||||
const simpleGit = require('simple-git');
|
|
||||||
const writeFileAtomicSync = require('write-file-atomic').sync;
|
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||||
const yargs = require('yargs/yargs');
|
const yargs = require('yargs/yargs');
|
||||||
const { hideBin } = require('yargs/helpers');
|
const { hideBin } = require('yargs/helpers');
|
||||||
|
@ -65,6 +64,7 @@ const contentManager = require('./src/content-manager');
|
||||||
const statsHelpers = require('./statsHelpers.js');
|
const statsHelpers = require('./statsHelpers.js');
|
||||||
const { writeSecret, readSecret, readSecretState, migrateSecrets, SECRET_KEYS, getAllSecrets } = require('./src/secrets');
|
const { writeSecret, readSecret, readSecretState, migrateSecrets, SECRET_KEYS, getAllSecrets } = require('./src/secrets');
|
||||||
const { delay, getVersion, getImageBuffers } = require('./src/util');
|
const { delay, getVersion, getImageBuffers } = require('./src/util');
|
||||||
|
const { invalidateThumbnail, ensureThumbnailCache } = require('./src/thumbnails');
|
||||||
|
|
||||||
// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
|
// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0.
|
||||||
// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
|
// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
|
||||||
|
@ -321,32 +321,7 @@ const AVATAR_WIDTH = 400;
|
||||||
const AVATAR_HEIGHT = 600;
|
const AVATAR_HEIGHT = 600;
|
||||||
const jsonParser = express.json({ limit: '100mb' });
|
const jsonParser = express.json({ limit: '100mb' });
|
||||||
const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' });
|
const urlencodedParser = express.urlencoded({ extended: true, limit: '100mb' });
|
||||||
const directories = {
|
const { directories } = require('./src/constants');
|
||||||
worlds: 'public/worlds/',
|
|
||||||
avatars: 'public/User Avatars',
|
|
||||||
images: 'public/img/',
|
|
||||||
userImages: 'public/user/images/',
|
|
||||||
groups: 'public/groups/',
|
|
||||||
groupChats: 'public/group chats',
|
|
||||||
chats: 'public/chats/',
|
|
||||||
characters: 'public/characters/',
|
|
||||||
backgrounds: 'public/backgrounds',
|
|
||||||
novelAI_Settings: 'public/NovelAI Settings',
|
|
||||||
koboldAI_Settings: 'public/KoboldAI Settings',
|
|
||||||
openAI_Settings: 'public/OpenAI Settings',
|
|
||||||
textGen_Settings: 'public/TextGen Settings',
|
|
||||||
thumbnails: 'thumbnails/',
|
|
||||||
thumbnailsBg: 'thumbnails/bg/',
|
|
||||||
thumbnailsAvatar: 'thumbnails/avatar/',
|
|
||||||
themes: 'public/themes',
|
|
||||||
movingUI: 'public/movingUI',
|
|
||||||
extensions: 'public/scripts/extensions',
|
|
||||||
instruct: 'public/instruct',
|
|
||||||
context: 'public/context',
|
|
||||||
backups: 'backups/',
|
|
||||||
quickreplies: 'public/QuickReplies',
|
|
||||||
assets: 'public/assets',
|
|
||||||
};
|
|
||||||
|
|
||||||
// CSRF Protection //
|
// CSRF Protection //
|
||||||
if (cliArguments.disableCsrf === false) {
|
if (cliArguments.disableCsrf === false) {
|
||||||
|
@ -2728,36 +2703,6 @@ app.post('/deletegroup', jsonParser, async (request, response) => {
|
||||||
return response.send({ ok: true });
|
return response.send({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Discover the extension folders
|
|
||||||
* If the folder is called third-party, search for subfolders instead
|
|
||||||
*/
|
|
||||||
app.get('/discover_extensions', jsonParser, function (_, response) {
|
|
||||||
|
|
||||||
// get all folders in the extensions folder, except third-party
|
|
||||||
const extensions = fs
|
|
||||||
.readdirSync(directories.extensions)
|
|
||||||
.filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory())
|
|
||||||
.filter(f => f !== 'third-party');
|
|
||||||
|
|
||||||
// get all folders in the third-party folder, if it exists
|
|
||||||
|
|
||||||
if (!fs.existsSync(path.join(directories.extensions, 'third-party'))) {
|
|
||||||
return response.send(extensions);
|
|
||||||
}
|
|
||||||
|
|
||||||
const thirdPartyExtensions = fs
|
|
||||||
.readdirSync(path.join(directories.extensions, 'third-party'))
|
|
||||||
.filter(f => fs.statSync(path.join(directories.extensions, 'third-party', f)).isDirectory());
|
|
||||||
|
|
||||||
// add the third-party extensions to the extensions array
|
|
||||||
extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`));
|
|
||||||
console.log(extensions);
|
|
||||||
|
|
||||||
|
|
||||||
return response.send(extensions);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the path to the sprites folder for the provided character name
|
* Gets the path to the sprites folder for the provided character name
|
||||||
* @param {string} name - The name of the character
|
* @param {string} name - The name of the character
|
||||||
|
@ -2816,47 +2761,6 @@ app.get('/get_sprites', jsonParser, function (request, response) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getThumbnailFolder(type) {
|
|
||||||
let thumbnailFolder;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'bg':
|
|
||||||
thumbnailFolder = directories.thumbnailsBg;
|
|
||||||
break;
|
|
||||||
case 'avatar':
|
|
||||||
thumbnailFolder = directories.thumbnailsAvatar;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return thumbnailFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOriginalFolder(type) {
|
|
||||||
let originalFolder;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'bg':
|
|
||||||
originalFolder = directories.backgrounds;
|
|
||||||
break;
|
|
||||||
case 'avatar':
|
|
||||||
originalFolder = directories.characters;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return originalFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
function invalidateThumbnail(type, file) {
|
|
||||||
const folder = getThumbnailFolder(type);
|
|
||||||
if (folder === undefined) throw new Error("Invalid thumbnail type")
|
|
||||||
|
|
||||||
const pathToThumbnail = path.join(folder, file);
|
|
||||||
|
|
||||||
if (fs.existsSync(pathToThumbnail)) {
|
|
||||||
fs.rmSync(pathToThumbnail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanUploads() {
|
function cleanUploads() {
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(UPLOADS_PATH)) {
|
if (fs.existsSync(UPLOADS_PATH)) {
|
||||||
|
@ -2877,118 +2781,6 @@ function cleanUploads() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureThumbnailCache() {
|
|
||||||
const cacheFiles = fs.readdirSync(directories.thumbnailsBg);
|
|
||||||
|
|
||||||
// files exist, all ok
|
|
||||||
if (cacheFiles.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Generating thumbnails cache. Please wait...');
|
|
||||||
|
|
||||||
const bgFiles = fs.readdirSync(directories.backgrounds);
|
|
||||||
const tasks = [];
|
|
||||||
|
|
||||||
for (const file of bgFiles) {
|
|
||||||
tasks.push(generateThumbnail('bg', file));
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(tasks);
|
|
||||||
console.log(`Done! Generated: ${bgFiles.length} preview images`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateThumbnail(type, file) {
|
|
||||||
let thumbnailFolder = getThumbnailFolder(type)
|
|
||||||
let originalFolder = getOriginalFolder(type)
|
|
||||||
if (thumbnailFolder === undefined || originalFolder === undefined) throw new Error("Invalid thumbnail type")
|
|
||||||
|
|
||||||
const pathToCachedFile = path.join(thumbnailFolder, file);
|
|
||||||
const pathToOriginalFile = path.join(originalFolder, file);
|
|
||||||
|
|
||||||
const cachedFileExists = fs.existsSync(pathToCachedFile);
|
|
||||||
const originalFileExists = fs.existsSync(pathToOriginalFile);
|
|
||||||
|
|
||||||
// to handle cases when original image was updated after thumb creation
|
|
||||||
let shouldRegenerate = false;
|
|
||||||
|
|
||||||
if (cachedFileExists && originalFileExists) {
|
|
||||||
const originalStat = fs.statSync(pathToOriginalFile);
|
|
||||||
const cachedStat = fs.statSync(pathToCachedFile);
|
|
||||||
|
|
||||||
if (originalStat.mtimeMs > cachedStat.ctimeMs) {
|
|
||||||
//console.log('Original file changed. Regenerating thumbnail...');
|
|
||||||
shouldRegenerate = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedFileExists && !shouldRegenerate) {
|
|
||||||
return pathToCachedFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!originalFileExists) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageSizes = { 'bg': [160, 90], 'avatar': [96, 144] };
|
|
||||||
const mySize = imageSizes[type];
|
|
||||||
|
|
||||||
try {
|
|
||||||
let buffer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const image = await jimp.read(pathToOriginalFile);
|
|
||||||
buffer = await image.cover(mySize[0], mySize[1]).quality(95).getBufferAsync('image/jpeg');
|
|
||||||
}
|
|
||||||
catch (inner) {
|
|
||||||
console.warn(`Thumbnailer can not process the image: ${pathToOriginalFile}. Using original size`);
|
|
||||||
buffer = fs.readFileSync(pathToOriginalFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileAtomicSync(pathToCachedFile, buffer);
|
|
||||||
}
|
|
||||||
catch (outer) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pathToCachedFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
app.get('/thumbnail', jsonParser, async function (request, response) {
|
|
||||||
if (typeof request.query.file !== 'string' || typeof request.query.type !== 'string') return response.sendStatus(400);
|
|
||||||
|
|
||||||
const type = request.query.type;
|
|
||||||
const file = sanitize(request.query.file);
|
|
||||||
|
|
||||||
if (!type || !file) {
|
|
||||||
return response.sendStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(type == 'bg' || type == 'avatar')) {
|
|
||||||
return response.sendStatus(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sanitize(file) !== file) {
|
|
||||||
console.error('Malicious filename prevented');
|
|
||||||
return response.sendStatus(403);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.disableThumbnails == true) {
|
|
||||||
let folder = getOriginalFolder(type);
|
|
||||||
if (folder === undefined) return response.sendStatus(400);
|
|
||||||
const pathToOriginalFile = path.join(folder, file);
|
|
||||||
return response.sendFile(pathToOriginalFile, { root: process.cwd() });
|
|
||||||
}
|
|
||||||
|
|
||||||
const pathToCachedFile = await generateThumbnail(type, file);
|
|
||||||
|
|
||||||
if (!pathToCachedFile) {
|
|
||||||
return response.sendStatus(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.sendFile(pathToCachedFile, { root: process.cwd() });
|
|
||||||
});
|
|
||||||
|
|
||||||
/* OpenAI */
|
/* OpenAI */
|
||||||
app.post("/getstatus_openai", jsonParser, async function (request, response_getstatus_openai) {
|
app.post("/getstatus_openai", jsonParser, async function (request, response_getstatus_openai) {
|
||||||
if (!request.body) return response_getstatus_openai.sendStatus(400);
|
if (!request.body) return response_getstatus_openai.sendStatus(400);
|
||||||
|
@ -4342,213 +4134,6 @@ function importRisuSprites(data) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function extracts the extension information from the manifest file.
|
|
||||||
* @param {string} extensionPath - The path of the extension folder
|
|
||||||
* @returns {Promise<Object>} - Returns the manifest data as an object
|
|
||||||
*/
|
|
||||||
async function getManifest(extensionPath) {
|
|
||||||
const manifestPath = path.join(extensionPath, 'manifest.json');
|
|
||||||
|
|
||||||
// Check if manifest.json exists
|
|
||||||
if (!fs.existsSync(manifestPath)) {
|
|
||||||
throw new Error(`Manifest file not found at ${manifestPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
||||||
return manifest;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkIfRepoIsUpToDate(extensionPath) {
|
|
||||||
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
|
||||||
const git = simpleGit();
|
|
||||||
await git.cwd(extensionPath).fetch('origin');
|
|
||||||
const currentBranch = await git.cwd(extensionPath).branch();
|
|
||||||
const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
|
||||||
const log = await git.cwd(extensionPath).log({
|
|
||||||
from: currentCommitHash,
|
|
||||||
to: `origin/${currentBranch.current}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch remote repository information
|
|
||||||
const remotes = await git.cwd(extensionPath).getRemotes(true);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isUpToDate: log.total === 0,
|
|
||||||
remoteUrl: remotes[0].refs.fetch, // URL of the remote repository
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,
|
|
||||||
* and return extension information and path.
|
|
||||||
*
|
|
||||||
* @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property.
|
|
||||||
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
app.post('/get_extension', jsonParser, async (request, response) => {
|
|
||||||
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
|
||||||
const git = simpleGit();
|
|
||||||
if (!request.body.url) {
|
|
||||||
return response.status(400).send('Bad Request: URL is required in the request body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// make sure the third-party directory exists
|
|
||||||
if (!fs.existsSync(directories.extensions + '/third-party')) {
|
|
||||||
fs.mkdirSync(directories.extensions + '/third-party');
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = request.body.url;
|
|
||||||
const extensionPath = path.join(directories.extensions, 'third-party', path.basename(url, '.git'));
|
|
||||||
|
|
||||||
if (fs.existsSync(extensionPath)) {
|
|
||||||
return response.status(409).send(`Directory already exists at ${extensionPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await git.clone(url, extensionPath);
|
|
||||||
console.log(`Extension has been cloned at ${extensionPath}`);
|
|
||||||
|
|
||||||
|
|
||||||
const { version, author, display_name } = await getManifest(extensionPath);
|
|
||||||
|
|
||||||
|
|
||||||
return response.send({ version, author, display_name, extensionPath });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Importing custom content failed', error);
|
|
||||||
return response.status(500).send(`Server Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP POST handler function to pull the latest updates from a git repository
|
|
||||||
* based on the extension name provided in the request body. It returns the latest commit hash,
|
|
||||||
* the path of the extension, the status of the repository (whether it's up-to-date or not),
|
|
||||||
* and the remote URL of the repository.
|
|
||||||
*
|
|
||||||
* @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property.
|
|
||||||
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
app.post('/update_extension', jsonParser, async (request, response) => {
|
|
||||||
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
|
||||||
const git = simpleGit();
|
|
||||||
if (!request.body.extensionName) {
|
|
||||||
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const extensionName = request.body.extensionName;
|
|
||||||
const extensionPath = path.join(directories.extensions, 'third-party', extensionName);
|
|
||||||
|
|
||||||
if (!fs.existsSync(extensionPath)) {
|
|
||||||
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
|
|
||||||
const currentBranch = await git.cwd(extensionPath).branch();
|
|
||||||
if (!isUpToDate) {
|
|
||||||
|
|
||||||
await git.cwd(extensionPath).pull('origin', currentBranch.current);
|
|
||||||
console.log(`Extension has been updated at ${extensionPath}`);
|
|
||||||
} else {
|
|
||||||
console.log(`Extension is up to date at ${extensionPath}`);
|
|
||||||
}
|
|
||||||
await git.cwd(extensionPath).fetch('origin');
|
|
||||||
const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
|
||||||
const shortCommitHash = fullCommitHash.slice(0, 7);
|
|
||||||
|
|
||||||
return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Updating custom content failed', error);
|
|
||||||
return response.status(500).send(`Server Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP POST handler function to get the current git commit hash and branch name for a given extension.
|
|
||||||
* It checks whether the repository is up-to-date with the remote, and returns the status along with
|
|
||||||
* the remote URL of the repository.
|
|
||||||
*
|
|
||||||
* @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property.
|
|
||||||
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
app.post('/get_extension_version', jsonParser, async (request, response) => {
|
|
||||||
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
|
||||||
const git = simpleGit();
|
|
||||||
if (!request.body.extensionName) {
|
|
||||||
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const extensionName = request.body.extensionName;
|
|
||||||
const extensionPath = path.join(directories.extensions, 'third-party', extensionName);
|
|
||||||
|
|
||||||
if (!fs.existsSync(extensionPath)) {
|
|
||||||
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentBranch = await git.cwd(extensionPath).branch();
|
|
||||||
// get only the working branch
|
|
||||||
const currentBranchName = currentBranch.current;
|
|
||||||
await git.cwd(extensionPath).fetch('origin');
|
|
||||||
const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
|
||||||
console.log(currentBranch, currentCommitHash);
|
|
||||||
const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
|
|
||||||
|
|
||||||
return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Getting extension version failed', error);
|
|
||||||
return response.status(500).send(`Server Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HTTP POST handler function to delete a git repository based on the extension name provided in the request body.
|
|
||||||
*
|
|
||||||
* @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property.
|
|
||||||
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
|
||||||
*
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
app.post('/delete_extension', jsonParser, async (request, response) => {
|
|
||||||
if (!request.body.extensionName) {
|
|
||||||
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanatize the extension name to prevent directory traversal
|
|
||||||
const extensionName = sanitize(request.body.extensionName);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const extensionPath = path.join(directories.extensions, 'third-party', extensionName);
|
|
||||||
|
|
||||||
if (!fs.existsSync(extensionPath)) {
|
|
||||||
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.promises.rmdir(extensionPath, { recursive: true });
|
|
||||||
console.log(`Extension has been deleted at ${extensionPath}`);
|
|
||||||
|
|
||||||
return response.send(`Extension has been deleted at ${extensionPath}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Deleting custom content failed', error);
|
|
||||||
return response.status(500).send(`Server Error: ${error.message}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTTP POST handler function to retrieve name of all files of a given folder path.
|
* HTTP POST handler function to retrieve name of all files of a given folder path.
|
||||||
*
|
*
|
||||||
|
@ -4781,9 +4366,15 @@ app.post('/get_character_assets_list', jsonParser, async (request, response) =>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Thumbnail generation
|
||||||
|
require('./src/thumbnails').registerEndpoints(app, jsonParser);
|
||||||
|
|
||||||
// NovelAI generation
|
// NovelAI generation
|
||||||
require('./src/novelai').registerEndpoints(app, jsonParser);
|
require('./src/novelai').registerEndpoints(app, jsonParser);
|
||||||
|
|
||||||
|
// Third-party extensions
|
||||||
|
require('./src/extensions').registerEndpoints(app, jsonParser);
|
||||||
|
|
||||||
// Stable Diffusion generation
|
// Stable Diffusion generation
|
||||||
require('./src/stable-diffusion').registerEndpoints(app, jsonParser);
|
require('./src/stable-diffusion').registerEndpoints(app, jsonParser);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
const directories = {
|
||||||
|
worlds: 'public/worlds/',
|
||||||
|
avatars: 'public/User Avatars',
|
||||||
|
images: 'public/img/',
|
||||||
|
userImages: 'public/user/images/',
|
||||||
|
groups: 'public/groups/',
|
||||||
|
groupChats: 'public/group chats',
|
||||||
|
chats: 'public/chats/',
|
||||||
|
characters: 'public/characters/',
|
||||||
|
backgrounds: 'public/backgrounds',
|
||||||
|
novelAI_Settings: 'public/NovelAI Settings',
|
||||||
|
koboldAI_Settings: 'public/KoboldAI Settings',
|
||||||
|
openAI_Settings: 'public/OpenAI Settings',
|
||||||
|
textGen_Settings: 'public/TextGen Settings',
|
||||||
|
thumbnails: 'thumbnails/',
|
||||||
|
thumbnailsBg: 'thumbnails/bg/',
|
||||||
|
thumbnailsAvatar: 'thumbnails/avatar/',
|
||||||
|
themes: 'public/themes',
|
||||||
|
movingUI: 'public/movingUI',
|
||||||
|
extensions: 'public/scripts/extensions',
|
||||||
|
instruct: 'public/instruct',
|
||||||
|
context: 'public/context',
|
||||||
|
backups: 'backups/',
|
||||||
|
quickreplies: 'public/QuickReplies',
|
||||||
|
assets: 'public/assets',
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
directories,
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const simpleGit = require('simple-git');
|
||||||
|
const sanitize = require('sanitize-filename');
|
||||||
|
const { directories } = require('./constants');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function extracts the extension information from the manifest file.
|
||||||
|
* @param {string} extensionPath - The path of the extension folder
|
||||||
|
* @returns {Promise<Object>} - Returns the manifest data as an object
|
||||||
|
*/
|
||||||
|
async function getManifest(extensionPath) {
|
||||||
|
const manifestPath = path.join(extensionPath, 'manifest.json');
|
||||||
|
|
||||||
|
// Check if manifest.json exists
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
throw new Error(`Manifest file not found at ${manifestPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function checks if the local repository is up-to-date with the remote repository.
|
||||||
|
* @param {string} extensionPath - The path of the extension folder
|
||||||
|
* @returns {Promise<Object>} - Returns the extension information as an object
|
||||||
|
*/
|
||||||
|
async function checkIfRepoIsUpToDate(extensionPath) {
|
||||||
|
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
||||||
|
const git = simpleGit();
|
||||||
|
await git.cwd(extensionPath).fetch('origin');
|
||||||
|
const currentBranch = await git.cwd(extensionPath).branch();
|
||||||
|
const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
||||||
|
const log = await git.cwd(extensionPath).log({
|
||||||
|
from: currentCommitHash,
|
||||||
|
to: `origin/${currentBranch.current}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch remote repository information
|
||||||
|
const remotes = await git.cwd(extensionPath).getRemotes(true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isUpToDate: log.total === 0,
|
||||||
|
remoteUrl: remotes[0].refs.fetch, // URL of the remote repository
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the endpoints for the third-party extensions API.
|
||||||
|
* @param {import('express').Express} app - Express app
|
||||||
|
* @param {any} jsonParser - JSON parser middleware
|
||||||
|
*/
|
||||||
|
function registerEndpoints(app, jsonParser) {
|
||||||
|
/**
|
||||||
|
* HTTP POST handler function to clone a git repository from a provided URL, read the extension manifest,
|
||||||
|
* and return extension information and path.
|
||||||
|
*
|
||||||
|
* @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property.
|
||||||
|
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.post('/api/extensions/install', jsonParser, async (request, response) => {
|
||||||
|
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
||||||
|
const git = simpleGit();
|
||||||
|
if (!request.body.url) {
|
||||||
|
return response.status(400).send('Bad Request: URL is required in the request body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// make sure the third-party directory exists
|
||||||
|
if (!fs.existsSync(path.join(directories.extensions, 'third-party'))) {
|
||||||
|
fs.mkdirSync(path.join(directories.extensions, 'third-party'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = request.body.url;
|
||||||
|
const extensionPath = path.join(directories.extensions, 'third-party', path.basename(url, '.git'));
|
||||||
|
|
||||||
|
if (fs.existsSync(extensionPath)) {
|
||||||
|
return response.status(409).send(`Directory already exists at ${extensionPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await git.clone(url, extensionPath);
|
||||||
|
console.log(`Extension has been cloned at ${extensionPath}`);
|
||||||
|
|
||||||
|
|
||||||
|
const { version, author, display_name } = await getManifest(extensionPath);
|
||||||
|
|
||||||
|
|
||||||
|
return response.send({ version, author, display_name, extensionPath });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Importing custom content failed', error);
|
||||||
|
return response.status(500).send(`Server Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP POST handler function to pull the latest updates from a git repository
|
||||||
|
* based on the extension name provided in the request body. It returns the latest commit hash,
|
||||||
|
* the path of the extension, the status of the repository (whether it's up-to-date or not),
|
||||||
|
* and the remote URL of the repository.
|
||||||
|
*
|
||||||
|
* @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property.
|
||||||
|
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.post('/api/extensions/update', jsonParser, async (request, response) => {
|
||||||
|
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
||||||
|
const git = simpleGit();
|
||||||
|
if (!request.body.extensionName) {
|
||||||
|
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const extensionName = request.body.extensionName;
|
||||||
|
const extensionPath = path.join(directories.extensions, 'third-party', extensionName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(extensionPath)) {
|
||||||
|
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
|
||||||
|
const currentBranch = await git.cwd(extensionPath).branch();
|
||||||
|
if (!isUpToDate) {
|
||||||
|
|
||||||
|
await git.cwd(extensionPath).pull('origin', currentBranch.current);
|
||||||
|
console.log(`Extension has been updated at ${extensionPath}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Extension is up to date at ${extensionPath}`);
|
||||||
|
}
|
||||||
|
await git.cwd(extensionPath).fetch('origin');
|
||||||
|
const fullCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
||||||
|
const shortCommitHash = fullCommitHash.slice(0, 7);
|
||||||
|
|
||||||
|
return response.send({ shortCommitHash, extensionPath, isUpToDate, remoteUrl });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Updating custom content failed', error);
|
||||||
|
return response.status(500).send(`Server Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP POST handler function to get the current git commit hash and branch name for a given extension.
|
||||||
|
* It checks whether the repository is up-to-date with the remote, and returns the status along with
|
||||||
|
* the remote URL of the repository.
|
||||||
|
*
|
||||||
|
* @param {Object} request - HTTP Request object, expects a JSON body with an 'extensionName' property.
|
||||||
|
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.post('/api/extensions/version', jsonParser, async (request, response) => {
|
||||||
|
// @ts-ignore - simple-git types are incorrect, this is apparently callable but no call signature
|
||||||
|
const git = simpleGit();
|
||||||
|
if (!request.body.extensionName) {
|
||||||
|
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const extensionName = request.body.extensionName;
|
||||||
|
const extensionPath = path.join(directories.extensions, 'third-party', extensionName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(extensionPath)) {
|
||||||
|
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentBranch = await git.cwd(extensionPath).branch();
|
||||||
|
// get only the working branch
|
||||||
|
const currentBranchName = currentBranch.current;
|
||||||
|
await git.cwd(extensionPath).fetch('origin');
|
||||||
|
const currentCommitHash = await git.cwd(extensionPath).revparse(['HEAD']);
|
||||||
|
console.log(currentBranch, currentCommitHash);
|
||||||
|
const { isUpToDate, remoteUrl } = await checkIfRepoIsUpToDate(extensionPath);
|
||||||
|
|
||||||
|
return response.send({ currentBranchName, currentCommitHash, isUpToDate, remoteUrl });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Getting extension version failed', error);
|
||||||
|
return response.status(500).send(`Server Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP POST handler function to delete a git repository based on the extension name provided in the request body.
|
||||||
|
*
|
||||||
|
* @param {Object} request - HTTP Request object, expects a JSON body with a 'url' property.
|
||||||
|
* @param {Object} response - HTTP Response object used to respond to the HTTP request.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
app.post('/api/extensions/delete', jsonParser, async (request, response) => {
|
||||||
|
if (!request.body.extensionName) {
|
||||||
|
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanatize the extension name to prevent directory traversal
|
||||||
|
const extensionName = sanitize(request.body.extensionName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const extensionPath = path.join(directories.extensions, 'third-party', extensionName);
|
||||||
|
|
||||||
|
if (!fs.existsSync(extensionPath)) {
|
||||||
|
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.promises.rmdir(extensionPath, { recursive: true });
|
||||||
|
console.log(`Extension has been deleted at ${extensionPath}`);
|
||||||
|
|
||||||
|
return response.send(`Extension has been deleted at ${extensionPath}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Deleting custom content failed', error);
|
||||||
|
return response.status(500).send(`Server Error: ${error.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discover the extension folders
|
||||||
|
* If the folder is called third-party, search for subfolders instead
|
||||||
|
*/
|
||||||
|
app.get('/api/extensions/discover', jsonParser, function (_, response) {
|
||||||
|
|
||||||
|
// get all folders in the extensions folder, except third-party
|
||||||
|
const extensions = fs
|
||||||
|
.readdirSync(directories.extensions)
|
||||||
|
.filter(f => fs.statSync(path.join(directories.extensions, f)).isDirectory())
|
||||||
|
.filter(f => f !== 'third-party');
|
||||||
|
|
||||||
|
// get all folders in the third-party folder, if it exists
|
||||||
|
|
||||||
|
if (!fs.existsSync(path.join(directories.extensions, 'third-party'))) {
|
||||||
|
return response.send(extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
const thirdPartyExtensions = fs
|
||||||
|
.readdirSync(path.join(directories.extensions, 'third-party'))
|
||||||
|
.filter(f => fs.statSync(path.join(directories.extensions, 'third-party', f)).isDirectory());
|
||||||
|
|
||||||
|
// add the third-party extensions to the extensions array
|
||||||
|
extensions.push(...thirdPartyExtensions.map(f => `third-party/${f}`));
|
||||||
|
console.log(extensions);
|
||||||
|
|
||||||
|
|
||||||
|
return response.send(extensions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
registerEndpoints,
|
||||||
|
}
|
|
@ -0,0 +1,201 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const sanitize = require('sanitize-filename');
|
||||||
|
const jimp = require('jimp');
|
||||||
|
const writeFileAtomicSync = require('write-file-atomic').sync;
|
||||||
|
const { directories } = require('./constants');
|
||||||
|
const { getConfigValue } = require('./util');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a path to thumbnail folder based on the type.
|
||||||
|
* @param {'bg' | 'avatar'} type Thumbnail type
|
||||||
|
* @returns {string} Path to the thumbnails folder
|
||||||
|
*/
|
||||||
|
function getThumbnailFolder(type) {
|
||||||
|
let thumbnailFolder;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'bg':
|
||||||
|
thumbnailFolder = directories.thumbnailsBg;
|
||||||
|
break;
|
||||||
|
case 'avatar':
|
||||||
|
thumbnailFolder = directories.thumbnailsAvatar;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return thumbnailFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a path to the original images folder based on the type.
|
||||||
|
* @param {'bg' | 'avatar'} type Thumbnail type
|
||||||
|
* @returns {string} Path to the original images folder
|
||||||
|
*/
|
||||||
|
function getOriginalFolder(type) {
|
||||||
|
let originalFolder;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'bg':
|
||||||
|
originalFolder = directories.backgrounds;
|
||||||
|
break;
|
||||||
|
case 'avatar':
|
||||||
|
originalFolder = directories.characters;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the generated thumbnail from the disk.
|
||||||
|
* @param {'bg' | 'avatar'} type Type of the thumbnail
|
||||||
|
* @param {string} file Name of the file
|
||||||
|
*/
|
||||||
|
function invalidateThumbnail(type, file) {
|
||||||
|
const folder = getThumbnailFolder(type);
|
||||||
|
if (folder === undefined) throw new Error("Invalid thumbnail type")
|
||||||
|
|
||||||
|
const pathToThumbnail = path.join(folder, file);
|
||||||
|
|
||||||
|
if (fs.existsSync(pathToThumbnail)) {
|
||||||
|
fs.rmSync(pathToThumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a thumbnail for the given file.
|
||||||
|
* @param {'bg' | 'avatar'} type Type of the thumbnail
|
||||||
|
* @param {string} file Name of the file
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function generateThumbnail(type, file) {
|
||||||
|
let thumbnailFolder = getThumbnailFolder(type)
|
||||||
|
let originalFolder = getOriginalFolder(type)
|
||||||
|
if (thumbnailFolder === undefined || originalFolder === undefined) throw new Error("Invalid thumbnail type")
|
||||||
|
|
||||||
|
const pathToCachedFile = path.join(thumbnailFolder, file);
|
||||||
|
const pathToOriginalFile = path.join(originalFolder, file);
|
||||||
|
|
||||||
|
const cachedFileExists = fs.existsSync(pathToCachedFile);
|
||||||
|
const originalFileExists = fs.existsSync(pathToOriginalFile);
|
||||||
|
|
||||||
|
// to handle cases when original image was updated after thumb creation
|
||||||
|
let shouldRegenerate = false;
|
||||||
|
|
||||||
|
if (cachedFileExists && originalFileExists) {
|
||||||
|
const originalStat = fs.statSync(pathToOriginalFile);
|
||||||
|
const cachedStat = fs.statSync(pathToCachedFile);
|
||||||
|
|
||||||
|
if (originalStat.mtimeMs > cachedStat.ctimeMs) {
|
||||||
|
//console.log('Original file changed. Regenerating thumbnail...');
|
||||||
|
shouldRegenerate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedFileExists && !shouldRegenerate) {
|
||||||
|
return pathToCachedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!originalFileExists) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageSizes = { 'bg': [160, 90], 'avatar': [96, 144] };
|
||||||
|
const mySize = imageSizes[type];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let buffer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const image = await jimp.read(pathToOriginalFile);
|
||||||
|
buffer = await image.cover(mySize[0], mySize[1]).quality(95).getBufferAsync('image/jpeg');
|
||||||
|
}
|
||||||
|
catch (inner) {
|
||||||
|
console.warn(`Thumbnailer can not process the image: ${pathToOriginalFile}. Using original size`);
|
||||||
|
buffer = fs.readFileSync(pathToOriginalFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileAtomicSync(pathToCachedFile, buffer);
|
||||||
|
}
|
||||||
|
catch (outer) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathToCachedFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the thumbnail cache for backgrounds is valid.
|
||||||
|
* @returns {Promise<void>} Promise that resolves when the cache is validated
|
||||||
|
*/
|
||||||
|
async function ensureThumbnailCache() {
|
||||||
|
const cacheFiles = fs.readdirSync(directories.thumbnailsBg);
|
||||||
|
|
||||||
|
// files exist, all ok
|
||||||
|
if (cacheFiles.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Generating thumbnails cache. Please wait...');
|
||||||
|
|
||||||
|
const bgFiles = fs.readdirSync(directories.backgrounds);
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
for (const file of bgFiles) {
|
||||||
|
tasks.push(generateThumbnail('bg', file));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(tasks);
|
||||||
|
console.log(`Done! Generated: ${bgFiles.length} preview images`);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers the endpoints for the thumbnail management.
|
||||||
|
* @param {import('express').Express} app Express app
|
||||||
|
* @param {any} jsonParser JSON parser middleware
|
||||||
|
*/
|
||||||
|
function registerEndpoints(app, jsonParser) {
|
||||||
|
// Important: Do not change a path to this endpoint. It is used in the client code and saved to chat files.
|
||||||
|
app.get('/thumbnail', jsonParser, async function (request, response) {
|
||||||
|
if (typeof request.query.file !== 'string' || typeof request.query.type !== 'string') return response.sendStatus(400);
|
||||||
|
|
||||||
|
const type = request.query.type;
|
||||||
|
const file = sanitize(request.query.file);
|
||||||
|
|
||||||
|
if (!type || !file) {
|
||||||
|
return response.sendStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(type == 'bg' || type == 'avatar')) {
|
||||||
|
return response.sendStatus(400);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitize(file) !== file) {
|
||||||
|
console.error('Malicious filename prevented');
|
||||||
|
return response.sendStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getConfigValue('disableThumbnails', false) == true) {
|
||||||
|
let folder = getOriginalFolder(type);
|
||||||
|
if (folder === undefined) return response.sendStatus(400);
|
||||||
|
const pathToOriginalFile = path.join(folder, file);
|
||||||
|
return response.sendFile(pathToOriginalFile, { root: process.cwd() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathToCachedFile = await generateThumbnail(type, file);
|
||||||
|
|
||||||
|
if (!pathToCachedFile) {
|
||||||
|
return response.sendStatus(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.sendFile(pathToCachedFile, { root: process.cwd() });
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
invalidateThumbnail,
|
||||||
|
registerEndpoints,
|
||||||
|
ensureThumbnailCache,
|
||||||
|
}
|
Loading…
Reference in New Issue