|
import path from 'node:path'; |
|
import fs from 'node:fs'; |
|
import http2 from 'node:http2'; |
|
import process from 'node:process'; |
|
import { Readable } from 'node:stream'; |
|
import { createRequire } from 'node:module'; |
|
import { Buffer } from 'node:buffer'; |
|
import { promises as dnsPromise } from 'node:dns'; |
|
import os from 'node:os'; |
|
|
|
import yaml from 'yaml'; |
|
import { sync as commandExistsSync } from 'command-exists'; |
|
import _ from 'lodash'; |
|
import yauzl from 'yauzl'; |
|
import mime from 'mime-types'; |
|
import { default as simpleGit } from 'simple-git'; |
|
import chalk from 'chalk'; |
|
import bytes from 'bytes'; |
|
import { LOG_LEVELS } from './constants.js'; |
|
import { serverDirectory } from './server-directory.js'; |
|
|
|
|
|
|
|
|
|
let CACHED_CONFIG = null; |
|
let CONFIG_PATH = null; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_'); |
|
|
|
|
|
|
|
|
|
|
|
export function setConfigFilePath(configFilePath) { |
|
if (CONFIG_PATH !== null) { |
|
console.error(color.red('Config file path already set. Please restart the server to change the config file path.')); |
|
} |
|
CONFIG_PATH = path.resolve(configFilePath); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function getConfig() { |
|
if (CONFIG_PATH === null) { |
|
console.trace(); |
|
console.error(color.red('No config file path set. Please set the config file path using setConfigFilePath().')); |
|
process.exit(1); |
|
} |
|
if (CACHED_CONFIG) { |
|
return CACHED_CONFIG; |
|
} |
|
if (!fs.existsSync(CONFIG_PATH)) { |
|
console.error(color.red('No config file found. Please create a config.yaml file. The default config file can be found in the /default folder.')); |
|
console.error(color.red('The program will now exit.')); |
|
process.exit(1); |
|
} |
|
|
|
try { |
|
const config = yaml.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); |
|
CACHED_CONFIG = config; |
|
return config; |
|
} catch (error) { |
|
console.error(color.red('FATAL: Failed to read config.yaml. Please check the file for syntax errors.')); |
|
console.error(error.message); |
|
process.exit(1); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getConfigValue(key, defaultValue = null, typeConverter = null) { |
|
function _getValue() { |
|
const envKey = keyToEnv(key); |
|
if (envKey in process.env) { |
|
const needsJsonParse = defaultValue && typeof defaultValue === 'object'; |
|
const envValue = process.env[envKey]; |
|
return needsJsonParse ? (tryParse(envValue) ?? defaultValue) : envValue; |
|
} |
|
const config = getConfig(); |
|
return _.get(config, key, defaultValue); |
|
} |
|
|
|
const value = _getValue(); |
|
switch (typeConverter) { |
|
case 'number': |
|
return isNaN(parseFloat(value)) ? defaultValue : parseFloat(value); |
|
case 'boolean': |
|
return toBoolean(value); |
|
default: |
|
return value; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function setConfigValue(_key, _value) { |
|
console.trace(color.yellow('setConfigValue is deprecated and should not be used.')); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getBasicAuthHeader(auth) { |
|
const encoded = Buffer.from(`${auth}`).toString('base64'); |
|
return `Basic ${encoded}`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function getVersion() { |
|
let pkgVersion = 'UNKNOWN'; |
|
let gitRevision = null; |
|
let gitBranch = null; |
|
let commitDate = null; |
|
let isLatest = true; |
|
|
|
try { |
|
const require = createRequire(import.meta.url); |
|
const pkgJson = require(path.join(serverDirectory, './package.json')); |
|
pkgVersion = pkgJson.version; |
|
if (commandExistsSync('git')) { |
|
const git = simpleGit({ baseDir: serverDirectory }); |
|
gitRevision = await git.revparse(['--short', 'HEAD']); |
|
gitBranch = await git.revparse(['--abbrev-ref', 'HEAD']); |
|
commitDate = await git.show(['-s', '--format=%ci', gitRevision]); |
|
|
|
const trackingBranch = await git.revparse(['--abbrev-ref', '@{u}']); |
|
|
|
|
|
const localLatest = await git.revparse(['HEAD']); |
|
const remoteLatest = await git.revparse([trackingBranch]); |
|
isLatest = localLatest === remoteLatest; |
|
} |
|
} |
|
catch { |
|
|
|
} |
|
|
|
const agent = `SillyTavern:${pkgVersion}:Cohee#1207`; |
|
return { agent, pkgVersion, gitRevision, gitBranch, commitDate: commitDate?.trim() ?? null, isLatest }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function delay(ms) { |
|
return new Promise(resolve => setTimeout(resolve, ms)); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getHexString(length) { |
|
const chars = '0123456789abcdef'; |
|
let result = ''; |
|
for (let i = 0; i < length; i++) { |
|
result += chars[Math.floor(Math.random() * chars.length)]; |
|
} |
|
return result; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function formatBytes(bytes) { |
|
if (bytes === 0) return '0 B'; |
|
|
|
const k = 1024; |
|
const sizes = ['B', 'KB', 'MB', 'GB']; |
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function extractFileFromZipBuffer(archiveBuffer, fileExtension) { |
|
return await new Promise((resolve) => { |
|
try { |
|
yauzl.fromBuffer(Buffer.from(archiveBuffer), { lazyEntries: true }, (err, zipfile) => { |
|
if (err) { |
|
console.warn(`Error opening ZIP file: ${err.message}`); |
|
return resolve(null); |
|
} |
|
|
|
zipfile.readEntry(); |
|
|
|
zipfile.on('entry', (entry) => { |
|
if (entry.fileName.endsWith(fileExtension) && !entry.fileName.startsWith('__MACOSX')) { |
|
console.info(`Extracting ${entry.fileName}`); |
|
zipfile.openReadStream(entry, (err, readStream) => { |
|
if (err) { |
|
console.warn(`Error opening read stream: ${err.message}`); |
|
return zipfile.readEntry(); |
|
} else { |
|
const chunks = []; |
|
readStream.on('data', (chunk) => { |
|
chunks.push(chunk); |
|
}); |
|
|
|
readStream.on('end', () => { |
|
const buffer = Buffer.concat(chunks); |
|
resolve(buffer); |
|
zipfile.readEntry(); |
|
}); |
|
|
|
readStream.on('error', (err) => { |
|
console.warn(`Error reading stream: ${err.message}`); |
|
zipfile.readEntry(); |
|
}); |
|
} |
|
}); |
|
} else { |
|
zipfile.readEntry(); |
|
} |
|
}); |
|
|
|
zipfile.on('error', (err) => { |
|
console.warn('ZIP processing error', err); |
|
resolve(null); |
|
}); |
|
|
|
zipfile.on('end', () => resolve(null)); |
|
}); |
|
} catch (error) { |
|
console.warn('Failed to process ZIP buffer', error); |
|
resolve(null); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function getImageBuffers(zipFilePath) { |
|
return new Promise((resolve, reject) => { |
|
|
|
if (!fs.existsSync(zipFilePath)) { |
|
reject(new Error('File not found')); |
|
return; |
|
} |
|
|
|
const imageBuffers = []; |
|
|
|
yauzl.open(zipFilePath, { lazyEntries: true }, (err, zipfile) => { |
|
if (err) { |
|
reject(err); |
|
} else { |
|
zipfile.readEntry(); |
|
zipfile.on('entry', (entry) => { |
|
const mimeType = mime.lookup(entry.fileName); |
|
if (mimeType && mimeType.startsWith('image/') && !entry.fileName.startsWith('__MACOSX')) { |
|
console.info(`Extracting ${entry.fileName}`); |
|
zipfile.openReadStream(entry, (err, readStream) => { |
|
if (err) { |
|
reject(err); |
|
} else { |
|
const chunks = []; |
|
readStream.on('data', (chunk) => { |
|
chunks.push(chunk); |
|
}); |
|
|
|
readStream.on('end', () => { |
|
imageBuffers.push([path.parse(entry.fileName).base, Buffer.concat(chunks)]); |
|
zipfile.readEntry(); |
|
}); |
|
} |
|
}); |
|
} else { |
|
zipfile.readEntry(); |
|
} |
|
}); |
|
|
|
zipfile.on('end', () => { |
|
resolve(imageBuffers); |
|
}); |
|
|
|
zipfile.on('error', (err) => { |
|
reject(err); |
|
}); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function readAllChunks(readableStream) { |
|
return new Promise((resolve, reject) => { |
|
|
|
const chunks = []; |
|
readableStream.on('data', (chunk) => { |
|
chunks.push(chunk); |
|
}); |
|
|
|
readableStream.on('end', () => { |
|
|
|
resolve(chunks); |
|
}); |
|
|
|
readableStream.on('error', (error) => { |
|
console.error('Error while reading the stream:', error); |
|
reject(); |
|
}); |
|
}); |
|
} |
|
|
|
function isObject(item) { |
|
return (item && typeof item === 'object' && !Array.isArray(item)); |
|
} |
|
|
|
export function deepMerge(target, source) { |
|
let output = Object.assign({}, target); |
|
if (isObject(target) && isObject(source)) { |
|
Object.keys(source).forEach(key => { |
|
if (isObject(source[key])) { |
|
if (!(key in target)) { |
|
Object.assign(output, { [key]: source[key] }); |
|
} else { |
|
output[key] = deepMerge(target[key], source[key]); |
|
} |
|
} else { |
|
Object.assign(output, { [key]: source[key] }); |
|
} |
|
}); |
|
} |
|
return output; |
|
} |
|
|
|
export const color = chalk; |
|
|
|
|
|
|
|
|
|
|
|
export function uuidv4() { |
|
if ('crypto' in globalThis && 'randomUUID' in globalThis.crypto) { |
|
return globalThis.crypto.randomUUID(); |
|
} |
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { |
|
const r = Math.random() * 16 | 0; |
|
const v = c === 'x' ? r : (r & 0x3 | 0x8); |
|
return v.toString(16); |
|
}); |
|
} |
|
|
|
export function humanizedISO8601DateTime(date) { |
|
let baseDate = typeof date === 'number' ? new Date(date) : new Date(); |
|
let humanYear = baseDate.getFullYear(); |
|
let humanMonth = (baseDate.getMonth() + 1); |
|
let humanDate = baseDate.getDate(); |
|
let humanHour = (baseDate.getHours() < 10 ? '0' : '') + baseDate.getHours(); |
|
let humanMinute = (baseDate.getMinutes() < 10 ? '0' : '') + baseDate.getMinutes(); |
|
let humanSecond = (baseDate.getSeconds() < 10 ? '0' : '') + baseDate.getSeconds(); |
|
let humanMillisecond = (baseDate.getMilliseconds() < 10 ? '0' : '') + baseDate.getMilliseconds(); |
|
let HumanizedDateTime = (humanYear + '-' + humanMonth + '-' + humanDate + ' @' + humanHour + 'h ' + humanMinute + 'm ' + humanSecond + 's ' + humanMillisecond + 'ms'); |
|
return HumanizedDateTime; |
|
} |
|
|
|
export function tryParse(str) { |
|
try { |
|
return JSON.parse(str); |
|
} catch { |
|
return undefined; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function clientRelativePath(root, inputPath) { |
|
if (!inputPath.startsWith(root)) { |
|
throw new Error('Input path does not start with the root directory'); |
|
} |
|
|
|
return inputPath.slice(root.length).split(path.sep).join('/'); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function removeFileExtension(filename) { |
|
return filename.replace(/\.[^.]+$/, ''); |
|
} |
|
|
|
export function generateTimestamp() { |
|
const now = new Date(); |
|
const year = now.getFullYear(); |
|
const month = String(now.getMonth() + 1).padStart(2, '0'); |
|
const day = String(now.getDate()).padStart(2, '0'); |
|
const hours = String(now.getHours()).padStart(2, '0'); |
|
const minutes = String(now.getMinutes()).padStart(2, '0'); |
|
const seconds = String(now.getSeconds()).padStart(2, '0'); |
|
|
|
return `${year}${month}${day}-${hours}${minutes}${seconds}`; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function removeOldBackups(directory, prefix, limit = null) { |
|
const MAX_BACKUPS = limit ?? Number(getConfigValue('backups.common.numberOfBackups', 50, 'number')); |
|
|
|
let files = fs.readdirSync(directory).filter(f => f.startsWith(prefix)); |
|
if (files.length > MAX_BACKUPS) { |
|
files = files.map(f => path.join(directory, f)); |
|
files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs); |
|
|
|
while (files.length > MAX_BACKUPS) { |
|
const oldest = files.shift(); |
|
if (!oldest) { |
|
break; |
|
} |
|
|
|
fs.unlinkSync(oldest); |
|
} |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getImages(directoryPath, sortBy = 'name') { |
|
function getSortFunction() { |
|
switch (sortBy) { |
|
case 'name': |
|
return Intl.Collator().compare; |
|
case 'date': |
|
return (a, b) => fs.statSync(path.join(directoryPath, a)).mtimeMs - fs.statSync(path.join(directoryPath, b)).mtimeMs; |
|
default: |
|
return (_a, _b) => 0; |
|
} |
|
} |
|
|
|
return fs |
|
.readdirSync(directoryPath) |
|
.filter(file => { |
|
const type = mime.lookup(file); |
|
return type && type.startsWith('image/'); |
|
}) |
|
.sort(getSortFunction()); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function forwardFetchResponse(from, to) { |
|
let statusCode = from.status; |
|
let statusText = from.statusText; |
|
|
|
if (!from.ok) { |
|
console.warn(`Streaming request failed with status ${statusCode} ${statusText}`); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
if (statusCode === 401) { |
|
statusCode = 400; |
|
} |
|
|
|
to.statusCode = statusCode; |
|
to.statusMessage = statusText; |
|
|
|
if (from.body && to.socket) { |
|
from.body.pipe(to); |
|
|
|
to.socket.on('close', function () { |
|
if (from.body instanceof Readable) from.body.destroy(); |
|
|
|
to.end(); |
|
}); |
|
|
|
from.body.on('end', function () { |
|
console.info('Streaming request finished'); |
|
to.end(); |
|
}); |
|
} else { |
|
to.end(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function makeHttp2Request(endpoint, method, body, headers) { |
|
return new Promise((resolve, reject) => { |
|
try { |
|
const url = new URL(endpoint); |
|
const client = http2.connect(url.origin); |
|
|
|
const req = client.request({ |
|
':method': method, |
|
':path': url.pathname, |
|
...headers, |
|
}); |
|
req.setEncoding('utf8'); |
|
|
|
req.on('response', (headers) => { |
|
const status = Number(headers[':status']); |
|
|
|
if (status < 200 || status >= 300) { |
|
reject(new Error(`Request failed with status ${status}`)); |
|
} |
|
|
|
let data = ''; |
|
|
|
req.on('data', (chunk) => { |
|
data += chunk; |
|
}); |
|
|
|
req.on('end', () => { |
|
console.debug(data); |
|
resolve(data); |
|
}); |
|
}); |
|
|
|
req.on('error', (err) => { |
|
reject(err); |
|
}); |
|
|
|
if (body) { |
|
req.write(body); |
|
} |
|
|
|
req.end(); |
|
} catch (e) { |
|
reject(e); |
|
} |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function mergeObjectWithYaml(obj, yamlString) { |
|
if (!yamlString) { |
|
return; |
|
} |
|
|
|
try { |
|
const parsedObject = yaml.parse(yamlString); |
|
|
|
if (Array.isArray(parsedObject)) { |
|
for (const item of parsedObject) { |
|
if (typeof item === 'object' && item && !Array.isArray(item)) { |
|
Object.assign(obj, item); |
|
} |
|
} |
|
} |
|
else if (parsedObject && typeof parsedObject === 'object') { |
|
Object.assign(obj, parsedObject); |
|
} |
|
} catch { |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function excludeKeysByYaml(obj, yamlString) { |
|
if (!yamlString) { |
|
return; |
|
} |
|
|
|
try { |
|
const parsedObject = yaml.parse(yamlString); |
|
|
|
if (Array.isArray(parsedObject)) { |
|
parsedObject.forEach(key => { |
|
delete obj[key]; |
|
}); |
|
} else if (typeof parsedObject === 'object') { |
|
Object.keys(parsedObject).forEach(key => { |
|
delete obj[key]; |
|
}); |
|
} else if (typeof parsedObject === 'string') { |
|
delete obj[parsedObject]; |
|
} |
|
} catch { |
|
|
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function trimV1(str) { |
|
return String(str ?? '').replace(/\/$/, '').replace(/\/v1$/, ''); |
|
} |
|
|
|
|
|
|
|
|
|
export class Cache { |
|
|
|
|
|
|
|
constructor(ttl) { |
|
this.cache = new Map(); |
|
this.ttl = ttl; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
get(key) { |
|
const value = this.cache.get(key); |
|
if (value?.expiry > Date.now()) { |
|
return value.value; |
|
} |
|
|
|
|
|
this.cache.delete(key); |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
set(key, value) { |
|
this.cache.set(key, { |
|
value: value, |
|
expiry: Date.now() + this.ttl, |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
remove(key) { |
|
this.cache.delete(key); |
|
} |
|
|
|
|
|
|
|
|
|
clear() { |
|
this.cache.clear(); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function removeColorFormatting(text) { |
|
|
|
return text.replace(/\x1b\[\d{1,2}(;\d{1,2})*m/g, ''); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getSeparator(n) { |
|
return '='.repeat(n); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function isValidUrl(url) { |
|
try { |
|
new URL(url); |
|
return true; |
|
} catch (error) { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function urlHostnameToIPv6(hostname) { |
|
if (hostname.startsWith('[')) { |
|
hostname = hostname.slice(1); |
|
} |
|
if (hostname.endsWith(']')) { |
|
hostname = hostname.slice(0, -1); |
|
} |
|
return hostname; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function canResolve(name, useIPv6 = true, useIPv4 = true) { |
|
try { |
|
let v6Resolved = false; |
|
let v4Resolved = false; |
|
|
|
if (useIPv6) { |
|
try { |
|
await dnsPromise.resolve6(name); |
|
v6Resolved = true; |
|
} catch (error) { |
|
v6Resolved = false; |
|
} |
|
} |
|
|
|
if (useIPv4) { |
|
try { |
|
await dnsPromise.resolve(name); |
|
v4Resolved = true; |
|
} catch (error) { |
|
v4Resolved = false; |
|
} |
|
} |
|
|
|
return v6Resolved || v4Resolved; |
|
|
|
} catch (error) { |
|
return false; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function getHasIP() { |
|
let hasIPv6Any = false; |
|
let hasIPv6Local = false; |
|
|
|
let hasIPv4Any = false; |
|
let hasIPv4Local = false; |
|
|
|
const interfaces = os.networkInterfaces(); |
|
|
|
for (const iface of Object.values(interfaces)) { |
|
if (iface === undefined) { |
|
continue; |
|
} |
|
|
|
for (const info of iface) { |
|
if (info.family === 'IPv6') { |
|
hasIPv6Any = true; |
|
if (info.address === '::1') { |
|
hasIPv6Local = true; |
|
} |
|
} |
|
|
|
if (info.family === 'IPv4') { |
|
hasIPv4Any = true; |
|
if (info.address === '127.0.0.1') { |
|
hasIPv4Local = true; |
|
} |
|
} |
|
if (hasIPv6Any && hasIPv4Any && hasIPv6Local && hasIPv4Local) break; |
|
} |
|
if (hasIPv6Any && hasIPv4Any && hasIPv6Local && hasIPv4Local) break; |
|
} |
|
|
|
return { hasIPv6Any, hasIPv4Any, hasIPv6Local, hasIPv4Local }; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function toBoolean(value) { |
|
|
|
if (typeof value === 'string') { |
|
|
|
const trimmedLower = value.trim().toLowerCase(); |
|
|
|
|
|
if (trimmedLower === 'true') return true; |
|
if (trimmedLower === 'false') return false; |
|
} |
|
|
|
|
|
return Boolean(value); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
export function stringToBool(str) { |
|
if (String(str).trim().toLowerCase() === 'true') return true; |
|
if (String(str).trim().toLowerCase() === 'false') return false; |
|
return str; |
|
} |
|
|
|
|
|
|
|
|
|
export function setupLogLevel() { |
|
const logLevel = getConfigValue('logging.minLogLevel', LOG_LEVELS.DEBUG, 'number'); |
|
|
|
globalThis.console.debug = logLevel <= LOG_LEVELS.DEBUG ? console.debug : () => { }; |
|
globalThis.console.info = logLevel <= LOG_LEVELS.INFO ? console.info : () => { }; |
|
globalThis.console.warn = logLevel <= LOG_LEVELS.WARN ? console.warn : () => { }; |
|
globalThis.console.error = logLevel <= LOG_LEVELS.ERROR ? console.error : () => { }; |
|
} |
|
|
|
|
|
|
|
|
|
export class MemoryLimitedMap { |
|
|
|
|
|
|
|
|
|
constructor(cacheCapacity) { |
|
this.maxMemory = bytes.parse(cacheCapacity) ?? 0; |
|
this.currentMemory = 0; |
|
this.map = new Map(); |
|
this.queue = []; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static estimateStringSize(str) { |
|
return str ? str.length * 2 : 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
set(key, value) { |
|
if (this.maxMemory <= 0) { |
|
return; |
|
} |
|
|
|
if (typeof key !== 'string' || typeof value !== 'string') { |
|
return; |
|
} |
|
|
|
const newValueSize = MemoryLimitedMap.estimateStringSize(value); |
|
|
|
|
|
if (newValueSize > this.maxMemory) { |
|
return; |
|
} |
|
|
|
|
|
if (this.map.has(key)) { |
|
const oldValue = this.map.get(key); |
|
const oldValueSize = MemoryLimitedMap.estimateStringSize(oldValue); |
|
this.currentMemory -= oldValueSize; |
|
|
|
const index = this.queue.indexOf(key); |
|
if (index > -1) { |
|
this.queue.splice(index, 1); |
|
} |
|
} |
|
|
|
|
|
while (this.currentMemory + newValueSize > this.maxMemory && this.queue.length > 0) { |
|
const oldestKey = this.queue.shift(); |
|
const oldestValue = this.map.get(oldestKey); |
|
const oldestValueSize = MemoryLimitedMap.estimateStringSize(oldestValue); |
|
this.map.delete(oldestKey); |
|
this.currentMemory -= oldestValueSize; |
|
} |
|
|
|
|
|
if (this.currentMemory + newValueSize > this.maxMemory) { |
|
return; |
|
} |
|
|
|
|
|
this.map.set(key, value); |
|
this.queue.push(key); |
|
this.currentMemory += newValueSize; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
get(key) { |
|
return this.map.get(key); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
has(key) { |
|
return this.map.has(key); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
delete(key) { |
|
if (!this.map.has(key)) { |
|
return false; |
|
} |
|
const value = this.map.get(key); |
|
const valueSize = MemoryLimitedMap.estimateStringSize(value); |
|
this.map.delete(key); |
|
this.currentMemory -= valueSize; |
|
|
|
|
|
const index = this.queue.indexOf(key); |
|
if (index > -1) { |
|
this.queue.splice(index, 1); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
|
|
|
|
|
|
clear() { |
|
this.map.clear(); |
|
this.queue = []; |
|
this.currentMemory = 0; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
size() { |
|
return this.map.size; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
totalMemory() { |
|
return this.currentMemory; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
keys() { |
|
return this.map.keys(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
values() { |
|
return this.map.values(); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
forEach(callback) { |
|
this.map.forEach((value, key) => { |
|
callback(value, key, this); |
|
}); |
|
} |
|
|
|
|
|
|
|
|
|
|
|
[Symbol.iterator]() { |
|
return this.map[Symbol.iterator](); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function safeReadFileSync(filePath, options = { encoding: 'utf-8' }) { |
|
if (fs.existsSync(filePath)) return fs.readFileSync(filePath, options); |
|
return null; |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function setWindowTitle(title) { |
|
if (process.platform === 'win32') { |
|
process.title = title; |
|
} |
|
else { |
|
process.stdout.write(`\x1b]2;${title}\x1b\x5c`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function mutateJsonString(jsonString, mutation) { |
|
try { |
|
const json = JSON.parse(jsonString); |
|
mutation(json); |
|
return JSON.stringify(json); |
|
} catch (error) { |
|
console.error('Error parsing or mutating JSON:', error); |
|
return jsonString; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export function setPermissionsSync(targetPath) { |
|
|
|
|
|
|
|
|
|
|
|
function appendWritablePermission(filePath, stats) { |
|
const currentMode = stats.mode; |
|
const newMode = currentMode | 0o200; |
|
if (newMode != currentMode) { |
|
fs.chmodSync(filePath, newMode); |
|
} |
|
} |
|
|
|
try { |
|
const stats = fs.statSync(targetPath); |
|
|
|
if (stats.isDirectory()) { |
|
appendWritablePermission(targetPath, stats); |
|
const files = fs.readdirSync(targetPath); |
|
|
|
files.forEach((file) => { |
|
setPermissionsSync(path.join(targetPath, file)); |
|
}); |
|
} else { |
|
appendWritablePermission(targetPath, stats); |
|
} |
|
} catch (error) { |
|
console.error(`Error setting write permissions for ${targetPath}:`, error); |
|
} |
|
} |
|
|