Files
zipline/src/server/index.ts
diced eadfa09570 refactor: migrate to fastify
- (maybe) faster http server
- easy to develop on
2022-12-07 19:21:26 -08:00

200 lines
5.9 KiB
TypeScript

import { version } from '../../package.json';
import config from '../lib/config';
import datasource from '../lib/datasource';
import Logger from '../lib/logger';
import { getStats } from './util';
import fastify, { FastifyInstance } from 'fastify';
import { createReadStream, existsSync } from 'fs';
import dbFileDecorator from './decorators/dbFile';
import notFound from './decorators/notFound';
import postFileDecorator from './decorators/postFile';
import postUrlDecorator from './decorators/postUrl';
import preFileDecorator from './decorators/preFile';
import rawFileDecorator from './decorators/rawFile';
import configPlugin from './plugins/config';
import datasourcePlugin from './plugins/datasource';
import loggerPlugin from './plugins/logger';
import nextPlugin from './plugins/next';
import prismaPlugin from './plugins/prisma';
import rawRoute from './routes/raw';
import uploadsRoute, { uploadsRouteOnResponse } from './routes/uploads';
import urlsRoute, { urlsRouteOnResponse } from './routes/urls';
const dev = process.env.NODE_ENV === 'development';
const logger = Logger.get('server');
const server = fastify();
if (dev) {
server.addHook('onRoute', (opts) => {
logger.child('route').debug(JSON.stringify(opts));
});
}
start();
async function start() {
logger.debug('Starting server');
// plugins
server
.register(loggerPlugin)
.register(configPlugin, config)
.register(datasourcePlugin, datasource)
.register(prismaPlugin)
.register(nextPlugin, {
dir: '.',
dev,
quiet: !dev,
hostname: config.core.host,
port: config.core.port,
});
// decorators
server
.register(notFound)
.register(postUrlDecorator)
.register(postFileDecorator)
.register(preFileDecorator)
.register(rawFileDecorator)
.register(dbFileDecorator);
server.addHook('onRequest', (req, reply, done) => {
if (config.features.headless) {
const url = req.url.toLowerCase();
if (!url.startsWith('/api') || url === '/api') return reply.notFound();
}
done();
});
server.addHook('onResponse', (req, reply, done) => {
if (config.core.logger || dev || process.env.DEBUG) {
if (req.url.startsWith('/_next')) return done();
server.logger.child('response').info(`${req.method} ${req.url} -> ${reply.statusCode}`);
server.logger.child('response').debug(
JSON.stringify({
method: req.method,
url: req.url,
headers: req.headers,
body: req.headers['content-type']?.startsWith('application/json') ? req.body : undefined,
})
);
}
done();
});
server.get('/favicon.ico', async (_, reply) => {
if (!existsSync('./public/favicon.ico')) return reply.notFound();
const favicon = createReadStream('./public/favicon.ico');
return reply.type('image/x-icon').send(favicon);
});
// makes sure to handle both in one route as you cant have two handlers with the same route
if (config.urls.route === '/' && config.uploader.route === '/') {
server.route({
method: 'GET',
url: '/:id',
handler: async (req, reply) => {
const { id } = req.params as { id: string };
if (id === '') return reply.notFound();
else if (id === 'dashboard' && !config.features.headless)
return server.nextServer.render(req.raw, reply.raw, '/dashboard');
const url = await server.prisma.url.findFirst({
where: {
OR: [{ id: id }, { vanity: id }, { invisible: { invis: decodeURI(id) } }],
},
});
if (url) return urlsRoute.bind(server)(req, reply);
else return uploadsRoute.bind(server)(req, reply);
},
onResponse: async (req, reply, done) => {
if (reply.statusCode === 200) {
const { id } = req.params as { id: string };
const url = await server.prisma.url.findFirst({
where: {
OR: [{ id: id }, { vanity: id }, { invisible: { invis: decodeURI(id) } }],
},
});
if (url) urlsRouteOnResponse.bind(server)(req, reply, done);
else uploadsRouteOnResponse.bind(server)(req, reply, done);
}
done();
},
});
} else {
server
.route({
method: 'GET',
url: config.urls.route === '/' ? '/:id' : `${config.urls.route}/:id`,
handler: urlsRoute.bind(server),
onResponse: urlsRouteOnResponse.bind(server),
})
.route({
method: 'GET',
url: config.uploader.route === '/' ? '/:id' : `${config.uploader.route}/:id`,
handler: uploadsRoute.bind(server),
onResponse: uploadsRouteOnResponse.bind(server),
});
}
server.get('/r/:id', rawRoute.bind(server));
server.get('/', (_, reply) => reply.redirect('/dashboard'));
await server.listen({
port: config.core.port,
host: config.core.host ?? '0.0.0.0',
});
server.logger
.info(`listening on ${config.core.host}:${config.core.port}`)
.info(
`started ${dev ? 'development' : 'production'} zipline@${version} server${
config.features.headless ? ' (headless)' : ''
}`
);
clearInvites.bind(server)();
stats.bind(server)();
setInterval(() => clearInvites.bind(server)(), config.core.invites_interval * 1000);
setInterval(() => stats.bind(server)(), config.core.stats_interval * 1000);
}
async function stats(this: FastifyInstance) {
const stats = await getStats(this.prisma, this.datasource);
await this.prisma.stats.create({
data: {
data: stats,
},
});
this.logger.child('stats').debug(`stats updated ${JSON.stringify(stats)}`);
}
async function clearInvites(this: FastifyInstance) {
const { count } = await this.prisma.invite.deleteMany({
where: {
OR: [
{
expires_at: { lt: new Date() },
},
{
used: true,
},
],
},
});
logger.child('invites').debug(`deleted ${count} used invites`);
}