diff --git a/IntegrationTests/Services/AWSS3IntegrationTests/S3PresignedMultiPartUploadTests.swift b/IntegrationTests/Services/AWSS3IntegrationTests/S3PresignedMultiPartUploadTests.swift new file mode 100644 index 00000000000..8e6787207af --- /dev/null +++ b/IntegrationTests/Services/AWSS3IntegrationTests/S3PresignedMultiPartUploadTests.swift @@ -0,0 +1,86 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +import XCTest +import AWSS3 + +/// Tests presigned multipart upload using S3. +class S3PresignedMultiPartUploadTests: S3XCTestCase { + private var createMultipartUploadOutput: CreateMultipartUploadOutput! + private var key: String! + private var originalData: String! + + override func setUp() async throws { + try await super.setUp() + originalData = UUID().uuidString + key = UUID().uuidString + createMultipartUploadOutput = try await client.createMultipartUpload( + input: .init(bucket: bucketName, key: key) + ) + } + + func test_multiPartUpload_putsObjectWithPresignedRequest() async throws { + let request = try await client.presignedRequestForUploadPart( + input: .init( + body: .data(originalData.data(using: .utf8)), + bucket: bucketName, + key: key, + partNumber: 1, + uploadId: createMultipartUploadOutput.uploadId + ), + expiration: 600.0 + ) + + try await completeMultiPartUpload(request: request) + + let data = try await getObject(key: key) + XCTAssertEqual(originalData, data) + } + + func test_multiPartUpload_putsObjectWithPresignedURL() async throws { + let url = try await client.presignedURLForUploadPart( + input: .init( + body: .data(originalData.data(using: .utf8)), + bucket: bucketName, + key: key, + partNumber: 1, + uploadId: createMultipartUploadOutput.uploadId + ), + expiration: 600.0 + ) + var request = URLRequest(url: url) + request.httpBody = Data(originalData.utf8) + request.httpMethod = "PUT" + + try await completeMultiPartUpload(request: request) + + let data = try await getObject(key: key) + XCTAssertEqual(originalData, data) + } + + // MARK: - Private + + private func completeMultiPartUpload(request: URLRequest, line: UInt = #line) async throws { + let (_, response) = try await perform(urlRequest: request) + + let eTag = try XCTUnwrap(response?.allHeaderFields["Etag"] as? String, line: line) + + _ = try await client.completeMultipartUpload( + input: .init( + bucket: bucketName, + key: key, + multipartUpload: .init( + parts: [.init(eTag: eTag, partNumber: 1)] + ), + uploadId: createMultipartUploadOutput.uploadId + ) + ) + } +} diff --git a/IntegrationTests/Services/AWSS3IntegrationTests/S3PresignedURLTests.swift b/IntegrationTests/Services/AWSS3IntegrationTests/S3PresignedURLTests.swift index bab15898918..2f76f851204 100644 --- a/IntegrationTests/Services/AWSS3IntegrationTests/S3PresignedURLTests.swift +++ b/IntegrationTests/Services/AWSS3IntegrationTests/S3PresignedURLTests.swift @@ -19,7 +19,7 @@ class S3PresignedURLTests: S3XCTestCase { try await putObject(body: originalData, key: key) let input = GetObjectInput(bucket: bucketName, key: key) let url = try await client.presignedURLForGetObject(input: input, expiration: 600.0) - let data = try await perform(urlRequest: URLRequest(url: url)) + let (data, _) = try await perform(urlRequest: URLRequest(url: url)) XCTAssertEqual(Data(originalData.utf8), data) } diff --git a/IntegrationTests/Services/AWSS3IntegrationTests/S3XCTestCase.swift b/IntegrationTests/Services/AWSS3IntegrationTests/S3XCTestCase.swift index f296e7573de..e8c1b7968c4 100644 --- a/IntegrationTests/Services/AWSS3IntegrationTests/S3XCTestCase.swift +++ b/IntegrationTests/Services/AWSS3IntegrationTests/S3XCTestCase.swift @@ -55,9 +55,9 @@ class S3XCTestCase: XCTestCase { /// /// Useful for testing presigned URLs. /// - Parameter urlRequest: The urlRequest to be performed. - /// - Returns: The data from the request, or empty data if the request has no body. + /// - Returns: The data and optional http response, or empty data if the response has no body. /// - Throws: Any error returned by the data task, or `HTTPError` if the request completes and the HTTP status code is not 200 series. - func perform(urlRequest: URLRequest) async throws -> Data { + func perform(urlRequest: URLRequest) async throws -> (Data, HTTPURLResponse?) { try await withCheckedThrowingContinuation { continuation in let task = URLSession.shared.dataTask(with: urlRequest) { data, urlResponse, error in if let error = error { @@ -66,7 +66,7 @@ class S3XCTestCase: XCTestCase { let error = HTTPError(code: code, data: data, url: urlRequest.url) continuation.resume(throwing: error) } else { - continuation.resume(returning: data ?? Data()) + continuation.resume(returning: (data ?? Data(), urlResponse as? HTTPURLResponse)) } } task.resume() diff --git a/Sources/Core/AWSSDKHTTPAuth/Sources/AWSSDKHTTPAuth/CustomSigningPropertiesSetter.swift b/Sources/Core/AWSSDKHTTPAuth/Sources/AWSSDKHTTPAuth/CustomSigningPropertiesSetter.swift index ae39f16c989..29ff40f862d 100644 --- a/Sources/Core/AWSSDKHTTPAuth/Sources/AWSSDKHTTPAuth/CustomSigningPropertiesSetter.swift +++ b/Sources/Core/AWSSDKHTTPAuth/Sources/AWSSDKHTTPAuth/CustomSigningPropertiesSetter.swift @@ -20,7 +20,7 @@ public class CustomSigningPropertiesSetter { private let usesSignedBodyHeader = ["S3", "Glacier", "S3 Control"] // Map of service::operation that use unsigned body for presign URL flow. private let forceUnsignedBodyForPresigningURL = [ - "S3": ["getObject", "putObject"] + "S3": ["getObject", "putObject", "uploadPart"] ] public init() {} diff --git a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/customization/presignable/PresignableUrlIntegration.kt b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/customization/presignable/PresignableUrlIntegration.kt index 3aba666c442..1a0148fd33e 100644 --- a/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/customization/presignable/PresignableUrlIntegration.kt +++ b/codegen/smithy-aws-swift-codegen/src/main/kotlin/software/amazon/smithy/aws/swift/codegen/customization/presignable/PresignableUrlIntegration.kt @@ -22,6 +22,7 @@ import software.amazon.smithy.swift.codegen.core.SwiftCodegenContext import software.amazon.smithy.swift.codegen.core.toProtocolGenerationContext import software.amazon.smithy.swift.codegen.integration.ProtocolGenerator import software.amazon.smithy.swift.codegen.integration.SwiftIntegration +import software.amazon.smithy.swift.codegen.integration.middlewares.OperationInputQueryItemMiddleware import software.amazon.smithy.swift.codegen.integration.middlewares.handlers.MiddlewareShapeUtils import software.amazon.smithy.swift.codegen.middleware.MiddlewareExecutionGenerator import software.amazon.smithy.swift.codegen.middleware.MiddlewareExecutionGenerator.Companion.ContextAttributeCodegenFlowType.PRESIGN_URL @@ -40,6 +41,7 @@ internal val PRESIGNABLE_URL_OPERATIONS: Map> = mapOf( "com.amazonaws.s3#AmazonS3" to setOf( "com.amazonaws.s3#GetObject", "com.amazonaws.s3#PutObject", + "com.amazonaws.s3#UploadPart" ) ) @@ -204,6 +206,10 @@ class PresignableUrlIntegration(private val presignedOperations: Map { + operationMiddlewareCopy.removeMiddleware(op, "OperationInputBodyMiddleware") + operationMiddlewareCopy.appendMiddleware(op, OperationInputQueryItemMiddleware(context.model, context.symbolProvider)) + } } return operationMiddlewareCopy @@ -269,7 +275,7 @@ class PresignableUrlIntegration(private val presignedOperations: Map "put" + "com.amazonaws.s3#PutObject", "com.amazonaws.s3#UploadPart" -> "put" else -> "get" } }