| @@ -20,10 +20,28 @@ import open from 'open'; | |||
| 20 | 20 | ||
| 21 | // local library imports | 21 | // local library imports |
| 22 | import './src/fetch-patch.js'; | 22 | import './src/fetch-patch.js'; |
| 23 | import { serverEvents, EVENT_NAMES } from './src/server-events.js'; | ||
| 24 | import { CommandLineParser } from './src/command-line.js'; | 23 | import { CommandLineParser } from './src/command-line.js'; |
| 25 | import { loadPlugins } from './src/plugin-loader.js'; | 24 | import { serverDirectory } from './src/server-directory.js'; |
| 26 | import { | 25 | |
| 26 | console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); | ||
| 27 | |||
| 28 | // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. | ||
| 29 | // https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 | ||
| 30 | // Safe to remove once support for Node v20 is dropped. | ||
| 31 | if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { | ||
| 32 | // @ts-ignore | ||
| 33 | if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); | ||
| 34 | } | ||
| 35 | |||
| 36 | // config.yaml will be set when parsing command line arguments | ||
| 37 | const cliArgs = new CommandLineParser().parse(process.argv); | ||
| 38 | globalThis.DATA_ROOT = cliArgs.dataRoot; | ||
| 39 | globalThis.COMMAND_LINE_ARGS = cliArgs; | ||
| 40 | process.chdir(serverDirectory); | ||
| 41 | |||
| 42 | const { serverEvents, EVENT_NAMES } = await import('./src/server-events.js'); | ||
| 43 | const { loadPlugins } = await import('./src/plugin-loader.js'); | ||
| 44 | const { | ||
| 27 | initUserStorage, | 45 | initUserStorage, |
| 28 | getCookieSecret, | 46 | getCookieSecret, |
| 29 | getCookieSessionName, | 47 | getCookieSessionName, |
| @@ -38,17 +56,17 @@ import { | |||
| 38 | getSessionCookieAge, | 56 | getSessionCookieAge, |
| 39 | verifySecuritySettings, | 57 | verifySecuritySettings, |
| 40 | loginPageMiddleware, | 58 | loginPageMiddleware, |
| 41 | } from './src/users.js'; | 59 | } = await import('./src/users.js'); |
| 42 | 60 | ||
| 43 | import getWebpackServeMiddleware from './src/middleware/webpack-serve.js'; | 61 | const { default: getWebpackServeMiddleware } = await import('./src/middleware/webpack-serve.js'); |
| 44 | import basicAuthMiddleware from './src/middleware/basicAuth.js'; | 62 | const { default: basicAuthMiddleware } = await import('./src/middleware/basicAuth.js'); |
| 45 | import getWhitelistMiddleware from './src/middleware/whitelist.js'; | 63 | const { default: getWhitelistMiddleware } = await import('./src/middleware/whitelist.js'); |
| 46 | import accessLoggerMiddleware, { getAccessLogPath, migrateAccessLog } from './src/middleware/accessLogWriter.js'; | 64 | const { default: accessLoggerMiddleware, getAccessLogPath, migrateAccessLog } = await import('./src/middleware/accessLogWriter.js'); |
| 47 | import multerMonkeyPatch from './src/middleware/multerMonkeyPatch.js'; | 65 | const { default: multerMonkeyPatch } = await import('./src/middleware/multerMonkeyPatch.js'); |
| 48 | import initRequestProxy from './src/request-proxy.js'; | 66 | const { default: initRequestProxy } = await import('./src/request-proxy.js'); |
| 49 | import getCacheBusterMiddleware from './src/middleware/cacheBuster.js'; | 67 | const { default: getCacheBusterMiddleware } = await import('./src/middleware/cacheBuster.js'); |
| 50 | import corsProxyMiddleware from './src/middleware/corsProxy.js'; | 68 | const { default: corsProxyMiddleware } = await import('./src/middleware/corsProxy.js'); |
| 51 | import { | 69 | const { |
| 52 | getVersion, | 70 | getVersion, |
| 53 | color, | 71 | color, |
| 54 | removeColorFormatting, | 72 | removeColorFormatting, |
| @@ -56,38 +74,23 @@ import { | |||
| 56 | safeReadFileSync, | 74 | safeReadFileSync, |
| 57 | setupLogLevel, | 75 | setupLogLevel, |
| 58 | setWindowTitle, | 76 | setWindowTitle, |
| 59 | } from './src/util.js'; | 77 | } = await import('./src/util.js'); |
| 60 | import { UPLOADS_DIRECTORY } from './src/constants.js'; | 78 | const { UPLOADS_DIRECTORY } = await import('./src/constants.js'); |
| 61 | import { ensureThumbnailCache } from './src/endpoints/thumbnails.js'; | 79 | const { ensureThumbnailCache } = await import('./src/endpoints/thumbnails.js'); |
| 62 | import { serverDirectory } from './src/server-directory.js'; | ||
| 63 | 80 | ||
| 64 | // Routers | 81 | // Routers |
| 65 | import { router as usersPublicRouter } from './src/endpoints/users-public.js'; | 82 | const { router : usersPublicRouter } = await import('./src/endpoints/users-public.js'); |
| 66 | import { init as statsInit, onExit as statsOnExit } from './src/endpoints/stats.js'; | 83 | const { init : statsInit, onExit : statsOnExit } = await import('./src/endpoints/stats.js'); |
| 67 | import { checkForNewContent } from './src/endpoints/content-manager.js'; | 84 | const { checkForNewContent } = await import('./src/endpoints/content-manager.js'); |
| 68 | import { init as settingsInit } from './src/endpoints/settings.js'; | 85 | const { init : settingsInit } = await import('./src/endpoints/settings.js'); |
| 69 | import { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } from './src/server-startup.js'; | 86 | const { redirectDeprecatedEndpoints, ServerStartup, setupPrivateEndpoints } = await import('./src/server-startup.js'); |
| 70 | import { diskCache } from './src/endpoints/characters.js'; | 87 | const { diskCache } = await import('./src/endpoints/characters.js'); |
| 71 | 88 | ||
| 72 | // Unrestrict console logs display limit | 89 | // Unrestrict console logs display limit |
| 73 | util.inspect.defaultOptions.maxArrayLength = null; | 90 | util.inspect.defaultOptions.maxArrayLength = null; |
| 74 | util.inspect.defaultOptions.maxStringLength = null; | 91 | util.inspect.defaultOptions.maxStringLength = null; |
| 75 | util.inspect.defaultOptions.depth = 4; | 92 | util.inspect.defaultOptions.depth = 4; |
| 76 | 93 | ||
| 77 | console.log(`Node version: ${process.version}. Running in ${process.env.NODE_ENV} environment. Server directory: ${serverDirectory}`); | ||
| 78 | |||
| 79 | // Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed in v20.3.0. | ||
| 80 | // https://github.com/nodejs/node/issues/47822#issuecomment-1564708870 | ||
| 81 | // Safe to remove once support for Node v20 is dropped. | ||
| 82 | if (process.versions && process.versions.node && process.versions.node.match(/20\.[0-2]\.0/)) { | ||
| 83 | // @ts-ignore | ||
| 84 | if (net.setDefaultAutoSelectFamily) net.setDefaultAutoSelectFamily(false); | ||
| 85 | } | ||
| 86 | |||
| 87 | const cliArgs = new CommandLineParser().parse(process.argv); | ||
| 88 | globalThis.DATA_ROOT = cliArgs.dataRoot; | ||
| 89 | globalThis.COMMAND_LINE_ARGS = cliArgs; | ||
| 90 | |||
| 91 | if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) { | 94 | if (!cliArgs.enableIPv6 && !cliArgs.enableIPv4) { |
| 92 | console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); | 95 | console.error('error: You can\'t disable all internet protocols: at least IPv6 or IPv4 must be enabled.'); |
| 93 | process.exit(1); | 96 | process.exit(1); |
| @@ -1,10 +1,11 @@ | |||
| 1 | import yargs from 'yargs/yargs'; | 1 | import yargs from 'yargs/yargs'; |
| 2 | import { hideBin } from 'yargs/helpers'; | 2 | import { hideBin } from 'yargs/helpers'; |
| 3 | import ipRegex from 'ip-regex'; | 3 | import ipRegex from 'ip-regex'; |
| 4 | import { canResolve, color, getConfigValue, stringToBool } from './util.js'; | 4 | import { canResolve, color, getConfigValue, setConfigFilePath, stringToBool } from './util.js'; |
| 5 | 5 | ||
| 6 | /** | 6 | /** |
| 7 | * @typedef {object} CommandLineArguments Parsed command line arguments | 7 | * @typedef {object} CommandLineArguments Parsed command line arguments |
| 8 | * @property {string} configPath Path to the config file | ||
| 8 | * @property {string} dataRoot Data root directory | 9 | * @property {string} dataRoot Data root directory |
| 9 | * @property {number} port Port number | 10 | * @property {number} port Port number |
| 10 | * @property {boolean} listen If SillyTavern is listening on all network interfaces | 11 | * @property {boolean} listen If SillyTavern is listening on all network interfaces |
| @@ -40,6 +41,7 @@ export class CommandLineParser { | |||
| 40 | constructor() { | 41 | constructor() { |
| 41 | /** @type {CommandLineArguments} */ | 42 | /** @type {CommandLineArguments} */ |
| 42 | this.default = Object.freeze({ | 43 | this.default = Object.freeze({ |
| 44 | configPath: './config.yaml', | ||
| 43 | dataRoot: './data', | 45 | dataRoot: './data', |
| 44 | port: 8000, | 46 | port: 8000, |
| 45 | listen: false, | 47 | listen: false, |
| @@ -88,6 +90,11 @@ export class CommandLineParser { | |||
| 88 | parse(args) { | 90 | parse(args) { |
| 89 | const cliArguments = yargs(hideBin(args)) | 91 | const cliArguments = yargs(hideBin(args)) |
| 90 | .usage('Usage: <your-start-script> [options]\nOptions that are not provided will be filled with config values.') | 92 | .usage('Usage: <your-start-script> [options]\nOptions that are not provided will be filled with config values.') |
| 93 | .option('configPath', { | ||
| 94 | type: 'string', | ||
| 95 | default: null, | ||
| 96 | describe: 'Path to the config file', | ||
| 97 | }) | ||
| 91 | .option('enableIPv6', { | 98 | .option('enableIPv6', { |
| 92 | type: 'string', | 99 | type: 'string', |
| 93 | default: null, | 100 | default: null, |
| @@ -177,8 +184,11 @@ export class CommandLineParser { | |||
| 177 | describe: 'Request proxy bypass list (space separated list of hosts)', | 184 | describe: 'Request proxy bypass list (space separated list of hosts)', |
| 178 | }).parseSync(); | 185 | }).parseSync(); |
| 179 | 186 | ||
| 187 | const configPath = cliArguments.configPath ?? this.default.configPath; | ||
| 188 | setConfigFilePath(configPath); | ||
| 180 | /** @type {CommandLineArguments} */ | 189 | /** @type {CommandLineArguments} */ |
| 181 | const result = { | 190 | const result = { |
| 191 | configPath: configPath, | ||
| 182 | dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot), | 192 | dataRoot: cliArguments.dataRoot ?? getConfigValue('dataRoot', this.default.dataRoot), |
| 183 | port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'), | 193 | port: cliArguments.port ?? getConfigValue('port', this.default.port, 'number'), |
| 184 | listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'), | 194 | listen: cliArguments.listen ?? getConfigValue('listen', this.default.listen, 'boolean'), |
| @@ -23,6 +23,7 @@ import { serverDirectory } from './server-directory.js'; | |||
| 23 | * Parsed config object. | 23 | * Parsed config object. |
| 24 | */ | 24 | */ |
| 25 | let CACHED_CONFIG = null; | 25 | let CACHED_CONFIG = null; |
| 26 | let CONFIG_FILE = null; | ||
| 26 | 27 | ||
| 27 | /** | 28 | /** |
| 28 | * Converts a configuration key to an environment variable key. | 29 | * Converts a configuration key to an environment variable key. |
| @@ -33,22 +34,37 @@ let CACHED_CONFIG = null; | |||
| 33 | export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_'); | 34 | export const keyToEnv = (key) => 'SILLYTAVERN_' + String(key).toUpperCase().replace(/\./g, '_'); |
| 34 | 35 | ||
| 35 | /** | 36 | /** |
| 37 | * Set the config file path. | ||
| 38 | * @param {string} configFilePath Path to the config file | ||
| 39 | */ | ||
| 40 | export function setConfigFilePath(configFilePath) { | ||
| 41 | if (CONFIG_FILE !== null) { | ||
| 42 | console.error(color.red('Config file path already set. Please restart the server to change the config file path.')); | ||
| 43 | } | ||
| 44 | CONFIG_FILE = path.resolve(configFilePath); | ||
| 45 | } | ||
| 46 | |||
| 47 | /** | ||
| 36 | * Returns the config object from the config.yaml file. | 48 | * Returns the config object from the config.yaml file. |
| 37 | * @returns {object} Config object | 49 | * @returns {object} Config object |
| 38 | */ | 50 | */ |
| 39 | export function getConfig() { | 51 | export function getConfig() { |
| 52 | if (CONFIG_FILE === null) { | ||
| 53 | console.trace(); | ||
| 54 | console.error(color.red('No config file path set. Please set the config file path using setConfigFilePath().')); | ||
| 55 | process.exit(1); | ||
| 56 | } | ||
| 40 | if (CACHED_CONFIG) { | 57 | if (CACHED_CONFIG) { |
| 41 | return CACHED_CONFIG; | 58 | return CACHED_CONFIG; |
| 42 | } | 59 | } |
| 43 | 60 | if (!fs.existsSync(CONFIG_FILE)) { | |
| 44 | if (!fs.existsSync('./config.yaml')) { | ||
| 45 | 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.')); | 61 | 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.')); |
| 46 | console.error(color.red('The program will now exit.')); | 62 | console.error(color.red('The program will now exit.')); |
| 47 | process.exit(1); | 63 | process.exit(1); |
| 48 | } | 64 | } |
| 49 | 65 | ||
| 50 | try { | 66 | try { |
| 51 | const config = yaml.parse(fs.readFileSync(path.join(process.cwd(), './config.yaml'), 'utf8')); | 67 | const config = yaml.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); |
| 52 | CACHED_CONFIG = config; | 68 | CACHED_CONFIG = config; |
| 53 | return config; | 69 | return config; |
| 54 | } catch (error) { | 70 | } catch (error) { |