using web::WebReq
using afIoc::Inject
using afIoc::NotFoundErr
** (Service) - Request Handler that maps URIs to files on the file system.
**
** Example, to map all uris prefixed with '/pub/' to files under the '<app>/etc/web/' directory,
** add the following to your 'AppModule':
**
** pre>
** @Contribute { serviceType=FileHandler# }
** static Void contributeFileHandler(MappedConfig conf) {
** conf[`/pub/`] = `etc/web/`.toFile
** }
** <pre
**
** Use the 'fromServerFile()' method to generate URIs to be used by the browser. Example:
**
** // note how the file uses a relative URI
** fromServerFile(`etc/web/css/mystyle.css`.toFile) // --> `/pub/css/mystyle.css`
**
** Now when the browser requests '/pub/css/mystyle.css', BedSheet will return the file '<app>/etc/web/css/mystyle.css'.
**
** It is common to serve files from the root uri:
**
** conf[`/`] = `etc/web/`
**
** `Route` mappings are automatically added to the `Routes` service, and are sandwiched in between 'FileHanderStart' and
** 'FileHandlerEnd' place holders. Use these when 'Route' precedence is important:
**
** pre>
** @Contribute { serviceId="Routes" }
** static Void contributeRoutes(OrderedConfig config) {
**
** // this Route will be served in place of the file 'uri1.txt'
** config.addOrdered("uri1", Route(`/uri1.txt`, ...), ["before: FileHandlerStart"])
**
** // this Route will be served if there is no file called 'uri.txt'
** config.addOrdered("uri2", Route(`/uri2.txt`, ...), ["after: FileHandlerEnd"])
** }
** <pre
**
** @uses MappedConfig of 'Uri:File'
const mixin FileHandler {
** Returns the map of uri to directory mappings
abstract Uri:File directoryMappings()
** Returns a `File` on the file system as mapped from the given uri, or 'null' if the file does not exist.
abstract File? service(Uri remainingUri)
** Returns the server file that the client-side asset URI maps to.
**
** If 'checked' is 'true' throw Err if the file does not exist, else return 'null'.
abstract File? fromClientUri(Uri assetUri, Bool checked)
** Returns the client URI that corresponds to the given asset file.
**
** Throws a 'NotFoundErr' if the file does not reside in a mapped directory.
abstract Uri fromServerFile(File assetFile)
}
internal const class FileHandlerImpl : FileHandler {
@Inject
private const HttpRequest? req
override const Uri:File directoryMappings
internal new make(Uri:File dirMappings, |This|? in := null) {
in?.call(this) // nullable for unit tests
// verify file and uri mappings, normalise the files
this.directoryMappings = dirMappings.map |file, uri -> File| {
if (!file.exists)
throw BedSheetErr(BsErrMsgs.fileHandlerFileNotExist(file))
if (!file.isDir)
throw BedSheetErr(BsErrMsgs.fileHandlerFileNotDir(file))
if (!uri.isPathOnly)
throw BedSheetErr(BsErrMsgs.fileHandlerUriNotPathOnly(uri, `/foo/bar/`))
if (!uri.isPathAbs)
throw BedSheetErr(BsErrMsgs.fileHandlerUriMustStartWithSlash(uri, `/foo/bar/`))
if (!uri.isDir)
throw BedSheetErr(BsErrMsgs.fileHandlerUriMustEndWithSlash(uri))
return file.normalize
}
}
override File? service(Uri remainingUri) {
// use pathStr to knockout any unwanted query str
matchedUri := req.modRel.pathStr[0..<-remainingUri.pathStr.size].toUri
return fromClientUri(matchedUri.plusSlash + remainingUri, false)
}
override File? fromClientUri(Uri clientUri, Bool checked) {
if (!clientUri.isPathOnly)
throw ArgErr(BsErrMsgs.fileHandlerUriNotPathOnly(clientUri, `/css/myStyles.css`))
if (!clientUri.isPathAbs)
throw ArgErr(BsErrMsgs.fileHandlerUriMustStartWithSlash(clientUri, `/css/myStyles.css`))
// match the deepest uri
prefix := (Uri?) directoryMappings.keys.findAll { clientUri.toStr.startsWith(it.toStr) }.sort |u1, u2 -> Int| { u1.toStr.size <=> u2.toStr.size }.last
if (prefix == null)
return null ?: (checked ? throw NotFoundErr(BsErrMsgs.fileHandlerUriNotMapped(clientUri), directoryMappings.keys) : null)
// We pass 'false' to prevent Errs being thrown if the uri is a dir but doesn't end in '/'.
// The 'false' appends a '/' automatically - it's nicer web behaviour
remaining := clientUri.getRange(prefix.path.size..-1).relTo(`/`)
file := directoryMappings[prefix].plus(remaining, false)
if (!file.exists && checked)
throw ArgErr(BsErrMsgs.fileHandlerUriDoesNotExist(clientUri, file))
return file.exists ? file : null
}
override Uri fromServerFile(File assetFile) {
if (assetFile.isDir)
throw ArgErr(BsErrMsgs.fileHandlerAssetFileIsDir(assetFile))
if (!assetFile.exists)
throw ArgErr(BsErrMsgs.fileHandlerAssetFileDoesNotExist(assetFile))
assetUriStr := assetFile.normalize.uri.toStr
prefix := directoryMappings.findAll |file, uri->Bool| { assetUriStr.startsWith(file.uri.toStr) }.keys.sort |u1, u2 -> Int| { u1.toStr.size <=> u2.toStr.size }.last
if (prefix == null)
throw NotFoundErr(BsErrMsgs.fileHandlerAssetFileNotMapped(assetFile), directoryMappings.vals.map { it.osPath })
matchedFile := directoryMappings[prefix]
remaining := assetUriStr[matchedFile.uri.toStr.size..-1]
assetUri := prefix + remaining.toUri
return assetUri
}
}