Files
massive-cloud/server/_core/index.ts

164 lines
5.0 KiB
TypeScript
Raw Permalink 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.
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<boolean> {
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<number> {
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);