Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update multipart-kit example #71

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Examples converted to Hummingbird 2.0
- [html-form](https://github.com/hummingbird-project/hummingbird-examples/tree/main/html-form) - Link HTML form to Hummingbird application.
- [http2](https://github.com/hummingbird-project/hummingbird-examples/tree/main/http2) - Basic application with HTTP2 upgrade added.
- [jobs](https://github.com/hummingbird-project/hummingbird-examples/tree/main/jobs) - Demonstrating offloading of jobs to another server.
- [multipart-form](https://github.com/hummingbird-project/hummingbird-examples/tree/main/multipart-form) - HTML form using Multipart form data, using MultipartKit
- [proxy-server](https://github.com/hummingbird-project/hummingbird-examples/tree/main/proxy-server) - Using AsyncHTTPClient to build a proxy server
- [sessions](https://github.com/hummingbird-project/hummingbird-examples/tree/main/sessions) - Username/password and session authentication.
- [todos-dynamodb](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-dynamodb) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB.
Expand All @@ -26,10 +27,9 @@ And finally

Examples still working with Hummingbird 1.0

- [auth-srp](https://github.com/hummingbird-project/hummingbird-examples/tree/main/auth-srp) - Secure Remote Password authentication.
- [ios-image-server](https://github.com/hummingbird-project/hummingbird-examples/tree/main/ios-image-server) - iOS web server that provides access to iPhone photo library.
- [multipart-form](https://github.com/hummingbird-project/hummingbird-examples/tree/main/multipart-form) - HTML form using Multipart form data, using MultipartKit
- [todos-fluent](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-fluent) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using Fluent
- [upload-s3](https://github.com/hummingbird-project/hummingbird-examples/tree/main/upload-s3) - File uploading and downloading using AWS S3 as backing store.
- [auth-srp](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/auth-srp) - Secure Remote Password authentication.
- [ios-image-server](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/ios-image-server) - iOS web server that provides access to iPhone photo library.
- [todos-fluent](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/todos-fluent) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using Fluent
- [upload-s3](https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x/upload-s3) - File uploading and downloading using AWS S3 as backing store.

The full set of Hummingbird 1.0 examples can be found at https://github.com/hummingbird-project/hummingbird-examples/tree/1.x.x
14 changes: 4 additions & 10 deletions multipart-form/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,22 @@ let package = Package(
name: "multipart-form",
platforms: [.macOS(.v14)],
products: [
.executable(name: "Server", targets: ["Server"]),
.executable(name: "App", targets: ["App"]),
],
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0-beta"),
.package(url: "https://github.com/hummingbird-project/hummingbird-mustache.git", branch: "main"),
.package(url: "https://github.com/hummingbird-project/swift-mustache.git", from: "2.0.0-beta"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
.package(url: "https://github.com/swift-extras/swift-extras-base64.git", .upToNextMinor(from: "0.7.0")),
.package(url: "https://github.com/vapor/multipart-kit.git", from: "4.0.0"),
],
targets: [
.executableTarget(
name: "Server",
dependencies: [
.byName(name: "App"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]
),
.target(
name: "App",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Hummingbird", package: "hummingbird"),
.product(name: "HummingbirdMustache", package: "hummingbird-mustache"),
.product(name: "Mustache", package: "swift-mustache"),
.product(name: "ExtrasBase64", package: "swift-extras-base64"),
.product(name: "MultipartKit", package: "multipart-kit"),
],
Expand Down
4 changes: 2 additions & 2 deletions multipart-form/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

Application demonstrating working with HTML forms using multipart form data. Set the working directory to the local folder, run app and open up a web browser and go to localhost:8080.

This example uses the [MultipartKit](https://github.com/vapor/multipart-kit) package for decoding the multipart form data sent by the web browser. The HTML form is generated using the [HummingbirdMustache](https://github.com/hummingbird-project/hummingbird-mustache) library. A new type `HTML` is added that conforms to `HBResponseGenerator`. This generates a response with the `HTML` text contents and a `content-type` header set to `text/html`.
This example uses the [MultipartKit](https://github.com/vapor/multipart-kit) package for decoding the multipart form data sent by the web browser. The HTML form is generated using the [Mustache](https://github.com/hummingbird-project/swift-mustache) library. A new type `HTML` is added that conforms to `ResponseGenerator`. This generates a response with the `HTML` text contents and a `content-type` header set to `text/html`.

Added a new `HBRequestDecoder` that checks the header value `content-type` and if its media type is `.multipartForm` then decodes request using `FormDataDecoder` from the MultipartKit package. Otherwise returns unsupported media type.
Added a new `RequestDecoder` that checks the header value `content-type` and if its media type is `.multipartForm` then decodes request using `FormDataDecoder` from the MultipartKit package. Otherwise returns unsupported media type.
16 changes: 16 additions & 0 deletions multipart-form/Sources/App/App.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ArgumentParser
import Hummingbird

@main
struct MultipartFormApp: AsyncParsableCommand, AppArguments {
@Option(name: .shortAndLong)
var hostname: String = "127.0.0.1"

@Option(name: .shortAndLong)
var port: Int = 8080

func run() async throws {
let app = try await buildApplication(self)
try await app.runService()
}
}
18 changes: 18 additions & 0 deletions multipart-form/Sources/App/Application+build.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Hummingbird
import MultipartKit
import Mustache

protocol AppArguments {
var hostname: String { get }
var port: Int { get }
}

func buildApplication(_ args: AppArguments) async throws -> some ApplicationProtocol {
let library = try await MustacheLibrary(directory: "templates")
assert(library.getTemplate(named: "page") != nil, "Set your working directory to the root folder of this example to get it to work")

let router = Router(context: MultipartRequestContext.self)
router.middlewares.add(FileMiddleware())
WebController(mustacheLibrary: library).addRoutes(to: router)
return Application(router: router, configuration: .init(address: .hostname(args.hostname, port: args.port)))
}
20 changes: 0 additions & 20 deletions multipart-form/Sources/App/Application+configure.swift

This file was deleted.

29 changes: 15 additions & 14 deletions multipart-form/Sources/App/Codable/FormDataCoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,37 +17,38 @@ import Hummingbird
import MultipartKit
import NIOFoundationCompat

extension FormDataEncoder: HBResponseEncoder {
extension FormDataEncoder {
/// Extend JSONEncoder to support encoding `HBResponse`'s. Sets body and header values
/// - Parameters:
/// - value: Value to encode
/// - request: Request used to generate response
public func encode<T: Encodable>(_ value: T, from request: HBRequest) throws -> HBResponse {
var buffer = request.allocator.buffer(capacity: 0)
public func encode<T: Encodable>(_ value: T, from request: Request, context: some BaseRequestContext) throws -> Response {
var buffer = context.allocator.buffer(capacity: 0)

let boundary = "----HBFormBoundary" + String(base32Encoding: (0..<4).map { _ in UInt8.random(in: 0...255) })
try self.encode(value, boundary: boundary, into: &buffer)
return HBResponse(
return Response(
status: .ok,
headers: ["content-type": "multipart/form-data; boundary=\(boundary)"],
body: .byteBuffer(buffer)
headers: [.contentType: "multipart/form-data; boundary=\(boundary)"],
body: .init(byteBuffer: buffer)
)
}
}

extension FormDataDecoder: HBRequestDecoder {
extension FormDataDecoder {
/// Extend JSONDecoder to decode from `HBRequest`.
/// - Parameters:
/// - type: Type to decode
/// - request: Request to decode from
public func decode<T: Decodable>(_ type: T.Type, from request: HBRequest) throws -> T {
guard let buffer = request.body.buffer,
let contentType = request.headers["content-type"].first,
let mediaType = HBMediaType(from: contentType),
public func decode<T: Decodable>(_ type: T.Type, from request: Request, context: some BaseRequestContext) async throws -> T {
guard let contentType = request.headers[.contentType],
let mediaType = MediaType(from: contentType),
let parameter = mediaType.parameter,
parameter.name == "boundary" else {
throw HBHTTPError(.badRequest)
}
parameter.name == "boundary"
else {
throw HTTPError(.unsupportedMediaType)
}
let buffer = try await request.body.collect(upTo: 1_000_000)
return try self.decode(T.self, from: buffer, boundary: parameter.value)
}
}
13 changes: 4 additions & 9 deletions multipart-form/Sources/App/Codable/RequestDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,9 @@
import Hummingbird
import MultipartKit

struct RequestDecoder: HBRequestDecoder {
let decoder = FormDataDecoder()

func decode<T>(_ type: T.Type, from request: HBRequest) throws -> T where T: Decodable {
if let contentType = request.headers["content-type"].first,
HBMediaType(from: contentType)?.isType(.multipartForm) == true {
return try self.decoder.decode(type, from: request)
}
throw HBHTTPError(.unsupportedMediaType)
struct MultipartRequestDecoder: RequestDecoder {
func decode<T>(_ type: T.Type, from request: Request, context: some BaseRequestContext) async throws -> T where T: Decodable {
let decoder = FormDataDecoder()
return try await decoder.decode(type, from: request, context: context)
}
}
33 changes: 23 additions & 10 deletions multipart-form/Sources/App/Controllers/WebController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,41 @@
//===----------------------------------------------------------------------===//

import Hummingbird
import HummingbirdMustache
import Mustache

struct HTML: ResponseGenerator {
let html: String

public func response(from request: Request) throws -> Response {
let buffer = request.allocator.buffer(string: self.html)
return .init(status: .ok, headers: ["content-type": "text/html"], body: .byteBuffer(buffer))
public func response(from request: Request, context: some BaseRequestContext) throws -> Response {
let buffer = context.allocator.buffer(string: self.html)
return .init(status: .ok, headers: [.contentType: "text/html"], body: .init(byteBuffer: buffer))
}
}

struct WebController {
let mustacheLibrary: MustacheLibrary
let library: MustacheLibrary
let enterTemplate: MustacheTemplate
let enteredTemplate: MustacheTemplate

func input(request: Request) -> HTML {
let html = mustacheLibrary.render((), withTemplate: "enter-details")!
init(mustacheLibrary: MustacheLibrary) {
self.library = mustacheLibrary
self.enterTemplate = mustacheLibrary.getTemplate(named: "enter-details")!
self.enteredTemplate = mustacheLibrary.getTemplate(named: "details-entered")!
}

func addRoutes(to router: some RouterMethods<some BaseRequestContext>) {
router.get("/", use: self.input)
router.post("/", use: self.post)
}

@Sendable func input(request: Request, context: some BaseRequestContext) -> HTML {
let html = self.enterTemplate.render((), library: self.library)
return HTML(html: html)
}

func post(request: Request) throws -> HTML {
guard let user = try? request.decode(as: User.self) else { throw HTTPError(.badRequest) }
let html = mustacheLibrary.render(user, withTemplate: "details-entered")!
@Sendable func post(request: Request, context: some BaseRequestContext) async throws -> HTML {
let user = try await request.decode(as: User.self, context: context)
let html = self.enteredTemplate.render(user, library: self.library)
return HTML(html: html)
}
}
12 changes: 12 additions & 0 deletions multipart-form/Sources/App/RequestContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Hummingbird
import Logging
import NIOCore

struct MultipartRequestContext: RequestContext {
var requestDecoder: MultipartRequestDecoder { .init() }
var coreContext: CoreRequestContext

init(channel: Channel, logger: Logger) {
self.coreContext = .init(allocator: channel.allocator, logger: logger)
}
}
25 changes: 0 additions & 25 deletions multipart-form/Sources/Server/main.swift

This file was deleted.

56 changes: 29 additions & 27 deletions multipart-form/Tests/AppTests/AppTests.swift
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import App
@testable import App
import Hummingbird
import HummingbirdXCT
import HummingbirdTesting
import XCTest

final class AppTests: XCTestCase {
func testApp() throws {
let app = HBApplication(testing: .live)
try app.configure()

try app.XCTStart()
defer { app.XCTStop() }
struct TestArguments: AppArguments {
let hostname = "127.0.0.1"
let port = 8080
}

let multipartForm = """
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="name"\r
\r
adam\r
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="age"\r
\r
50\r
------HBTestFormBoundaryXD6BXJI--\r
"""
let contentType = "multipart/form-data; boundary=----HBTestFormBoundaryXD6BXJI"
try app.XCTExecute(
uri: "/",
method: .POST,
headers: ["content-type": contentType],
body: ByteBufferAllocator().buffer(string: multipartForm)
) { response in
XCTAssertEqual(response.status, .ok)
func testApp() async throws {
let app = try await buildApplication(TestArguments())
try await app.test(.router) { client in
let multipartForm = """
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="name"\r
\r
adam\r
------HBTestFormBoundaryXD6BXJI\r
Content-Disposition: form-data; name="age"\r
\r
50\r
------HBTestFormBoundaryXD6BXJI--\r
"""
let contentType = "multipart/form-data; boundary=----HBTestFormBoundaryXD6BXJI"
try await client.execute(
uri: "/",
method: .post,
headers: [.contentType: contentType],
body: ByteBufferAllocator().buffer(string: multipartForm)
) { response in
XCTAssertEqual(response.status, .ok)
}
}
}
}
Binary file added multipart-form/public/images/hummingbird.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 9 additions & 13 deletions multipart-form/templates/details-entered.mustache
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
{{>head}}
<body>
<div>
<h1>
You entered
</h1>
<ul>
<li>Name: {{name}}</li>
<li>Age: {{age}}</li>
</ul>
</div>
</body>
</html>
{{<page}}
{{$body}}
<h1 class="text-xl">You entered</h1>
<ul>
<li>Name: {{name}}</li>
<li>Age: {{age}}</li>
</ul>
{{/body}}
{{/page}}
26 changes: 12 additions & 14 deletions multipart-form/templates/enter-details.mustache
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
{{>head}}
<body>
<div>
<h1>Please enter your details</h1>
<form action="/" method="post" enctype="multipart/form-data">
<label for="name">Name</label><br/>
<input type="text" id="name" name="name"/><br/>
<label for="age">Age</label><br/>
<input type="text" id="age" name="age"/><br/>
<input type="submit" value="Submit"/>
</form>
</div>
</body>
</html>
{{<page}}
{{$body}}
<h1 class="text-xl">Please enter your details</h1>
<form action="/" method="post" enctype="multipart/form-data">
<label for="name">Name</label><br/>
<input type="text" id="name" name="name" class="border"/><br/>
<label for="age">Age</label><br/>
<input type="text" id="age" name="age" class="border"/><br/>
<input type="submit" value="Submit" class="p-1 my-2 border shadow-lg" />
</form>
{{/body}}
{{/page}}
Loading