Files
rainblogweb/cli.js
T
miaomiao ad8883cc61 feat: 博客功能后端 - 搜索/RSS/标签/归档/views/点赞/评论增强/备份
- migrateSchema v2: tags/views/parent_id/status 列 + post_likes 表
- 接口: search/tags/tag/archive/prevnext/点赞三连/评论审核三连/feed.xml
- 评论: 嵌套回复/审核模式(comment_moderate)/限流/邮件通知(comment_notify)
- cli backup 命令 + status 显示最近备份;backups/ 入 gitignore
- ssr blogSSR 阅读量计数;settings 白名单补新键
2026-08-07 01:57:01 +08:00

329 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
const { execSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
const readline = require('readline');
const bcrypt = require('bcryptjs');
const http = require('http');
const CONFIG_PATH = path.join(__dirname, '.env.json');
const PKG = require('./package.json');
const db = require('./db');
const HELP = `
RainWeb CLI v${PKG.version}
Usage: node cli.js <command> [options]
Commands:
status Show server and system status
start Start the server
restart Restart the server
stop Stop the server
port [number] Show or change listen port (default: 3001)
password [new-pass] Change admin password (leave empty for prompt)
captcha Interactive captcha rule configuration
config Show all current settings
backup Backup database to backups/ (readonly, safe to run while running)
upgrade Git pull + npm install + restart (one-click upgrade)
help Show this help
Examples:
node cli.js status
node cli.js port 8080
node cli.js password MyNewP@ss123
node cli.js captcha
node cli.js backup
node cli.js upgrade
`;
async function main() {
const cmd = process.argv[2] || 'help';
switch (cmd) {
case 'status': return cmdStatus();
case 'start': return cmdStart();
case 'restart': return cmdRestart();
case 'stop': return cmdStop();
case 'port': return cmdPort();
case 'password': return cmdPassword();
case 'captcha': return cmdCaptcha();
case 'config': return cmdConfig();
case 'backup': return cmdBackup();
case 'upgrade': return cmdUpgrade();
case 'help':
default:
console.log(HELP);
}
}
// === Status ===
async function cmdStatus() {
console.log(`RainWeb v${PKG.version}`);
console.log(`Node.js: ${process.version}`);
console.log(`Platform: ${process.platform}`);
console.log(`Data DB: ${fs.existsSync(path.join(__dirname, 'data', 'rainweb.db')) ? fs.statSync(path.join(__dirname, 'data', 'rainweb.db')).size + ' bytes' : 'NOT FOUND'}`);
// 最近一次备份时间
const latestBackup = getLatestBackup();
console.log('Last backup: ' + (latestBackup ? formatMtime(latestBackup.mtime) + ' (' + latestBackup.file + ')' : '从未备份'));
// Check if server is running
try {
await httpGet('http://localhost:' + (getConfigPort()));
console.log('Server: RUNNING');
} catch {
console.log('Server: STOPPED');
}
// Show admin info
try {
await db.getDb();
const admin = db.get("SELECT id, username, email, email_verified FROM users WHERE role = 'admin'");
if (admin) {
console.log(`Admin: ${admin.username} (email: ${admin.email || 'not set'}, verified: ${admin.email_verified ? 'yes' : 'no'})`);
}
} catch {}
}
// === Port ===
function getConfigPort() {
try {
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
return config.port || 3001;
} catch { return 3001; }
}
function setConfigPort(port) {
let config = {};
try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')); } catch {}
config.port = port;
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
}
function cmdPort() {
const arg = process.argv[3];
if (arg) {
const port = parseInt(arg);
if (isNaN(port) || port < 1 || port > 65535) {
console.error('Invalid port number. Use 1-65535.');
process.exit(1);
}
setConfigPort(port);
console.log(`Port set to ${port}. Restart to apply.`);
} else {
console.log(`Current port: ${getConfigPort()}`);
}
}
// === Password ===
async function cmdPassword() {
await warnIfServerRunning();
let newPass = process.argv[3];
if (!newPass) {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
newPass = await new Promise(resolve => {
rl.question('New admin password (min 6 chars): ', resolve);
});
rl.close();
}
if (!newPass || newPass.length < 6) {
console.error('Password must be at least 6 characters.');
process.exit(1);
}
await db.getDb();
const admin = db.get("SELECT id FROM users WHERE role = 'admin'");
if (!admin) { console.error('No admin user found.'); process.exit(1); }
const hash = bcrypt.hashSync(newPass, 10);
db.run('UPDATE users SET password = ? WHERE id = ?', [hash, admin.id]);
console.log('Admin password updated successfully.');
}
// === Captcha ===
async function cmdCaptcha() {
await warnIfServerRunning();
await db.getDb();
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
const q = (q) => new Promise(resolve => rl.question(q, resolve));
console.log('=== Captcha Rule Configuration ===\n');
console.log('Current settings:');
['login','register','forum','failed'].forEach(k => {
const v = db.get("SELECT value FROM site_settings WHERE key = 'captcha_" + k + "'");
console.log(` ${k}: ${v ? v.value : '0'}`);
});
const type = db.get("SELECT value FROM site_settings WHERE key = 'captcha_type'");
console.log(` type: ${type ? type.value : 'builtin'}\n`);
const typeAns = await q('Captcha type (builtin/recaptcha/both) [' + (type ? type.value : 'builtin') + ']: ');
if (typeAns) db.run("UPDATE site_settings SET value=? WHERE key='captcha_type'", [typeAns]);
for (const scope of ['login', 'register', 'forum']) {
const current = db.get("SELECT value FROM site_settings WHERE key='captcha_" + scope + "'");
const ans = await q(`Enable captcha for ${scope}? (y/n) [${current && current.value === '1' ? 'y' : 'n'}]: `);
db.run("UPDATE site_settings SET value=? WHERE key='captcha_" + scope + "'", [ans.toLowerCase() === 'y' ? '1' : '0']);
}
const failAns = await q('Enable captcha after failed attempts? (y/n): ');
db.run("UPDATE site_settings SET value=? WHERE key='captcha_failed'", [failAns.toLowerCase() === 'y' ? '1' : '0']);
if (failAns.toLowerCase() === 'y') {
const threshold = await q('Failed attempts threshold (default 5): ');
if (threshold) db.run("UPDATE site_settings SET value=? WHERE key='captcha_failed_threshold'", [threshold]);
}
rl.close();
console.log('\nCaptcha rules updated.');
}
// === Config ===
async function cmdConfig() {
await db.getDb();
const rows = db.all("SELECT key, value FROM site_settings ORDER BY key");
console.log('=== Site Configuration ===\n');
const secrets = ['smtp_pass', 'recaptcha_secret_key'];
rows.forEach(r => {
let val = r.value;
if (secrets.includes(r.key) && val) val = '****' + val.slice(-4);
console.log(` ${r.key}: ${val || '(empty)'}`);
});
console.log(`\n listen_port: ${getConfigPort()}`);
}
// === Start / Stop / Restart ===
function findPidFile() { return path.join(__dirname, 'server.pid'); }
function isRunning(pid) {
try { process.kill(pid, 0); return true; } catch { return false; }
}
async function cmdStop() {
const pidFile = findPidFile();
if (fs.existsSync(pidFile)) {
const pid = parseInt(fs.readFileSync(pidFile, 'utf8'));
if (isRunning(pid)) {
try { process.kill(pid); console.log('Server stopped (PID: ' + pid + ')'); } catch { console.log('Could not stop process.'); }
} else { console.log('Server not running.'); }
fs.unlinkSync(pidFile);
} else {
// Try to find node process
console.log('No PID file found. Try: taskkill /F /IM node.exe (Windows) or pkill node (Linux/Mac)');
}
}
async function cmdStart() {
const port = getConfigPort();
const proc = spawn('node', ['server.js'], {
cwd: __dirname,
stdio: 'inherit',
env: { ...process.env, PORT: String(port) },
detached: true,
});
proc.unref();
fs.writeFileSync(findPidFile(), String(proc.pid));
console.log(`Server starting on port ${port} (PID: ${proc.pid})`);
}
async function cmdRestart() {
await cmdStop();
await new Promise(r => setTimeout(r, 1000));
await cmdStart();
}
// === Backup ===
async function cmdBackup() {
const srcPath = path.join(__dirname, 'data', 'rainweb.db');
if (!fs.existsSync(srcPath)) {
console.error('数据库不存在:' + srcPath + '(首次启动 server 后才会创建)');
process.exit(1);
}
const backupDir = path.join(__dirname, 'backups');
fs.mkdirSync(backupDir, { recursive: true });
const now = new Date();
const destPath = path.join(backupDir,
`rainweb-${now.getFullYear()}${pad2(now.getMonth() + 1)}${pad2(now.getDate())}-${pad2(now.getHours())}${pad2(now.getMinutes())}${pad2(now.getSeconds())}.db`);
// 只读打开源库,不写数据,安全(server 运行中也可执行)
// better-sqlite3 v13src.backup(destPath) 接受目标文件路径(string),返回 Promise
const Database = require('better-sqlite3');
const src = new Database(srcPath, { readonly: true });
try {
await src.backup(destPath);
} finally {
src.close();
}
const size = fs.statSync(destPath).size;
console.log(`备份成功:${destPath}${size} bytes`);
}
// 读取 backups/ 目录下最新的备份文件(按 mtime),无备份返回 null
function getLatestBackup() {
const backupDir = path.join(__dirname, 'backups');
if (!fs.existsSync(backupDir)) return null;
let latest = null;
for (const f of fs.readdirSync(backupDir)) {
if (!f.startsWith('rainweb-') || !f.endsWith('.db')) continue;
const filePath = path.join(backupDir, f);
const mtime = fs.statSync(filePath).mtime;
if (!latest || mtime > latest.mtime) latest = { file: f, mtime };
}
return latest;
}
function pad2(n) { return String(n).padStart(2, '0'); }
function formatMtime(date) {
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${pad2(date.getHours())}:${pad2(date.getMinutes())}`;
}
// === Upgrade ===
async function cmdUpgrade() {
console.log('=== RainWeb Upgrade ===\n');
if (!fs.existsSync(path.join(__dirname, '.git'))) {
console.error('不是 git 仓库,无法 upgrade。请先 git clone 安装。');
process.exit(1);
}
console.log('1. Pulling latest code via git...');
try {
execSync('git pull', { cwd: __dirname, stdio: 'inherit' });
} catch {
console.error('Git pull failed. Check for conflicts.');
process.exit(1);
}
console.log('\n2. Installing dependencies...');
try {
execSync('npm install', { cwd: __dirname, stdio: 'inherit' });
} catch {
console.error('npm install failed.');
process.exit(1);
}
console.log('\n3. Restarting server...');
await cmdRestart();
console.log('\n=== Upgrade complete! ===');
}
// === Helper ===
// 写命令(password/captcha)在连接库前探测 server 是否运行,仅提示不阻止执行
async function warnIfServerRunning() {
try {
await httpGet('http://localhost:' + (getConfigPort()));
console.log('警告:server 正在运行,并发写库可能失败或等待,建议先停止 server 再执行');
} catch {}
}
function httpGet(url) {
return new Promise((resolve, reject) => {
http.get(url, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(d)); })
.on('error', reject);
});
}
main().catch(console.error);