Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3b56fb1cc | |||
| 315820d487 | |||
| 878fd1c766 |
@@ -17,6 +17,13 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Restore npm cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: ~/AppData/Local/npm-cache
|
||||
key: npm-windows-${{ hashFiles('package-lock.json', 'website/package-lock.json') }}
|
||||
restore-keys: npm-windows-
|
||||
|
||||
- name: Install root dependencies
|
||||
env:
|
||||
NODE_ENV: development
|
||||
|
||||
@@ -48,18 +48,30 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Restore npm cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: /root/.npm
|
||||
key: npm-linux-${{ hashFiles('package-lock.json', 'server/package-lock.json') }}
|
||||
restore-keys: npm-linux-
|
||||
|
||||
- name: Restore Electron cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/root/.cache/electron
|
||||
/root/.cache/electron-builder
|
||||
key: electron-linux-${{ hashFiles('package.json') }}
|
||||
restore-keys: electron-linux-
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
NODE_ENV: development
|
||||
run: |
|
||||
apt-get update && apt-get install -y --no-install-recommends zip
|
||||
npm ci
|
||||
cd server && npm ci
|
||||
|
||||
- name: Install zip utility
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y zip
|
||||
|
||||
- name: Set CI release version
|
||||
run: >
|
||||
node tools/set-release-version.js
|
||||
@@ -108,6 +120,22 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Restore npm cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: ~/AppData/Local/npm-cache
|
||||
key: npm-windows-${{ hashFiles('package-lock.json', 'server/package-lock.json') }}
|
||||
restore-keys: npm-windows-
|
||||
|
||||
- name: Restore Electron cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/AppData/Local/electron/Cache
|
||||
~/AppData/Local/electron-builder/Cache
|
||||
key: electron-windows-${{ hashFiles('package.json') }}
|
||||
restore-keys: electron-windows-
|
||||
|
||||
- name: Install dependencies
|
||||
env:
|
||||
NODE_ENV: development
|
||||
@@ -217,9 +245,6 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --omit=dev
|
||||
|
||||
- name: Download previous manifest
|
||||
env:
|
||||
GITEA_RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
|
||||
@@ -146,9 +146,13 @@
|
||||
"output": "dist-electron"
|
||||
},
|
||||
"files": [
|
||||
"!node_modules",
|
||||
"dist/client/**/*",
|
||||
"dist/electron/**/*",
|
||||
"node_modules/**/*",
|
||||
"node_modules/{ansi-regex,ansi-styles,ansis,app-root-path,applescript,argparse,auto-launch,available-typed-arrays,balanced-match,base64-js,brace-expansion,buffer,builder-util-runtime,call-bind,call-bind-apply-helpers,call-bound,cliui,concat-map,cross-spawn,dayjs,debug,dedent,define-data-property,dotenv,dunder-proto,electron-updater,emoji-regex,es-define-property,es-errors,es-object-atoms,escalade,for-each,foreground-child,fs-extra,function-bind,get-caller-file,get-east-asian-width,get-intrinsic,get-proto,glob,gopd,graceful-fs,has-property-descriptors,has-symbols,has-tostringtag,hasown,ieee754,inherits,is-callable,is-fullwidth-code-point,is-typed-array,isarray,isexe,jackspeak,js-yaml,jsonfile,lazy-val,lodash.escaperegexp,lodash.isequal,lru-cache,math-intrinsics,minimatch,minimist,minipass,mkdirp,ms,package-json-from-dist,path-is-absolute,path-key,path-scurry,possible-typed-array-names,reflect-metadata,safe-buffer,sax,semver,set-function-length,sha.js,shebang-command,shebang-regex,signal-exit,sql-highlight,sql.js,string-width,string-width-cjs,strip-ansi,strip-ansi-cjs,tiny-typed-emitter,to-buffer,tslib,typed-array-buffer,typeorm,universalify,untildify,uuid,which,which-typed-array,winreg,wrap-ansi,wrap-ansi-cjs,y18n,yallist,yargs,yargs-parser}/**/*",
|
||||
"node_modules/@isaacs/cliui/**/*",
|
||||
"node_modules/@pkgjs/parseargs/**/*",
|
||||
"node_modules/@sqltools/formatter/**/*",
|
||||
"!node_modules/**/test/**/*",
|
||||
"!node_modules/**/tests/**/*",
|
||||
"!node_modules/**/*.d.ts",
|
||||
|
||||
@@ -17,11 +17,77 @@ import {
|
||||
import { serverMigrations } from '../migrations';
|
||||
import { findExistingPath, resolveRuntimePath } from '../runtime-paths';
|
||||
|
||||
const DATA_DIR = resolveRuntimePath('data');
|
||||
const DB_FILE = path.join(DATA_DIR, 'metoyou.sqlite');
|
||||
function resolveDbFile(): string {
|
||||
const envPath = process.env.DB_PATH;
|
||||
|
||||
if (envPath) {
|
||||
return path.resolve(envPath);
|
||||
}
|
||||
|
||||
return path.join(resolveRuntimePath('data'), 'metoyou.sqlite');
|
||||
}
|
||||
|
||||
const DB_FILE = resolveDbFile();
|
||||
const DB_BACKUP = DB_FILE + '.bak';
|
||||
const DATA_DIR = path.dirname(DB_FILE);
|
||||
// SQLite files start with this 16-byte header string.
|
||||
const SQLITE_MAGIC = 'SQLite format 3\0';
|
||||
|
||||
let applicationDataSource: DataSource | undefined;
|
||||
|
||||
/**
|
||||
* Returns true when `data` looks like a valid SQLite file
|
||||
* (correct header magic and at least one complete page).
|
||||
*/
|
||||
function isValidSqlite(data: Uint8Array): boolean {
|
||||
if (data.length < 100)
|
||||
return false;
|
||||
|
||||
const header = Buffer.from(data.buffer, data.byteOffset, 16).toString('ascii');
|
||||
|
||||
return header === SQLITE_MAGIC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Back up the current DB file so there is always a recovery point.
|
||||
* If the main file is corrupted/empty but a valid backup exists,
|
||||
* restore the backup before the server loads the database.
|
||||
*/
|
||||
function safeguardDbFile(): Uint8Array | undefined {
|
||||
if (!fs.existsSync(DB_FILE))
|
||||
return undefined;
|
||||
|
||||
const data = new Uint8Array(fs.readFileSync(DB_FILE));
|
||||
|
||||
if (isValidSqlite(data)) {
|
||||
// Good file - rotate it into the backup slot.
|
||||
fs.copyFileSync(DB_FILE, DB_BACKUP);
|
||||
console.log('[DB] Backed up database to', DB_BACKUP);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// The main file is corrupt or empty.
|
||||
console.warn(`[DB] ${DB_FILE} appears corrupt (${data.length} bytes) - checking backup`);
|
||||
|
||||
if (fs.existsSync(DB_BACKUP)) {
|
||||
const backup = new Uint8Array(fs.readFileSync(DB_BACKUP));
|
||||
|
||||
if (isValidSqlite(backup)) {
|
||||
fs.copyFileSync(DB_BACKUP, DB_FILE);
|
||||
console.warn('[DB] Restored database from backup', DB_BACKUP);
|
||||
|
||||
return backup;
|
||||
}
|
||||
|
||||
console.error('[DB] Backup is also invalid - starting with a fresh database');
|
||||
} else {
|
||||
console.error('[DB] No backup available - starting with a fresh database');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveSqlJsConfig(): { locateFile: (file: string) => string } {
|
||||
return {
|
||||
locateFile: (file) => {
|
||||
@@ -47,10 +113,7 @@ export async function initDatabase(): Promise<void> {
|
||||
if (!fs.existsSync(DATA_DIR))
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
|
||||
let database: Uint8Array | undefined;
|
||||
|
||||
if (fs.existsSync(DB_FILE))
|
||||
database = fs.readFileSync(DB_FILE);
|
||||
const database = safeguardDbFile();
|
||||
|
||||
try {
|
||||
applicationDataSource = new DataSource({
|
||||
@@ -94,7 +157,7 @@ export async function initDatabase(): Promise<void> {
|
||||
await applicationDataSource.runMigrations();
|
||||
console.log('[DB] Migrations executed');
|
||||
} else {
|
||||
console.log('[DB] Synchronize mode — migrations skipped');
|
||||
console.log('[DB] Synchronize mode - migrations skipped');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { resolveCertificateDirectory, resolveEnvFilePath } from './runtime-paths
|
||||
// Load .env from project root (one level up from server/)
|
||||
dotenv.config({ path: resolveEnvFilePath() });
|
||||
|
||||
import { initDatabase } from './db/database';
|
||||
import { initDatabase, destroyDatabase } from './db/database';
|
||||
import { deleteStaleJoinRequests } from './cqrs';
|
||||
import { createApp } from './app';
|
||||
import {
|
||||
@@ -119,6 +119,26 @@ async function bootstrap(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
let shuttingDown = false;
|
||||
|
||||
async function gracefulShutdown(signal: string): Promise<void> {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
|
||||
console.log(`\n[Shutdown] ${signal} received — closing database…`);
|
||||
|
||||
try {
|
||||
await destroyDatabase();
|
||||
} catch (err) {
|
||||
console.error('[Shutdown] Error closing database:', err);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
|
||||
bootstrap().catch((err) => {
|
||||
console.error('Failed to start server:', err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -4,8 +4,12 @@ export class ServerChannels1000000000002 implements MigrationInterface {
|
||||
name = 'ServerChannels1000000000002';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const columns: { name: string }[] = await queryRunner.query(`PRAGMA table_info("servers")`);
|
||||
const hasChannels = columns.some(c => c.name === 'channels');
|
||||
if (!hasChannels) {
|
||||
await queryRunner.query(`ALTER TABLE "servers" ADD COLUMN "channels" TEXT NOT NULL DEFAULT '[]'`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "servers" DROP COLUMN "channels"`);
|
||||
|
||||
Reference in New Issue
Block a user