support route handler arguments; add daton::static file server
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import {HTTPResponse} from './type/HTTPResponse.ts'
|
||||
import {HTTPRequest} from './type/HTTPRequest.ts'
|
||||
import {ServerRequest} from '../external/http.ts'
|
||||
import {file_server} from '../external/std.ts'
|
||||
import {CookieJar} from './CookieJar.ts'
|
||||
|
||||
/**
|
||||
|
||||
@@ -49,10 +49,10 @@ export default class ApplyRouteHandlers extends Module {
|
||||
for ( const handler of request.route.handlers ) {
|
||||
try {
|
||||
let result
|
||||
if ( isRouteHandlerClass(handler) ) {
|
||||
result = await handler.handleRequest(current_request)
|
||||
if ( isRouteHandlerClass(handler.handler) ) {
|
||||
result = await handler.handler.handleRequest(current_request, handler.arg)
|
||||
} else {
|
||||
result = await handler(current_request)
|
||||
result = await handler.handler(current_request, handler.arg)
|
||||
}
|
||||
|
||||
if ( result instanceof Request ) {
|
||||
|
||||
45
lib/src/http/response/FileResponseFactory.ts
Normal file
45
lib/src/http/response/FileResponseFactory.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import ResponseFactory from './ResponseFactory.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
import {file_server} from '../../external/std.ts'
|
||||
import {HTTPStatus} from '../../const/http.ts'
|
||||
import {Logging} from '../../service/logging/Logging.ts'
|
||||
import {Injectable} from '../../../../di/src/decorator/Injection.ts'
|
||||
|
||||
/**
|
||||
* Response factory to send the contents of a file given its path.
|
||||
* @extends ResponseFactory
|
||||
*/
|
||||
@Injectable()
|
||||
export default class FileResponseFactory extends ResponseFactory {
|
||||
constructor(
|
||||
protected readonly logger: Logging,
|
||||
|
||||
/**
|
||||
* The path to the file to be sent.
|
||||
* @type string
|
||||
*/
|
||||
public readonly path: string,
|
||||
) { super() }
|
||||
|
||||
/**
|
||||
* Write this response factory to the given request's response.
|
||||
* @param {Request} request
|
||||
* @return Request
|
||||
*/
|
||||
public async write(request: Request): Promise<Request> {
|
||||
request = await super.write(request)
|
||||
|
||||
const content = await file_server.serveFile(request.to_native, this.path)
|
||||
const length = content.headers && content.headers.get('content-length')
|
||||
|
||||
if ( content.headers && content.body && length ) {
|
||||
request.response.body = content.body
|
||||
request.response.headers.set('Content-Length', length)
|
||||
} else {
|
||||
this.logger.debug(`Tried to serve file that does not exist: ${this.path}`)
|
||||
request.response.status = HTTPStatus.NOT_FOUND
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {HTTPStatus} from '../../const/http.ts'
|
||||
import HTTPErrorResponseFactory from './HTTPErrorResponseFactory.ts'
|
||||
import HTTPError from '../../error/HTTPError.ts'
|
||||
import ViewResponseFactory from './ViewResponseFactory.ts'
|
||||
import FileResponseFactory from './FileResponseFactory.ts'
|
||||
|
||||
/**
|
||||
* Get a new JSON response factory that writes the given object as JSON.
|
||||
@@ -76,3 +77,12 @@ export function http(status: HTTPStatus, message?: string): HTTPErrorResponseFac
|
||||
export function view(view: string, data?: any): ViewResponseFactory {
|
||||
return make(ViewResponseFactory, view, data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new file response factory for the given file path.
|
||||
* @param {string} path
|
||||
* @return FileResponseFactory
|
||||
*/
|
||||
export function file(path: string): FileResponseFactory {
|
||||
return make(FileResponseFactory, path)
|
||||
}
|
||||
|
||||
75
lib/src/http/system_middleware/StaticServer.ts
Normal file
75
lib/src/http/system_middleware/StaticServer.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import Middleware from '../Middleware.ts'
|
||||
import {Request} from '../Request.ts'
|
||||
import {Injectable} from '../../../../di/src/decorator/Injection.ts'
|
||||
import {file, http} from '../response/helpers.ts'
|
||||
import {HTTPStatus} from '../../const/http.ts'
|
||||
import {Logging} from '../../service/logging/Logging.ts'
|
||||
|
||||
/**
|
||||
* Daton-provided middleware that serves files from a static directory.
|
||||
* @extends Middleware
|
||||
*/
|
||||
@Injectable()
|
||||
export default class StaticServer extends Middleware {
|
||||
|
||||
constructor(
|
||||
protected readonly logger: Logging,
|
||||
) { super() }
|
||||
|
||||
/**
|
||||
* Handle an incoming request. Get the path to the file from the route params, and
|
||||
* resolve it using the asset_dir argument. If the file exists, serve it.
|
||||
* @param {Request} request
|
||||
* @param {string} asset_dir
|
||||
*/
|
||||
public async handleRequest(request: Request, asset_dir?: string) {
|
||||
if ( !asset_dir ) {
|
||||
throw new Error(`This static server is mis-configured. You must provide, as an argument in the route definition, the relative path to the base directory of the static files to be served.`)
|
||||
}
|
||||
|
||||
const params = request.route.params
|
||||
const rel_file = params.$1
|
||||
|
||||
if ( !rel_file || !rel_file.trim() || rel_file.trim().startsWith('/') || rel_file.trim().startsWith('.') ) {
|
||||
this.logger.info(`Blocked attempt to access invalid static file path: "${rel_file}"`)
|
||||
return http(HTTPStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
const abs_file = this.resolve(asset_dir, rel_file)
|
||||
if ( !(await this.fileExists(abs_file)) ) {
|
||||
this.logger.debug(`File does not exist: ${abs_file}`)
|
||||
return http(HTTPStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
return file(abs_file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the asset base dir and a relative path, resolve the fully-qualified
|
||||
* path to the file.
|
||||
* @param {string} asset_dir
|
||||
* @param {string} rel_file
|
||||
* @return string
|
||||
*/
|
||||
protected resolve(asset_dir: string, rel_file: string): string {
|
||||
return this.app.path(asset_dir, rel_file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves true if the given path exists, and is a file.
|
||||
* @param {string} path
|
||||
* @return Promise<boolean>
|
||||
*/
|
||||
protected async fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await Deno.lstat(path)
|
||||
return stat && stat.isFile
|
||||
} catch (e) {
|
||||
if ( e && e instanceof Deno.errors.NotFound ) {
|
||||
return false
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {logger} from '../../service/logging/global.ts'
|
||||
import {RouteHandlerDefinition} from '../../unit/Routing.ts'
|
||||
|
||||
/**
|
||||
* Type representing valid HTTP verbs.
|
||||
@@ -8,7 +9,7 @@ export type RouteVerb = 'get' | 'post' | 'patch' | 'delete' | 'head' | 'put' | '
|
||||
/**
|
||||
* Type representing a route verb group from a router definition.
|
||||
*/
|
||||
export type RouteVerbGroup = { [key: string]: string | string[] }
|
||||
export type RouteVerbGroup = { [key: string]: RouteHandlerDefinition | RouteHandlerDefinition[] }
|
||||
|
||||
/**
|
||||
* Type representing a router definition.
|
||||
@@ -51,10 +52,24 @@ export function isRouteVerbGroup(something: any): something is RouteVerbGroup {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
!(typeof something[key] === 'string')
|
||||
&& !(Array.isArray(something[key]) && something[key].every((x: any) => typeof x === 'string'))
|
||||
!(Array.isArray(something[key]) && something[key].every((x: any) => {
|
||||
return (
|
||||
typeof x === 'string'
|
||||
|| (
|
||||
typeof x === 'object'
|
||||
&& typeof x.handler === 'string'
|
||||
)
|
||||
)
|
||||
}))
|
||||
&& !(
|
||||
typeof something[key] === 'string'
|
||||
|| (
|
||||
typeof something[key] === 'object'
|
||||
&& typeof something[key].handler === 'string'
|
||||
)
|
||||
)
|
||||
) {
|
||||
logger.info(`Route verb group for key ${key} is not a string or array of strings.`)
|
||||
logger.info(`Route verb group for key ${key} is not valid. Must be valid RouteHandlerDefinition, or array of them.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user