Spaces:
Runtime error
Runtime error
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 }; | |