1
0
mirror of https://github.com/apple/swift-nio-extras.git synced 2025-05-22 13:39:01 +08:00

Import HTTP resumable upload sample code ()

Support HTTP resumable upload.

### Motivation:

Supporting HTTP resumable upload protocol defined in
https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-resumable-upload-05

* Interop version 3: iOS 17.0, macOS 14.0
* Interop version 5: iOS 18.0, macOS 15.0
* Interop version 6: iOS 18.1, macOS 15.1

### Modifications:

2 new public classes, `HTTPResumableUploadHandler` and
`HTTPResumableUploadContext`, and a few other supporting objects to
manage resumable uploads and translate them into regular uploads.

---------

Co-authored-by: Jonathan Flat <jflat@apple.com>
Co-authored-by: Cory Benfield <lukasa@apple.com>
This commit is contained in:
Guoye Zhang 2024-12-10 23:39:13 -08:00 committed by GitHub
parent 3efabe7202
commit fde9d65d2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 2206 additions and 3 deletions

@ -1,4 +1,4 @@
version: 1
builder:
configs:
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS, NIOHTTPTypes, NIOHTTPTypesHTTP1, NIOHTTPTypesHTTP2]
- documentation_targets: [NIOExtras, NIOHTTPCompression, NIOSOCKS, NIOHTTPTypes, NIOHTTPTypesHTTP1, NIOHTTPTypesHTTP2, NIOResumableUpload]

@ -3,7 +3,7 @@
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors
// Copyright (c) 2017-2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
@ -170,6 +170,33 @@ var targets: [PackageDescription.Target] = [
"NIOHTTPTypesHTTP2"
]
),
.target(
name: "NIOResumableUpload",
dependencies: [
"NIOHTTPTypes",
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "StructuredFieldValues", package: "swift-http-structured-headers"),
.product(name: "Atomics", package: "swift-atomics"),
]
),
.executableTarget(
name: "NIOResumableUploadDemo",
dependencies: [
"NIOResumableUpload",
"NIOHTTPTypesHTTP1",
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
]
),
.testTarget(
name: "NIOResumableUploadTests",
dependencies: [
"NIOResumableUpload",
.product(name: "NIOEmbedded", package: "swift-nio"),
]
),
]
let package = Package(
@ -181,11 +208,14 @@ let package = Package(
.library(name: "NIOHTTPTypes", targets: ["NIOHTTPTypes"]),
.library(name: "NIOHTTPTypesHTTP1", targets: ["NIOHTTPTypesHTTP1"]),
.library(name: "NIOHTTPTypesHTTP2", targets: ["NIOHTTPTypesHTTP2"]),
.library(name: "NIOResumableUpload", targets: ["NIOResumableUpload"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-nio.git", from: "2.67.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.27.0"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-http-structured-headers.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-atomics.git", from: "1.2.0"),
],
targets: targets
)

@ -58,3 +58,4 @@ On the [`nio-extras-0.1`](https://github.com/apple/swift-nio-extras/tree/nio-ext
- [`HTTPToHTTP1ServerCodec`](Sources/NIOHTTPTypesHTTP1/HTTPToHTTP1Codec.swift) A `ChannelHandler` that translates shared HTTP types into HTTP/1 messages for the server side for compatibility purposes.
- [`HTTP2FramePayloadToHTTPClientCodec`](Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/2 concepts into shared HTTP types for the client side.
- [`HTTP2FramePayloadToHTTPServerCodec`](Sources/NIOHTTPTypesHTTP2/HTTP2ToHTTPCodec.swift) A `ChannelHandler` that translates HTTP/2 concepts into shared HTTP types for the server side.
- [`HTTPResumableUploadHandler`](Sources/NIOResumableUpload/HTTPResumableUploadHandler.swift) A `ChannelHandler` that translates HTTP resumable uploads to regular uploads.

@ -0,0 +1,490 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023-2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import HTTPTypes
import NIOConcurrencyHelpers
import NIOCore
import NIOHTTPTypes
/// `HTTPResumableUpload` tracks a logical upload. It manages an `HTTPResumableUploadChannel` and
/// connects a series of `HTTPResumableUploadHandler` objects to this channel.
final class HTTPResumableUpload {
private let context: HTTPResumableUploadContext
private let channelConfigurator: (Channel) -> Void
private var eventLoop: EventLoop!
private var uploadHandler: HTTPResumableUploadHandler?
private let uploadHandlerChannel: NIOLockedValueBox<Channel?> = .init(nil)
private var uploadChannel: HTTPResumableUploadChannel?
/// The resumption path containing the unique token identifying the upload.
private(set) var resumePath: String?
/// The current upload offset.
private var offset: Int64 = 0
/// The total length of the upload (if known).
private var uploadLength: Int64?
/// The current request is an upload creation request.
private var requestIsCreation: Bool = false
/// The end of the current request is the end of the upload.
private var requestIsComplete: Bool = true
/// Whether the request is OPTIONS
private var requestIsOptions: Bool = false
/// The interop version of the current request
private var interopVersion: HTTPResumableUploadProtocol.InteropVersion = .latest
/// Whether you have received the entire upload.
private var uploadComplete: Bool = false
/// The response has started.
private var responseStarted: Bool = false
/// The child channel enqueued a read while no upload handler was present.
private var pendingRead: Bool = false
/// Last error that the upload handler delivered.
private var pendingError: Error?
/// Idle time since the last upload handler detached.
private var idleTimer: Scheduled<Void>?
init(
context: HTTPResumableUploadContext,
channelConfigurator: @escaping (Channel) -> Void
) {
self.context = context
self.channelConfigurator = channelConfigurator
}
private func createChannel(handler: HTTPResumableUploadHandler, parent: Channel) -> HTTPResumableUploadChannel {
let channel = HTTPResumableUploadChannel(
upload: self,
parent: parent,
channelConfigurator: self.channelConfigurator
)
channel.start()
self.uploadChannel = channel
return channel
}
private func destroyChannel(error: Error?) {
if let uploadChannel = self.uploadChannel {
self.context.stopUpload(self)
self.uploadChannel = nil
uploadChannel.end(error: error)
}
}
private func respondAndDetach(_ response: HTTPResponse, handler: HTTPResumableUploadHandler) {
handler.write(.head(response), promise: nil)
handler.writeAndFlush(.end(nil), promise: nil)
if handler === self.uploadHandler {
detachUploadHandler(close: false)
}
}
}
// For `HTTPResumableUploadHandler`.
extension HTTPResumableUpload {
/// `HTTPResumableUpload` runs on the same event loop as the initial upload handler that started the upload.
/// - Parameter eventLoop: The event loop to schedule work in.
func scheduleOnEventLoop(_ eventLoop: EventLoop) {
eventLoop.assertInEventLoop()
assert(self.eventLoop == nil)
self.eventLoop = eventLoop
}
private func runInEventLoop(_ work: @escaping () -> Void) {
if self.eventLoop.inEventLoop {
work()
} else {
self.eventLoop.execute(work)
}
}
private func runInEventLoop(checkHandler handler: HTTPResumableUploadHandler, _ work: @escaping () -> Void) {
self.runInEventLoop {
if self.uploadHandler === handler {
work()
}
}
}
func attachUploadHandler(_ handler: HTTPResumableUploadHandler, channel: Channel) {
self.eventLoop.preconditionInEventLoop()
self.pendingError = nil
self.idleTimer?.cancel()
self.idleTimer = nil
self.uploadHandler = handler
self.uploadHandlerChannel.withLockedValue { $0 = channel }
self.uploadChannel?.writabilityChanged()
if self.pendingRead {
self.pendingRead = false
handler.read()
}
}
private func detachUploadHandler(close: Bool) {
self.eventLoop.preconditionInEventLoop()
if let uploadHandler = self.uploadHandler {
self.uploadHandler = nil
self.uploadHandlerChannel.withLockedValue { $0 = nil }
self.uploadChannel?.writabilityChanged()
if close {
uploadHandler.close(mode: .all, promise: nil)
}
uploadHandler.detach()
if self.uploadChannel != nil {
self.idleTimer?.cancel()
self.idleTimer = self.eventLoop.scheduleTask(in: self.context.timeout) {
let error = self.pendingError ?? HTTPResumableUploadError.timeoutWaitingForResumption
self.uploadChannel?.end(error: error)
self.uploadChannel = nil
}
}
}
}
private func offsetRetrieving(
otherHandler: HTTPResumableUploadHandler,
version: HTTPResumableUploadProtocol.InteropVersion
) {
self.runInEventLoop {
self.detachUploadHandler(close: true)
let response = HTTPResumableUploadProtocol.offsetRetrievingResponse(
offset: self.offset,
complete: self.uploadComplete,
version: version
)
self.respondAndDetach(response, handler: otherHandler)
}
}
private func saveUploadLength(complete: Bool, contentLength: Int64?, uploadLength: Int64?) -> Bool {
let computedUploadLength = complete ? contentLength.map { self.offset + $0 } : nil
if let knownUploadLength = self.uploadLength {
if let computedUploadLength, knownUploadLength != computedUploadLength {
return false
}
} else {
self.uploadLength = computedUploadLength
}
if let knownUploadLength = self.uploadLength {
if let uploadLength, knownUploadLength != uploadLength {
return false
}
} else {
self.uploadLength = uploadLength
}
return true
}
private func uploadAppending(
otherHandler: HTTPResumableUploadHandler,
channel: Channel,
offset: Int64,
complete: Bool,
contentLength: Int64?,
uploadLength: Int64?,
version: HTTPResumableUploadProtocol.InteropVersion
) {
self.runInEventLoop {
let conflict: Bool
if self.uploadHandler == nil && self.offset == offset && !self.responseStarted {
conflict = !self.saveUploadLength(
complete: complete,
contentLength: contentLength,
uploadLength: uploadLength
)
} else {
conflict = true
}
guard !conflict else {
self.detachUploadHandler(close: true)
self.destroyChannel(error: HTTPResumableUploadError.badResumption)
let response = HTTPResumableUploadProtocol.conflictResponse(
offset: self.offset,
complete: self.uploadComplete,
version: version
)
self.respondAndDetach(response, handler: otherHandler)
return
}
self.requestIsCreation = false
self.requestIsComplete = complete
self.interopVersion = version
self.attachUploadHandler(otherHandler, channel: channel)
}
}
private func uploadCancellation() {
self.runInEventLoop {
self.detachUploadHandler(close: true)
self.destroyChannel(error: HTTPResumableUploadError.uploadCancelled)
}
}
private func receiveHead(handler: HTTPResumableUploadHandler, channel: Channel, request: HTTPRequest) {
self.eventLoop.preconditionInEventLoop()
do {
guard let (type, version) = try HTTPResumableUploadProtocol.identifyRequest(request, in: self.context)
else {
let channel = self.createChannel(handler: handler, parent: channel)
channel.receive(.head(request))
return
}
self.interopVersion = version
switch type {
case .uploadCreation(let complete, let contentLength, let uploadLength):
self.requestIsCreation = true
self.requestIsComplete = complete
self.uploadLength = uploadLength
if !self.saveUploadLength(complete: complete, contentLength: contentLength, uploadLength: uploadLength)
{
let response = HTTPResumableUploadProtocol.conflictResponse(
offset: self.offset,
complete: self.uploadComplete,
version: version
)
self.respondAndDetach(response, handler: handler)
return
}
let resumePath = self.context.startUpload(self)
self.resumePath = resumePath
let informationalResponse = HTTPResumableUploadProtocol.featureDetectionResponse(
resumePath: resumePath,
in: self.context,
version: version
)
handler.writeAndFlush(.head(informationalResponse), promise: nil)
let strippedRequest = HTTPResumableUploadProtocol.stripRequest(request)
let channel = self.createChannel(handler: handler, parent: channel)
channel.receive(.head(strippedRequest))
case .offsetRetrieving:
if let path = request.path, let upload = self.context.findUpload(path: path) {
self.uploadHandler = nil
self.uploadHandlerChannel.withLockedValue { $0 = nil }
upload.offsetRetrieving(otherHandler: handler, version: version)
} else {
let response = HTTPResumableUploadProtocol.notFoundResponse(version: version)
self.respondAndDetach(response, handler: handler)
}
case .uploadAppending(let offset, let complete, let contentLength, let uploadLength):
if let path = request.path, let upload = self.context.findUpload(path: path) {
handler.upload = upload
self.uploadHandler = nil
self.uploadHandlerChannel.withLockedValue { $0 = nil }
upload.uploadAppending(
otherHandler: handler,
channel: channel,
offset: offset,
complete: complete,
contentLength: contentLength,
uploadLength: uploadLength,
version: version
)
} else {
let response = HTTPResumableUploadProtocol.notFoundResponse(version: version)
self.respondAndDetach(response, handler: handler)
}
case .uploadCancellation:
if let path = request.path, let upload = self.context.findUpload(path: path) {
upload.uploadCancellation()
let response = HTTPResumableUploadProtocol.cancelledResponse(version: version)
self.respondAndDetach(response, handler: handler)
} else {
let response = HTTPResumableUploadProtocol.notFoundResponse(version: version)
self.respondAndDetach(response, handler: handler)
}
case .options:
self.requestIsOptions = true
let channel = self.createChannel(handler: handler, parent: channel)
channel.receive(.head(request))
}
} catch {
let response = HTTPResumableUploadProtocol.badRequestResponse()
self.respondAndDetach(response, handler: handler)
}
}
private func receiveBody(handler: HTTPResumableUploadHandler, body: ByteBuffer) {
self.eventLoop.preconditionInEventLoop()
self.offset += Int64(body.readableBytes)
if let uploadLength = self.uploadLength, self.offset > uploadLength {
let response = HTTPResumableUploadProtocol.conflictResponse(
offset: self.offset,
complete: self.uploadComplete,
version: self.interopVersion
)
self.respondAndDetach(response, handler: handler)
return
}
self.uploadChannel?.receive(.body(body))
}
private func receiveEnd(handler: HTTPResumableUploadHandler, trailers: HTTPFields?) {
self.eventLoop.preconditionInEventLoop()
if let resumePath = self.resumePath {
if self.requestIsComplete {
self.uploadComplete = true
self.uploadChannel?.receive(.end(trailers))
} else {
let response = HTTPResumableUploadProtocol.incompleteResponse(
offset: self.offset,
resumePath: resumePath,
forUploadCreation: self.requestIsCreation,
in: self.context,
version: self.interopVersion
)
self.respondAndDetach(response, handler: handler)
}
} else {
self.uploadChannel?.receive(.end(trailers))
}
}
func receive(handler: HTTPResumableUploadHandler, channel: Channel, part: HTTPRequestPart) {
self.runInEventLoop(checkHandler: handler) {
switch part {
case .head(let request):
self.receiveHead(handler: handler, channel: channel, request: request)
case .body(let body):
self.receiveBody(handler: handler, body: body)
case .end(let trailers):
self.receiveEnd(handler: handler, trailers: trailers)
}
}
}
func receiveComplete(handler: HTTPResumableUploadHandler) {
self.runInEventLoop(checkHandler: handler) {
self.uploadChannel?.receiveComplete()
}
}
func writabilityChanged(handler: HTTPResumableUploadHandler) {
self.runInEventLoop(checkHandler: handler) {
self.uploadChannel?.writabilityChanged()
}
}
func end(handler: HTTPResumableUploadHandler, error: Error?) {
self.runInEventLoop(checkHandler: handler) {
if !self.uploadComplete && self.resumePath != nil {
self.pendingError = error
self.detachUploadHandler(close: false)
} else {
self.destroyChannel(error: error)
self.detachUploadHandler(close: false)
}
}
}
}
// For `HTTPResumableUploadChannel`.
extension HTTPResumableUpload {
var parentChannel: Channel? {
self.uploadHandlerChannel.withLockedValue { $0 }
}
func write(_ part: HTTPResponsePart, promise: EventLoopPromise<Void>?) {
self.eventLoop.preconditionInEventLoop()
guard let uploadHandler = self.uploadHandler else {
promise?.fail(HTTPResumableUploadError.parentNotPresent)
self.destroyChannel(error: HTTPResumableUploadError.parentNotPresent)
return
}
self.responseStarted = true
if let resumePath = self.resumePath {
switch part {
case .head(let head):
let response = HTTPResumableUploadProtocol.processResponse(
head,
offset: self.offset,
resumePath: resumePath,
forUploadCreation: self.requestIsCreation,
in: self.context,
version: self.interopVersion
)
uploadHandler.write(.head(response), promise: promise)
case .body, .end:
uploadHandler.write(part, promise: promise)
}
} else {
if self.requestIsOptions {
switch part {
case .head(let head):
let response = HTTPResumableUploadProtocol.processOptionsResponse(head)
uploadHandler.write(.head(response), promise: promise)
case .body, .end:
uploadHandler.write(part, promise: promise)
}
} else {
uploadHandler.write(part, promise: promise)
}
}
}
func flush() {
self.eventLoop.preconditionInEventLoop()
guard let uploadHandler = self.uploadHandler else {
self.destroyChannel(error: HTTPResumableUploadError.parentNotPresent)
return
}
uploadHandler.flush()
}
func read() {
self.eventLoop.preconditionInEventLoop()
if let handler = self.uploadHandler {
handler.read()
} else {
self.pendingRead = true
}
}
func close(mode: CloseMode, promise: EventLoopPromise<Void>?) {
self.eventLoop.preconditionInEventLoop()
precondition(mode != .input)
self.destroyChannel(error: nil)
self.uploadHandler?.close(mode: mode, promise: promise)
self.uploadHandler?.detach()
self.uploadHandler = nil
self.idleTimer?.cancel()
self.idleTimer = nil
}
}
/// Errors produced by resumable upload.
enum HTTPResumableUploadError: Error {
/// An upload cancelation request received.
case uploadCancelled
/// No upload handler is attached.
case parentNotPresent
/// Timed out waiting for an upload handler to attach.
case timeoutWaitingForResumption
/// A resumption request from the client is invalid.
case badResumption
}

@ -0,0 +1,254 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023-2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Atomics
import NIOCore
import NIOHTTPTypes
/// The child channel that persists across upload resumption attempts, delivering data as if it is
/// a single HTTP upload.
final class HTTPResumableUploadChannel: Channel, ChannelCore {
let upload: HTTPResumableUpload
let allocator: ByteBufferAllocator
private let closePromise: EventLoopPromise<Void>
var closeFuture: EventLoopFuture<Void> {
self.closePromise.futureResult
}
private var _pipeline: ChannelPipeline!
var pipeline: ChannelPipeline {
self._pipeline
}
var localAddress: SocketAddress? {
self.parent?.localAddress
}
var remoteAddress: SocketAddress? {
self.parent?.remoteAddress
}
var parent: Channel? {
self.upload.parentChannel
}
var isWritable: Bool {
self.parent?.isWritable ?? false
}
private let _isActiveAtomic: ManagedAtomic<Bool> = .init(true)
var isActive: Bool {
self._isActiveAtomic.load(ordering: .relaxed)
}
var _channelCore: ChannelCore {
self
}
let eventLoop: EventLoop
private var autoRead: Bool
init(
upload: HTTPResumableUpload,
parent: Channel,
channelConfigurator: (Channel) -> Void
) {
self.upload = upload
self.allocator = parent.allocator
self.closePromise = parent.eventLoop.makePromise()
self.eventLoop = parent.eventLoop
// Only support Channels that implement sync options
self.autoRead = try! parent.syncOptions!.getOption(ChannelOptions.autoRead)
self._pipeline = ChannelPipeline(channel: self)
channelConfigurator(self)
}
func setOption<Option>(_ option: Option, value: Option.Value) -> EventLoopFuture<Void> where Option: ChannelOption {
if self.eventLoop.inEventLoop {
do {
return try self.eventLoop.makeSucceededFuture(self.setOption0(option, value: value))
} catch {
return self.eventLoop.makeFailedFuture(error)
}
} else {
return self.eventLoop.submit { try self.setOption0(option, value: value) }
}
}
func getOption<Option>(_ option: Option) -> EventLoopFuture<Option.Value> where Option: ChannelOption {
if self.eventLoop.inEventLoop {
do {
return try self.eventLoop.makeSucceededFuture(self.getOption0(option))
} catch {
return self.eventLoop.makeFailedFuture(error)
}
} else {
return self.eventLoop.submit { try self.getOption0(option) }
}
}
private func setOption0<Option: ChannelOption>(_ option: Option, value: Option.Value) throws {
self.eventLoop.preconditionInEventLoop()
switch option {
case _ as ChannelOptions.Types.AutoReadOption:
self.autoRead = value as! Bool
default:
if let parent = self.parent {
// Only support Channels that implement sync options
try parent.syncOptions!.setOption(option, value: value)
} else {
throw HTTPResumableUploadError.parentNotPresent
}
}
}
private func getOption0<Option: ChannelOption>(_ option: Option) throws -> Option.Value {
self.eventLoop.preconditionInEventLoop()
switch option {
case _ as ChannelOptions.Types.AutoReadOption:
return self.autoRead as! Option.Value
default:
if let parent = self.parent {
// Only support Channels that implement sync options
return try parent.syncOptions!.getOption(option)
} else {
throw HTTPResumableUploadError.parentNotPresent
}
}
}
func localAddress0() throws -> SocketAddress {
fatalError()
}
func remoteAddress0() throws -> SocketAddress {
fatalError()
}
func register0(promise: EventLoopPromise<Void>?) {
fatalError()
}
func bind0(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
fatalError()
}
func connect0(to address: SocketAddress, promise: EventLoopPromise<Void>?) {
fatalError()
}
func write0(_ data: NIOAny, promise: EventLoopPromise<Void>?) {
self.eventLoop.preconditionInEventLoop()
self.upload.write(unwrapData(data), promise: promise)
}
func flush0() {
self.eventLoop.preconditionInEventLoop()
self.upload.flush()
}
func read0() {
self.eventLoop.preconditionInEventLoop()
self.upload.read()
}
func close0(error: Error, mode: CloseMode, promise: EventLoopPromise<Void>?) {
self.eventLoop.preconditionInEventLoop()
self.upload.close(mode: mode, promise: promise)
}
func triggerUserOutboundEvent0(_ event: Any, promise: EventLoopPromise<Void>?) {
// Do nothing.
}
func channelRead0(_ data: NIOAny) {
// Do nothing.
}
func errorCaught0(error: Error) {
// Do nothing.
}
}
extension HTTPResumableUploadChannel {
private struct SynchronousOptions: NIOSynchronousChannelOptions {
private let channel: HTTPResumableUploadChannel
init(channel: HTTPResumableUploadChannel) {
self.channel = channel
}
func setOption<Option: ChannelOption>(_ option: Option, value: Option.Value) throws {
try self.channel.setOption0(option, value: value)
}
func getOption<Option: ChannelOption>(_ option: Option) throws -> Option.Value {
try self.channel.getOption0(option)
}
}
var syncOptions: NIOSynchronousChannelOptions? {
SynchronousOptions(channel: self)
}
}
// For `HTTPResumableUpload`.
extension HTTPResumableUploadChannel {
func start() {
self.eventLoop.preconditionInEventLoop()
self.pipeline.fireChannelRegistered()
self.pipeline.fireChannelActive()
}
func receive(_ part: HTTPRequestPart) {
self.eventLoop.preconditionInEventLoop()
self.pipeline.fireChannelRead(NIOAny(part))
}
func receiveComplete() {
self.eventLoop.preconditionInEventLoop()
self.pipeline.fireChannelReadComplete()
if self.autoRead {
self.pipeline.read()
}
}
func writabilityChanged() {
self.eventLoop.preconditionInEventLoop()
self.pipeline.fireChannelWritabilityChanged()
}
func end(error: Error?) {
self.eventLoop.preconditionInEventLoop()
if let error {
self.pipeline.fireErrorCaught(error)
}
self._isActiveAtomic.store(false, ordering: .relaxed)
self.pipeline.fireChannelInactive()
self.pipeline.fireChannelUnregistered()
self.eventLoop.execute {
self.removeHandlers(pipeline: self.pipeline)
self.closePromise.succeed()
}
}
}

@ -0,0 +1,74 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023-2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIOConcurrencyHelpers
import NIOCore
/// `HTTPResumableUploadContext` manages ongoing uploads.
public final class HTTPResumableUploadContext {
let origin: String
let path: String
let timeout: TimeAmount
private let uploads: NIOLockedValueBox<[String: HTTPResumableUpload]> = .init([:])
/// Create an `HTTPResumableUploadContext` for use with `HTTPResumableUploadHandler`.
/// - Parameters:
/// - origin: Scheme and authority of the upload server. For example, "https://www.example.com".
/// - path: Request path for resumption URLs. `HTTPResumableUploadHandler` intercepts all requests to this path.
/// - timeout: Time to wait before failure if the client didn't attempt an upload resumption.
public init(origin: String, path: String = "/resumable_upload/", timeout: TimeAmount = .hours(1)) {
self.origin = origin
self.path = path
self.timeout = timeout
}
func isResumption(path: String) -> Bool {
path.hasPrefix(self.path)
}
private func path(fromToken token: String) -> String {
"\(self.path)\(token)"
}
private func token(fromPath path: String) -> String {
assert(self.isResumption(path: path))
return String(path.dropFirst(self.path.count))
}
func startUpload(_ upload: HTTPResumableUpload) -> String {
var random = SystemRandomNumberGenerator()
let token = "\(random.next())-\(random.next())"
self.uploads.withLockedValue {
assert($0[token] == nil)
$0[token] = upload
}
return self.path(fromToken: token)
}
func stopUpload(_ upload: HTTPResumableUpload) {
if let path = upload.resumePath {
let token = token(fromPath: path)
self.uploads.withLockedValue {
$0[token] = nil
}
}
}
func findUpload(path: String) -> HTTPResumableUpload? {
let token = token(fromPath: path)
return self.uploads.withLockedValue {
$0[token]
}
}
}

@ -0,0 +1,172 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023-2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import NIOCore
import NIOHTTPTypes
/// A channel handler that translates resumable uploads into regular uploads, and passes through
/// other HTTP traffic.
public final class HTTPResumableUploadHandler: ChannelDuplexHandler {
public typealias InboundIn = HTTPRequestPart
public typealias InboundOut = Never
public typealias OutboundIn = Never
public typealias OutboundOut = HTTPResponsePart
var upload: HTTPResumableUpload? = nil
let createUpload: () -> HTTPResumableUpload
var shouldReset: Bool = false
private var context: ChannelHandlerContext?
private var eventLoop: EventLoop!
/// Create an `HTTPResumableUploadHandler` within a given `HTTPResumableUploadContext`.
/// - Parameters:
/// - context: The context for this upload handler.
/// Use the same context across upload handlers, as uploads can't resume across different contexts.
/// - channelConfigurator: A closure for configuring the child HTTP server channel.
public init(
context: HTTPResumableUploadContext,
channelConfigurator: @escaping (Channel) -> Void
) {
self.createUpload = {
HTTPResumableUpload(
context: context,
channelConfigurator: channelConfigurator
)
}
}
/// Create an `HTTPResumableUploadHandler` within a given `HTTPResumableUploadContext`.
/// - Parameters:
/// - context: The context for this upload handler.
/// Use the same context across upload handlers, as uploads can't resume across different contexts.
/// - handlers: Handlers to add to the child HTTP server channel.
public init(
context: HTTPResumableUploadContext,
handlers: [ChannelHandler] = []
) {
self.createUpload = {
HTTPResumableUpload(context: context) { channel in
if !handlers.isEmpty {
_ = channel.pipeline.addHandlers(handlers)
}
}
}
}
private func resetUpload(context: ChannelHandlerContext) {
if let existingUpload = self.upload {
existingUpload.end(handler: self, error: nil)
}
let upload = self.createUpload()
upload.scheduleOnEventLoop(self.eventLoop)
upload.attachUploadHandler(self, channel: context.channel)
self.upload = upload
self.shouldReset = false
}
public func handlerAdded(context: ChannelHandlerContext) {
self.context = context
self.eventLoop = context.eventLoop
self.resetUpload(context: context)
}
public func channelActive(context: ChannelHandlerContext) {
context.read()
}
public func channelInactive(context: ChannelHandlerContext) {
self.upload?.end(handler: self, error: nil)
}
public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
if self.shouldReset {
self.resetUpload(context: context)
}
let part = self.unwrapInboundIn(data)
if case .end = part {
self.shouldReset = true
}
self.upload?.receive(handler: self, channel: context.channel, part: part)
}
public func channelReadComplete(context: ChannelHandlerContext) {
self.upload?.receiveComplete(handler: self)
}
public func channelWritabilityChanged(context: ChannelHandlerContext) {
self.upload?.writabilityChanged(handler: self)
}
public func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {}
public func errorCaught(context: ChannelHandlerContext, error: Error) {
self.upload?.end(handler: self, error: error)
}
public func read(context: ChannelHandlerContext) {
if self.shouldReset {
context.read()
}
}
}
// For `HTTPResumableUpload`.
extension HTTPResumableUploadHandler {
private func runInEventLoop(_ work: @escaping () -> Void) {
if self.eventLoop.inEventLoop {
work()
} else {
self.eventLoop.execute(work)
}
}
func write(_ part: HTTPResponsePart, promise: EventLoopPromise<Void>?) {
self.runInEventLoop {
self.context?.write(self.wrapOutboundOut(part), promise: promise)
}
}
func flush() {
self.runInEventLoop {
self.context?.flush()
}
}
func writeAndFlush(_ part: HTTPResponsePart, promise: EventLoopPromise<Void>?) {
self.runInEventLoop {
self.context?.writeAndFlush(self.wrapOutboundOut(part), promise: promise)
}
}
func read() {
self.runInEventLoop {
self.context?.read()
}
}
func close(mode: CloseMode, promise: EventLoopPromise<Void>?) {
self.runInEventLoop {
self.context?.close(mode: mode, promise: promise)
}
}
func detach() {
self.runInEventLoop {
self.context = nil
}
}
}

@ -0,0 +1,429 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023-2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import HTTPTypes
import StructuredFieldValues
/// Implements `draft-ietf-httpbis-resumable-upload-01` internet-draft.
///
/// Draft document:
/// https://datatracker.ietf.org/doc/draft-ietf-httpbis-resumable-upload/01/
enum HTTPResumableUploadProtocol {
enum InteropVersion: Int, Comparable {
case v3 = 3
case v5 = 5
case v6 = 6
static let latest: Self = .v6
static func < (lhs: Self, rhs: Self) -> Bool {
lhs.rawValue < rhs.rawValue
}
}
static func featureDetectionResponse(
resumePath: String,
in context: HTTPResumableUploadContext,
version: InteropVersion
) -> HTTPResponse {
var response = HTTPResponse(status: .init(code: 104, reasonPhrase: "Upload Resumption Supported"))
response.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
response.headerFields[.location] = context.origin + resumePath
return response
}
static func offsetRetrievingResponse(offset: Int64, complete: Bool, version: InteropVersion) -> HTTPResponse {
var response = HTTPResponse(status: .noContent)
response.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
if version >= .v5 {
response.headerFields.uploadComplete = complete
} else {
response.headerFields.uploadIncomplete = !complete
}
response.headerFields.uploadOffset = offset
response.headerFields[.cacheControl] = "no-store"
return response
}
static func incompleteResponse(
offset: Int64,
resumePath: String,
forUploadCreation: Bool,
in context: HTTPResumableUploadContext,
version: InteropVersion
) -> HTTPResponse {
var response = HTTPResponse(status: .created)
response.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
if forUploadCreation {
response.headerFields[.location] = context.origin + resumePath
}
if version >= .v5 {
response.headerFields.uploadComplete = false
} else {
response.headerFields.uploadIncomplete = true
}
response.headerFields.uploadOffset = offset
return response
}
static func optionsResponse(version: InteropVersion) -> HTTPResponse {
var response = HTTPResponse(status: .ok)
response.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
response.headerFields.uploadLimit = .init(minSize: 0)
return response
}
static func cancelledResponse(version: InteropVersion) -> HTTPResponse {
var response = HTTPResponse(status: .noContent)
response.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
return response
}
static func notFoundResponse(version: InteropVersion) -> HTTPResponse {
var response = HTTPResponse(status: .notFound)
response.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
response.headerFields[.contentLength] = "0"
return response
}
static func conflictResponse(offset: Int64, complete: Bool, version: InteropVersion) -> HTTPResponse {
var response = HTTPResponse(status: .conflict)
response.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
if version >= .v5 {
response.headerFields.uploadComplete = complete
} else {
response.headerFields.uploadIncomplete = !complete
}
response.headerFields.uploadOffset = offset
response.headerFields[.contentLength] = "0"
return response
}
static func badRequestResponse() -> HTTPResponse {
var response = HTTPResponse(status: .badRequest)
response.headerFields[.uploadDraftInteropVersion] = "\(InteropVersion.latest.rawValue)"
response.headerFields[.contentLength] = "0"
return response
}
enum RequestType {
case uploadCreation(complete: Bool, contentLength: Int64?, uploadLength: Int64?)
case offsetRetrieving
case uploadAppending(offset: Int64, complete: Bool, contentLength: Int64?, uploadLength: Int64?)
case uploadCancellation
case options
}
enum InvalidRequestError: Error {
case unsupportedInteropVersion
case unknownMethod
case invalidPath
case missingHeaderField
case extraHeaderField
}
static func identifyRequest(
_ request: HTTPRequest,
in context: HTTPResumableUploadContext
) throws -> (RequestType, InteropVersion)? {
guard let versionValue = request.headerFields[.uploadDraftInteropVersion] else {
return nil
}
guard let versionNumber = Int(versionValue),
let version = InteropVersion(rawValue: versionNumber)
else {
throw InvalidRequestError.unsupportedInteropVersion
}
let complete: Bool?
if version >= .v5 {
complete = request.headerFields.uploadComplete
} else {
complete = request.headerFields.uploadIncomplete.map { !$0 }
}
let offset = request.headerFields.uploadOffset
let contentLength = request.headerFields[.contentLength].flatMap(Int64.init)
let uploadLength = request.headerFields.uploadLength
if request.method == .options {
guard complete == nil && offset == nil && uploadLength == nil else {
throw InvalidRequestError.extraHeaderField
}
return (.options, version)
}
if let path = request.path, context.isResumption(path: path) {
switch request.method {
case .head:
guard complete == nil && offset == nil && uploadLength == nil else {
throw InvalidRequestError.extraHeaderField
}
return (.offsetRetrieving, version)
case .patch:
guard let offset else {
throw InvalidRequestError.missingHeaderField
}
if version >= .v6 && request.headerFields[.contentType] != "application/partial-upload" {
throw InvalidRequestError.missingHeaderField
}
return (
.uploadAppending(
offset: offset,
complete: complete ?? true,
contentLength: contentLength,
uploadLength: uploadLength
), version
)
case .delete:
guard complete == nil && offset == nil && uploadLength == nil else {
throw InvalidRequestError.extraHeaderField
}
return (.uploadCancellation, version)
default:
throw InvalidRequestError.unknownMethod
}
} else {
if let complete {
if let offset, offset != 0 {
throw InvalidRequestError.invalidPath
}
return (
.uploadCreation(complete: complete, contentLength: contentLength, uploadLength: uploadLength),
version
)
} else {
return nil
}
}
}
static func stripRequest(_ request: HTTPRequest) -> HTTPRequest {
var strippedRequest = request
strippedRequest.headerFields[.uploadComplete] = nil
strippedRequest.headerFields[.uploadIncomplete] = nil
strippedRequest.headerFields[.uploadOffset] = nil
return strippedRequest
}
static func processResponse(
_ response: HTTPResponse,
offset: Int64,
resumePath: String,
forUploadCreation: Bool,
in context: HTTPResumableUploadContext,
version: InteropVersion
) -> HTTPResponse {
var finalResponse = response
finalResponse.headerFields[.uploadDraftInteropVersion] = "\(version.rawValue)"
if forUploadCreation {
finalResponse.headerFields[.location] = context.origin + resumePath
}
if version >= .v5 {
finalResponse.headerFields.uploadIncomplete = false
} else {
finalResponse.headerFields.uploadComplete = true
}
finalResponse.headerFields.uploadOffset = offset
return finalResponse
}
static func processOptionsResponse(_ response: HTTPResponse) -> HTTPResponse {
var response = response
if response.status == .notImplemented {
response = HTTPResponse(status: .ok)
}
response.headerFields.uploadLimit = .init(minSize: 0)
return response
}
}
extension HTTPField.Name {
fileprivate static let uploadDraftInteropVersion = Self("Upload-Draft-Interop-Version")!
fileprivate static let uploadComplete = Self("Upload-Complete")!
fileprivate static let uploadIncomplete = Self("Upload-Incomplete")!
fileprivate static let uploadOffset = Self("Upload-Offset")!
fileprivate static let uploadLength = Self("Upload-Length")!
fileprivate static let uploadLimit = Self("Upload-Limit")!
}
extension HTTPFields {
private struct BoolFieldValue: StructuredFieldValue {
static var structuredFieldType: StructuredFieldValues.StructuredFieldType { .item }
var item: Bool
}
fileprivate var uploadComplete: Bool? {
get {
guard let headerValue = self[.uploadComplete] else {
return nil
}
do {
let value = try StructuredFieldValueDecoder().decode(
BoolFieldValue.self,
from: Array(headerValue.utf8)
)
return value.item
} catch {
return nil
}
}
set {
if let newValue {
let value = String(
decoding: try! StructuredFieldValueEncoder().encode(BoolFieldValue(item: newValue)),
as: UTF8.self
)
self[.uploadComplete] = value
} else {
self[.uploadComplete] = nil
}
}
}
fileprivate var uploadIncomplete: Bool? {
get {
guard let headerValue = self[.uploadIncomplete] else {
return nil
}
do {
let value = try StructuredFieldValueDecoder().decode(
BoolFieldValue.self,
from: Array(headerValue.utf8)
)
return value.item
} catch {
return nil
}
}
set {
if let newValue {
let value = String(
decoding: try! StructuredFieldValueEncoder().encode(BoolFieldValue(item: newValue)),
as: UTF8.self
)
self[.uploadIncomplete] = value
} else {
self[.uploadIncomplete] = nil
}
}
}
private struct Int64FieldValue: StructuredFieldValue {
static var structuredFieldType: StructuredFieldValues.StructuredFieldType { .item }
var item: Int64
}
fileprivate var uploadOffset: Int64? {
get {
guard let headerValue = self[.uploadOffset] else {
return nil
}
do {
let value = try StructuredFieldValueDecoder().decode(
Int64FieldValue.self,
from: Array(headerValue.utf8)
)
return value.item
} catch {
return nil
}
}
set {
if let newValue {
let value = String(
decoding: try! StructuredFieldValueEncoder().encode(Int64FieldValue(item: newValue)),
as: UTF8.self
)
self[.uploadOffset] = value
} else {
self[.uploadOffset] = nil
}
}
}
fileprivate var uploadLength: Int64? {
get {
guard let headerValue = self[.uploadLength] else {
return nil
}
do {
let value = try StructuredFieldValueDecoder().decode(
Int64FieldValue.self,
from: Array(headerValue.utf8)
)
return value.item
} catch {
return nil
}
}
set {
if let newValue {
let value = String(
decoding: try! StructuredFieldValueEncoder().encode(Int64FieldValue(item: newValue)),
as: UTF8.self
)
self[.uploadLength] = value
} else {
self[.uploadLength] = nil
}
}
}
fileprivate struct UploadLimitFieldValue: StructuredFieldValue {
static var structuredFieldType: StructuredFieldValues.StructuredFieldType { .dictionary }
var maxSize: Int64?
var minSize: Int64?
var maxAppendSize: Int64?
var minAppendSize: Int64?
var expires: Int64?
enum CodingKeys: String, CodingKey {
case maxSize = "max-size"
case minSize = "min-size"
case maxAppendSize = "max-append-size"
case minAppendSize = "min-append-size"
case expires = "expires"
}
}
fileprivate var uploadLimit: UploadLimitFieldValue? {
get {
guard let headerValue = self[.uploadLimit] else {
return nil
}
do {
let value = try StructuredFieldValueDecoder().decode(
UploadLimitFieldValue.self,
from: Array(headerValue.utf8)
)
return value
} catch {
return nil
}
}
set {
if let newValue {
let value = String(
decoding: try! StructuredFieldValueEncoder().encode(newValue),
as: UTF8.self
)
self[.uploadLimit] = value
} else {
self[.uploadLimit] = nil
}
}
}
}

@ -0,0 +1,126 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Foundation
import HTTPTypes
import NIOCore
import NIOHTTP1
import NIOHTTPTypes
import NIOHTTPTypesHTTP1
import NIOPosix
import NIOResumableUpload
@available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *)
final class UploadServerHandler: ChannelDuplexHandler {
typealias InboundIn = HTTPRequestPart
typealias OutboundIn = Never
typealias OutboundOut = HTTPResponsePart
let directory: URL
var fileHandle: FileHandle? = nil
init(directory: URL) {
self.directory = directory.standardized
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
switch self.unwrapInboundIn(data) {
case .head(let request):
switch request.method {
case .post, .put:
if let path = request.path {
let url = self.directory.appendingPathComponent(path, isDirectory: false).standardized
if url.path.hasPrefix(self.directory.path) {
try? FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
_ = FileManager.default.createFile(atPath: url.path, contents: nil)
self.fileHandle = try? FileHandle(forWritingTo: url)
print("Creating \(url)")
}
}
if self.fileHandle == nil {
let response = HTTPResponse(status: .badRequest)
self.write(context: context, data: self.wrapOutboundOut(.head(response)), promise: nil)
self.write(context: context, data: self.wrapOutboundOut(.end(nil)), promise: nil)
self.flush(context: context)
}
default:
let response = HTTPResponse(status: .notImplemented)
self.write(context: context, data: self.wrapOutboundOut(.head(response)), promise: nil)
self.write(context: context, data: self.wrapOutboundOut(.end(nil)), promise: nil)
self.flush(context: context)
}
case .body(let body):
do {
try body.withUnsafeReadableBytes { buffer in
try fileHandle?.write(contentsOf: buffer)
}
} catch {
print("failed to write \(error)")
exit(1)
}
case .end:
if let fileHandle = self.fileHandle {
do {
try fileHandle.close()
let response = HTTPResponse(status: .created)
self.write(context: context, data: self.wrapOutboundOut(.head(response)), promise: nil)
self.write(context: context, data: self.wrapOutboundOut(.end(nil)), promise: nil)
self.flush(context: context)
} catch {
let response = HTTPResponse(status: .internalServerError)
self.write(context: context, data: self.wrapOutboundOut(.head(response)), promise: nil)
self.write(context: context, data: self.wrapOutboundOut(.end(nil)), promise: nil)
self.flush(context: context)
}
}
}
}
}
guard let outputFile = CommandLine.arguments.dropFirst().first else {
print("Usage: \(CommandLine.arguments[0]) <Upload Directory>")
exit(1)
}
if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
let uploadContext = HTTPResumableUploadContext(origin: "http://localhost:8080")
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
let server = try ServerBootstrap(group: group).childChannelInitializer { channel in
channel.pipeline.configureHTTPServerPipeline().flatMap {
channel.pipeline.addHandlers([
HTTP1ToHTTPServerCodec(secure: false),
HTTPResumableUploadHandler(
context: uploadContext,
handlers: [
UploadServerHandler(
directory: URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true)
)
]
),
])
}
}
.bind(host: "0.0.0.0", port: 8080)
.wait()
print("Listening on 8080")
try server.closeFuture.wait()
} else {
print("Unsupported OS")
exit(1)
}

@ -0,0 +1,627 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2023-2024 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import HTTPTypes
import NIOCore
import NIOEmbedded
import NIOHTTPTypes
import NIOResumableUpload
import XCTest
/// A handler that keeps track of all reads made on a channel.
private final class InboundRecorder<FrameIn, FrameOut>: ChannelDuplexHandler {
typealias InboundIn = FrameIn
typealias OutboundIn = Never
typealias OutboundOut = FrameOut
private var context: ChannelHandlerContext! = nil
var receivedFrames: [FrameIn] = []
func handlerAdded(context: ChannelHandlerContext) {
self.context = context
}
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
self.receivedFrames.append(self.unwrapInboundIn(data))
}
func write(_ frame: FrameOut) {
self.write(context: self.context, data: self.wrapOutboundOut(frame), promise: nil)
self.flush(context: self.context)
}
}
final class NIOResumableUploadTests: XCTestCase {
func testNonUpload() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
let request = HTTPRequest(method: .get, scheme: "https", authority: "example.com", path: "/")
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 2)
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(request))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel.finish().isClean)
}
func testNotResumableUpload() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
let request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "Hello")))
try channel.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 3)
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(request))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "Hello")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel.finish().isClean)
}
func testOptions() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, HTTPResponsePart>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .options, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "6"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 2)
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(request))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.end(nil))
recorder.write(HTTPResponsePart.head(HTTPResponse(status: .notImplemented)))
recorder.write(HTTPResponsePart.end(nil))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status, .ok)
XCTAssertEqual(response.headerFields[.uploadLimit], "min-size=0")
guard let responsePart = try channel.readOutbound(as: HTTPResponsePart.self), case .end = responsePart else {
XCTFail("Part is not response end")
return
}
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadUninterruptedV3() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "3"
request.headerFields[.uploadIncomplete] = "?0"
request.headerFields[.contentLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "Hello")))
try channel.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 3)
var expectedRequest = request
expectedRequest.headerFields[.uploadIncomplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "Hello")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.end(nil))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
XCTAssertNotNil(response.headerFields[.location])
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadUninterruptedV5() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "5"
request.headerFields[.uploadComplete] = "?1"
request.headerFields[.contentLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "Hello")))
try channel.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 3)
var expectedRequest = request
expectedRequest.headerFields[.uploadComplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "Hello")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.end(nil))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
XCTAssertNotNil(response.headerFields[.location])
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadUninterruptedV6() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "6"
request.headerFields[.uploadComplete] = "?1"
request.headerFields[.contentLength] = "5"
request.headerFields[.uploadLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "Hello")))
try channel.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 3)
var expectedRequest = request
expectedRequest.headerFields[.uploadComplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "Hello")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.end(nil))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
XCTAssertNotNil(response.headerFields[.location])
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadInterruptedV3() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "3"
request.headerFields[.uploadIncomplete] = "?0"
request.headerFields[.contentLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "He")))
channel.pipeline.fireErrorCaught(POSIXError(.ENOTCONN))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
let location = try XCTUnwrap(response.headerFields[.location])
let resumptionPath = try XCTUnwrap(URLComponents(string: location)?.path)
let channel2 = EmbeddedChannel()
try channel2.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request2 = HTTPRequest(method: .head, scheme: "https", authority: "example.com", path: resumptionPath)
request2.headerFields[.uploadDraftInteropVersion] = "3"
try channel2.writeInbound(HTTPRequestPart.head(request2))
try channel2.writeInbound(HTTPRequestPart.end(nil))
let responsePart2 = try channel2.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response2) = responsePart2 else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response2.status.code, 204)
XCTAssertEqual(response2.headerFields[.uploadOffset], "2")
XCTAssertEqual(try channel2.readOutbound(as: HTTPResponsePart.self), .end(nil))
XCTAssertTrue(try channel2.finish().isClean)
let channel3 = EmbeddedChannel()
try channel3.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request3 = HTTPRequest(method: .patch, scheme: "https", authority: "example.com", path: resumptionPath)
request3.headerFields[.uploadDraftInteropVersion] = "3"
request3.headerFields[.uploadIncomplete] = "?0"
request3.headerFields[.uploadOffset] = "2"
request3.headerFields[.contentLength] = "3"
try channel3.writeInbound(HTTPRequestPart.head(request3))
try channel3.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "llo")))
try channel3.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 4)
var expectedRequest = request
expectedRequest.headerFields[.uploadIncomplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "He")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.body(ByteBuffer(string: "llo")))
XCTAssertEqual(recorder.receivedFrames[3], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel3.finish().isClean)
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadInterruptedV5() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "5"
request.headerFields[.uploadComplete] = "?1"
request.headerFields[.contentLength] = "5"
request.headerFields[.uploadLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "He")))
channel.pipeline.fireErrorCaught(POSIXError(.ENOTCONN))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
let location = try XCTUnwrap(response.headerFields[.location])
let resumptionPath = try XCTUnwrap(URLComponents(string: location)?.path)
let channel2 = EmbeddedChannel()
try channel2.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request2 = HTTPRequest(method: .head, scheme: "https", authority: "example.com", path: resumptionPath)
request2.headerFields[.uploadDraftInteropVersion] = "3"
try channel2.writeInbound(HTTPRequestPart.head(request2))
try channel2.writeInbound(HTTPRequestPart.end(nil))
let responsePart2 = try channel2.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response2) = responsePart2 else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response2.status.code, 204)
XCTAssertEqual(response2.headerFields[.uploadOffset], "2")
XCTAssertEqual(try channel2.readOutbound(as: HTTPResponsePart.self), .end(nil))
XCTAssertTrue(try channel2.finish().isClean)
let channel3 = EmbeddedChannel()
try channel3.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request3 = HTTPRequest(method: .patch, scheme: "https", authority: "example.com", path: resumptionPath)
request3.headerFields[.uploadDraftInteropVersion] = "5"
request3.headerFields[.uploadComplete] = "?1"
request3.headerFields[.uploadOffset] = "2"
request3.headerFields[.contentLength] = "3"
request3.headerFields[.uploadLength] = "5"
try channel3.writeInbound(HTTPRequestPart.head(request3))
try channel3.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "llo")))
try channel3.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 4)
var expectedRequest = request
expectedRequest.headerFields[.uploadComplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "He")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.body(ByteBuffer(string: "llo")))
XCTAssertEqual(recorder.receivedFrames[3], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel3.finish().isClean)
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadInterruptedV6() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "6"
request.headerFields[.uploadComplete] = "?1"
request.headerFields[.contentLength] = "5"
request.headerFields[.uploadLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "He")))
channel.pipeline.fireErrorCaught(POSIXError(.ENOTCONN))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
let location = try XCTUnwrap(response.headerFields[.location])
let resumptionPath = try XCTUnwrap(URLComponents(string: location)?.path)
let channel2 = EmbeddedChannel()
try channel2.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request2 = HTTPRequest(method: .head, scheme: "https", authority: "example.com", path: resumptionPath)
request2.headerFields[.uploadDraftInteropVersion] = "3"
try channel2.writeInbound(HTTPRequestPart.head(request2))
try channel2.writeInbound(HTTPRequestPart.end(nil))
let responsePart2 = try channel2.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response2) = responsePart2 else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response2.status.code, 204)
XCTAssertEqual(response2.headerFields[.uploadOffset], "2")
XCTAssertEqual(try channel2.readOutbound(as: HTTPResponsePart.self), .end(nil))
XCTAssertTrue(try channel2.finish().isClean)
let channel3 = EmbeddedChannel()
try channel3.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request3 = HTTPRequest(method: .patch, scheme: "https", authority: "example.com", path: resumptionPath)
request3.headerFields[.uploadDraftInteropVersion] = "6"
request3.headerFields[.uploadComplete] = "?1"
request3.headerFields[.uploadOffset] = "2"
request3.headerFields[.contentLength] = "3"
request3.headerFields[.uploadLength] = "5"
request3.headerFields[.contentType] = "application/partial-upload"
try channel3.writeInbound(HTTPRequestPart.head(request3))
try channel3.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "llo")))
try channel3.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 4)
var expectedRequest = request
expectedRequest.headerFields[.uploadComplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "He")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.body(ByteBuffer(string: "llo")))
XCTAssertEqual(recorder.receivedFrames[3], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel3.finish().isClean)
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadChunkedV3() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "3"
request.headerFields[.uploadIncomplete] = "?1"
request.headerFields[.contentLength] = "2"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "He")))
try channel.writeInbound(HTTPRequestPart.end(nil))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
let location = try XCTUnwrap(response.headerFields[.location])
let resumptionPath = try XCTUnwrap(URLComponents(string: location)?.path)
let finalResponsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let finalResponse) = finalResponsePart else {
XCTFail("Part is not final response headers")
return
}
XCTAssertEqual(finalResponse.status.code, 201)
XCTAssertEqual(try channel.readOutbound(as: HTTPResponsePart.self), .end(nil))
let channel2 = EmbeddedChannel()
try channel2.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request2 = HTTPRequest(method: .head, scheme: "https", authority: "example.com", path: resumptionPath)
request2.headerFields[.uploadDraftInteropVersion] = "3"
try channel2.writeInbound(HTTPRequestPart.head(request2))
try channel2.writeInbound(HTTPRequestPart.end(nil))
let responsePart2 = try channel2.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response2) = responsePart2 else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response2.status.code, 204)
XCTAssertEqual(response2.headerFields[.uploadOffset], "2")
XCTAssertEqual(try channel2.readOutbound(as: HTTPResponsePart.self), .end(nil))
XCTAssertTrue(try channel2.finish().isClean)
let channel3 = EmbeddedChannel()
try channel3.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request3 = HTTPRequest(method: .patch, scheme: "https", authority: "example.com", path: resumptionPath)
request3.headerFields[.uploadDraftInteropVersion] = "3"
request3.headerFields[.uploadIncomplete] = "?0"
request3.headerFields[.uploadOffset] = "2"
request3.headerFields[.contentLength] = "3"
try channel3.writeInbound(HTTPRequestPart.head(request3))
try channel3.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "llo")))
try channel3.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 4)
var expectedRequest = request
expectedRequest.headerFields[.uploadIncomplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "He")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.body(ByteBuffer(string: "llo")))
XCTAssertEqual(recorder.receivedFrames[3], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel3.finish().isClean)
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadChunkedV5() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "5"
request.headerFields[.uploadComplete] = "?0"
request.headerFields[.contentLength] = "2"
request.headerFields[.uploadLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "He")))
try channel.writeInbound(HTTPRequestPart.end(nil))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
let location = try XCTUnwrap(response.headerFields[.location])
let resumptionPath = try XCTUnwrap(URLComponents(string: location)?.path)
let finalResponsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let finalResponse) = finalResponsePart else {
XCTFail("Part is not final response headers")
return
}
XCTAssertEqual(finalResponse.status.code, 201)
XCTAssertEqual(try channel.readOutbound(as: HTTPResponsePart.self), .end(nil))
let channel2 = EmbeddedChannel()
try channel2.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request2 = HTTPRequest(method: .head, scheme: "https", authority: "example.com", path: resumptionPath)
request2.headerFields[.uploadDraftInteropVersion] = "5"
try channel2.writeInbound(HTTPRequestPart.head(request2))
try channel2.writeInbound(HTTPRequestPart.end(nil))
let responsePart2 = try channel2.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response2) = responsePart2 else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response2.status.code, 204)
XCTAssertEqual(response2.headerFields[.uploadOffset], "2")
XCTAssertEqual(try channel2.readOutbound(as: HTTPResponsePart.self), .end(nil))
XCTAssertTrue(try channel2.finish().isClean)
let channel3 = EmbeddedChannel()
try channel3.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request3 = HTTPRequest(method: .patch, scheme: "https", authority: "example.com", path: resumptionPath)
request3.headerFields[.uploadDraftInteropVersion] = "5"
request3.headerFields[.uploadComplete] = "?1"
request3.headerFields[.uploadOffset] = "2"
request3.headerFields[.contentLength] = "3"
request3.headerFields[.uploadLength] = "5"
try channel3.writeInbound(HTTPRequestPart.head(request3))
try channel3.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "llo")))
try channel3.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 4)
var expectedRequest = request
expectedRequest.headerFields[.uploadComplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "He")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.body(ByteBuffer(string: "llo")))
XCTAssertEqual(recorder.receivedFrames[3], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel3.finish().isClean)
XCTAssertTrue(try channel.finish().isClean)
}
func testResumableUploadChunkedV6() throws {
let channel = EmbeddedChannel()
let recorder = InboundRecorder<HTTPRequestPart, Never>()
let context = HTTPResumableUploadContext(origin: "https://example.com")
try channel.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [recorder])).wait()
var request = HTTPRequest(method: .post, scheme: "https", authority: "example.com", path: "/")
request.headerFields[.uploadDraftInteropVersion] = "6"
request.headerFields[.uploadComplete] = "?0"
request.headerFields[.contentLength] = "2"
request.headerFields[.uploadLength] = "5"
try channel.writeInbound(HTTPRequestPart.head(request))
try channel.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "He")))
try channel.writeInbound(HTTPRequestPart.end(nil))
let responsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response) = responsePart else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response.status.code, 104)
let location = try XCTUnwrap(response.headerFields[.location])
let resumptionPath = try XCTUnwrap(URLComponents(string: location)?.path)
let finalResponsePart = try channel.readOutbound(as: HTTPResponsePart.self)
guard case .head(let finalResponse) = finalResponsePart else {
XCTFail("Part is not final response headers")
return
}
XCTAssertEqual(finalResponse.status.code, 201)
XCTAssertEqual(try channel.readOutbound(as: HTTPResponsePart.self), .end(nil))
let channel2 = EmbeddedChannel()
try channel2.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request2 = HTTPRequest(method: .head, scheme: "https", authority: "example.com", path: resumptionPath)
request2.headerFields[.uploadDraftInteropVersion] = "6"
try channel2.writeInbound(HTTPRequestPart.head(request2))
try channel2.writeInbound(HTTPRequestPart.end(nil))
let responsePart2 = try channel2.readOutbound(as: HTTPResponsePart.self)
guard case .head(let response2) = responsePart2 else {
XCTFail("Part is not response headers")
return
}
XCTAssertEqual(response2.status.code, 204)
XCTAssertEqual(response2.headerFields[.uploadOffset], "2")
XCTAssertEqual(try channel2.readOutbound(as: HTTPResponsePart.self), .end(nil))
XCTAssertTrue(try channel2.finish().isClean)
let channel3 = EmbeddedChannel()
try channel3.pipeline.addHandler(HTTPResumableUploadHandler(context: context, handlers: [])).wait()
var request3 = HTTPRequest(method: .patch, scheme: "https", authority: "example.com", path: resumptionPath)
request3.headerFields[.uploadDraftInteropVersion] = "6"
request3.headerFields[.uploadComplete] = "?1"
request3.headerFields[.uploadOffset] = "2"
request3.headerFields[.contentLength] = "3"
request3.headerFields[.uploadLength] = "5"
request3.headerFields[.contentType] = "application/partial-upload"
try channel3.writeInbound(HTTPRequestPart.head(request3))
try channel3.writeInbound(HTTPRequestPart.body(ByteBuffer(string: "llo")))
try channel3.writeInbound(HTTPRequestPart.end(nil))
XCTAssertEqual(recorder.receivedFrames.count, 4)
var expectedRequest = request
expectedRequest.headerFields[.uploadComplete] = nil
XCTAssertEqual(recorder.receivedFrames[0], HTTPRequestPart.head(expectedRequest))
XCTAssertEqual(recorder.receivedFrames[1], HTTPRequestPart.body(ByteBuffer(string: "He")))
XCTAssertEqual(recorder.receivedFrames[2], HTTPRequestPart.body(ByteBuffer(string: "llo")))
XCTAssertEqual(recorder.receivedFrames[3], HTTPRequestPart.end(nil))
XCTAssertTrue(try channel3.finish().isClean)
XCTAssertTrue(try channel.finish().isClean)
}
}
extension HTTPField.Name {
fileprivate static let uploadDraftInteropVersion = Self("Upload-Draft-Interop-Version")!
fileprivate static let uploadComplete = Self("Upload-Complete")!
fileprivate static let uploadIncomplete = Self("Upload-Incomplete")!
fileprivate static let uploadOffset = Self("Upload-Offset")!
fileprivate static let uploadLength = Self("Upload-Length")!
fileprivate static let uploadLimit = Self("Upload-Limit")!
}