butlerx
4 years ago
5 changed files with 129 additions and 41 deletions
@ -1,15 +1,99 @@ |
|||
import type express from 'express'; |
|||
import { join } from 'path'; |
|||
import { default as _favicon } from 'serve-favicon'; |
|||
import type { Request, Response, NextFunction, RequestHandler } from 'express'; |
|||
import etag from 'etag'; |
|||
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( |
|||
req: express.Request, |
|||
res: express.Response, |
|||
next: Function, |
|||
) { |
|||
req: Request, |
|||
res: Response, |
|||
next: NextFunction, |
|||
): void { |
|||
if (req.path.substr(-1) === '/' && req.path.length > 1) |
|||
res.redirect(301, req.path.slice(0, -1) + req.url.slice(req.path.length)); |
|||
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