2026-05-18 07:14:35 +07:00
|
|
|
import { serve } from 'bun';
|
2026-05-18 07:27:06 +07:00
|
|
|
import { startBot } from './bot';
|
2026-05-18 07:29:01 +07:00
|
|
|
import { config } from './env';
|
|
|
|
|
import { handleFileInfo, handleFileRedirect } from './routes/files';
|
2026-05-18 07:27:06 +07:00
|
|
|
import { handleHealth } from './routes/health';
|
2026-07-07 03:07:51 +07:00
|
|
|
import { handleHome } from './routes/home';
|
|
|
|
|
import { handleS3Request } from './routes/s3';
|
2026-05-18 07:39:27 +07:00
|
|
|
import { handleSwaggerHtml, handleSwaggerJson } from './routes/swagger';
|
2026-05-18 07:29:01 +07:00
|
|
|
import { handleUpload } from './routes/upload';
|
2026-07-06 20:56:55 +07:00
|
|
|
import { handleWebApiV1 } from './routes/web-api';
|
2026-07-06 01:25:18 +07:00
|
|
|
import { fileInfoCache } from './utils/cache';
|
2026-05-18 07:29:01 +07:00
|
|
|
import logger from './utils/logger';
|
2026-07-06 01:25:18 +07:00
|
|
|
import { metricsCollector } from './utils/metrics';
|
2026-05-29 03:33:39 +07:00
|
|
|
import { cleanupRateLimitCache, withRateLimit } from './utils/rateLimit';
|
2026-07-07 03:07:51 +07:00
|
|
|
import { isS3Request } from './utils/s3/auth';
|
2026-05-18 07:14:35 +07:00
|
|
|
|
2026-07-06 22:32:06 +07:00
|
|
|
// ─── Auto-run migration at startup ──────────────────────────────────────────
|
|
|
|
|
try {
|
|
|
|
|
const { runMigration } = await import('./db/migrate');
|
|
|
|
|
await runMigration();
|
|
|
|
|
} catch {
|
|
|
|
|
logger.warn('Auto-migration skipped (non-fatal)');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Auto-run migration at startup ───
|
|
|
|
|
try {
|
|
|
|
|
await import('./db/migrate');
|
|
|
|
|
} catch {
|
|
|
|
|
// migrate.ts calls process.exit(1) on failure — if it throws, log and continue
|
|
|
|
|
logger.warn('Auto-migration warning (non-fatal)');
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 07:14:35 +07:00
|
|
|
const server = serve({
|
|
|
|
|
port: config.port,
|
|
|
|
|
routes: {
|
|
|
|
|
'/api/upload': {
|
2026-05-29 03:33:39 +07:00
|
|
|
POST: withRateLimit(handleUpload),
|
2026-05-18 07:14:35 +07:00
|
|
|
},
|
|
|
|
|
'/f/:public_id': {
|
2026-05-29 03:33:39 +07:00
|
|
|
GET: withRateLimit(handleFileRedirect),
|
2026-05-18 07:14:35 +07:00
|
|
|
},
|
|
|
|
|
'/file/:public_id/info': {
|
2026-05-29 03:33:39 +07:00
|
|
|
GET: withRateLimit(handleFileInfo),
|
2026-05-18 07:14:35 +07:00
|
|
|
},
|
|
|
|
|
'/health': {
|
2026-05-18 07:29:01 +07:00
|
|
|
GET: handleHealth,
|
|
|
|
|
},
|
2026-05-18 07:39:27 +07:00
|
|
|
'/docs': {
|
|
|
|
|
GET: handleSwaggerHtml,
|
|
|
|
|
},
|
|
|
|
|
'/swagger.json': {
|
|
|
|
|
GET: handleSwaggerJson,
|
|
|
|
|
},
|
2026-07-06 20:56:55 +07:00
|
|
|
'/': {
|
2026-07-07 00:49:45 +07:00
|
|
|
GET: (req: Request) => {
|
|
|
|
|
const headers = Object.fromEntries(req.headers);
|
|
|
|
|
const url = new URL(req.url);
|
|
|
|
|
if (isS3Request(headers) || url.searchParams.has('X-Amz-Signature')) {
|
|
|
|
|
return handleS3Request(req);
|
|
|
|
|
}
|
|
|
|
|
return handleHome();
|
|
|
|
|
},
|
|
|
|
|
PUT: (req: Request) => {
|
|
|
|
|
const headers = Object.fromEntries(req.headers);
|
|
|
|
|
if (isS3Request(headers)) {
|
|
|
|
|
return handleS3Request(req);
|
|
|
|
|
}
|
|
|
|
|
return new Response('Not Allowed', { status: 405 });
|
|
|
|
|
},
|
|
|
|
|
HEAD: (req: Request) => {
|
|
|
|
|
const headers = Object.fromEntries(req.headers);
|
|
|
|
|
if (isS3Request(headers)) {
|
|
|
|
|
return handleS3Request(req);
|
|
|
|
|
}
|
|
|
|
|
return new Response('Not Allowed', { status: 405 });
|
|
|
|
|
},
|
|
|
|
|
DELETE: (req: Request) => {
|
|
|
|
|
const headers = Object.fromEntries(req.headers);
|
|
|
|
|
if (isS3Request(headers)) {
|
|
|
|
|
return handleS3Request(req);
|
|
|
|
|
}
|
|
|
|
|
return new Response('Not Allowed', { status: 405 });
|
|
|
|
|
},
|
|
|
|
|
POST: (req: Request) => {
|
|
|
|
|
const headers = Object.fromEntries(req.headers);
|
|
|
|
|
if (isS3Request(headers)) {
|
|
|
|
|
return handleS3Request(req);
|
|
|
|
|
}
|
|
|
|
|
return new Response('Not Allowed', { status: 405 });
|
|
|
|
|
},
|
2026-07-06 20:56:55 +07:00
|
|
|
},
|
|
|
|
|
'/api/v1/*': {
|
|
|
|
|
GET: handleWebApiV1,
|
|
|
|
|
POST: handleWebApiV1,
|
|
|
|
|
DELETE: handleWebApiV1,
|
|
|
|
|
PUT: handleWebApiV1,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
fetch: async (req: Request) => {
|
|
|
|
|
const headers = Object.fromEntries(req.headers);
|
2026-07-07 00:49:45 +07:00
|
|
|
if (isS3Request(headers) || new URL(req.url).searchParams.has('X-Amz-Signature')) {
|
2026-07-06 20:56:55 +07:00
|
|
|
return handleS3Request(req);
|
|
|
|
|
}
|
|
|
|
|
return new Response('Not Found', { status: 404 });
|
2026-05-18 07:29:01 +07:00
|
|
|
},
|
2026-05-18 07:14:35 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const bot = await startBot();
|
|
|
|
|
|
|
|
|
|
logger.info('Server started', { port: config.port, url: config.baseUrl });
|
|
|
|
|
|
2026-05-18 07:27:06 +07:00
|
|
|
const gracefulShutdown = async (signal: string): Promise<void> => {
|
2026-05-18 07:14:35 +07:00
|
|
|
logger.info('Graceful shutdown signal received', { signal });
|
|
|
|
|
|
|
|
|
|
logger.info('Closing HTTP server');
|
|
|
|
|
server.stop();
|
|
|
|
|
|
|
|
|
|
logger.info('Stopping Telegram bot');
|
2026-05-18 07:27:06 +07:00
|
|
|
bot.stop(signal);
|
2026-05-18 07:14:35 +07:00
|
|
|
|
|
|
|
|
logger.info('Server shutdown complete');
|
|
|
|
|
process.exit(0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
|
|
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
|
|
|
|
2026-07-06 01:25:18 +07:00
|
|
|
// Periodic maintenance intervals
|
2026-05-18 07:14:35 +07:00
|
|
|
setInterval(cleanupRateLimitCache, 60000);
|
2026-07-06 01:25:18 +07:00
|
|
|
setInterval(
|
|
|
|
|
() => {
|
|
|
|
|
const removed = fileInfoCache.cleanup();
|
|
|
|
|
if (removed > 0) {
|
|
|
|
|
logger.info(`Cleaned up ${removed} expired cache entries`);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
5 * 60 * 1000,
|
|
|
|
|
);
|
|
|
|
|
setInterval(
|
|
|
|
|
() => {
|
|
|
|
|
const snapshot = metricsCollector.getSnapshot();
|
|
|
|
|
logger.info('Metrics snapshot', {
|
|
|
|
|
uploadLatency: snapshot.uploadLatency,
|
|
|
|
|
uploadThroughput: snapshot.uploadThroughput.toFixed(2),
|
|
|
|
|
errorRate: snapshot.errorRate.toFixed(2),
|
|
|
|
|
cacheHitRate: snapshot.cacheHitRate.toFixed(2),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
5 * 60 * 1000,
|
|
|
|
|
);
|
2026-05-18 07:14:35 +07:00
|
|
|
|
|
|
|
|
logger.info('Application running successfully');
|