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 (#203)
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:
parent
3efabe7202
commit
fde9d65d2e
.spi.ymlPackage.swiftREADME.md
Sources
NIOResumableUpload
HTTPResumableUpload.swiftHTTPResumableUploadChannel.swiftHTTPResumableUploadContext.swiftHTTPResumableUploadHandler.swiftHTTPResumableUploadProtocol.swift
NIOResumableUploadDemo
Tests/NIOResumableUploadTests
2
.spi.yml
2
.spi.yml
@ -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.
|
||||
|
490
Sources/NIOResumableUpload/HTTPResumableUpload.swift
Normal file
490
Sources/NIOResumableUpload/HTTPResumableUpload.swift
Normal file
@ -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
|
||||
}
|
254
Sources/NIOResumableUpload/HTTPResumableUploadChannel.swift
Normal file
254
Sources/NIOResumableUpload/HTTPResumableUploadChannel.swift
Normal file
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
74
Sources/NIOResumableUpload/HTTPResumableUploadContext.swift
Normal file
74
Sources/NIOResumableUpload/HTTPResumableUploadContext.swift
Normal file
@ -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]
|
||||
}
|
||||
}
|
||||
}
|
172
Sources/NIOResumableUpload/HTTPResumableUploadHandler.swift
Normal file
172
Sources/NIOResumableUpload/HTTPResumableUploadHandler.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
429
Sources/NIOResumableUpload/HTTPResumableUploadProtocol.swift
Normal file
429
Sources/NIOResumableUpload/HTTPResumableUploadProtocol.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
126
Sources/NIOResumableUploadDemo/main.swift
Normal file
126
Sources/NIOResumableUploadDemo/main.swift
Normal file
@ -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)
|
||||
}
|
627
Tests/NIOResumableUploadTests/NIOResumableUploadTests.swift
Normal file
627
Tests/NIOResumableUploadTests/NIOResumableUploadTests.swift
Normal file
@ -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")!
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user