sillytavern / src /endpoints /settings.js
Nocigar's picture
Upload 72 files
1307964 verified
const fs = require('fs');
const path = require('path');
const express = require('express');
const _ = require('lodash');
const writeFileAtomicSync = require('write-file-atomic').sync;
const { SETTINGS_FILE } = require('../constants');
const { getConfigValue, generateTimestamp, removeOldBackups } = require('../util');
const { jsonParser } = require('../express-common');
const { getAllUserHandles, getUserDirectories } = require('../users');
const ENABLE_EXTENSIONS = getConfigValue('enableExtensions', true);
const ENABLE_ACCOUNTS = getConfigValue('enableUserAccounts', false);
// 10 minutes
const AUTOSAVE_INTERVAL = 10 * 60 * 1000;
/**
* Map of functions to trigger settings autosave for a user.
* @type {Map<string, function>}
*/
const AUTOSAVE_FUNCTIONS = new Map();
/**
* Triggers autosave for a user every 10 minutes.
* @param {string} handle User handle
* @returns {void}
*/
function triggerAutoSave(handle) {
if (!AUTOSAVE_FUNCTIONS.has(handle)) {
const throttledAutoSave = _.throttle(() => backupUserSettings(handle, true), AUTOSAVE_INTERVAL);
AUTOSAVE_FUNCTIONS.set(handle, throttledAutoSave);
}
const functionToCall = AUTOSAVE_FUNCTIONS.get(handle);
if (functionToCall && typeof functionToCall === 'function') {
functionToCall();
}
}
/**
* Reads and parses files from a directory.
* @param {string} directoryPath Path to the directory
* @param {string} fileExtension File extension
* @returns {Array} Parsed files
*/
function readAndParseFromDirectory(directoryPath, fileExtension = '.json') {
const files = fs
.readdirSync(directoryPath)
.filter(x => path.parse(x).ext == fileExtension)
.sort();
const parsedFiles = [];
files.forEach(item => {
try {
const file = fs.readFileSync(path.join(directoryPath, item), 'utf-8');
parsedFiles.push(fileExtension == '.json' ? JSON.parse(file) : file);
}
catch {
// skip
}
});
return parsedFiles;
}
/**
* Gets a sort function for sorting strings.
* @param {*} _
* @returns {(a: string, b: string) => number} Sort function
*/
function sortByName(_) {
return (a, b) => a.localeCompare(b);
}
/**
* Gets backup file prefix for user settings.
* @param {string} handle User handle
* @returns {string} File prefix
*/
function getFilePrefix(handle) {
return `settings_${handle}_`;
}
function readPresetsFromDirectory(directoryPath, options = {}) {
const {
sortFunction,
removeFileExtension = false,
fileExtension = '.json',
} = options;
const files = fs.readdirSync(directoryPath).sort(sortFunction).filter(x => path.parse(x).ext == fileExtension);
const fileContents = [];
const fileNames = [];
files.forEach(item => {
try {
const file = fs.readFileSync(path.join(directoryPath, item), 'utf8');
JSON.parse(file);
fileContents.push(file);
fileNames.push(removeFileExtension ? item.replace(/\.[^/.]+$/, '') : item);
} catch {
// skip
console.log(`${item} is not a valid JSON`);
}
});
return { fileContents, fileNames };
}
async function backupSettings() {
try {
const userHandles = await getAllUserHandles();
for (const handle of userHandles) {
backupUserSettings(handle, true);
}
} catch (err) {
console.log('Could not backup settings file', err);
}
}
/**
* Makes a backup of the user's settings file.
* @param {string} handle User handle
* @param {boolean} preventDuplicates Prevent duplicate backups
* @returns {void}
*/
function backupUserSettings(handle, preventDuplicates) {
const userDirectories = getUserDirectories(handle);
const backupFile = path.join(userDirectories.backups, `${getFilePrefix(handle)}${generateTimestamp()}.json`);
const sourceFile = path.join(userDirectories.root, SETTINGS_FILE);
if (preventDuplicates && isDuplicateBackup(handle, sourceFile)) {
return;
}
if (!fs.existsSync(sourceFile)) {
return;
}
fs.copyFileSync(sourceFile, backupFile);
removeOldBackups(userDirectories.backups, `settings_${handle}`);
}
/**
* Checks if the backup would be a duplicate.
* @param {string} handle User handle
* @param {string} sourceFile Source file path
* @returns {boolean} True if the backup is a duplicate
*/
function isDuplicateBackup(handle, sourceFile) {
const latestBackup = getLatestBackup(handle);
if (!latestBackup) {
return false;
}
return areFilesEqual(latestBackup, sourceFile);
}
/**
* Returns true if the two files are equal.
* @param {string} file1 File path
* @param {string} file2 File path
*/
function areFilesEqual(file1, file2) {
if (!fs.existsSync(file1) || !fs.existsSync(file2)) {
return false;
}
const content1 = fs.readFileSync(file1);
const content2 = fs.readFileSync(file2);
return content1.toString() === content2.toString();
}
/**
* Gets the latest backup file for a user.
* @param {string} handle User handle
* @returns {string|null} Latest backup file. Null if no backup exists.
*/
function getLatestBackup(handle) {
const userDirectories = getUserDirectories(handle);
const backupFiles = fs.readdirSync(userDirectories.backups)
.filter(x => x.startsWith(getFilePrefix(handle)))
.map(x => ({ name: x, ctime: fs.statSync(path.join(userDirectories.backups, x)).ctimeMs }));
const latestBackup = backupFiles.sort((a, b) => b.ctime - a.ctime)[0]?.name;
if (!latestBackup) {
return null;
}
return path.join(userDirectories.backups, latestBackup);
}
const router = express.Router();
router.post('/save', jsonParser, function (request, response) {
try {
const pathToSettings = path.join(request.user.directories.root, SETTINGS_FILE);
writeFileAtomicSync(pathToSettings, JSON.stringify(request.body, null, 4), 'utf8');
triggerAutoSave(request.user.profile.handle);
response.send({ result: 'ok' });
} catch (err) {
console.log(err);
response.send(err);
}
});
// Wintermute's code
router.post('/get', jsonParser, (request, response) => {
let settings;
try {
const pathToSettings = path.join(request.user.directories.root, SETTINGS_FILE);
settings = fs.readFileSync(pathToSettings, 'utf8');
} catch (e) {
return response.sendStatus(500);
}
// NovelAI Settings
const { fileContents: novelai_settings, fileNames: novelai_setting_names }
= readPresetsFromDirectory(request.user.directories.novelAI_Settings, {
sortFunction: sortByName(request.user.directories.novelAI_Settings),
removeFileExtension: true,
});
// OpenAI Settings
const { fileContents: openai_settings, fileNames: openai_setting_names }
= readPresetsFromDirectory(request.user.directories.openAI_Settings, {
sortFunction: sortByName(request.user.directories.openAI_Settings), removeFileExtension: true,
});
// TextGenerationWebUI Settings
const { fileContents: textgenerationwebui_presets, fileNames: textgenerationwebui_preset_names }
= readPresetsFromDirectory(request.user.directories.textGen_Settings, {
sortFunction: sortByName(request.user.directories.textGen_Settings), removeFileExtension: true,
});
//Kobold
const { fileContents: koboldai_settings, fileNames: koboldai_setting_names }
= readPresetsFromDirectory(request.user.directories.koboldAI_Settings, {
sortFunction: sortByName(request.user.directories.koboldAI_Settings), removeFileExtension: true,
});
const worldFiles = fs
.readdirSync(request.user.directories.worlds)
.filter(file => path.extname(file).toLowerCase() === '.json')
.sort((a, b) => a.localeCompare(b));
const world_names = worldFiles.map(item => path.parse(item).name);
const themes = readAndParseFromDirectory(request.user.directories.themes);
const movingUIPresets = readAndParseFromDirectory(request.user.directories.movingUI);
const quickReplyPresets = readAndParseFromDirectory(request.user.directories.quickreplies);
const instruct = readAndParseFromDirectory(request.user.directories.instruct);
const context = readAndParseFromDirectory(request.user.directories.context);
response.send({
settings,
koboldai_settings,
koboldai_setting_names,
world_names,
novelai_settings,
novelai_setting_names,
openai_settings,
openai_setting_names,
textgenerationwebui_presets,
textgenerationwebui_preset_names,
themes,
movingUIPresets,
quickReplyPresets,
instruct,
context,
enable_extensions: ENABLE_EXTENSIONS,
enable_accounts: ENABLE_ACCOUNTS,
});
});
router.post('/get-snapshots', jsonParser, async (request, response) => {
try {
const snapshots = fs.readdirSync(request.user.directories.backups);
const userFilesPattern = getFilePrefix(request.user.profile.handle);
const userSnapshots = snapshots.filter(x => x.startsWith(userFilesPattern));
const result = userSnapshots.map(x => {
const stat = fs.statSync(path.join(request.user.directories.backups, x));
return { date: stat.ctimeMs, name: x, size: stat.size };
});
response.json(result);
} catch (error) {
console.log(error);
response.sendStatus(500);
}
});
router.post('/load-snapshot', jsonParser, async (request, response) => {
try {
const userFilesPattern = getFilePrefix(request.user.profile.handle);
if (!request.body.name || !request.body.name.startsWith(userFilesPattern)) {
return response.status(400).send({ error: 'Invalid snapshot name' });
}
const snapshotName = request.body.name;
const snapshotPath = path.join(request.user.directories.backups, snapshotName);
if (!fs.existsSync(snapshotPath)) {
return response.sendStatus(404);
}
const content = fs.readFileSync(snapshotPath, 'utf8');
response.send(content);
} catch (error) {
console.log(error);
response.sendStatus(500);
}
});
router.post('/make-snapshot', jsonParser, async (request, response) => {
try {
backupUserSettings(request.user.profile.handle, false);
response.sendStatus(204);
} catch (error) {
console.log(error);
response.sendStatus(500);
}
});
router.post('/restore-snapshot', jsonParser, async (request, response) => {
try {
const userFilesPattern = getFilePrefix(request.user.profile.handle);
if (!request.body.name || !request.body.name.startsWith(userFilesPattern)) {
return response.status(400).send({ error: 'Invalid snapshot name' });
}
const snapshotName = request.body.name;
const snapshotPath = path.join(request.user.directories.backups, snapshotName);
if (!fs.existsSync(snapshotPath)) {
return response.sendStatus(404);
}
const pathToSettings = path.join(request.user.directories.root, SETTINGS_FILE);
fs.rmSync(pathToSettings, { force: true });
fs.copyFileSync(snapshotPath, pathToSettings);
response.sendStatus(204);
} catch (error) {
console.log(error);
response.sendStatus(500);
}
});
/**
* Initializes the settings endpoint
*/
async function init() {
await backupSettings();
}
module.exports = { router, init };