Initial commit
This commit is contained in:
153
remote/src/config/index.js
Normal file
153
remote/src/config/index.js
Normal file
@@ -0,0 +1,153 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { validate, getDefaults, mergeWithDefaults } = require('./schema');
|
||||
const paths = require('../utils/paths');
|
||||
|
||||
let cachedConfig = null;
|
||||
|
||||
function getConfigPath() {
|
||||
const configDir = paths.getConfigPath();
|
||||
return path.join(configDir, 'default.json');
|
||||
}
|
||||
|
||||
function loadConfigFile() {
|
||||
const configPath = getConfigPath();
|
||||
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
const parsed = JSON.parse(content);
|
||||
return parsed;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load config file: ${error.message}`);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
function getEnvOverrides() {
|
||||
const overrides = {};
|
||||
const envPrefix = 'REMOTE_';
|
||||
|
||||
for (const [envKey, envValue] of Object.entries(process.env)) {
|
||||
if (!envKey.startsWith(envPrefix)) continue;
|
||||
|
||||
const parts = envKey.slice(envPrefix.length).split('_');
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const section = parts[0].toLowerCase();
|
||||
const configKey = parts.slice(1).join('_').toLowerCase();
|
||||
|
||||
if (!overrides[section]) {
|
||||
overrides[section] = {};
|
||||
}
|
||||
|
||||
let parsedValue = envValue;
|
||||
|
||||
if (envValue === 'true') {
|
||||
parsedValue = true;
|
||||
} else if (envValue === 'false') {
|
||||
parsedValue = false;
|
||||
} else if (!isNaN(envValue) && envValue !== '') {
|
||||
parsedValue = parseFloat(envValue);
|
||||
if (Number.isInteger(parsedValue)) {
|
||||
parsedValue = parseInt(envValue, 10);
|
||||
}
|
||||
}
|
||||
|
||||
overrides[section][configKey] = parsedValue;
|
||||
}
|
||||
|
||||
return overrides;
|
||||
}
|
||||
|
||||
function applyEnvOverrides(config, envOverrides) {
|
||||
const result = { ...config };
|
||||
|
||||
for (const [section, values] of Object.entries(envOverrides)) {
|
||||
if (!result[section]) {
|
||||
result[section] = {};
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
result[section][key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
if (cachedConfig) {
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
const defaults = getDefaults();
|
||||
const fileConfig = loadConfigFile();
|
||||
const envOverrides = getEnvOverrides();
|
||||
|
||||
let merged = mergeWithDefaults(fileConfig);
|
||||
merged = applyEnvOverrides(merged, envOverrides);
|
||||
|
||||
const validation = validate(merged);
|
||||
if (!validation.valid) {
|
||||
console.warn('Config validation warnings:', validation.errors);
|
||||
}
|
||||
|
||||
cachedConfig = merged;
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
function get(key, defaultValue = undefined) {
|
||||
const config = loadConfig();
|
||||
|
||||
if (!key) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const keys = key.split('.');
|
||||
let result = config;
|
||||
|
||||
for (const k of keys) {
|
||||
if (result && typeof result === 'object' && k in result) {
|
||||
result = result[k];
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSection(section) {
|
||||
const config = loadConfig();
|
||||
|
||||
if (!section) {
|
||||
return config;
|
||||
}
|
||||
|
||||
return config[section] || null;
|
||||
}
|
||||
|
||||
function getAll() {
|
||||
return loadConfig();
|
||||
}
|
||||
|
||||
function reload() {
|
||||
cachedConfig = null;
|
||||
return loadConfig();
|
||||
}
|
||||
|
||||
function clearCache() {
|
||||
cachedConfig = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get,
|
||||
getSection,
|
||||
getAll,
|
||||
reload,
|
||||
clearCache,
|
||||
validate
|
||||
};
|
||||
201
remote/src/config/schema.js
Normal file
201
remote/src/config/schema.js
Normal file
@@ -0,0 +1,201 @@
|
||||
const defaultConfig = {
|
||||
server: {
|
||||
port: 3000,
|
||||
host: '0.0.0.0'
|
||||
},
|
||||
stream: {
|
||||
fps: 30,
|
||||
bitrate: '4000k',
|
||||
gop: 10,
|
||||
preset: 'ultrafast',
|
||||
resolution: {
|
||||
width: 1920,
|
||||
height: 1080
|
||||
}
|
||||
},
|
||||
input: {
|
||||
mouseEnabled: true,
|
||||
keyboardEnabled: true,
|
||||
sensitivity: 1.0
|
||||
},
|
||||
security: {
|
||||
password: '',
|
||||
tokenExpiry: 3600
|
||||
},
|
||||
frp: {
|
||||
enabled: true
|
||||
},
|
||||
gitea: {
|
||||
enabled: true
|
||||
}
|
||||
};
|
||||
|
||||
const schema = {
|
||||
server: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
port: { type: 'number', required: true, min: 1, max: 65535 },
|
||||
host: { type: 'string', required: true }
|
||||
}
|
||||
},
|
||||
stream: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fps: { type: 'number', required: true, min: 1, max: 120 },
|
||||
bitrate: { type: 'string', required: true },
|
||||
gop: { type: 'number', required: true, min: 1 },
|
||||
preset: { type: 'string', required: true },
|
||||
resolution: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
width: { type: 'number', required: true, min: 1 },
|
||||
height: { type: 'number', required: true, min: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
mouseEnabled: { type: 'boolean', required: true },
|
||||
keyboardEnabled: { type: 'boolean', required: true },
|
||||
sensitivity: { type: 'number', required: true, min: 0.1, max: 10 }
|
||||
}
|
||||
},
|
||||
security: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string', required: false },
|
||||
tokenExpiry: { type: 'number', required: true, min: 60 }
|
||||
}
|
||||
},
|
||||
frp: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', required: true }
|
||||
}
|
||||
},
|
||||
gitea: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean', required: true }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function validateType(value, expectedType) {
|
||||
if (expectedType === 'number') {
|
||||
return typeof value === 'number' && !isNaN(value);
|
||||
}
|
||||
if (expectedType === 'string') {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
if (expectedType === 'boolean') {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
if (expectedType === 'object') {
|
||||
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function validateField(value, fieldSchema, fieldPath) {
|
||||
const errors = [];
|
||||
|
||||
if (!validateType(value, fieldSchema.type)) {
|
||||
errors.push(`${fieldPath}: expected type ${fieldSchema.type}, got ${typeof value}`);
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (fieldSchema.type === 'number') {
|
||||
if (fieldSchema.min !== undefined && value < fieldSchema.min) {
|
||||
errors.push(`${fieldPath}: value ${value} is less than minimum ${fieldSchema.min}`);
|
||||
}
|
||||
if (fieldSchema.max !== undefined && value > fieldSchema.max) {
|
||||
errors.push(`${fieldPath}: value ${value} is greater than maximum ${fieldSchema.max}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldSchema.type === 'object' && fieldSchema.properties) {
|
||||
const nestedErrors = validateObject(value, fieldSchema, fieldPath);
|
||||
errors.push(...nestedErrors);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateObject(obj, objectSchema, basePath) {
|
||||
const errors = [];
|
||||
|
||||
if (!objectSchema.properties) return errors;
|
||||
|
||||
for (const [key, fieldSchema] of Object.entries(objectSchema.properties)) {
|
||||
const fieldPath = basePath ? `${basePath}.${key}` : key;
|
||||
|
||||
if (!(key in obj)) {
|
||||
if (fieldSchema.required) {
|
||||
errors.push(`${fieldPath}: required field is missing`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const fieldErrors = validateField(obj[key], fieldSchema, fieldPath);
|
||||
errors.push(...fieldErrors);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validate(config) {
|
||||
const errors = [];
|
||||
|
||||
if (!config || typeof config !== 'object') {
|
||||
return { valid: false, errors: ['Config must be a non-null object'] };
|
||||
}
|
||||
|
||||
for (const [sectionKey, sectionSchema] of Object.entries(schema)) {
|
||||
if (!(sectionKey in config)) {
|
||||
errors.push(`${sectionKey}: required section is missing`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const sectionErrors = validateObject(config[sectionKey], sectionSchema, sectionKey);
|
||||
errors.push(...sectionErrors);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaults() {
|
||||
return JSON.parse(JSON.stringify(defaultConfig));
|
||||
}
|
||||
|
||||
function mergeWithDefaults(config) {
|
||||
const defaults = getDefaults();
|
||||
return deepMerge(defaults, config);
|
||||
}
|
||||
|
||||
function deepMerge(target, source) {
|
||||
const result = { ...target };
|
||||
|
||||
for (const key of Object.keys(source)) {
|
||||
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
||||
result[key] = deepMerge(target[key], source[key]);
|
||||
} else {
|
||||
result[key] = source[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
schema,
|
||||
defaultConfig,
|
||||
validate,
|
||||
getDefaults,
|
||||
mergeWithDefaults
|
||||
};
|
||||
Reference in New Issue
Block a user