import "dotenv/config"; import express from "express"; import { createServer } from "http"; import net from "net"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import mysql from "mysql2/promise"; import { createExpressMiddleware } from "@trpc/server/adapters/express"; import { appRouter } from "../routers"; import { createContext } from "./context"; import { serveStatic, setupVite } from "./vite"; import { fileRouter } from "../fileRoutes"; import { ensureBootstrapAdmin } from "./auth"; const SERVER_ROOT = path.dirname(fileURLToPath(import.meta.url)); const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); /** * Run SQL migrations using a raw mysql2 connection. * Drizzle's execute() requires tagged template sql``; using mysql2 directly * is safer for running arbitrary migration statements. */ async function runMigrations() { if (!process.env.DATABASE_URL) return; // Wait for MySQL to be ready (up to 40 s) let conn: mysql.Connection | null = null; for (let i = 1; i <= 20; i++) { try { conn = await mysql.createConnection(process.env.DATABASE_URL); console.log("[Migration] Database ready."); break; } catch { console.log(`[Migration] Waiting for database... (${i}/20)`); await sleep(2000); } } if (!conn) { console.warn("[Migration] Database not available after 40 s, skipping migrations."); return; } try { const migrationsDir = path.resolve(SERVER_ROOT, "../../drizzle"); if (!fs.existsSync(migrationsDir)) return; const sqlFiles = fs.readdirSync(migrationsDir) .filter(f => f.endsWith(".sql")) .sort(); // Tracking table await conn.execute(` CREATE TABLE IF NOT EXISTS \`_migrations\` ( \`name\` VARCHAR(255) PRIMARY KEY, \`appliedAt\` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); for (const file of sqlFiles) { const [rows] = await conn.execute( `SELECT name FROM \`_migrations\` WHERE name = ?`, [file] ) as [mysql.RowDataPacket[], mysql.FieldPacket[]]; if (rows.length > 0) continue; // already applied const sqlText = fs.readFileSync(path.join(migrationsDir, file), "utf8"); // Split on ";". Do NOT drop statements that start with "--": each CREATE block // begins with comment lines, and filtering those skipped all tables (bug). const statements = sqlText .split(";") .map(s => s.trim()) .filter(s => s.length > 0); for (const stmt of statements) { try { await conn.execute(stmt); } catch (e: any) { if (!e.message?.includes("already exists")) { console.error(`[Migration] Error in ${file}:`, e.message); } } } await conn.execute( `INSERT IGNORE INTO \`_migrations\` (name) VALUES (?)`, [file] ); console.log(`[Migration] Applied: ${file}`); } } finally { await conn.end(); } } function isPortAvailable(port: number): Promise { return new Promise(resolve => { const server = net.createServer(); server.listen(port, () => { server.close(() => resolve(true)); }); server.on("error", () => resolve(false)); }); } async function findAvailablePort(startPort: number = 3000): Promise { for (let port = startPort; port < startPort + 20; port++) { if (await isPortAvailable(port)) { return port; } } throw new Error(`No available port found starting from ${startPort}`); } async function startServer() { await runMigrations(); await ensureBootstrapAdmin(); const app = express(); const server = createServer(app); // Configure body parser with larger size limit for file uploads app.use(express.json({ limit: "50mb" })); app.use(express.urlencoded({ limit: "50mb", extended: true })); // 文件上传路由单独设置超长超时(50 GB 点云,按 100 MB/s 约需 8 分钟,留 60 分钟余量) server.timeout = 0; // 禁用 Node http.Server 全局超时 server.keepAliveTimeout = 0; // 长连接不超时 server.headersTimeout = 0; // 读请求头不超时 // 禁止代理(NPS 等)缓存 API 响应 app.use('/api', (_req, res, next) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); res.setHeader('Pragma', 'no-cache'); next(); }); // File API for local file browsing app.use('/api/files', fileRouter); // tRPC API app.use( "/api/trpc", createExpressMiddleware({ router: appRouter, createContext, }) ); // development mode uses Vite, production mode uses static files if (process.env.NODE_ENV === "development") { await setupVite(app, server); } else { serveStatic(app); } const preferredPort = parseInt(process.env.PORT || "3000"); const port = await findAvailablePort(preferredPort); if (port !== preferredPort) { console.log(`Port ${preferredPort} is busy, using port ${port} instead`); } server.listen(port, () => { console.log(`Server running on http://localhost:${port}/`); }); } startServer().catch(console.error);