From f4263c56f2b19fc52b8e5ba641ebf1cee1272bca Mon Sep 17 00:00:00 2001 From: Steven Meyer <108885656+meyertst-aws@users.noreply.github.com> Date: Fri, 20 Sep 2024 10:02:03 -0400 Subject: [PATCH 1/2] Updating remaining Swift S3 --- .../Sources/DeleteObjects/DeleteObjects.swift | 18 ++--- .../ServiceHandler/ServiceHandler.swift | 46 ++++++------- .../DeleteObjectsTests.swift | 2 +- .../DeleteObjectsTests/S3DemoCleanup.swift | 4 +- .../ServiceHandler_Ext.swift | 8 +-- .../s3/ListBuckets-Simple/Sources/entry.swift | 60 ++++++++++------ .../Sources/ListBuckets/S3Session.swift | 68 ++++++++++++++----- .../Sources/ListBuckets/listbuckets.swift | 12 +++- .../ListBucketsTests/ListBucketsTests.swift | 25 +++---- .../Tests/basics-tests/S3DemoCleanup.swift | 4 +- 10 files changed, 144 insertions(+), 103 deletions(-) diff --git a/swift/example_code/s3/DeleteObjects/Sources/DeleteObjects/DeleteObjects.swift b/swift/example_code/s3/DeleteObjects/Sources/DeleteObjects/DeleteObjects.swift index b1a4e55ec65..49e60471c55 100644 --- a/swift/example_code/s3/DeleteObjects/Sources/DeleteObjects/DeleteObjects.swift +++ b/swift/example_code/s3/DeleteObjects/Sources/DeleteObjects/DeleteObjects.swift @@ -8,9 +8,10 @@ // snippet-start:[s3.swift.deleteobjects.example] // snippet-start:[s3.swift.deleteobjects.main.imports] +import ArgumentParser import Foundation import ServiceHandler -import ArgumentParser + // snippet-end:[s3.swift.deleteobjects.main.imports] /// The command-line arguments and options available for this @@ -18,7 +19,7 @@ import ArgumentParser // snippet-start:[s3.swift.deleteobjects.command] struct ExampleCommand: ParsableCommand { @Option(help: "AWS Region the bucket where the bucket is") - var region = "us-east-1" + var region: String? @Argument(help: "Name of the S3 bucket to delete objects in") var bucketName: String @@ -37,17 +38,17 @@ struct ExampleCommand: ParsableCommand { /// example. // snippet-start:[s3.swift.deleteobjects.command.runasync] func runAsync() async throws { - let serviceHandler = await ServiceHandler(region: region) - do { - try await serviceHandler.deleteObjects(bucket: bucketName, - keys: fileNames) + let serviceHandler = try await ServiceHandler(region: region) + try await serviceHandler.deleteObjects(bucket: bucketName, + keys: fileNames) } catch { print("*** Error. Unable to complete deleting the objects.") } } // snippet-end:[s3.swift.deleteobjects.command.runasync] } + // snippet-end:[s3.swift.deleteobjects.command] // @@ -65,7 +66,8 @@ struct Main { } catch { ExampleCommand.exit(withError: error) } - } + } } + // snippet-end:[s3.swift.deleteobjects.main] -// snippet-end:[s3.swift.deleteobjects.example] \ No newline at end of file +// snippet-end:[s3.swift.deleteobjects.example] diff --git a/swift/example_code/s3/DeleteObjects/Sources/ServiceHandler/ServiceHandler.swift b/swift/example_code/s3/DeleteObjects/Sources/ServiceHandler/ServiceHandler.swift index 618c2d18a3f..31c7a87ff63 100644 --- a/swift/example_code/s3/DeleteObjects/Sources/ServiceHandler/ServiceHandler.swift +++ b/swift/example_code/s3/DeleteObjects/Sources/ServiceHandler/ServiceHandler.swift @@ -2,14 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 /* - A class containing functions that interact with AWS services. -*/ + A class containing functions that interact with AWS services. + */ // snippet-start:[s3.swift.deleteobjects.handler] // snippet-start:[s3.swift.deleteobjects.handler.imports] -import Foundation import AWSS3 import ClientRuntime +import Foundation + // snippet-end:[s3.swift.deleteobjects.handler.imports] /// Errors returned by `ServiceHandler` functions. @@ -17,32 +18,36 @@ import ClientRuntime public enum ServiceHandlerError: Error { case deleteObjectsError } + // snippet-end:[s3.swift.deleteobjects.enum.service-error] /// A class containing all the code that interacts with the AWS SDK for Swift. public class ServiceHandler { public let client: S3Client - public var region: S3ClientTypes.BucketLocationConstraint? - + /// Initialize and return a new ``ServiceHandler`` object, which is used /// to drive the AWS calls /// used for the example. - /// + /// /// - Parameters: - /// - region: The AWS Region to access. + /// - region: The optional AWS Region to access. /// /// - Returns: A new ``ServiceHandler`` object, ready to be called to /// execute AWS operations. // snippet-start:[s3.swift.deleteobjects.handler.init] - public init(region: String = "us-east-2") async { - self.region = S3ClientTypes.BucketLocationConstraint(rawValue: region) + public init(region: String? = nil) async throws { do { - client = try S3Client(region: region) + let config = try await S3Client.S3ClientConfiguration() + if let region = region { + config.region = region + } + client = S3Client(config: config) } catch { print("ERROR: ", dump(error, name: "Initializing Amazon S3 client")) - exit(1) + throw error } } + // snippet-end:[s3.swift.deleteobjects.handler.init] /// Deletes the specified objects from Amazon S3. @@ -56,30 +61,19 @@ public class ServiceHandler { let input = DeleteObjectsInput( bucket: bucket, delete: S3ClientTypes.Delete( - objects: keys.map({ S3ClientTypes.ObjectIdentifier(key: $0) }), + objects: keys.map { S3ClientTypes.ObjectIdentifier(key: $0) }, quiet: true ) ) do { - let output = try await client.deleteObjects(input: input) - - // As of the last update to this example, any errors are returned - // in the `output` object's `errors` property. If there are any - // errors in this array, throw an exception. Once the error - // handling is finalized in later updates to the AWS SDK for - // Swift, this example will be updated to handle errors better. - - guard let errors = output.errors else { - return // No errors. - } - if errors.count != 0 { - throw ServiceHandlerError.deleteObjectsError - } + _ = try await client.deleteObjects(input: input) } catch { + print("ERROR: deleteObjects:", dump(error)) throw error } } // snippet-end:[s3.swift.deleteobjects.handler.DeleteObjects] } + // snippet-end:[s3.swift.deleteobjects.handler] diff --git a/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/DeleteObjectsTests.swift b/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/DeleteObjectsTests.swift index 770139715d8..8e46f33bbc5 100644 --- a/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/DeleteObjectsTests.swift +++ b/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/DeleteObjectsTests.swift @@ -32,7 +32,7 @@ final class DeleteObjectsTests: XCTestCase { super.setUp() Task() { - DeleteObjectsTests.serviceHandler = await ServiceHandler() + DeleteObjectsTests.serviceHandler = try await ServiceHandler() DeleteObjectsTests.demoCleanup = await S3DemoCleanup() tdSem.signal() } diff --git a/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/S3DemoCleanup.swift b/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/S3DemoCleanup.swift index 4f1e4369309..e90b9b28ba7 100644 --- a/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/S3DemoCleanup.swift +++ b/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/S3DemoCleanup.swift @@ -54,7 +54,7 @@ public class S3DemoCleanup { init() async { do { - self.client = try S3Client(region: "us-east-2") + self.client = try await S3Client() } catch { print("Error initializing S3 client for tracking and deleting Amazon S3 buckets and files created by the example:") dump(error) @@ -237,4 +237,4 @@ public class S3DemoCleanup { } } } -} \ No newline at end of file +} diff --git a/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/ServiceHandler_Ext.swift b/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/ServiceHandler_Ext.swift index 4af1583a1cd..91a52ad4a88 100644 --- a/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/ServiceHandler_Ext.swift +++ b/swift/example_code/s3/DeleteObjects/Tests/DeleteObjectsTests/ServiceHandler_Ext.swift @@ -20,12 +20,8 @@ public extension ServiceHandler { /// - name: Name of the bucket to create. /// Throws an exception if an error occurs. func createBucket(name: String) async throws { - let config = S3ClientTypes.CreateBucketConfiguration( - locationConstraint: self.region - ) let input = CreateBucketInput( - bucket: name, - createBucketConfiguration: config + bucket: name ) _ = try await client.createBucket(input: input) } @@ -100,4 +96,4 @@ public extension ServiceHandler { } return names - }} \ No newline at end of file + }} diff --git a/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift b/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift index c589bfafa48..5b789f310db 100644 --- a/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift +++ b/swift/example_code/s3/ListBuckets-Simple/Sources/entry.swift @@ -7,9 +7,10 @@ /// Amazon Simple Storage Service (Amazon S3) function `ListBuckets`. // snippet-start:[s3.swift.intro.imports] -import Foundation -import AWSS3 import AWSClientRuntime +import AWSS3 +import Foundation + // snippet-end:[s3.swift.intro.imports] // snippet-start:[s3.swift.intro.getbucketnames] @@ -17,29 +18,45 @@ import AWSClientRuntime // // - Returns: An array of strings listing the buckets. func getBucketNames() async throws -> [String] { - // Get an S3Client with which to access Amazon S3. - // snippet-start:[s3.swift.intro.client-init] - let client = try S3Client(region: "us-east-1") - // snippet-end:[s3.swift.intro.client-init] + do { + // Get an S3Client with which to access Amazon S3. + // snippet-start:[s3.swift.intro.client-init] + let configuration = try await S3Client.S3ClientConfiguration() + // configuration.region = "us-east-2" // Uncomment this to set the region programmatically. + let client = S3Client(config: configuration) + // snippet-end:[s3.swift.intro.client-init] - // snippet-start:[s3.swift.intro.listbuckets] - let output = try await client.listBuckets( - input: ListBucketsInput() - ) - // snippet-end:[s3.swift.intro.listbuckets] - - // Get the bucket names. - var bucketNames: [String] = [] + // snippet-start:[s3.swift.intro.listbuckets] + // Use "Paginated" to get all the buckets. + // This lets the SDK handle the 'continuationToken' in "ListBucketsOutput". + let pages = client.listBucketsPaginated( + input: ListBucketsInput( maxBuckets: 10) + ) + // snippet-end:[s3.swift.intro.listbuckets] - guard let buckets = output.buckets else { - return bucketNames - } - for bucket in buckets { - bucketNames.append(bucket.name ?? "") - } + // Get the bucket names. + var bucketNames: [String] = [] + + do { + for try await page in pages { + guard let buckets = page.buckets else { + print("Error: no buckets returned.") + continue + } + + for bucket in buckets { + bucketNames.append(bucket.name ?? "") + } + } - return bucketNames + return bucketNames + } catch { + print("ERROR: listBuckets:", dump(error)) + throw error + } + } } + // snippet-end:[s3.swift.intro.getbucketnames] // snippet-start:[s3.swift.intro.main] @@ -61,4 +78,5 @@ struct Main { } } } + // snippet-end:[s3.swift.intro.main] diff --git a/swift/example_code/s3/ListBuckets/Sources/ListBuckets/S3Session.swift b/swift/example_code/s3/ListBuckets/Sources/ListBuckets/S3Session.swift index cc4e8d75d92..edb02bde1c4 100644 --- a/swift/example_code/s3/ListBuckets/Sources/ListBuckets/S3Session.swift +++ b/swift/example_code/s3/ListBuckets/Sources/ListBuckets/S3Session.swift @@ -11,16 +11,17 @@ /// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. /// SPDX-License-Identifier: Apache-2.0 -import Foundation import AWSClientRuntime import AWSS3 +import Foundation // snippet-start:[s3.swift.listbuckets.s3sessionprotocol] /// A protocol defining the Amazon S3 functions we want to mock /// during testing. public protocol S3SessionProtocol { - func listBuckets(input: ListBucketsInput) async throws -> ListBucketsOutput + func listBuckets(input: ListBucketsInput) async throws -> [S3ClientTypes.Bucket] } + // snippet-end:[s3.swift.listbuckets.s3sessionprotocol] // snippet-start:[s3.swift.listbuckets.s3session] @@ -28,27 +29,61 @@ public protocol S3SessionProtocol { /// functions in the AWS SDK for Swift. public struct S3Session: S3SessionProtocol { let client: S3Client - let awsRegion: String /// Initialize the session to use the specified AWS Region. /// - /// - Parameter region: The AWS Region to use. Default is `us-east-1`. - init(region: String = "us-east-1") throws { - self.awsRegion = region + /// - Parameter region: The optional AWS Region to use. + init(region: String? = nil) async throws { + do { + let config = try await S3Client.S3ClientConfiguration() + if let region = region { + config.region = region + } - // Create an ``S3Client`` to use for AWS SDK for Swift calls. - self.client = try S3Client(region: awsRegion) + // Create an ``S3Client`` to use for AWS SDK for Swift calls. + self.client = S3Client(config: config) + } catch { + print("ERROR: ", dump(error, name: "Initializing Amazon S3 client")) + throw error + } } /// Call through to the ``S3Client`` function `listBuckets()`. /// - Parameter input: The input to pass through to the SDK function /// `listBuckets()`. - /// - Returns: A ``ListBucketsOutput`` with the returned data. + /// - Returns: An array of ``S3ClientTypes.Bucket`` objects describing + /// each bucket. public func listBuckets(input: ListBucketsInput) async throws - -> ListBucketsOutput { - return try await self.client.listBuckets(input: input) + -> [S3ClientTypes.Bucket] + { + // snippet-start:[s3.swift.intro.listbuckets] + // Use "Paginated" to get all the buckets. + // This lets the SDK handle the 'continuationToken' in "ListBucketsOutput". + let pages = client.listBucketsPaginated( + input: input + ) + // snippet-end:[s3.swift.intro.listbuckets] + + // Get the bucket names. + var allBuckets: [S3ClientTypes.Bucket] = [] + + do { + for try await page in pages { + guard let buckets = page.buckets else { + print("Error: no buckets returned.") + continue + } + allBuckets += buckets + } + + return allBuckets + } catch { + print("ERROR: listBuckets:", dump(error)) + throw error + } } } + // snippet-end:[s3.swift.listbuckets.s3session] // snippet-start:[s3.swift.listbuckets.s3manager] @@ -65,21 +100,18 @@ public class S3Manager { init(session: S3SessionProtocol) { self.client = session } + // snippet-end:[s3.swift.listbuckets.s3manager.init] // snippet-start:[s3.swift.listbuckets.ListBuckets] /// Return an array containing information about every available bucket. - /// + /// /// - Returns: An array of ``S3ClientTypes.Bucket`` objects describing /// each bucket. public func getAllBuckets() async throws -> [S3ClientTypes.Bucket] { - let output = try await client.listBuckets(input: ListBucketsInput()) - - guard let buckets = output.buckets else { - return [] - } - return buckets + return try await client.listBuckets(input: ListBucketsInput()) } // snippet-end:[s3.swift.listbuckets.ListBuckets] } + // snippet-end:[s3.swift.listbuckets.s3manager] diff --git a/swift/example_code/s3/ListBuckets/Sources/ListBuckets/listbuckets.swift b/swift/example_code/s3/ListBuckets/Sources/ListBuckets/listbuckets.swift index 23a66582974..ad3329e93d7 100644 --- a/swift/example_code/s3/ListBuckets/Sources/ListBuckets/listbuckets.swift +++ b/swift/example_code/s3/ListBuckets/Sources/ListBuckets/listbuckets.swift @@ -8,10 +8,11 @@ // snippet-start:[s3.swift.listbuckets.command] // snippet-start:[s3.swift.listbuckets.command.imports] -import Foundation import ArgumentParser import AWSClientRuntime import AWSS3 +import Foundation + // snippet-end:[s3.swift.listbuckets.command.imports] // snippet-start:[s3.swift.listbuckets.command.parsable] @@ -56,7 +57,7 @@ struct ExampleCommand: ParsableCommand { /// Called by ``main()`` to asynchronously run the AWS example. func runAsync() async throws { - let s3 = S3Manager(session: try S3Session(region: region)) + let s3 = try await S3Manager(session: S3Session(region: region)) let bucketList = try await s3.getAllBuckets() if bucketList.count != 0 { @@ -69,6 +70,7 @@ struct ExampleCommand: ParsableCommand { } } } + // snippet-end:[s3.swift.listbuckets.command.parsable] // snippet-start:[s3.swift.listbuckets.datetostring] @@ -78,10 +80,11 @@ struct ExampleCommand: ParsableCommand { /// /// - Returns: A string containing the date in the format `mm/dd/yy, h:mm:ss /// pp UTC`, or `` if the date is `nil`. -func dateToString(_ date:Date?) -> String { +func dateToString(_ date: Date?) -> String { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .short dateFormatter.timeStyle = .long + dateFormatter.timeZone = TimeZone(identifier: "GMT") if date != nil { return dateFormatter.string(from: date!) @@ -89,6 +92,7 @@ func dateToString(_ date:Date?) -> String { return "" } } + // snippet-end:[s3.swift.listbuckets.datetostring] // snippet-start:[s3.swift.listbuckets.bucketstring] @@ -102,6 +106,7 @@ func bucketString(_ bucket: S3ClientTypes.Bucket) -> String { let dateString = dateToString(bucket.creationDate) return "\(bucket.name ?? "") (created \(dateString))" } + // snippet-end:[s3.swift.listbuckets.bucketstring] // snippet-start:[s3.swift.listbuckets.main] @@ -122,5 +127,6 @@ struct Main { } } } + // snippet-end:[s3.swift.listbuckets.main] // snippet-end:[s3.swift.listbuckets.command] diff --git a/swift/example_code/s3/ListBuckets/Tests/ListBucketsTests/ListBucketsTests.swift b/swift/example_code/s3/ListBuckets/Tests/ListBucketsTests/ListBucketsTests.swift index 20ba1778b05..2de1222f5f6 100644 --- a/swift/example_code/s3/ListBuckets/Tests/ListBucketsTests/ListBucketsTests.swift +++ b/swift/example_code/s3/ListBuckets/Tests/ListBucketsTests/ListBucketsTests.swift @@ -46,7 +46,7 @@ public struct MockS3Session: S3SessionProtocol { /// - Returns: A `ListBucketsOutput` object containing the list of /// buckets. public func listBuckets(input: ListBucketsInput) async throws - -> ListBucketsOutput { + -> [S3ClientTypes.Bucket] { var bucketList: [S3ClientTypes.Bucket] = [] let df = DateFormatter() df.dateFormat = "M/d/yy, h:mm:ss a z" @@ -60,15 +60,8 @@ public struct MockS3Session: S3SessionProtocol { ) bucketList.append(bucket) } - - // Create and return the `ListBucketsOutput` object containing - // the results. - - let response = ListBucketsOutput( - buckets: bucketList, - owner: nil - ) - return response + + return bucketList } } @@ -94,9 +87,9 @@ final class ListBucketsTests: XCTestCase { // non-breaking space Unicode character. This is what the // `DateFormatter` class uses. var testData: [BucketInfo] = [] - testData.append(BucketInfo(name: "Testfile-1", date: "2/4/65, 1:23:45 AM UTC")) - testData.append(BucketInfo(name: "Another-file", date: "1/13/72, 4:43:21 PM UTC")) - testData.append(BucketInfo(name: "Very-foo-file", date: "8/17/47, 12:34:00 PM UTC")) + testData.append(BucketInfo(name: "Testfile-1", date: "2/4/65, 1:23:45 AM GMT")) + testData.append(BucketInfo(name: "Another-file", date: "1/13/72, 4:43:21 PM GMT")) + testData.append(BucketInfo(name: "Very-foo-file", date: "8/17/47, 12:34:00 PM GMT")) self.session = MockS3Session(data: testData) self.s3 = S3Manager(session: self.session!) @@ -128,7 +121,7 @@ final class ListBucketsTests: XCTestCase { /// name and a date for which the string is known. Then compare the result /// to the expected value. func testBucketString() async throws { - let testDate = "1/23/45, 6:07:08 PM UTC" + let testDate = "1/23/45, 6:07:08 PM GMT" let testName = "test-bucket-name" let testString = "\(testName) (created \(testDate))" @@ -150,7 +143,7 @@ final class ListBucketsTests: XCTestCase { /// object for which we know the expected string result. func testDateToString() async throws { let testDate = Date(timeIntervalSinceReferenceDate: -123456789.0) - let testString = "2/2/97, 2:26:51 AM UTC" + let testString = "2/2/97, 2:26:51 AM GMT" let ds = dateToString(testDate) @@ -164,4 +157,4 @@ final class ListBucketsTests: XCTestCase { XCTAssertEqual(ds, "", "Result of dateToString(nil) should be 'unknown' but was '\(ds)'") } -} \ No newline at end of file +} diff --git a/swift/example_code/s3/basics/Tests/basics-tests/S3DemoCleanup.swift b/swift/example_code/s3/basics/Tests/basics-tests/S3DemoCleanup.swift index 3309219fe6a..fdd21f5f74b 100644 --- a/swift/example_code/s3/basics/Tests/basics-tests/S3DemoCleanup.swift +++ b/swift/example_code/s3/basics/Tests/basics-tests/S3DemoCleanup.swift @@ -54,7 +54,7 @@ public class S3DemoCleanup { init() async { do { - self.client = try S3Client(region: "us-east-2") + self.client = try await S3Client() } catch { print("Error initializing S3 client for tracking and deleting Amazon S3 buckets and files created by the example:") dump(error) @@ -222,4 +222,4 @@ public class S3DemoCleanup { } } } -} \ No newline at end of file +} From b7d309a982b2a7f180b0094e0f04a7594d70a720 Mon Sep 17 00:00:00 2001 From: Steven Meyer <108885656+meyertst-aws@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:00:35 -0400 Subject: [PATCH 2/2] Updated the README --- swift/example_code/s3/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/swift/example_code/s3/README.md b/swift/example_code/s3/README.md index f3c196780d8..c51d6f69b4b 100644 --- a/swift/example_code/s3/README.md +++ b/swift/example_code/s3/README.md @@ -44,9 +44,9 @@ Code excerpts that show you how to call individual service functions. - [CreateBucket](basics/Sources/ServiceHandler/ServiceHandler.swift#L56) - [DeleteBucket](basics/Sources/ServiceHandler/ServiceHandler.swift#L87) - [DeleteObject](basics/Sources/ServiceHandler/ServiceHandler.swift#L257) -- [DeleteObjects](DeleteObjects/Sources/ServiceHandler/ServiceHandler.swift#L54) +- [DeleteObjects](DeleteObjects/Sources/ServiceHandler/ServiceHandler.swift#L59) - [GetObject](basics/Sources/ServiceHandler/ServiceHandler.swift#L163) -- [ListBuckets](ListBuckets/Sources/ListBuckets/S3Session.swift#L70) +- [ListBuckets](ListBuckets/Sources/ListBuckets/S3Session.swift#L106) - [ListObjectsV2](basics/Sources/ServiceHandler/ServiceHandler.swift#L280) - [PutObject](basics/Sources/ServiceHandler/ServiceHandler.swift#L107)