butlerx
4 years ago
5 changed files with 129 additions and 41 deletions
@ -1,15 +1,99 @@ |
|||||
import type express from 'express'; |
import type { Request, Response, NextFunction, RequestHandler } from 'express'; |
||||
import { join } from 'path'; |
import etag from 'etag'; |
||||
import { default as _favicon } from 'serve-favicon'; |
import fresh from 'fresh'; |
||||
|
import parseUrl from 'parseurl'; |
||||
|
import fs from 'fs'; |
||||
|
import { join, resolve } from 'path'; |
||||
|
|
||||
export const favicon = _favicon(join('build', 'assets', 'favicon.ico')); |
const ONE_YEAR_MS = 60 * 60 * 24 * 365 * 1000; // 1 year
|
||||
|
|
||||
|
/** |
||||
|
* Determine if the cached representation is fresh. |
||||
|
* @param req - server request |
||||
|
* @param res - server response |
||||
|
* @returns if the cache is fresh or not |
||||
|
*/ |
||||
|
const isFresh = (req: Request, res: Response): boolean => |
||||
|
fresh(req.headers, { |
||||
|
etag: res.getHeader('ETag'), |
||||
|
'last-modified': res.getHeader('Last-Modified'), |
||||
|
}); |
||||
|
|
||||
|
/** |
||||
|
* redirect requests with trailing / to remove it |
||||
|
* |
||||
|
* @param req - server request |
||||
|
* @param res - server response |
||||
|
* @param next - next middleware to call on finish |
||||
|
*/ |
||||
export function redirect( |
export function redirect( |
||||
req: express.Request, |
req: Request, |
||||
res: express.Response, |
res: Response, |
||||
next: Function, |
next: NextFunction, |
||||
) { |
): void { |
||||
if (req.path.substr(-1) === '/' && req.path.length > 1) |
if (req.path.substr(-1) === '/' && req.path.length > 1) |
||||
res.redirect(301, req.path.slice(0, -1) + req.url.slice(req.path.length)); |
res.redirect(301, req.path.slice(0, -1) + req.url.slice(req.path.length)); |
||||
else next(); |
else next(); |
||||
} |
} |
||||
|
|
||||
|
/** |
||||
|
* Serves the favicon located by the given `path`. |
||||
|
* |
||||
|
* @param basePath - server base path |
||||
|
* @returns middleware |
||||
|
*/ |
||||
|
export function favicon(basePath: string): RequestHandler { |
||||
|
const path = resolve(join('build', 'assets', 'favicon.ico')); |
||||
|
return (req: Request, res: Response, next: NextFunction): void => { |
||||
|
if (getPathName(req) !== `${basePath}/favicon.ico`) { |
||||
|
next(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (req.method !== 'GET' && req.method !== 'HEAD') { |
||||
|
res.statusCode = req.method === 'OPTIONS' ? 200 : 405; |
||||
|
res.setHeader('Allow', 'GET, HEAD, OPTIONS'); |
||||
|
res.setHeader('Content-Length', '0'); |
||||
|
res.end(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
fs.readFile(path, (err: Error | null, buf: Buffer) => { |
||||
|
if (err) return next(err); |
||||
|
Object.entries({ |
||||
|
'Cache-Control': `public, max-age=${Math.floor(ONE_YEAR_MS / 1000)}`, |
||||
|
ETag: etag(buf), |
||||
|
}).forEach(([key, value]) => { |
||||
|
res.setHeader(key, value); |
||||
|
}); |
||||
|
|
||||
|
// Validate freshness
|
||||
|
if (isFresh(req, res)) { |
||||
|
res.statusCode = 304; |
||||
|
return res.end(); |
||||
|
} |
||||
|
|
||||
|
// Send icon
|
||||
|
res.statusCode = 200; |
||||
|
res.setHeader('Content-Length', buf.length); |
||||
|
res.setHeader('Content-Type', 'image/x-icon'); |
||||
|
return res.end(buf); |
||||
|
}); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get the request pathname. |
||||
|
* |
||||
|
* @param requests |
||||
|
* @returns path name or undefined |
||||
|
*/ |
||||
|
|
||||
|
function getPathName(req: Request): string | undefined { |
||||
|
try { |
||||
|
const url = parseUrl(req); |
||||
|
return url?.pathname ? url.pathname : undefined; |
||||
|
} catch (e) { |
||||
|
return undefined; |
||||
|
} |
||||
|
} |
||||
|
Loading…
Reference in new issue