database.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. /**
  2. * SQLite 数据库管理模块
  3. */
  4. const sqlite3 = require('sqlite3').verbose();
  5. const path = require('path');
  6. const fs = require('fs').promises;
  7. const logger = require('../logger');
  8. const bcrypt = require('bcrypt');
  9. // 数据库文件路径
  10. const DB_PATH = path.join(__dirname, '../data/app.db');
  11. class Database {
  12. constructor() {
  13. this.db = null;
  14. }
  15. /**
  16. * 初始化数据库连接
  17. */
  18. async connect() {
  19. try {
  20. // 确保数据目录存在
  21. const dbDir = path.dirname(DB_PATH);
  22. await fs.mkdir(dbDir, { recursive: true });
  23. return new Promise((resolve, reject) => {
  24. this.db = new sqlite3.Database(DB_PATH, (err) => {
  25. if (err) {
  26. logger.error('数据库连接失败:', err);
  27. reject(err);
  28. } else {
  29. logger.info('SQLite 数据库连接成功');
  30. resolve();
  31. }
  32. });
  33. });
  34. } catch (error) {
  35. logger.error('初始化数据库失败:', error);
  36. throw error;
  37. }
  38. }
  39. /**
  40. * 创建数据表
  41. */
  42. async createTables() {
  43. const tables = [
  44. // 用户表
  45. `CREATE TABLE IF NOT EXISTS users (
  46. id INTEGER PRIMARY KEY AUTOINCREMENT,
  47. username TEXT UNIQUE NOT NULL,
  48. password TEXT NOT NULL,
  49. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  50. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  51. login_count INTEGER DEFAULT 0,
  52. last_login DATETIME
  53. )`,
  54. // 配置表
  55. `CREATE TABLE IF NOT EXISTS configs (
  56. id INTEGER PRIMARY KEY AUTOINCREMENT,
  57. key TEXT UNIQUE NOT NULL,
  58. value TEXT NOT NULL,
  59. type TEXT DEFAULT 'string',
  60. description TEXT,
  61. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  62. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  63. )`,
  64. // 文档表
  65. `CREATE TABLE IF NOT EXISTS documents (
  66. id INTEGER PRIMARY KEY AUTOINCREMENT,
  67. doc_id TEXT UNIQUE NOT NULL,
  68. title TEXT NOT NULL,
  69. content TEXT NOT NULL,
  70. published BOOLEAN DEFAULT 0,
  71. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  72. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  73. )`,
  74. // 系统日志表
  75. `CREATE TABLE IF NOT EXISTS system_logs (
  76. id INTEGER PRIMARY KEY AUTOINCREMENT,
  77. level TEXT NOT NULL,
  78. message TEXT NOT NULL,
  79. details TEXT,
  80. created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  81. )`,
  82. // Session表 - 用于存储用户会话
  83. `CREATE TABLE IF NOT EXISTS sessions (
  84. sid TEXT PRIMARY KEY,
  85. sess TEXT NOT NULL,
  86. expire DATETIME NOT NULL
  87. )`,
  88. // 菜单项表 - 用于存储导航菜单配置
  89. `CREATE TABLE IF NOT EXISTS menu_items (
  90. id INTEGER PRIMARY KEY AUTOINCREMENT,
  91. text TEXT NOT NULL,
  92. link TEXT NOT NULL,
  93. new_tab BOOLEAN DEFAULT 0,
  94. sort_order INTEGER DEFAULT 0,
  95. enabled BOOLEAN DEFAULT 1,
  96. created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  97. updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  98. )`
  99. ];
  100. for (const sql of tables) {
  101. await this.run(sql);
  102. }
  103. logger.info('数据表创建完成');
  104. }
  105. /**
  106. * 执行SQL语句
  107. */
  108. async run(sql, params = []) {
  109. return new Promise((resolve, reject) => {
  110. this.db.run(sql, params, function(err) {
  111. if (err) {
  112. logger.error('SQL执行失败:', err);
  113. reject(err);
  114. } else {
  115. resolve({ id: this.lastID, changes: this.changes });
  116. }
  117. });
  118. });
  119. }
  120. /**
  121. * 查询单条记录
  122. */
  123. async get(sql, params = []) {
  124. return new Promise((resolve, reject) => {
  125. this.db.get(sql, params, (err, row) => {
  126. if (err) {
  127. logger.error('SQL查询失败:', err);
  128. reject(err);
  129. } else {
  130. resolve(row);
  131. }
  132. });
  133. });
  134. }
  135. /**
  136. * 查询多条记录
  137. */
  138. async all(sql, params = []) {
  139. return new Promise((resolve, reject) => {
  140. this.db.all(sql, params, (err, rows) => {
  141. if (err) {
  142. logger.error('SQL查询失败:', err);
  143. reject(err);
  144. } else {
  145. resolve(rows);
  146. }
  147. });
  148. });
  149. }
  150. /**
  151. * 初始化默认管理员用户
  152. */
  153. async createDefaultAdmin() {
  154. try {
  155. const adminUser = await this.get('SELECT id FROM users WHERE username = ?', ['root']);
  156. if (!adminUser) {
  157. const hashedPassword = await bcrypt.hash('admin@123', 10);
  158. await this.run(
  159. 'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)',
  160. ['root', hashedPassword, new Date().toISOString(), 0, null]
  161. );
  162. logger.info('默认管理员用户创建成功: root/admin@123');
  163. }
  164. } catch (error) {
  165. logger.error('创建默认管理员用户失败:', error);
  166. }
  167. }
  168. /**
  169. * 创建默认文档
  170. */
  171. async createDefaultDocuments() {
  172. try {
  173. const docCount = await this.get('SELECT COUNT(*) as count FROM documents');
  174. if (docCount.count === 0) {
  175. const defaultDocs = [
  176. {
  177. doc_id: 'welcome',
  178. title: '欢迎使用 Docker 镜像代理加速系统',
  179. content: `## 系统介绍
  180. 这是一个基于 Nginx 的 Docker 镜像代理加速系统,可以帮助您加速 Docker 镜像的下载和部署。
  181. ## 主要功能
  182. - 🚀 **镜像加速**: 提供多个 Docker 镜像仓库的代理加速
  183. - 🔧 **配置管理**: 简单易用的 Web 管理界面
  184. - 📊 **监控统计**: 实时监控代理服务状态
  185. - 📖 **文档管理**: 内置文档系统,方便管理和分享
  186. ## 快速开始
  187. 1. 访问管理面板进行基础配置
  188. 2. 配置 Docker 客户端使用代理地址
  189. 3. 开始享受加速的镜像下载体验
  190. ## 更多信息
  191. 如需更多帮助,请查看项目文档或访问 GitHub 仓库。`,
  192. published: 1
  193. },
  194. {
  195. doc_id: 'docker-config',
  196. title: 'Docker 客户端配置指南',
  197. content: `## 配置说明
  198. 使用本代理服务需要配置 Docker 客户端的镜像仓库地址。
  199. ## Linux/macOS 配置
  200. 编辑或创建 \`/etc/docker/daemon.json\` 文件:
  201. \`\`\`json
  202. {
  203. "registry-mirrors": [
  204. "http://your-proxy-domain.com"
  205. ]
  206. }
  207. \`\`\`
  208. 重启 Docker 服务:
  209. \`\`\`bash
  210. sudo systemctl restart docker
  211. \`\`\`
  212. ## Windows 配置
  213. 在 Docker Desktop 设置中:
  214. 1. 打开 Settings -> Docker Engine
  215. 2. 添加配置到 JSON 文件中
  216. 3. 点击 "Apply & Restart"
  217. ## 验证配置
  218. 运行以下命令验证配置是否生效:
  219. \`\`\`bash
  220. docker info
  221. \`\`\`
  222. 在输出中查看 "Registry Mirrors" 部分。`,
  223. published: 1
  224. }
  225. ];
  226. for (const doc of defaultDocs) {
  227. await this.run(
  228. 'INSERT INTO documents (doc_id, title, content, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
  229. [doc.doc_id, doc.title, doc.content, doc.published, new Date().toISOString(), new Date().toISOString()]
  230. );
  231. }
  232. }
  233. } catch (error) {
  234. logger.error('创建默认文档失败:', error);
  235. }
  236. }
  237. /**
  238. * 检查数据库是否已经初始化
  239. */
  240. async isInitialized() {
  241. try {
  242. // 先检查是否有用户表
  243. const tableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
  244. if (!tableExists) {
  245. return false;
  246. }
  247. // 检查是否有初始化标记
  248. const configTableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='configs'");
  249. if (configTableExists) {
  250. const initFlag = await this.get('SELECT value FROM configs WHERE key = ?', ['db_initialized']);
  251. if (initFlag) {
  252. return true;
  253. }
  254. }
  255. // 检查是否有用户数据
  256. const userCount = await this.get('SELECT COUNT(*) as count FROM users');
  257. return userCount && userCount.count > 0;
  258. } catch (error) {
  259. // 如果查询失败,认为数据库未初始化
  260. return false;
  261. }
  262. }
  263. /**
  264. * 标记数据库已初始化
  265. */
  266. async markAsInitialized() {
  267. try {
  268. await this.run(
  269. 'INSERT OR REPLACE INTO configs (key, value, type, description) VALUES (?, ?, ?, ?)',
  270. ['db_initialized', 'true', 'boolean', '数据库初始化标记']
  271. );
  272. logger.info('数据库已标记为已初始化');
  273. } catch (error) {
  274. logger.error('标记数据库初始化状态失败:', error);
  275. }
  276. }
  277. /**
  278. * 关闭数据库连接
  279. */
  280. async close() {
  281. return new Promise((resolve, reject) => {
  282. if (this.db) {
  283. this.db.close((err) => {
  284. if (err) {
  285. logger.error('关闭数据库连接失败:', err);
  286. reject(err);
  287. } else {
  288. logger.info('数据库连接已关闭');
  289. resolve();
  290. }
  291. });
  292. } else {
  293. resolve();
  294. }
  295. });
  296. }
  297. /**
  298. * 清理过期的会话
  299. */
  300. async cleanExpiredSessions() {
  301. try {
  302. const result = await this.run(
  303. 'DELETE FROM sessions WHERE expire < ?',
  304. [new Date().toISOString()]
  305. );
  306. if (result.changes > 0) {
  307. logger.info(`清理了 ${result.changes} 个过期会话`);
  308. }
  309. } catch (error) {
  310. logger.error('清理过期会话失败:', error);
  311. }
  312. }
  313. /**
  314. * 创建默认菜单项
  315. */
  316. async createDefaultMenuItems() {
  317. try {
  318. const menuCount = await this.get('SELECT COUNT(*) as count FROM menu_items');
  319. if (menuCount.count === 0) {
  320. const defaultMenuItems = [
  321. { text: '控制台', link: '/admin', new_tab: 0, sort_order: 1 },
  322. { text: '镜像搜索', link: '/', new_tab: 0, sort_order: 2 },
  323. { text: '文档', link: '/docs', new_tab: 0, sort_order: 3 },
  324. { text: 'GitHub', link: 'https://github.com/dqzboy/hubcmdui', new_tab: 1, sort_order: 4 }
  325. ];
  326. for (const item of defaultMenuItems) {
  327. await this.run(
  328. 'INSERT INTO menu_items (text, link, new_tab, sort_order, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
  329. [item.text, item.link, item.new_tab, item.sort_order, 1, new Date().toISOString(), new Date().toISOString()]
  330. );
  331. }
  332. }
  333. } catch (error) {
  334. logger.error('创建默认菜单项失败:', error);
  335. }
  336. }
  337. }
  338. // 创建数据库实例
  339. const database = new Database();
  340. module.exports = database;