From 7becd8d72259b8da12e9c3ffa24bf96e55b970b2 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 17 Jan 2024 21:58:36 +0000 Subject: [PATCH 1/2] Update README --- README.md | 10 ++-- .../Sources/App/Application+configure.swift | 24 -------- upload-async/Sources/Server/main.swift | 29 ---------- upload-async/Tests/AppTests/AppTests.swift | 52 ------------------ {upload-async => upload}/.dockerignore | 0 {upload-async => upload}/Dockerfile | 0 {upload-async => upload}/Package.swift | 21 ++----- {upload-async => upload}/README.md | 0 upload/Sources/App/Application+build.swift | 36 ++++++++++++ .../App/Controllers/FileController.swift | 46 ++++++++-------- .../Sources/App/Models/UploadModel.swift | 13 +++-- upload/Sources/App/app.swift | 21 +++++++ upload/Tests/AppTests/AppTests.swift | 42 ++++++++++++++ {upload-async => upload}/upload-async.paw | Bin 14 files changed, 141 insertions(+), 153 deletions(-) delete mode 100644 upload-async/Sources/App/Application+configure.swift delete mode 100644 upload-async/Sources/Server/main.swift delete mode 100644 upload-async/Tests/AppTests/AppTests.swift rename {upload-async => upload}/.dockerignore (100%) rename {upload-async => upload}/Dockerfile (100%) rename {upload-async => upload}/Package.swift (79%) rename {upload-async => upload}/README.md (100%) create mode 100644 upload/Sources/App/Application+build.swift rename {upload-async => upload}/Sources/App/Controllers/FileController.swift (68%) rename {upload-async => upload}/Sources/App/Models/UploadModel.swift (72%) create mode 100644 upload/Sources/App/app.swift create mode 100644 upload/Tests/AppTests/AppTests.swift rename {upload-async => upload}/upload-async.paw (100%) diff --git a/README.md b/README.md index 495561c8..43bbfdad 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # Hummingbird Example Code Examples converted to Hummingbird 2.0 -- [hello](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/hello) - Basic application setup -- [html-form](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/html-form) - Link HTML form to Hummingbird application -- [http2](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/http2) - Basic application with HTTP2 upgrade added +- [hello](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/hello) - Basic application setup. +- [html-form](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/html-form) - Link HTML form to Hummingbird application. +- [http2](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/http2) - Basic application with HTTP2 upgrade added. - [sessions](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/sessions) - Username/password and session authentication. -- [todos-dynamodb](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-dynamodb) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB +- [todos-dynamodb](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-dynamodb) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB. - [todos-postgres-tutorial](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/todos-postgres-tutorial) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using PostgresNIO. Sample code that goes along with the [Todos tutorial](https://hummingbird-project.github.io/hummingbird-docs/2.0/tutorials/todos). +- [upload](https://github.com/hummingbird-project/hummingbird-examples/tree/2.x.x/upload) - File uploading and downloading. Examples still working with Hummingbird 1.0 - [auth-cognito](https://github.com/hummingbird-project/hummingbird-examples/tree/main/auth-cognito) - Authentication via AWS Cognito. @@ -20,7 +21,6 @@ Examples still working with Hummingbird 1.0 - [proxy-server-core](https://github.com/hummingbird-project/hummingbird-examples/tree/main/proxy-server-core) - Version of proxy server only using HummingbirdCore - [todos-fluent](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-fluent) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using Fluent - [todos-lambda](https://github.com/hummingbird-project/hummingbird-examples/tree/main/todos-lambda) - Todos application, based off [TodoBackend](http://todobackend.com) spec, using DynamoDB and running on AWS Lambda. -- [upload-async](https://github.com/hummingbird-project/hummingbird-examples/tree/main/upload-async) - File uploading and downloading using the async/await APIs - [upload-s3](https://github.com/hummingbird-project/hummingbird-examples/tree/main/upload-s3) - File uploading and downloading using AWS S3 as backing store. - [webauthn](https://github.com/hummingbird-project/hummingbird-examples/tree/main/webauthn) - Web app demonstrating WebAuthn(PassKey) authentication. - [websocket-chat](https://github.com/hummingbird-project/hummingbird-examples/tree/main/websocket-chat) - Simple chat application using WebSockets. diff --git a/upload-async/Sources/App/Application+configure.swift b/upload-async/Sources/App/Application+configure.swift deleted file mode 100644 index 2823c68f..00000000 --- a/upload-async/Sources/App/Application+configure.swift +++ /dev/null @@ -1,24 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the Hummingbird server framework project -// -// Copyright (c) 2021-2021 the Hummingbird authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Hummingbird -import HummingbirdFoundation - -extension HBApplication { - public func configure() throws { - self.encoder = JSONEncoder() - let fileController = FileController() - fileController.addRoutes(to: self.router.group("files")) - } -} diff --git a/upload-async/Sources/Server/main.swift b/upload-async/Sources/Server/main.swift deleted file mode 100644 index 638c0d7c..00000000 --- a/upload-async/Sources/Server/main.swift +++ /dev/null @@ -1,29 +0,0 @@ -import App -import ArgumentParser -import Hummingbird - -struct HummingbirdArguments: ParsableCommand { - @Option(name: .shortAndLong) - var hostname: String = "127.0.0.1" - - @Option(name: .shortAndLong) - var port: Int = 8080 - - @Option(name: .long) - var maxsize: Int = 10_000_000_000 // 10 GB - - func run() throws { - let app = HBApplication( - configuration: .init( - address: .hostname(self.hostname, port: self.port), - serverName: "Hummingbird", - maxUploadSize: self.maxsize - ) - ) - try app.configure() - try app.start() - app.wait() - } -} - -HummingbirdArguments.main() diff --git a/upload-async/Tests/AppTests/AppTests.swift b/upload-async/Tests/AppTests/AppTests.swift deleted file mode 100644 index 748d1511..00000000 --- a/upload-async/Tests/AppTests/AppTests.swift +++ /dev/null @@ -1,52 +0,0 @@ -@testable import App -import Hummingbird -import HummingbirdXCT -import XCTest - -final class AppTests: XCTestCase { - func testApp() throws { - let app = HBApplication(testing: .live) - try app.configure() - - try app.XCTStart() - defer { app.XCTStop() } - - let textString = "Hello, World!" - let testFileName = "Hello.txt" - let testUpload = UploadModel(filename: testFileName) - let uploadURL = try testUpload.destinationURL(allowsOverwrite: true) - defer { - try? FileManager.default.removeItem(at: uploadURL) - } - let buffer = ByteBufferAllocator().buffer(string: textString) - - try app.XCTExecute( - uri: "/files", - method: .POST, - headers: ["File-Name": testFileName], - body: buffer - ) { response in - XCTAssertEqual(response.status, .ok) - guard let body = response.body else { - XCTFail("Response should contain a valid body") - return - } - XCTAssertTrue(body.contains(string: testFileName)) - } - - try app.XCTExecute(uri: "/files/\(testFileName)", method: .GET) { response in - guard let body = response.body else { - XCTFail("Response should contain a valid body") - return - } - let downloadString = String(buffer: body) - XCTAssertEqual(downloadString, textString, "Downloaded bytes should match uploaded bytes") - } - } -} - -private extension ByteBuffer { - func contains(string: String) -> Bool { - return String(buffer: self).contains(string) - } -} diff --git a/upload-async/.dockerignore b/upload/.dockerignore similarity index 100% rename from upload-async/.dockerignore rename to upload/.dockerignore diff --git a/upload-async/Dockerfile b/upload/Dockerfile similarity index 100% rename from upload-async/Dockerfile rename to upload/Dockerfile diff --git a/upload-async/Package.swift b/upload/Package.swift similarity index 79% rename from upload-async/Package.swift rename to upload/Package.swift index 90139784..454e2269 100644 --- a/upload-async/Package.swift +++ b/upload/Package.swift @@ -1,21 +1,19 @@ -// swift-tools-version:5.5 +// swift-tools-version:5.9 import PackageDescription let package = Package( name: "upload-async", - platforms: [.macOS("12.0")], - products: [ - .executable(name: "Server", targets: ["Server"]), - ], + platforms: [.macOS(.v14)], dependencies: [ - .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "1.0.0"), - .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), + .package(url: "https://github.com/hummingbird-project/hummingbird.git", branch: "2.x.x"), + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"), ], targets: [ - .target( + .executableTarget( name: "App", dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser"), .product(name: "Hummingbird", package: "hummingbird"), .product(name: "HummingbirdFoundation", package: "hummingbird"), ], @@ -26,13 +24,6 @@ let package = Package( .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)), ] ), - .executableTarget( - name: "Server", - dependencies: [ - .byName(name: "App"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - ] - ), .testTarget( name: "AppTests", dependencies: [ diff --git a/upload-async/README.md b/upload/README.md similarity index 100% rename from upload-async/README.md rename to upload/README.md diff --git a/upload/Sources/App/Application+build.swift b/upload/Sources/App/Application+build.swift new file mode 100644 index 00000000..6c271028 --- /dev/null +++ b/upload/Sources/App/Application+build.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2021-2021 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Hummingbird +import HummingbirdFoundation +import Logging +import NIOCore + +struct UploadRequestContext: HBRequestContext { + var coreContext: HBCoreRequestContext + init(allocator: ByteBufferAllocator, logger: Logger) { + self.coreContext = .init( + requestDecoder: JSONDecoder(), + responseEncoder: JSONEncoder(), + allocator: allocator, + logger: logger + ) + } +} + +func buildApplication(args: AppArguments) -> some HBApplicationProtocol { + let router = HBRouter(context: UploadRequestContext.self) + FileController().addRoutes(to: router.group("files")) + return HBApplication(router: router) +} diff --git a/upload-async/Sources/App/Controllers/FileController.swift b/upload/Sources/App/Controllers/FileController.swift similarity index 68% rename from upload-async/Sources/App/Controllers/FileController.swift rename to upload/Sources/App/Controllers/FileController.swift index 7f97357d..5a3f044b 100644 --- a/upload-async/Sources/App/Controllers/FileController.swift +++ b/upload/Sources/App/Controllers/FileController.swift @@ -13,14 +13,17 @@ //===----------------------------------------------------------------------===// import Foundation +import HTTPTypes import Hummingbird import HummingbirdFoundation /// Handles file transfers struct FileController { - func addRoutes(to group: HBRouterGroup) { + let fileIO = HBFileIO() + + func addRoutes(to group: HBRouterGroup) { group.get(":filename", use: self.download) - group.post("/", options: .streamBody, use: self.upload) + group.post("/", use: self.upload) } // MARK: - Upload @@ -34,20 +37,18 @@ struct FileController { /// then that name will be used as the file name on disk, otherwise /// a UUID will be used. /// - Returns: A JSONEncoded ``UploadModel`` - private func upload(_ request: HBRequest) async throws -> UploadModel { - guard request.body.stream != nil else { throw HBHTTPError(.unauthorized) } + @Sendable private func upload(_ request: HBRequest, context: some HBRequestContext) async throws -> UploadModel { let fileName = fileName(for: request) let uploadModel = UploadModel(filename: fileName) let fileURL = try uploadModel.destinationURL() - request.logger.info(.init(stringLiteral: "Uploading: \(uploadModel)")) - let fileIO = HBFileIO(application: request.application) - try await fileIO.writeFile( + context.logger.info(.init(stringLiteral: "Uploading: \(uploadModel)")) + try await self.fileIO.writeFile( contents: request.body, path: fileURL.path, - context: request.context, - logger: request.logger + context: context, + logger: context.logger ) return uploadModel } @@ -59,17 +60,14 @@ struct FileController { /// - Returns: HBResponse of chunked bytes if success /// Note that this download has no login checks and allows anyone to download /// by its filename alone. - private func download(_ request: HBRequest) async throws -> HBResponse { - guard let filename = request.parameters.get("filename", as: String.self) else { - throw HBHTTPError(.badRequest) - } + @Sendable private func download(_ request: HBRequest, context: some HBRequestContext) async throws -> HBResponse { + let filename = try context.parameters.require("filename", as: String.self) let uploadModel = UploadModel(filename: filename) let uploadURL = try uploadModel.destinationURL(allowsOverwrite: true) - let fileIO = HBFileIO(application: request.application) - let body = try await fileIO.loadFile( + let body = try await self.fileIO.loadFile( path: uploadURL.path, - context: request.context, - logger: request.logger + context: context, + logger: context.logger ) return HBResponse( status: .ok, @@ -80,10 +78,10 @@ struct FileController { /// Adds headers for a given filename /// IDEA: this is a good place to set the "Content-Type" property - private func headers(for filename: String) -> HTTPHeaders { - return HTTPHeaders([ - ("Content-Disposition", "attachment;filename=\"\(filename)\""), - ]) + private func headers(for filename: String) -> HTTPFields { + return [ + .contentDisposition: "attachment;filename=\"\(filename)\"", + ] } } @@ -95,9 +93,13 @@ extension FileController { } private func fileName(for request: HBRequest) -> String { - guard let fileName = request.headers["File-Name"].first else { + guard let fileName = request.headers[.fileName] else { return self.uuidFileName() } return fileName } } + +extension HTTPField.Name { + static var fileName: Self { .init("File-Name")! } +} diff --git a/upload-async/Sources/App/Models/UploadModel.swift b/upload/Sources/App/Models/UploadModel.swift similarity index 72% rename from upload-async/Sources/App/Models/UploadModel.swift rename to upload/Sources/App/Models/UploadModel.swift index a03ddc6e..45a2239d 100644 --- a/upload-async/Sources/App/Models/UploadModel.swift +++ b/upload/Sources/App/Models/UploadModel.swift @@ -10,7 +10,7 @@ struct UploadModel: HBResponseCodable { } extension UploadModel: CustomStringConvertible { - var description: String { filename } + var description: String { self.filename } } extension UploadModel { @@ -20,11 +20,12 @@ extension UploadModel { /// - allowsOverwrite: set `true` to overwrite any file with the same filename /// - Returns: the target directory for uploads func destinationURL(searchPath: FileManager.SearchPathDirectory = .documentDirectory, allowsOverwrite: Bool = false) throws -> URL { - let fileURL = try FileManager.default.url(for: .documentDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true) - .appendingPathComponent(filename) + let fileURL = try FileManager.default.url( + for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ).appendingPathComponent(self.filename) guard allowsOverwrite == false else { return fileURL } guard FileManager.default.fileExists(atPath: fileURL.path) == false else { diff --git a/upload/Sources/App/app.swift b/upload/Sources/App/app.swift new file mode 100644 index 00000000..e358c84b --- /dev/null +++ b/upload/Sources/App/app.swift @@ -0,0 +1,21 @@ +import ArgumentParser +import Hummingbird + +@main +struct HummingbirdArguments: 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 = buildApplication(args: self) + try await app.runService() + } +} + +protocol AppArguments { + var hostname: String { get } + var port: Int { get } +} diff --git a/upload/Tests/AppTests/AppTests.swift b/upload/Tests/AppTests/AppTests.swift new file mode 100644 index 00000000..88109764 --- /dev/null +++ b/upload/Tests/AppTests/AppTests.swift @@ -0,0 +1,42 @@ +@testable import App +import Hummingbird +import HummingbirdXCT +import XCTest + +final class AppTests: XCTestCase { + struct TestArguments: AppArguments { + var hostname: String { "127.0.0.1" } + var port: Int { 0 } + } + + func testUploadDownload() async throws { + let app = buildApplication(args: TestArguments()) + + try await app.test(.live) { client in + let textString = "Hello, World!" + let testFileName = "Hello.txt" + let testUpload = UploadModel(filename: testFileName) + let uploadURL = try testUpload.destinationURL(allowsOverwrite: true) + defer { + try? FileManager.default.removeItem(at: uploadURL) + } + let buffer = ByteBuffer(string: textString) + + try await client.XCTExecute( + uri: "/files", + method: .post, + headers: [.fileName: testFileName], + body: buffer + ) { response in + XCTAssertEqual(response.status, .ok) + let bodyString = String(buffer: response.body) + XCTAssertTrue(bodyString.contains(testFileName)) + } + + try await client.XCTExecute(uri: "/files/\(testFileName)", method: .get) { response in + let downloadString = String(buffer: response.body) + XCTAssertEqual(downloadString, textString, "Downloaded bytes should match uploaded bytes") + } + } + } +} diff --git a/upload-async/upload-async.paw b/upload/upload-async.paw similarity index 100% rename from upload-async/upload-async.paw rename to upload/upload-async.paw From 583a16bb7810fed7890cd8bf3a8b4683fd995a56 Mon Sep 17 00:00:00 2001 From: Adam Fowler Date: Wed, 17 Jan 2024 21:57:48 +0000 Subject: [PATCH 2/2] Remove logger parameter from HBFileIO functions --- upload/Package.swift | 1 + upload/Sources/App/Controllers/FileController.swift | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/upload/Package.swift b/upload/Package.swift index 454e2269..dd2fb0b0 100644 --- a/upload/Package.swift +++ b/upload/Package.swift @@ -28,6 +28,7 @@ let package = Package( name: "AppTests", dependencies: [ .byName(name: "App"), + .product(name: "Hummingbird", package: "hummingbird"), .product(name: "HummingbirdXCT", package: "hummingbird"), ] ), diff --git a/upload/Sources/App/Controllers/FileController.swift b/upload/Sources/App/Controllers/FileController.swift index 5a3f044b..03a079f3 100644 --- a/upload/Sources/App/Controllers/FileController.swift +++ b/upload/Sources/App/Controllers/FileController.swift @@ -47,8 +47,7 @@ struct FileController { try await self.fileIO.writeFile( contents: request.body, path: fileURL.path, - context: context, - logger: context.logger + context: context ) return uploadModel } @@ -66,8 +65,7 @@ struct FileController { let uploadURL = try uploadModel.destinationURL(allowsOverwrite: true) let body = try await self.fileIO.loadFile( path: uploadURL.path, - context: context, - logger: context.logger + context: context ) return HBResponse( status: .ok,