2025-02-25 01:55:49 +01:00
|
|
|
import Hummingbird
|
|
|
|
import Logging
|
|
|
|
import NIOPosix
|
|
|
|
|
2025-03-08 13:52:20 +01:00
|
|
|
/// A middleware that proxy requests to content inside `.doccarchive` archive containers located in a hosting app.
|
|
|
|
///
|
|
|
|
/// The routing logic this middleware implements are:
|
|
|
|
/// 1. Send all requests starting with `/documentation/` or `/tutorials/` to the `index.html` file;
|
|
|
|
/// 2. Send all requests starting with `/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`, `/index/`, `/js/`, or `/videos/` to their respective folders;
|
|
|
|
/// 3. Send all requests to `favicon.ico`, `favicon.svg`, and `theme-settings.json` to their respective files;
|
|
|
|
/// 4. Send all requests to `/data/documentation.json` to the file in the `data/documentation/` folder that has the name of the module and ends with the `.json` extension;
|
|
|
|
/// 5. Redirect requests to `/` and `/documentation` to the`/documentation/` folder;
|
|
|
|
/// 6. Redirect requests to `/tutorials` to the`/tutorials/` folder.
|
2025-02-26 00:59:26 +01:00
|
|
|
struct DocCMiddleware<
|
|
|
|
Context: RequestContext,
|
|
|
|
AssetProvider: FileProvider
|
|
|
|
>: RouterMiddleware {
|
2025-02-25 01:55:49 +01:00
|
|
|
|
|
|
|
// MARK: Properties
|
|
|
|
|
|
|
|
private let assetProvider: AssetProvider
|
|
|
|
|
|
|
|
// MARK: Initialisers
|
|
|
|
|
2025-03-08 13:52:20 +01:00
|
|
|
/// Initialises this middleware with the local file system provider.
|
|
|
|
/// - Parameters:
|
|
|
|
/// - rootFolder: A root folder in the local file system where the *DocC* archive containers are located.
|
|
|
|
/// - threadPool: A thread pool used when loading archives from the file system.
|
|
|
|
/// - logger: A Logger that outputs information about the root folder requests.
|
2025-02-25 01:55:49 +01:00
|
|
|
init(
|
|
|
|
_ rootFolder: String,
|
|
|
|
threadPool: NIOThreadPool = .singleton,
|
|
|
|
logger: Logger = .init(label: "DocCMiddleware")
|
|
|
|
) where AssetProvider == LocalFileSystem {
|
|
|
|
self.assetProvider = LocalFileSystem(
|
|
|
|
rootFolder: rootFolder,
|
|
|
|
threadPool: threadPool,
|
|
|
|
logger: logger
|
|
|
|
)
|
|
|
|
}
|
2025-03-08 13:52:20 +01:00
|
|
|
|
|
|
|
/// Initialises this middleware with an asset provider, that conforms to the `FileProvider` protocol.
|
|
|
|
/// - Parameter assetProvider: An asset provider to use with the middleware.
|
2025-02-25 01:55:49 +01:00
|
|
|
init(assetProvider: AssetProvider) {
|
|
|
|
self.assetProvider = assetProvider
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: Functions
|
|
|
|
|
|
|
|
func handle(
|
|
|
|
_ input: Request,
|
|
|
|
context: Context,
|
|
|
|
next: (Request, Context) async throws -> Response
|
|
|
|
) async throws -> Response {
|
|
|
|
guard
|
|
|
|
let uriPath = input.uri.path.removingPercentEncoding,
|
|
|
|
!uriPath.contains(.previousFolder),
|
|
|
|
uriPath.hasPrefix(.forwardSlash)
|
|
|
|
else {
|
|
|
|
throw HTTPError(.badRequest)
|
|
|
|
}
|
2025-02-26 00:59:26 +01:00
|
|
|
|
2025-02-26 00:22:14 +01:00
|
|
|
guard uriPath.starts(with: /^\/archives\/\w+/) else {
|
2025-02-25 01:55:49 +01:00
|
|
|
return try await next(input, context)
|
|
|
|
}
|
|
|
|
|
2025-03-08 13:52:20 +01:00
|
|
|
let pathArchive = PathProvider.archivePath(from: uriPath)
|
|
|
|
let nameArchive = PathProvider.archiveName(from: uriPath)
|
|
|
|
let uriResource = PathProvider.resourcePath(from: uriPath)
|
2025-02-26 00:59:26 +01:00
|
|
|
|
2025-03-08 13:52:20 +01:00
|
|
|
// rule #5: Redirects URI resources with `/` to `/documentation`.
|
2025-02-25 01:55:49 +01:00
|
|
|
if uriResource == .forwardSlash {
|
2025-03-13 00:06:02 +01:00
|
|
|
return if uriPath.hasSuffix(.forwardSlash) {
|
|
|
|
.redirect(to: String(format: .Format.Path.documentation, uriPath))
|
|
|
|
} else {
|
|
|
|
.redirect(to: String(format: .Format.Path.forwardSlash, uriPath))
|
|
|
|
}
|
2025-02-25 01:55:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
for staticFile in StaticFile.allCases {
|
|
|
|
if uriResource.contains(staticFile.path) {
|
|
|
|
if staticFile == .documentation {
|
2025-03-08 13:52:20 +01:00
|
|
|
// Rule #4: Redirects URI resources with `/data/documentation.json` to the file in the `data/documentation/`
|
|
|
|
// folder that has the name of the module and ends with the `.json` extension in the *DocC* archive container.
|
2025-02-26 00:22:14 +01:00
|
|
|
return try await serveFile(
|
2025-03-08 13:52:20 +01:00
|
|
|
String(format: .Format.Path.documentationJSON, nameArchive),
|
2025-02-26 00:22:14 +01:00
|
|
|
at: pathArchive,
|
|
|
|
context: context
|
|
|
|
)
|
2025-02-25 01:55:49 +01:00
|
|
|
} else {
|
2025-03-08 13:52:20 +01:00
|
|
|
// Rule #3: Redirect URI resources for static files (`favicon.ico`, `favicon.svg`, and `theme-settings.json`)
|
|
|
|
// to their respective files in the *DocC* archive container.
|
2025-02-26 00:22:14 +01:00
|
|
|
return try await serveFile(
|
|
|
|
uriResource,
|
|
|
|
at: pathArchive,
|
|
|
|
context: context
|
|
|
|
)
|
2025-02-25 01:55:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for assetPrefix in AssetPrefix.allCases {
|
|
|
|
if uriResource.contains(assetPrefix.path) {
|
2025-03-08 13:52:20 +01:00
|
|
|
// Rule #2: Redirect URI resources for asset files (`/css/`, `/data/`, `/downloads/`, `/images/`, `/img/`,
|
|
|
|
// `/index/`, `/js/`, or `/videos/`)to their respective files in the *DocC* archive container.
|
2025-02-26 00:22:14 +01:00
|
|
|
return try await serveFile(
|
|
|
|
uriResource,
|
|
|
|
at: pathArchive,
|
|
|
|
context: context
|
|
|
|
)
|
2025-02-25 01:55:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for indexPrefix in IndexPrefix.allCases {
|
|
|
|
if uriResource.contains(indexPrefix.path) {
|
2025-03-08 13:52:20 +01:00
|
|
|
// Rule #1: Redirect URI resources for `/documentation/` and `/tutorials/` folders to their respective `index.html` file.
|
2025-02-25 01:55:49 +01:00
|
|
|
if uriResource.hasSuffix(.forwardSlash) {
|
|
|
|
return try await serveFile(
|
2025-03-08 13:52:20 +01:00
|
|
|
String(format: .Format.Path.index, indexPrefix.path, nameArchive),
|
2025-02-26 00:22:14 +01:00
|
|
|
at: pathArchive,
|
2025-02-25 01:55:49 +01:00
|
|
|
context: context
|
|
|
|
)
|
|
|
|
} else {
|
2025-03-08 13:52:20 +01:00
|
|
|
// rule #5: Redirects URI resources with `/documentation` to `/documentation/`.
|
|
|
|
// rule #6: Redirects URI resources with `/tutorials` to `/tutorials/`.
|
|
|
|
return .redirect(to: String(format: .Format.Path.forwardSlash, uriPath))
|
2025-02-25 01:55:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-08 13:52:20 +01:00
|
|
|
throw HTTPError(.notImplemented)
|
2025-02-25 01:55:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Helpers
|
|
|
|
|
|
|
|
private extension DocCMiddleware {
|
|
|
|
|
|
|
|
// MARK: Functions
|
|
|
|
|
2025-03-08 13:52:20 +01:00
|
|
|
/// Serves a resource file from a provider as a HTTP response.
|
|
|
|
/// - Parameters:
|
|
|
|
/// - path: A relative path to a resource file.
|
|
|
|
/// - folder: A folder accessible to the provider where to find resource files.
|
|
|
|
/// - context: A request context.
|
|
|
|
/// - Returns: A HTTP response containing the content of a given resource file inside its body.
|
|
|
|
/// - Throws:An error...
|
2025-02-25 01:55:49 +01:00
|
|
|
func serveFile(
|
|
|
|
_ path: String,
|
2025-02-26 00:59:26 +01:00
|
|
|
at folder: String,
|
2025-02-25 01:55:49 +01:00
|
|
|
context: Context
|
|
|
|
) async throws -> Response {
|
2025-03-08 13:52:20 +01:00
|
|
|
guard let fileIdentifier = assetProvider.getFileIdentifier(folder + path) else {
|
2025-02-25 01:55:49 +01:00
|
|
|
throw HTTPError(.notFound)
|
|
|
|
}
|
|
|
|
|
|
|
|
let body = try await assetProvider.loadFile(id: fileIdentifier, context: context)
|
|
|
|
|
2025-03-08 13:52:20 +01:00
|
|
|
return .init(
|
|
|
|
status: .ok,
|
|
|
|
headers: [:],
|
|
|
|
body: body
|
|
|
|
)
|
2025-02-25 01:55:49 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|