123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- import { log } from "./log";
- import { R } from "redbean-node";
- import { DockgeServer } from "./dockge-server";
- import fs from "fs";
- import path from "path";
- import knex from "knex";
- // @ts-ignore
- import Dialect from "knex/lib/dialects/sqlite3/index.js";
- import sqlite from "@louislam/sqlite3";
- import { sleep } from "./util-common";
- interface DBConfig {
- type?: "sqlite" | "mysql";
- hostname?: string;
- port?: string;
- database?: string;
- username?: string;
- password?: string;
- }
- export class Database {
- /**
- * SQLite file path (Default: ./data/dockge.db)
- * @type {string}
- */
- static sqlitePath : string;
- static noReject = true;
- static dbConfig: DBConfig = {};
- static knexMigrationsPath = "./backend/migrations";
- private static server : DockgeServer;
- /**
- * Use for decode the auth object
- */
- jwtSecret? : string;
- static async init(server : DockgeServer) {
- this.server = server;
- log.debug("server", "Connecting to the database");
- await Database.connect();
- log.info("server", "Connected to the database");
- // Patch the database
- await Database.patch();
- }
- /**
- * Read the database config
- * @throws {Error} If the config is invalid
- * @typedef {string|undefined} envString
- * @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
- */
- static readDBConfig() : DBConfig {
- const dbConfigString = fs.readFileSync(path.join(this.server.config.dataDir, "db-config.json")).toString("utf-8");
- const dbConfig = JSON.parse(dbConfigString);
- if (typeof dbConfig !== "object") {
- throw new Error("Invalid db-config.json, it must be an object");
- }
- if (typeof dbConfig.type !== "string") {
- throw new Error("Invalid db-config.json, type must be a string");
- }
- return dbConfig;
- }
- /**
- * @typedef {string|undefined} envString
- * @param dbConfig the database configuration that should be written
- * @returns {void}
- */
- static writeDBConfig(dbConfig : DBConfig) {
- fs.writeFileSync(path.join(this.server.config.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
- }
- /**
- * Connect to the database
- * @param {boolean} autoloadModels Should models be automatically loaded?
- * @param {boolean} noLog Should logs not be output?
- * @returns {Promise<void>}
- */
- static async connect(autoloadModels = true) {
- const acquireConnectionTimeout = 120 * 1000;
- let dbConfig : DBConfig;
- try {
- dbConfig = this.readDBConfig();
- Database.dbConfig = dbConfig;
- } catch (err) {
- if (err instanceof Error) {
- log.warn("db", err.message);
- }
- dbConfig = {
- type: "sqlite",
- };
- this.writeDBConfig(dbConfig);
- }
- let config = {};
- log.info("db", `Database Type: ${dbConfig.type}`);
- if (dbConfig.type === "sqlite") {
- this.sqlitePath = path.join(this.server.config.dataDir, "dockge.db");
- Dialect.prototype._driver = () => sqlite;
- config = {
- client: Dialect,
- connection: {
- filename: Database.sqlitePath,
- acquireConnectionTimeout: acquireConnectionTimeout,
- },
- useNullAsDefault: true,
- pool: {
- min: 1,
- max: 1,
- idleTimeoutMillis: 120 * 1000,
- propagateCreateError: false,
- acquireTimeoutMillis: acquireConnectionTimeout,
- }
- };
- } else {
- throw new Error("Unknown Database type: " + dbConfig.type);
- }
- const knexInstance = knex(config);
- // @ts-ignore
- R.setup(knexInstance);
- if (process.env.SQL_LOG === "1") {
- R.debug(true);
- }
- // Auto map the model to a bean object
- R.freeze(true);
- if (autoloadModels) {
- R.autoloadModels("./backend/models", "ts");
- }
- if (dbConfig.type === "sqlite") {
- await this.initSQLite();
- }
- }
- /**
- @returns {Promise<void>}
- */
- static async initSQLite() {
- await R.exec("PRAGMA foreign_keys = ON");
- // Change to WAL
- await R.exec("PRAGMA journal_mode = WAL");
- await R.exec("PRAGMA cache_size = -12000");
- await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
- // This ensures that an operating system crash or power failure will not corrupt the database.
- // FULL synchronous is very safe, but it is also slower.
- // Read more: https://sqlite.org/pragma.html#pragma_synchronous
- await R.exec("PRAGMA synchronous = NORMAL");
- log.debug("db", "SQLite config:");
- log.debug("db", await R.getAll("PRAGMA journal_mode"));
- log.debug("db", await R.getAll("PRAGMA cache_size"));
- log.debug("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
- }
- /**
- * Patch the database
- * @returns {void}
- */
- static async patch() {
- // Using knex migrations
- // https://knexjs.org/guide/migrations.html
- // https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
- try {
- await R.knex.migrate.latest({
- directory: Database.knexMigrationsPath,
- });
- } catch (e) {
- if (e instanceof Error) {
- // Allow missing patch files for downgrade or testing pr.
- if (e.message.includes("the following files are missing:")) {
- log.warn("db", e.message);
- log.warn("db", "Database migration failed, you may be downgrading Dockge.");
- } else {
- log.error("db", "Database migration failed");
- throw e;
- }
- }
- }
- }
- /**
- * Special handle, because tarn.js throw a promise reject that cannot be caught
- * @returns {Promise<void>}
- */
- static async close() {
- const listener = () => {
- Database.noReject = false;
- };
- process.addListener("unhandledRejection", listener);
- log.info("db", "Closing the database");
- // Flush WAL to main database
- if (Database.dbConfig.type === "sqlite") {
- await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
- }
- while (true) {
- Database.noReject = true;
- await R.close();
- await sleep(2000);
- if (Database.noReject) {
- break;
- } else {
- log.info("db", "Waiting to close the database");
- }
- }
- log.info("db", "Database closed");
- process.removeListener("unhandledRejection", listener);
- }
- /**
- * Get the size of the database (SQLite only)
- * @returns {number} Size of database
- */
- static getSize() {
- if (Database.dbConfig.type === "sqlite") {
- log.debug("db", "Database.getSize()");
- const stats = fs.statSync(Database.sqlitePath);
- log.debug("db", stats);
- return stats.size;
- }
- return 0;
- }
- /**
- * Shrink the database
- * @returns {Promise<void>}
- */
- static async shrink() {
- if (Database.dbConfig.type === "sqlite") {
- await R.exec("VACUUM");
- }
- }
- }
|