import 'dotenv/config' import express from 'express' import session from 'express-session' import path from 'node:path' import fsp from 'node:fs/promises' import { dataDir, foldersDir, jobsDir, tmpDir, publicDir, viewsDir } from './paths.js' import { publicRouter } from './routes/public.js' import { opRouter } from './routes/op.js' import { initDatabase } from './db.js' async function ensureDirs(): Promise { for (const dir of [dataDir, foldersDir, jobsDir, tmpDir]) { await fsp.mkdir(dir, { recursive: true }) } } async function main(): Promise { await ensureDirs() // SQLite 스키마 보장 + 비어 있으면 기존 디스크 트리에서 마이그레이션. await initDatabase() const PORT = Number(process.env.PORT ?? 3000) const HOST = process.env.HOST ?? '127.0.0.1' const app = express() app.set('view engine', 'ejs') app.set('views', viewsDir) app.set('trust proxy', 1) app.use(express.urlencoded({ extended: true })) app.use(express.json({ limit: '4mb' })) // EJS 에서 app.use((_req, res, next) => { res.locals.jsonForScript = (value: unknown): string => { return JSON.stringify(value) .replace(//g, '\\u003e') .replace(/&/g, '\\u0026') .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029') } next() }) app.use(session({ secret: process.env.SESSION_SECRET ?? 'make-video-site-dev-secret', resave: false, saveUninitialized: false, cookie: { httpOnly: true, sameSite: 'lax', maxAge: 1000 * 60 * 60 * 8 } })) // account.json 직접 노출 차단 app.use((req, res, next) => { if (/^\/account\.json/i.test(req.path)) { res.status(404).send('Not Found') return } next() }) app.use('/static', express.static(publicDir)) app.use('/', publicRouter) app.use('/', opRouter) app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { console.error(err) const message = err instanceof Error ? err.message : '알 수 없는 오류' if (res.headersSent) return res.status(500).send(`서버 오류: ${message}`) }) const server = app.listen(PORT, HOST, () => { console.log(`[server] http://${HOST}:${PORT}`) console.log(`[server] views: ${path.relative(process.cwd(), viewsDir)}`) }) // 10GB 같은 큰 영상 업로드가 Node 의 기본 requestTimeout(5분) 에 걸려 끊기는 걸 막는다. // 0 = 무제한. 필요하면 HTTP_REQUEST_TIMEOUT_MS 환경변수로 조정. server.requestTimeout = Number(process.env.HTTP_REQUEST_TIMEOUT_MS ?? 0) // 헤더 단계 타임아웃은 짧게 유지(기본 60초). 0 으로 해버리면 slowloris 류에 취약. } main().catch((err) => { console.error(err) process.exit(1) })