feat: initial commit - massive-cloud point cloud processing system
This commit is contained in:
163
server/_core/index.ts
Normal file
163
server/_core/index.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
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 { registerOAuthRoutes } from "./oauth";
|
||||
import { appRouter } from "../routers";
|
||||
import { createContext } from "./context";
|
||||
import { serveStatic, setupVite } from "./vite";
|
||||
import { fileRouter } from "../fileRoutes";
|
||||
|
||||
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() {
|
||||
try {
|
||||
await runMigrations();
|
||||
} catch (e) {
|
||||
console.error("[Migration] Unexpected error, server will still start:", e);
|
||||
}
|
||||
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 }));
|
||||
// OAuth callback under /api/oauth/callback
|
||||
registerOAuthRoutes(app);
|
||||
// 禁止代理(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);
|
||||
Reference in New Issue
Block a user