feat(backup): 添加远端备份功能并优化备份管理
This commit is contained in:
@@ -7,6 +7,8 @@ const cron = require('node-cron');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { createBackup, createIncrementalBackup, getBackupPath } = require('./backup');
|
||||
const { uploadToRemote } = require('./remoteBackup');
|
||||
const { getEnabledTargets, getGlobalSettings } = require('./remoteBackupConfig');
|
||||
|
||||
// 全局调度器存储
|
||||
const schedulers = new Map();
|
||||
@@ -108,6 +110,10 @@ function createAutoBackupTask(settings) {
|
||||
if (result) {
|
||||
console.log('自动备份完成:', result.filename);
|
||||
console.log(`备份类型:${result.isIncremental ? '增量备份' : '全量备份'}`);
|
||||
|
||||
// 上传到远端
|
||||
await uploadToRemoteTargets(result.path, result.filename);
|
||||
|
||||
console.log('========================\n');
|
||||
} else {
|
||||
console.log('无数据变化,跳过备份');
|
||||
@@ -234,6 +240,69 @@ function updateAutoBackupSettings(newSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传备份到所有启用的远端目标
|
||||
*/
|
||||
async function uploadToRemoteTargets(localFilePath, filename) {
|
||||
const globalSettings = getGlobalSettings();
|
||||
|
||||
if (!globalSettings.enabled || !globalSettings.uploadAfterBackup) {
|
||||
console.log('远端备份已禁用');
|
||||
return [];
|
||||
}
|
||||
|
||||
const enabledTargets = getEnabledTargets();
|
||||
|
||||
if (enabledTargets.length === 0) {
|
||||
console.log('没有启用的远端备份目标');
|
||||
return [];
|
||||
}
|
||||
|
||||
const uploadResults = [];
|
||||
|
||||
for (const target of enabledTargets) {
|
||||
try {
|
||||
console.log(`开始上传到目标:${target.name} (${target.protocol})`);
|
||||
|
||||
const remotePath = `${target.prefix || 'backups/'}${filename}`;
|
||||
|
||||
const result = await uploadToRemote(target, localFilePath, remotePath);
|
||||
|
||||
uploadResults.push({
|
||||
targetId: target.id,
|
||||
targetName: target.name,
|
||||
protocol: target.protocol,
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
|
||||
console.log(`上传到 ${target.name} 成功`);
|
||||
} catch (error) {
|
||||
console.error(`上传到 ${target.name} 失败:`, error.message);
|
||||
uploadResults.push({
|
||||
targetId: target.id,
|
||||
targetName: target.name,
|
||||
protocol: target.protocol,
|
||||
success: false,
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要删除本地文件
|
||||
const settings = getGlobalSettings();
|
||||
if (settings.deleteLocalAfterUpload && uploadResults.every(r => r.success)) {
|
||||
try {
|
||||
fs.unlinkSync(localFilePath);
|
||||
console.log('本地备份文件已删除');
|
||||
} catch (error) {
|
||||
console.error('删除本地备份文件失败:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return uploadResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即执行一次备份
|
||||
*/
|
||||
@@ -251,8 +320,16 @@ async function executeBackupNow(options = {}) {
|
||||
});
|
||||
|
||||
console.log('手动备份完成:', result.filename);
|
||||
|
||||
// 上传到远端
|
||||
const uploadResults = await uploadToRemoteTargets(result.path, result.filename);
|
||||
|
||||
console.log('====================\n');
|
||||
return { success: true, result };
|
||||
return {
|
||||
success: true,
|
||||
result,
|
||||
remoteUploads: uploadResults,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('手动备份失败:', error);
|
||||
console.error('====================\n');
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 远端备份上传工具模块
|
||||
* 支持 FTP、SFTP、WebDAV、SMB 等协议
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
|
||||
// 协议类型枚举
|
||||
const PROTOCOL_TYPES = {
|
||||
FTP: 'ftp',
|
||||
SFTP: 'sftp',
|
||||
WEBDAV: 'webdav',
|
||||
SMB: 'smb',
|
||||
};
|
||||
|
||||
// 协议显示名称映射
|
||||
const PROTOCOL_LABELS = {
|
||||
[PROTOCOL_TYPES.FTP]: 'FTP',
|
||||
[PROTOCOL_TYPES.SFTP]: 'SFTP (SSH 文件传输)',
|
||||
[PROTOCOL_TYPES.WEBDAV]: 'WebDAV',
|
||||
[PROTOCOL_TYPES.SMB]: 'SMB/CIFS 网络共享',
|
||||
};
|
||||
|
||||
/**
|
||||
* FTP 上传实现
|
||||
*/
|
||||
async function uploadViaFTP(config, localFilePath, remotePath) {
|
||||
const { Client } = require('basic-ftp');
|
||||
|
||||
const client = new Client();
|
||||
|
||||
try {
|
||||
await client.access({
|
||||
host: config.host,
|
||||
port: config.port || 21,
|
||||
user: config.username,
|
||||
password: config.password,
|
||||
secure: config.secure || false,
|
||||
secureOptions: {
|
||||
rejectUnauthorized: config.rejectUnauthorized !== false,
|
||||
},
|
||||
});
|
||||
|
||||
await client.cd(config.rootPath || '/');
|
||||
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '.') {
|
||||
await ensureRemoteDir(client, dirPath, 'ftp');
|
||||
}
|
||||
|
||||
await client.uploadFrom(localFilePath, path.basename(remotePath));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `FTP 上传成功:${remotePath}`,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`FTP 上传失败:${error.message}`);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SFTP 上传实现
|
||||
*/
|
||||
async function uploadViaSFTP(config, localFilePath, remotePath) {
|
||||
const Client = require('ssh2-sftp-client');
|
||||
const client = new Client();
|
||||
|
||||
try {
|
||||
await client.connect({
|
||||
host: config.host,
|
||||
port: config.port || 22,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
privateKey: config.privateKey ? fs.readFileSync(config.privateKey) : undefined,
|
||||
passphrase: config.passphrase,
|
||||
readyTimeout: config.timeout || 10000,
|
||||
});
|
||||
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '.') {
|
||||
await ensureRemoteDir(client, dirPath, 'sftp');
|
||||
}
|
||||
|
||||
await client.put(fs.createReadStream(localFilePath), remotePath);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `SFTP 上传成功:${remotePath}`,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`SFTP 上传失败:${error.message}`);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebDAV 上传实现
|
||||
*/
|
||||
async function uploadViaWebDAV(config, localFilePath, remotePath) {
|
||||
const { createClient } = require('webdav');
|
||||
|
||||
const client = createClient(config.url, {
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
authType: config.authType || 'password',
|
||||
headers: {
|
||||
'User-Agent': 'IDC-Backup-Client/1.0',
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '/') {
|
||||
await ensureRemoteDir(client, dirPath, 'webdav');
|
||||
}
|
||||
|
||||
const fileContent = fs.readFileSync(localFilePath);
|
||||
await client.putFileContents(remotePath, fileContent, {
|
||||
overwrite: true,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `WebDAV 上传成功:${remotePath}`,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`WebDAV 上传失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SMB/CIFS 网络共享上传实现
|
||||
*/
|
||||
async function uploadViaSMB(config, localFilePath, remotePath) {
|
||||
const smb = require('smb2');
|
||||
|
||||
const client = new smb({
|
||||
share: `\\\\${config.host}\\${config.share}`,
|
||||
domain: config.domain || '',
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
});
|
||||
|
||||
try {
|
||||
const dirPath = path.dirname(remotePath);
|
||||
if (dirPath !== '.') {
|
||||
await ensureRemoteDir(client, dirPath, 'smb');
|
||||
}
|
||||
|
||||
await client.writeFile(remotePath, fs.readFileSync(localFilePath));
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `SMB 上传成功:${remotePath}`,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`SMB 上传失败:${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保远程目录存在
|
||||
*/
|
||||
async function ensureRemoteDir(client, dirPath, protocol) {
|
||||
try {
|
||||
if (protocol === 'ftp') {
|
||||
const dirs = dirPath.split('/').filter(d => d);
|
||||
for (const dir of dirs) {
|
||||
try {
|
||||
await client.cd(dir);
|
||||
} catch {
|
||||
await client.mkdir(dir);
|
||||
await client.cd(dir);
|
||||
}
|
||||
}
|
||||
} else if (protocol === 'sftp') {
|
||||
await client.mkdir(dirPath, true);
|
||||
} else if (protocol === 'webdav') {
|
||||
const parts = dirPath.split('/').filter(p => p);
|
||||
let currentPath = '';
|
||||
for (const part of parts) {
|
||||
currentPath += '/' + part;
|
||||
try {
|
||||
await client.createDirectory(currentPath);
|
||||
} catch (error) {
|
||||
if (!error.status || error.status !== 405) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (protocol === 'smb') {
|
||||
const parts = dirPath.split(path.sep).filter(p => p);
|
||||
let currentPath = '';
|
||||
for (const part of parts) {
|
||||
currentPath += part + path.sep;
|
||||
try {
|
||||
await client.exists(currentPath);
|
||||
} catch {
|
||||
await client.mkdir(currentPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`创建远程目录失败 [${protocol}]:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主上传函数 - 根据配置选择对应协议
|
||||
*/
|
||||
async function uploadToRemote(config, localFilePath, remotePath) {
|
||||
console.log(`开始上传到远端 [${config.protocol}]: ${remotePath}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
let result;
|
||||
|
||||
switch (config.protocol) {
|
||||
case PROTOCOL_TYPES.FTP:
|
||||
result = await uploadViaFTP(config, localFilePath, remotePath);
|
||||
break;
|
||||
case PROTOCOL_TYPES.SFTP:
|
||||
result = await uploadViaSFTP(config, localFilePath, remotePath);
|
||||
break;
|
||||
case PROTOCOL_TYPES.WEBDAV:
|
||||
result = await uploadViaWebDAV(config, localFilePath, remotePath);
|
||||
break;
|
||||
case PROTOCOL_TYPES.SMB:
|
||||
result = await uploadViaSMB(config, localFilePath, remotePath);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`不支持的协议类型:${config.protocol}`);
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const fileSize = fs.statSync(localFilePath).size;
|
||||
|
||||
console.log(`远端上传完成 [${config.protocol}] - 耗时:${duration}ms, 文件大小:${(fileSize / 1024).toFixed(2)}KB`);
|
||||
|
||||
return {
|
||||
...result,
|
||||
protocol: config.protocol,
|
||||
protocolLabel: PROTOCOL_LABELS[config.protocol],
|
||||
duration,
|
||||
fileSize,
|
||||
uploadedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试远端连接
|
||||
*/
|
||||
async function testRemoteConnection(config) {
|
||||
console.log(`测试远端连接 [${config.protocol}]...`);
|
||||
|
||||
try {
|
||||
const testContent = `IDC Backup Connection Test - ${new Date().toISOString()}`;
|
||||
const testFile = path.join(require('os').tmpdir(), `backup-test-${Date.now()}.txt`);
|
||||
fs.writeFileSync(testFile, testContent);
|
||||
|
||||
const testRemotePath = `test/backup-connection-test-${Date.now()}.txt`;
|
||||
|
||||
const result = await uploadToRemote(config, testFile, testRemotePath);
|
||||
|
||||
try {
|
||||
fs.unlinkSync(testFile);
|
||||
} catch (e) {
|
||||
console.warn('删除测试文件失败:', e.message);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '连接测试成功',
|
||||
details: result,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `连接测试失败:${error.message}`,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROTOCOL_TYPES,
|
||||
PROTOCOL_LABELS,
|
||||
uploadToRemote,
|
||||
testRemoteConnection,
|
||||
uploadViaFTP,
|
||||
uploadViaSFTP,
|
||||
uploadViaWebDAV,
|
||||
uploadViaSMB,
|
||||
};
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* 远端备份配置管理模块
|
||||
* 管理多个远端备份目标的配置
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { PROTOCOL_TYPES } = require('./remoteBackup');
|
||||
|
||||
// 配置文件路径
|
||||
const CONFIG_FILE = path.join(__dirname, '..', 'config', 'remote-backup-configs.json');
|
||||
|
||||
// 默认配置
|
||||
const DEFAULT_CONFIG = {
|
||||
targets: [],
|
||||
globalSettings: {
|
||||
enabled: false,
|
||||
uploadAfterBackup: true,
|
||||
deleteLocalAfterUpload: false,
|
||||
retryCount: 3,
|
||||
retryDelay: 5000,
|
||||
timeout: 300000,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 加密敏感信息
|
||||
*/
|
||||
function encrypt(text) {
|
||||
if (!text) return '';
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
||||
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
return iv.toString('hex') + ':' + encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密敏感信息
|
||||
*/
|
||||
function decrypt(text) {
|
||||
if (!text) return '';
|
||||
try {
|
||||
const algorithm = 'aes-256-cbc';
|
||||
const key = crypto.scryptSync(process.env.JWT_SECRET || 'default-secret', 'salt', 32);
|
||||
const parts = text.split(':');
|
||||
const iv = Buffer.from(parts[0], 'hex');
|
||||
const encrypted = parts[1];
|
||||
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
||||
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
console.error('解密失败:', error.message);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载配置
|
||||
*/
|
||||
function loadConfig() {
|
||||
try {
|
||||
if (fs.existsSync(CONFIG_FILE)) {
|
||||
const content = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
const config = JSON.parse(content);
|
||||
return { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载远端备份配置失败:', error);
|
||||
}
|
||||
return { ...DEFAULT_CONFIG };
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存配置
|
||||
*/
|
||||
function saveConfig(config) {
|
||||
try {
|
||||
const configDir = path.dirname(CONFIG_FILE);
|
||||
if (!fs.existsSync(configDir)) {
|
||||
fs.mkdirSync(configDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('保存远端备份配置失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有远端备份目标(隐藏敏感信息)
|
||||
*/
|
||||
function getAllTargets() {
|
||||
const config = loadConfig();
|
||||
return config.targets.map(target => ({
|
||||
...target,
|
||||
password: target.password ? '********' : undefined,
|
||||
accessKeySecret: target.accessKeySecret ? '********' : undefined,
|
||||
secretKey: target.secretKey ? '********' : undefined,
|
||||
privateKey: target.privateKey ? '********' : undefined,
|
||||
passphrase: target.passphrase ? '********' : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个目标(包含解密后的敏感信息)
|
||||
*/
|
||||
function getTarget(id) {
|
||||
const config = loadConfig();
|
||||
const target = config.targets.find(t => t.id === id);
|
||||
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...target,
|
||||
password: target.password ? decrypt(target.password) : undefined,
|
||||
accessKeySecret: target.accessKeySecret ? decrypt(target.accessKeySecret) : undefined,
|
||||
secretKey: target.secretKey ? decrypt(target.secretKey) : undefined,
|
||||
privateKey: target.privateKey ? decrypt(target.privateKey) : undefined,
|
||||
passphrase: target.passphrase ? decrypt(target.passphrase) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加远端备份目标
|
||||
*/
|
||||
function addTarget(targetData) {
|
||||
const config = loadConfig();
|
||||
|
||||
const newTarget = {
|
||||
id: `target_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: targetData.name,
|
||||
protocol: targetData.protocol,
|
||||
enabled: targetData.enabled !== false,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
switch (targetData.protocol) {
|
||||
case PROTOCOL_TYPES.FTP:
|
||||
case PROTOCOL_TYPES.SFTP:
|
||||
Object.assign(newTarget, {
|
||||
host: targetData.host,
|
||||
port: targetData.port,
|
||||
username: targetData.username,
|
||||
password: targetData.password ? encrypt(targetData.password) : undefined,
|
||||
rootPath: targetData.rootPath || '/',
|
||||
secure: targetData.secure,
|
||||
});
|
||||
break;
|
||||
|
||||
case PROTOCOL_TYPES.WEBDAV:
|
||||
Object.assign(newTarget, {
|
||||
url: targetData.url,
|
||||
username: targetData.username,
|
||||
password: targetData.password ? encrypt(targetData.password) : undefined,
|
||||
authType: targetData.authType || 'password',
|
||||
rootPath: targetData.rootPath || '/',
|
||||
});
|
||||
break;
|
||||
|
||||
case PROTOCOL_TYPES.SMB:
|
||||
Object.assign(newTarget, {
|
||||
host: targetData.host,
|
||||
share: targetData.share,
|
||||
domain: targetData.domain,
|
||||
username: targetData.username,
|
||||
password: targetData.password ? encrypt(targetData.password) : undefined,
|
||||
rootPath: targetData.rootPath || '/',
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error(`不支持的协议类型:${targetData.protocol}`);
|
||||
}
|
||||
|
||||
config.targets.push(newTarget);
|
||||
|
||||
if (saveConfig(config)) {
|
||||
return newTarget;
|
||||
}
|
||||
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新远端备份目标
|
||||
*/
|
||||
function updateTarget(id, updates) {
|
||||
const config = loadConfig();
|
||||
const targetIndex = config.targets.findIndex(t => t.id === id);
|
||||
|
||||
if (targetIndex === -1) {
|
||||
throw new Error('目标不存在');
|
||||
}
|
||||
|
||||
const existingTarget = config.targets[targetIndex];
|
||||
const updatedTarget = { ...existingTarget, ...updates, updatedAt: new Date().toISOString() };
|
||||
|
||||
if (updates.password) {
|
||||
updatedTarget.password = encrypt(updates.password);
|
||||
}
|
||||
if (updates.accessKeySecret) {
|
||||
updatedTarget.accessKeySecret = encrypt(updates.accessKeySecret);
|
||||
}
|
||||
if (updates.secretKey) {
|
||||
updatedTarget.secretKey = encrypt(updates.secretKey);
|
||||
}
|
||||
if (updates.privateKey) {
|
||||
updatedTarget.privateKey = encrypt(updates.privateKey);
|
||||
}
|
||||
if (updates.passphrase) {
|
||||
updatedTarget.passphrase = encrypt(updates.passphrase);
|
||||
}
|
||||
|
||||
config.targets[targetIndex] = updatedTarget;
|
||||
|
||||
if (saveConfig(config)) {
|
||||
return updatedTarget;
|
||||
}
|
||||
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除远端备份目标
|
||||
*/
|
||||
function deleteTarget(id) {
|
||||
const config = loadConfig();
|
||||
const initialLength = config.targets.length;
|
||||
|
||||
config.targets = config.targets.filter(t => t.id !== id);
|
||||
|
||||
if (config.targets.length < initialLength) {
|
||||
if (saveConfig(config)) {
|
||||
return true;
|
||||
}
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局设置
|
||||
*/
|
||||
function getGlobalSettings() {
|
||||
const config = loadConfig();
|
||||
return config.globalSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新全局设置
|
||||
*/
|
||||
function updateGlobalSettings(settings) {
|
||||
const config = loadConfig();
|
||||
config.globalSettings = { ...config.globalSettings, ...settings };
|
||||
|
||||
if (saveConfig(config)) {
|
||||
return config.globalSettings;
|
||||
}
|
||||
|
||||
throw new Error('保存配置失败');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的目标列表
|
||||
*/
|
||||
function getEnabledTargets() {
|
||||
const config = loadConfig();
|
||||
if (!config.globalSettings.enabled) {
|
||||
return [];
|
||||
}
|
||||
return config.targets.filter(t => t.enabled);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadConfig,
|
||||
saveConfig,
|
||||
getAllTargets,
|
||||
getTarget,
|
||||
addTarget,
|
||||
updateTarget,
|
||||
deleteTarget,
|
||||
getGlobalSettings,
|
||||
updateGlobalSettings,
|
||||
getEnabledTargets,
|
||||
encrypt,
|
||||
decrypt,
|
||||
};
|
||||
Reference in New Issue
Block a user