904 lines
31 KiB
JavaScript
904 lines
31 KiB
JavaScript
#!/usr/bin/env node
|
||
|
||
/**
|
||
* 云睿资产管理系统 - 卸载脚本 v2.0.0
|
||
*
|
||
* 功能说明:
|
||
* - 停止并删除 PM2 管理的后端和前端服务
|
||
* - 清理 Nginx 配置文件(可选)
|
||
* - 删除生成的配置文件(.env、ecosystem.config.js、nginx-yunrui.conf)
|
||
* - 可选删除数据库文件(SQLite)
|
||
* - 可选删除 node_modules 和构建产物
|
||
*
|
||
* 使用方法:
|
||
* node uninstall.js # 交互式卸载
|
||
* node uninstall.js --help # 查看帮助
|
||
* node uninstall.js --force # 强制卸载(无需确认)
|
||
* node uninstall.js --backup # 卸载前自动备份
|
||
*/
|
||
|
||
const { execSync } = require('child_process');
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const os = require('os');
|
||
const { colors, ICONS, log, padCenter, initLogFile, closeLogFile } = require('./install/logger');
|
||
const { ask, closeReadline } = require('./install/ui');
|
||
const { SCRIPT_VERSION } = require('./install/constants');
|
||
|
||
const BACKUP_DIR = path.join(__dirname, 'backup');
|
||
const LOG_DIR = path.join(__dirname, 'logs');
|
||
|
||
const UNINSTALL_STEPS = [
|
||
'停止服务',
|
||
'清理 Nginx',
|
||
'清理配置',
|
||
'数据库清理',
|
||
'日志清理',
|
||
'上传清理',
|
||
'依赖清理',
|
||
];
|
||
|
||
let logFileStream = null;
|
||
let uninstallStartTime = null;
|
||
let deletedItems = [];
|
||
let backedUpItems = [];
|
||
|
||
function parseArgs() {
|
||
const args = process.argv.slice(2);
|
||
return {
|
||
force: args.includes('--force') || args.includes('-f') || args.includes('--yes') || args.includes('-y'),
|
||
backup: args.includes('--backup') || args.includes('-b'),
|
||
dryRun: args.includes('--dry-run'),
|
||
skipDb: args.includes('--skip-db'),
|
||
skipDeps: args.includes('--skip-deps'),
|
||
skipUploads: args.includes('--skip-uploads'),
|
||
help: args.includes('--help') || args.includes('-h')
|
||
};
|
||
}
|
||
|
||
function showHelp() {
|
||
console.log(`
|
||
${colors.bright}云睿资产管理系统 - 卸载脚本 v${SCRIPT_VERSION}${colors.reset}
|
||
|
||
${colors.bright}用法:${colors.reset} node uninstall.js [选项]
|
||
|
||
${colors.bright}选项:${colors.reset}
|
||
${ICONS.pointer} ${colors.cyan}-f, --force${colors.reset} 强制卸载(无需确认)
|
||
${ICONS.pointer} ${colors.cyan}-y, --yes${colors.reset} 强制卸载别名(同 --force)
|
||
${ICONS.pointer} ${colors.cyan}-b, --backup${colors.reset} 卸载前自动备份数据库
|
||
${colors.dim}--dry-run${colors.reset} 预览模式:仅显示将要删除的内容
|
||
${colors.dim}--skip-db${colors.reset} 跳过数据库删除
|
||
${colors.dim}--skip-deps${colors.reset} 跳过依赖删除
|
||
${colors.dim}--skip-uploads${colors.reset} 跳过上传文件目录删除
|
||
${colors.dim}-h, --help${colors.reset} 显示帮助信息
|
||
|
||
${colors.bright}示例:${colors.reset}
|
||
${colors.cyan}node uninstall.js${colors.reset} # 交互式卸载
|
||
${colors.cyan}node uninstall.js --force${colors.reset} # 强制卸载
|
||
${colors.cyan}node uninstall.js --backup${colors.reset} # 卸载前备份
|
||
${colors.cyan}node uninstall.js --dry-run${colors.reset} # 预览将删除的内容
|
||
`);
|
||
process.exit(0);
|
||
}
|
||
|
||
function runCommand(command, options = {}) {
|
||
try {
|
||
const result = execSync(command, {
|
||
encoding: 'utf8',
|
||
stdio: options.silent ? 'pipe' : 'inherit',
|
||
cwd: options.cwd || process.cwd(),
|
||
shell: true
|
||
});
|
||
return { success: true, output: result };
|
||
} catch (error) {
|
||
return { success: false, error: error.message };
|
||
}
|
||
}
|
||
|
||
function commandExists(command) {
|
||
try {
|
||
execSync(`${command} --version`, { stdio: 'pipe', shell: true });
|
||
return true;
|
||
} catch {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function getPM2ServiceNames() {
|
||
const defaultNames = { backendName: 'yunrui_asset-backend', frontendName: 'yunrui_asset-frontend' };
|
||
const configPath = path.join(__dirname, 'deploy', 'ecosystem.config.js');
|
||
|
||
if (!fs.existsSync(configPath)) {
|
||
return defaultNames;
|
||
}
|
||
|
||
try {
|
||
const config = require(configPath);
|
||
if (!config || !Array.isArray(config.apps)) {
|
||
return defaultNames;
|
||
}
|
||
|
||
const names = { ...defaultNames };
|
||
config.apps.forEach(app => {
|
||
if (app.name && app.name.startsWith('idc-')) {
|
||
if (app.name.includes('backend') || (app.script && app.script.includes('server'))) {
|
||
names.backendName = app.name;
|
||
} else if (app.name.includes('frontend') || app.name === 'yunrui_asset-frontend') {
|
||
names.frontendName = app.name;
|
||
}
|
||
}
|
||
});
|
||
return names;
|
||
} catch {
|
||
return defaultNames;
|
||
}
|
||
}
|
||
|
||
function getDatabaseConfig() {
|
||
const defaultConfig = {
|
||
dbType: 'sqlite',
|
||
sqliteDbPath: path.join(__dirname, 'backend', 'yunrui_management.db')
|
||
};
|
||
|
||
const envPath = path.join(__dirname, 'backend', '.env');
|
||
if (!fs.existsSync(envPath)) {
|
||
return defaultConfig;
|
||
}
|
||
|
||
try {
|
||
const envContent = fs.readFileSync(envPath, 'utf8');
|
||
const config = { ...defaultConfig };
|
||
|
||
const dbTypeMatch = envContent.match(/^DB_TYPE\s*=\s*(\w+)/m);
|
||
if (dbTypeMatch) {
|
||
config.dbType = dbTypeMatch[1].toLowerCase();
|
||
}
|
||
|
||
const dbPathMatch = envContent.match(/^DB_PATH\s*=\s*(.+)$/m);
|
||
if (dbPathMatch && config.dbType === 'sqlite') {
|
||
const rawPath = dbPathMatch[1].trim().replace(/['"]/g, '');
|
||
if (path.isAbsolute(rawPath)) {
|
||
config.sqliteDbPath = rawPath;
|
||
} else {
|
||
config.sqliteDbPath = path.resolve(__dirname, 'backend', rawPath);
|
||
}
|
||
}
|
||
|
||
return config;
|
||
} catch {
|
||
return defaultConfig;
|
||
}
|
||
}
|
||
|
||
function isRootUser() {
|
||
return process.getuid && process.getuid() === 0;
|
||
}
|
||
|
||
async function stopAndDeleteServices() {
|
||
log.step('停止服务', 0, 7);
|
||
|
||
if (!commandExists('pm2')) {
|
||
log.warning('未检测到 PM2,跳过服务停止步骤');
|
||
return;
|
||
}
|
||
|
||
const { backendName, frontendName } = getPM2ServiceNames();
|
||
const isWindows = process.platform === 'win32';
|
||
const nullRedirect = isWindows ? '2>nul' : '2>/dev/null';
|
||
const orTrue = isWindows ? '|| exit 0' : '|| true';
|
||
|
||
function pm2ProcessExists(name) {
|
||
const result = runCommand(`pm2 describe ${name} ${nullRedirect}`, { silent: true });
|
||
return result.success;
|
||
}
|
||
|
||
const services = [
|
||
{ name: backendName, label: '后端服务' },
|
||
{ name: frontendName, label: '前端服务' }
|
||
];
|
||
|
||
for (const service of services) {
|
||
if (!pm2ProcessExists(service.name)) {
|
||
log.info(`${service.label} (${service.name}) 不存在于 PM2 中,跳过`);
|
||
continue;
|
||
}
|
||
|
||
log.info(`停止 ${service.label} (${service.name})...`);
|
||
const stopResult = runCommand(`pm2 stop ${service.name} ${nullRedirect} ${orTrue}`, { silent: true });
|
||
if (stopResult.success) {
|
||
log.success(`${service.label} 已停止`);
|
||
} else {
|
||
log.warning(`${service.label} 停止失败(可能已停止)`);
|
||
}
|
||
|
||
log.info(`删除 ${service.label} (${service.name})...`);
|
||
const deleteResult = runCommand(`pm2 delete ${service.name} ${nullRedirect} ${orTrue}`, { silent: true });
|
||
if (deleteResult.success) {
|
||
log.success(`${service.label} 已删除`);
|
||
} else {
|
||
log.warning(`${service.label} 删除失败`);
|
||
}
|
||
}
|
||
|
||
log.info('保存 PM2 配置...');
|
||
runCommand(`pm2 save ${nullRedirect} ${orTrue}`, { silent: true });
|
||
|
||
log.info('清理 PM2 开机自启配置...');
|
||
const startupResult = runCommand(`pm2 unstartup ${nullRedirect} ${orTrue}`, { silent: true });
|
||
if (startupResult.success) {
|
||
log.success('PM2 开机自启配置已清理');
|
||
}
|
||
|
||
log.info('清理 PM2 日志文件...');
|
||
const pm2LogDir = path.join(os.homedir(), '.pm2', 'logs');
|
||
if (fs.existsSync(pm2LogDir)) {
|
||
const pm2LogFiles = fs.readdirSync(pm2LogDir).filter(f =>
|
||
f.startsWith(backendName) || f.startsWith(frontendName)
|
||
);
|
||
if (pm2LogFiles.length > 0) {
|
||
pm2LogFiles.forEach(f => {
|
||
try {
|
||
fs.unlinkSync(path.join(pm2LogDir, f));
|
||
log.subStep(`PM2 日志已清理: ${f}`);
|
||
} catch (e) {
|
||
log.warning(`PM2 日志清理失败: ${f}`);
|
||
}
|
||
});
|
||
} else {
|
||
log.subStep('未找到 PM2 相关日志文件');
|
||
}
|
||
}
|
||
|
||
log.divider();
|
||
}
|
||
|
||
async function cleanupNginxConfig(cmdArgs) {
|
||
log.step('清理 Nginx', 1, 7);
|
||
|
||
const isWindows = process.platform === 'win32';
|
||
const isLinux = process.platform === 'linux';
|
||
const isMac = process.platform === 'darwin';
|
||
|
||
function nginxInstalled() {
|
||
return commandExists('nginx');
|
||
}
|
||
|
||
if (isWindows) {
|
||
log.info('Windows Nginx 清理:');
|
||
console.log(` ${ICONS.pipe} 请手动完成以下操作:`);
|
||
console.log(` ${ICONS.pipe} 删除配置文件: ${colors.cyan}C:/nginx/conf/conf.d/idc.conf${colors.reset}`);
|
||
console.log(` ${ICONS.pipe} 编辑主配置: 从 ${colors.cyan}C:/nginx/conf/nginx.conf${colors.reset} 中移除 include 语句`);
|
||
console.log(` ${ICONS.pipe} 重载 Nginx: ${colors.cyan}nginx -s reload${colors.reset}`);
|
||
|
||
if (!cmdArgs?.force) {
|
||
const confirm = await ask('是否已完成以上清理操作? (Y/n)', 'Y');
|
||
if (confirm.toLowerCase() !== 'y') {
|
||
log.warning('请记得手动清理 Nginx 配置');
|
||
}
|
||
}
|
||
log.divider();
|
||
return;
|
||
}
|
||
|
||
if (isMac) {
|
||
log.info('macOS Nginx 清理:');
|
||
console.log(` ${ICONS.pipe} 请手动完成以下操作:`);
|
||
console.log(` ${ICONS.pipe} 删除配置文件: ${colors.cyan}/usr/local/etc/nginx/servers/idc${colors.reset}`);
|
||
console.log(` ${ICONS.pipe} 重载 Nginx: ${colors.cyan}nginx -s reload${colors.reset}`);
|
||
|
||
if (commandExists('brew')) {
|
||
console.log(` ${ICONS.pipe} 如需卸载 Nginx:${colors.cyan}brew uninstall nginx${colors.reset}`);
|
||
}
|
||
|
||
if (!cmdArgs?.force) {
|
||
const confirm = await ask('是否已完成以上清理操作? (Y/n)', 'Y');
|
||
if (confirm.toLowerCase() !== 'y') {
|
||
log.warning('请记得手动清理 Nginx 配置');
|
||
}
|
||
}
|
||
log.divider();
|
||
return;
|
||
}
|
||
|
||
const root = isRootUser();
|
||
const sudoPrefix = root ? '' : 'sudo ';
|
||
|
||
const idcConfigPaths = [
|
||
{ path: '/etc/nginx/sites-available/yunrui_asset', label: 'sites-available/yunrui_asset' },
|
||
{ path: '/etc/nginx/sites-enabled/yunrui_asset', label: 'sites-enabled/yunrui_asset(软链接)' },
|
||
{ path: '/etc/nginx/conf.d/idc', label: 'conf.d/idc' },
|
||
{ path: '/etc/nginx/conf.d/idc.conf', label: 'conf.d/idc.conf' },
|
||
];
|
||
|
||
const wwwDir = '/var/www/yunrui_asset';
|
||
const defaultSitePath = '/etc/nginx/sites-enabled/default';
|
||
const defaultAvailablePath = '/etc/nginx/sites-available/default';
|
||
|
||
let configRemoved = false;
|
||
|
||
for (const config of idcConfigPaths) {
|
||
if (fs.existsSync(config.path)) {
|
||
log.info(`删除 ${config.label}...`);
|
||
const result = runCommand(`${sudoPrefix}rm -f "${config.path}"`, { silent: true });
|
||
if (result.success) {
|
||
log.success(`${config.label} 已删除`);
|
||
configRemoved = true;
|
||
deletedItems.push({ type: 'Nginx配置', name: config.path });
|
||
} else {
|
||
log.error(`${config.label} 删除失败`);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!configRemoved) {
|
||
log.info('未找到云睿资产 Nginx 站点配置文件');
|
||
}
|
||
|
||
if (fs.existsSync(wwwDir)) {
|
||
log.info('清理前端部署目录...');
|
||
const result = runCommand(`${sudoPrefix}rm -rf "${wwwDir}"`, { silent: true });
|
||
if (result.success) {
|
||
log.success('前端部署目录已删除 (/var/www/yunrui_asset)');
|
||
deletedItems.push({ type: '目录', name: '/var/www/yunrui_asset' });
|
||
} else {
|
||
log.warning('前端部署目录删除失败,请手动清理');
|
||
}
|
||
}
|
||
|
||
if (!fs.existsSync(defaultSitePath) && fs.existsSync(defaultAvailablePath)) {
|
||
log.info('恢复 Nginx 默认站点...');
|
||
const result = runCommand(
|
||
`${sudoPrefix}ln -sf "${defaultAvailablePath}" "${defaultSitePath}"`,
|
||
{ silent: true }
|
||
);
|
||
if (result.success) {
|
||
log.success('Nginx 默认站点已恢复');
|
||
} else {
|
||
log.warning('恢复默认站点失败,请手动执行:');
|
||
console.log(` ${ICONS.pipe} ${colors.cyan}${sudoPrefix}ln -sf ${defaultAvailablePath} ${defaultSitePath}${colors.reset}`);
|
||
}
|
||
}
|
||
|
||
if (nginxInstalled() && configRemoved) {
|
||
log.info('重载 Nginx 配置...');
|
||
const testResult = runCommand(`${sudoPrefix}nginx -t`, { silent: true });
|
||
if (testResult.success) {
|
||
const reloadResult = runCommand(`${sudoPrefix}nginx -s reload`, { silent: true });
|
||
if (reloadResult.success) {
|
||
log.success('Nginx 配置已重载');
|
||
} else {
|
||
log.warning('Nginx 重载失败,请手动执行: sudo nginx -s reload');
|
||
}
|
||
} else {
|
||
log.warning('Nginx 配置测试未通过,跳过重载');
|
||
log.info('请手动检查并重载: sudo nginx -t && sudo nginx -s reload');
|
||
}
|
||
}
|
||
|
||
log.divider();
|
||
}
|
||
|
||
async function cleanupConfigFiles() {
|
||
log.step('清理配置', 2, 7);
|
||
|
||
const filesToDelete = [
|
||
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)', type: '配置文件' },
|
||
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置 (ecosystem.config.js)', type: '配置文件' },
|
||
{ path: path.join(__dirname, 'deploy', 'nginx-yunrui.conf'), name: 'Nginx 配置 (nginx-yunrui.conf)', type: '配置文件' }
|
||
];
|
||
|
||
for (const file of filesToDelete) {
|
||
if (fs.existsSync(file.path)) {
|
||
try {
|
||
fs.unlinkSync(file.path);
|
||
log.success(`${file.name} 已删除`);
|
||
deletedItems.push({ type: file.type, name: file.name });
|
||
} catch (error) {
|
||
log.error(`删除 ${file.name} 失败: ${error.message}`);
|
||
}
|
||
} else {
|
||
log.info(`${file.name} 不存在,跳过`);
|
||
}
|
||
}
|
||
|
||
const deployDir = path.join(__dirname, 'deploy');
|
||
if (fs.existsSync(deployDir)) {
|
||
try {
|
||
const files = fs.readdirSync(deployDir);
|
||
if (files.length === 0) {
|
||
fs.rmdirSync(deployDir);
|
||
log.success('deploy 目录已删除');
|
||
deletedItems.push({ type: '目录', name: 'deploy/' });
|
||
} else {
|
||
log.info('deploy 目录不为空,保留');
|
||
}
|
||
} catch (error) {
|
||
log.error(`删除 deploy 目录失败: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
log.divider();
|
||
}
|
||
|
||
async function cleanupDatabase(cmdArgs) {
|
||
log.step('数据库清理', 3, 7);
|
||
|
||
const dbConfig = getDatabaseConfig();
|
||
const sqliteDbPath = dbConfig.sqliteDbPath;
|
||
const sqliteExists = fs.existsSync(sqliteDbPath);
|
||
|
||
let dbSize = 0;
|
||
let dbCreateTime = '';
|
||
if (sqliteExists) {
|
||
const stats = fs.statSync(sqliteDbPath);
|
||
dbSize = (stats.size / 1024 / 1024).toFixed(2);
|
||
dbCreateTime = stats.birthtime.toLocaleString();
|
||
}
|
||
|
||
const dbType = dbConfig.dbType;
|
||
|
||
log.section('当前数据库配置');
|
||
log.keyValue('数据库类型', dbType === 'sqlite' ? 'SQLite' : 'MySQL');
|
||
|
||
if (dbType === 'sqlite') {
|
||
if (sqliteExists) {
|
||
log.keyValue('文件路径', sqliteDbPath);
|
||
log.keyValue('文件大小', `${dbSize} MB`);
|
||
log.keyValue('创建时间', dbCreateTime);
|
||
|
||
log.divider();
|
||
log.warning('删除数据库将永久丢失所有数据!');
|
||
log.divider();
|
||
|
||
const confirm = await ask('是否删除 SQLite 数据库文件? (y/N)', 'N');
|
||
if (confirm.toLowerCase() === 'y') {
|
||
if (!cmdArgs?.backup && !cmdArgs?.force) {
|
||
const backupConfirm = await ask('是否先备份数据库再删除? (Y/n)', 'Y');
|
||
if (backupConfirm.toLowerCase() === 'y') {
|
||
await backupDatabase();
|
||
}
|
||
} else if (cmdArgs?.backup) {
|
||
await backupDatabase();
|
||
}
|
||
try {
|
||
log.info('正在删除数据库文件...');
|
||
fs.unlinkSync(sqliteDbPath);
|
||
log.success('SQLite 数据库已删除');
|
||
deletedItems.push({ type: '数据库', name: `SQLite (${dbSize} MB)` });
|
||
} catch (error) {
|
||
log.error(`直接删除失败: ${error.message}`);
|
||
if (error.message.includes('EBUSY') || error.message.includes('resource busy') || error.message.includes('used by another process')) {
|
||
log.warning('数据库文件被占用,尝试强制删除...');
|
||
try {
|
||
const isWindows = process.platform === 'win32';
|
||
let forceResult;
|
||
if (isWindows) {
|
||
forceResult = runCommand(`del /f /q "${sqliteDbPath}" 2>nul`, { silent: true });
|
||
if (!forceResult.success) {
|
||
forceResult = runCommand(`powershell -Command "Remove-Item -Path '${sqliteDbPath}' -Force -ErrorAction SilentlyContinue" 2>nul`, { silent: true });
|
||
}
|
||
} else {
|
||
forceResult = runCommand(`rm -f "${sqliteDbPath}"`, { silent: true });
|
||
}
|
||
if (forceResult.success && !fs.existsSync(sqliteDbPath)) {
|
||
log.success('数据库文件已强制删除');
|
||
deletedItems.push({ type: '数据库', name: `SQLite (${dbSize} MB)` });
|
||
} else {
|
||
log.error('强制删除失败,文件仍被占用');
|
||
log.warning('请手动停止占用数据库文件的进程后再删除');
|
||
console.log(` ${ICONS.pipe} 手动删除命令:`);
|
||
if (process.platform === 'win32') {
|
||
console.log(` ${ICONS.pipe} ${colors.cyan}del /f "${sqliteDbPath}"${colors.reset}`);
|
||
} else {
|
||
console.log(` ${ICONS.pipe} ${colors.cyan}rm -f "${sqliteDbPath}"${colors.reset}`);
|
||
}
|
||
}
|
||
} catch (forceError) {
|
||
log.error(`强制删除失败: ${forceError.message}`);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
log.info('保留 SQLite 数据库文件');
|
||
log.keyValue('文件位置', path.relative(__dirname, sqliteDbPath));
|
||
}
|
||
} else {
|
||
log.info('未检测到 SQLite 数据库文件');
|
||
}
|
||
} else {
|
||
log.divider();
|
||
log.warning('当前使用 MySQL 数据库');
|
||
log.info('卸载脚本不会自动删除 MySQL 数据库,请手动清理:');
|
||
|
||
log.section('MySQL 删除方法');
|
||
console.log(` ${ICONS.pipe} 方法1 - MySQL 命令行:`);
|
||
console.log(` ${ICONS.pipe} ${colors.cyan}mysql -u root -p${colors.reset}`);
|
||
console.log(` ${ICONS.pipe} ${colors.cyan}DROP DATABASE yunrui_management;${colors.reset}`);
|
||
console.log(` ${ICONS.pipe} 方法2 - 使用数据库管理工具(如 Navicat、DBeaver、phpMyAdmin)`);
|
||
console.log(` ${ICONS.pipe} 方法3 - 如果不再需要 MySQL 数据,可直接卸载 MySQL 服务`);
|
||
}
|
||
|
||
log.divider();
|
||
log.section('数据备份建议');
|
||
if (dbType === 'sqlite' && sqliteExists) {
|
||
console.log(` ${ICONS.pipe} 备份 SQLite:直接复制 ${colors.cyan}${path.relative(__dirname, sqliteDbPath)}${colors.reset} 文件`);
|
||
}
|
||
console.log(` ${ICONS.pipe} 如需保留数据,请在卸载前手动备份`);
|
||
|
||
log.divider();
|
||
}
|
||
|
||
async function cleanupDependencies(cmdArgs) {
|
||
log.step('依赖和构建产物清理', 6, 7);
|
||
|
||
log.warning('此操作将删除 node_modules 和构建产物,需要重新安装依赖才能再次运行');
|
||
const confirm = cmdArgs?.force ? 'y' : await ask('是否删除依赖和构建产物? (y/N)', 'N');
|
||
|
||
if (confirm.toLowerCase() !== 'y') {
|
||
log.info('跳过依赖清理');
|
||
return;
|
||
}
|
||
|
||
const dirsToDelete = [
|
||
{ path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules', type: '依赖' },
|
||
{ path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules', type: '依赖' },
|
||
{ path: path.join(__dirname, 'frontend', 'node_modules', '.vite'), name: '前端 Vite 缓存 (.vite)', type: '构建产物' },
|
||
{ path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物 (dist)', type: '构建产物' },
|
||
{ path: path.join(__dirname, 'backend', 'temp'), name: '后端临时文件 (temp)', type: '临时文件' }
|
||
];
|
||
|
||
for (const dir of dirsToDelete) {
|
||
if (fs.existsSync(dir.path)) {
|
||
try {
|
||
fs.rmSync(dir.path, { recursive: true, force: true });
|
||
log.success(`${dir.name} 已删除`);
|
||
deletedItems.push({ type: dir.type, name: dir.name });
|
||
} catch (error) {
|
||
log.error(`删除 ${dir.name} 失败: ${error.message}`);
|
||
}
|
||
} else {
|
||
log.info(`${dir.name} 不存在,跳过`);
|
||
}
|
||
}
|
||
|
||
log.divider();
|
||
}
|
||
|
||
async function cleanupLogs(cmdArgs) {
|
||
log.step('日志清理', 4, 7);
|
||
|
||
const logsDir = path.join(__dirname, 'backend', 'logs');
|
||
if (fs.existsSync(logsDir)) {
|
||
if (cmdArgs?.force) {
|
||
log.info('强制模式:跳过日志文件删除');
|
||
return;
|
||
}
|
||
const confirm = await ask('是否删除后端日志文件? (y/N)', 'N');
|
||
if (confirm.toLowerCase() === 'y') {
|
||
try {
|
||
fs.rmSync(logsDir, { recursive: true, force: true });
|
||
log.success('后端日志文件已删除');
|
||
} catch (error) {
|
||
log.error(`删除失败: ${error.message}`);
|
||
}
|
||
} else {
|
||
log.info('保留后端日志文件');
|
||
}
|
||
} else {
|
||
log.info('未找到后端日志目录');
|
||
}
|
||
|
||
const rootLogsDir = path.join(__dirname, 'logs');
|
||
if (fs.existsSync(rootLogsDir)) {
|
||
if (cmdArgs?.force) {
|
||
log.info('强制模式:跳过脚本日志删除');
|
||
} else {
|
||
const rootLogConfirm = await ask('是否删除安装/卸载脚本日志? (y/N)', 'N');
|
||
if (rootLogConfirm.toLowerCase() === 'y') {
|
||
try {
|
||
fs.rmSync(rootLogsDir, { recursive: true, force: true });
|
||
log.success('脚本日志文件已删除');
|
||
} catch (error) {
|
||
log.error(`删除失败: ${error.message}`);
|
||
}
|
||
} else {
|
||
log.info('保留脚本日志文件');
|
||
}
|
||
}
|
||
}
|
||
|
||
log.divider();
|
||
}
|
||
|
||
async function cleanupUploads(cmdArgs) {
|
||
log.step('上传清理', 5, 7);
|
||
|
||
const uploadsDir = path.join(__dirname, 'backend', 'uploads');
|
||
if (fs.existsSync(uploadsDir)) {
|
||
if (cmdArgs?.force) {
|
||
log.info('强制模式:跳过上传文件目录删除');
|
||
return;
|
||
}
|
||
const confirm = await ask('是否删除上传文件目录? (y/N)', 'N');
|
||
if (confirm.toLowerCase() === 'y') {
|
||
try {
|
||
fs.rmSync(uploadsDir, { recursive: true, force: true });
|
||
log.success('上传文件目录已删除');
|
||
} catch (error) {
|
||
log.error(`删除失败: ${error.message}`);
|
||
}
|
||
} else {
|
||
log.info('保留上传文件目录');
|
||
}
|
||
} else {
|
||
log.info('未找到上传文件目录');
|
||
}
|
||
|
||
log.divider();
|
||
}
|
||
|
||
async function backupDatabase() {
|
||
log.step('备份数据库', 0, 7);
|
||
|
||
const dbConfig = getDatabaseConfig();
|
||
const sqliteDbPath = dbConfig.sqliteDbPath;
|
||
if (!fs.existsSync(sqliteDbPath)) {
|
||
log.info('未找到 SQLite 数据库文件,跳过备份');
|
||
return false;
|
||
}
|
||
|
||
if (!fs.existsSync(BACKUP_DIR)) {
|
||
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||
}
|
||
|
||
const backupPath = path.join(BACKUP_DIR, `database_backup_${Date.now()}.db`);
|
||
|
||
try {
|
||
fs.copyFileSync(sqliteDbPath, backupPath);
|
||
backedUpItems.push({ type: '数据库', path: backupPath });
|
||
log.success(`数据库已备份: ${path.basename(backupPath)}`);
|
||
return true;
|
||
} catch (error) {
|
||
log.error(`备份失败: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function showDryRunPreview(cmdArgs) {
|
||
const { backendName, frontendName } = getPM2ServiceNames();
|
||
const dbConfig = getDatabaseConfig();
|
||
|
||
log.banner('卸载预览', 'Dry Run Mode', null);
|
||
|
||
console.log(` ${colors.bright}${colors.cyan}以下是将要执行的操作(预览模式):${colors.reset}\n`);
|
||
|
||
console.log(` ${ICONS.pointer} ${colors.bright}1. 停止并删除 PM2 服务${colors.reset}`);
|
||
console.log(` ${ICONS.pipe} ${backendName}`);
|
||
console.log(` ${ICONS.pipe} ${frontendName}`);
|
||
console.log(` ${ICONS.pipe} 清理 PM2 开机自启配置`);
|
||
console.log(` ${ICONS.pipe} 清理 PM2 相关日志文件`);
|
||
|
||
console.log(`\n ${ICONS.pointer} ${colors.bright}2. 清理 Nginx 配置${colors.reset}`);
|
||
|
||
if (process.platform === 'linux') {
|
||
const nginxConfigPaths = ['/etc/nginx/sites-available/yunrui_asset', '/etc/nginx/sites-enabled/yunrui_asset',
|
||
'/etc/nginx/conf.d/idc', '/etc/nginx/conf.d/idc.conf'];
|
||
nginxConfigPaths.forEach(p => {
|
||
if (fs.existsSync(p)) {
|
||
console.log(` ${ICONS.pipe} 删除云睿资产站点配置: ${p}`);
|
||
}
|
||
});
|
||
if (fs.existsSync('/var/www/yunrui_asset')) {
|
||
console.log(` ${ICONS.pipe} 删除前端部署目录 (/var/www/yunrui_asset)`);
|
||
}
|
||
if (!fs.existsSync('/etc/nginx/sites-enabled/default') && fs.existsSync('/etc/nginx/sites-available/default')) {
|
||
console.log(` ${ICONS.pipe} 恢复 Nginx 默认站点`);
|
||
}
|
||
console.log(` ${ICONS.pipe} 重载 Nginx 配置 (nginx -s reload)`);
|
||
} else if (process.platform === 'win32') {
|
||
console.log(` ${ICONS.pipe} 删除 Nginx 配置文件 (C:/nginx/conf/conf.d/idc.conf)`);
|
||
console.log(` ${ICONS.pipe} 重载 Nginx (nginx -s reload)`);
|
||
} else if (process.platform === 'darwin') {
|
||
console.log(` ${ICONS.pipe} 删除 Nginx 配置 (/usr/local/etc/nginx/servers/idc)`);
|
||
console.log(` ${ICONS.pipe} 重载 Nginx (nginx -s reload)`);
|
||
}
|
||
|
||
console.log(`\n ${ICONS.pointer} ${colors.bright}3. 清理生成的配置文件${colors.reset}`);
|
||
const configFiles = [
|
||
{ path: path.join(__dirname, 'backend', '.env'), name: '后端环境变量 (.env)' },
|
||
{ path: path.join(__dirname, 'deploy', 'ecosystem.config.js'), name: 'PM2 配置' },
|
||
{ path: path.join(__dirname, 'deploy', 'nginx-yunrui.conf'), name: 'Nginx 配置' }
|
||
];
|
||
configFiles.forEach(f => {
|
||
if (fs.existsSync(f.path)) {
|
||
console.log(` ${ICONS.pipe} 删除: ${f.name}`);
|
||
}
|
||
});
|
||
|
||
console.log(`\n ${ICONS.pointer} ${colors.bright}4. 数据库${colors.reset}`);
|
||
if (cmdArgs?.skipDb) {
|
||
console.log(` ${ICONS.pipe} ${colors.dim}[跳过] 数据库清理${colors.reset}`);
|
||
} else if (dbConfig.dbType === 'sqlite' && fs.existsSync(dbConfig.sqliteDbPath)) {
|
||
console.log(` ${ICONS.pipe} 删除 SQLite 数据库: ${path.relative(__dirname, dbConfig.sqliteDbPath)}`);
|
||
if (cmdArgs?.backup) {
|
||
console.log(` ${ICONS.pipe} 备份数据库到: backup/`);
|
||
}
|
||
} else if (dbConfig.dbType === 'mysql') {
|
||
console.log(` ${ICONS.pipe} 提示手动删除 MySQL 数据库`);
|
||
} else {
|
||
console.log(` ${ICONS.pipe} 未检测到数据库文件`);
|
||
}
|
||
|
||
console.log(`\n ${ICONS.pointer} ${colors.bright}5. 日志文件${colors.reset}`);
|
||
if (fs.existsSync(path.join(__dirname, 'backend', 'logs'))) {
|
||
console.log(` ${ICONS.pipe} 后端日志目录 (backend/logs/)`);
|
||
}
|
||
if (fs.existsSync(path.join(__dirname, 'logs'))) {
|
||
console.log(` ${ICONS.pipe} 脚本日志目录 (logs/)`);
|
||
}
|
||
|
||
console.log(`\n ${ICONS.pointer} ${colors.bright}6. 上传文件${colors.reset}`);
|
||
if (cmdArgs?.skipUploads) {
|
||
console.log(` ${ICONS.pipe} ${colors.dim}[跳过] 上传文件目录${colors.reset}`);
|
||
} else if (fs.existsSync(path.join(__dirname, 'backend', 'uploads'))) {
|
||
console.log(` ${ICONS.pipe} 上传文件目录 (backend/uploads/)`);
|
||
} else {
|
||
console.log(` ${ICONS.pipe} ${colors.dim}未检测到上传文件目录${colors.reset}`);
|
||
}
|
||
|
||
console.log(`\n ${ICONS.pointer} ${colors.bright}7. 依赖和构建产物${colors.reset}`);
|
||
if (cmdArgs?.skipDeps) {
|
||
console.log(` ${ICONS.pipe} ${colors.dim}[跳过] 依赖和构建产物清理${colors.reset}`);
|
||
} else {
|
||
const depDirs = [
|
||
{ path: path.join(__dirname, 'backend', 'node_modules'), name: '后端 node_modules' },
|
||
{ path: path.join(__dirname, 'frontend', 'node_modules'), name: '前端 node_modules' },
|
||
{ path: path.join(__dirname, 'frontend', 'node_modules', '.vite'), name: '前端 Vite 缓存' },
|
||
{ path: path.join(__dirname, 'frontend', 'dist'), name: '前端构建产物' },
|
||
{ path: path.join(__dirname, 'backend', 'temp'), name: '后端临时文件' }
|
||
];
|
||
depDirs.forEach(d => {
|
||
if (fs.existsSync(d.path)) {
|
||
console.log(` ${ICONS.pipe} 删除: ${d.name}`);
|
||
}
|
||
});
|
||
}
|
||
|
||
log.divider();
|
||
log.warning('移除 --dry-run 参数以实际执行卸载');
|
||
log.divider();
|
||
}
|
||
|
||
function printSummary() {
|
||
const duration = ((Date.now() - uninstallStartTime) / 1000).toFixed(1);
|
||
|
||
log.banner('卸载完成', 'Uninstallation Complete', null);
|
||
|
||
log.keyValue('卸载耗时', `${duration} 秒`);
|
||
|
||
if (deletedItems.length > 0) {
|
||
log.section('已删除项目');
|
||
deletedItems.forEach(item => {
|
||
console.log(` ${ICONS.pipe} ${colors.dim}${item.type}:${colors.reset} ${item.name}`);
|
||
});
|
||
}
|
||
|
||
if (backedUpItems.length > 0) {
|
||
log.section('已备份项目');
|
||
backedUpItems.forEach(item => {
|
||
console.log(` ${ICONS.pipe} ${colors.dim}${item.type}:${colors.reset} ${path.basename(item.path)}`);
|
||
});
|
||
log.keyValue('备份位置', BACKUP_DIR);
|
||
}
|
||
|
||
log.section('保留的文件');
|
||
const backendExists = fs.existsSync(path.join(__dirname, 'backend'));
|
||
const frontendExists = fs.existsSync(path.join(__dirname, 'frontend'));
|
||
const uploadsExist = fs.existsSync(path.join(__dirname, 'backend', 'uploads'));
|
||
|
||
if (backendExists || frontendExists) {
|
||
console.log(` ${ICONS.pipe} 项目源代码 (backend/, frontend/)`);
|
||
}
|
||
if (uploadsExist) {
|
||
console.log(` ${ICONS.pipe} 上传的文件 (backend/uploads/)`);
|
||
}
|
||
if (!backendExists && !frontendExists && !uploadsExist) {
|
||
console.log(` ${ICONS.pipe} 无`);
|
||
}
|
||
|
||
log.section('重新部署');
|
||
console.log(` ${ICONS.pipe} ${colors.cyan}node install.js${colors.reset}`);
|
||
|
||
log.divider();
|
||
log.success('卸载完成!');
|
||
}
|
||
|
||
async function main() {
|
||
uninstallStartTime = Date.now();
|
||
const cmdArgs = parseArgs();
|
||
|
||
if (cmdArgs.help) {
|
||
showHelp();
|
||
return;
|
||
}
|
||
|
||
initLogFile(LOG_DIR);
|
||
|
||
log.banner(
|
||
'云睿资产管理系统',
|
||
'卸载脚本',
|
||
SCRIPT_VERSION
|
||
);
|
||
|
||
log.warning('此脚本将卸载 云睿资产管理系统');
|
||
|
||
if (cmdArgs.force) {
|
||
log.info('运行模式: 强制卸载(无需确认)');
|
||
}
|
||
|
||
if (cmdArgs.dryRun) {
|
||
log.info('运行模式: 预览模式(不实际执行)');
|
||
}
|
||
|
||
log.divider();
|
||
|
||
if (!cmdArgs.force && !cmdArgs.dryRun) {
|
||
const confirm = await ask('确认要开始卸载? (y/N)', 'N');
|
||
if (confirm.toLowerCase() !== 'y') {
|
||
log.info('已取消卸载');
|
||
closeReadline();
|
||
closeLogFile();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (cmdArgs.dryRun) {
|
||
showDryRunPreview(cmdArgs);
|
||
closeReadline();
|
||
closeLogFile();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
if (cmdArgs.backup) {
|
||
await backupDatabase();
|
||
}
|
||
|
||
await stopAndDeleteServices();
|
||
await cleanupNginxConfig(cmdArgs);
|
||
await cleanupConfigFiles();
|
||
|
||
if (!cmdArgs.skipDb) {
|
||
await cleanupDatabase(cmdArgs);
|
||
} else {
|
||
log.info('已跳过数据库删除');
|
||
}
|
||
|
||
await cleanupLogs(cmdArgs);
|
||
|
||
if (!cmdArgs.skipUploads) {
|
||
await cleanupUploads(cmdArgs);
|
||
} else {
|
||
log.info('已跳过上传文件目录删除');
|
||
}
|
||
|
||
if (!cmdArgs.skipDeps) {
|
||
await cleanupDependencies(cmdArgs);
|
||
} else {
|
||
log.info('已跳过依赖删除');
|
||
}
|
||
|
||
printSummary();
|
||
|
||
} catch (error) {
|
||
log.error(`卸载失败: ${error.message}`);
|
||
console.error(error);
|
||
process.exit(1);
|
||
} finally {
|
||
closeReadline();
|
||
closeLogFile();
|
||
}
|
||
}
|
||
|
||
main();
|