From 576c0c51f424be726e48a789bfed8a13de0766eb Mon Sep 17 00:00:00 2001 From: Eric Shepherd Date: Tue, 10 Dec 2024 16:10:07 +0000 Subject: [PATCH] Streamed binary downloading example for Swift Added an example showing streaming binary downloads using the Swift SDK. Also made minor adjustments to the upload streaming example, including moving files a bit to build both examples in one package. --- .../s3/binary-streaming/Package.swift | 13 +- .../{ => streamdown}/TransferError.swift | 12 -- .../Sources/streamdown/streamdown.swift | 165 ++++++++++++++++++ .../Sources/streamup/TransferError.swift | 19 ++ .../{entry.swift => streamup/streamup.swift} | 12 +- .../Sources/presigned-download/entry.swift | 2 +- 6 files changed, 202 insertions(+), 21 deletions(-) rename swift/example_code/s3/binary-streaming/Sources/{ => streamdown}/TransferError.swift (59%) create mode 100644 swift/example_code/s3/binary-streaming/Sources/streamdown/streamdown.swift create mode 100644 swift/example_code/s3/binary-streaming/Sources/streamup/TransferError.swift rename swift/example_code/s3/binary-streaming/Sources/{entry.swift => streamup/streamup.swift} (91%) diff --git a/swift/example_code/s3/binary-streaming/Package.swift b/swift/example_code/s3/binary-streaming/Package.swift index 571bb99a37d..3d007a8d6bc 100644 --- a/swift/example_code/s3/binary-streaming/Package.swift +++ b/swift/example_code/s3/binary-streaming/Package.swift @@ -8,7 +8,7 @@ import PackageDescription let package = Package( - name: "streamup", + name: "binary-streaming", // Let Xcode know the minimum Apple platforms supported. platforms: [ .macOS(.v13), @@ -34,6 +34,15 @@ let package = Package( .product(name: "AWSS3", package: "aws-sdk-swift"), .product(name: "ArgumentParser", package: "swift-argument-parser"), ], - path: "Sources"), + path: "Sources/streamup" + ), + .executableTarget( + name: "streamdown", + dependencies: [ + .product(name: "AWSS3", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ], + path: "Sources/streamdown" + ), ] ) diff --git a/swift/example_code/s3/binary-streaming/Sources/TransferError.swift b/swift/example_code/s3/binary-streaming/Sources/streamdown/TransferError.swift similarity index 59% rename from swift/example_code/s3/binary-streaming/Sources/TransferError.swift rename to swift/example_code/s3/binary-streaming/Sources/streamdown/TransferError.swift index 1e6ae99151e..41b5ef57e14 100644 --- a/swift/example_code/s3/binary-streaming/Sources/TransferError.swift +++ b/swift/example_code/s3/binary-streaming/Sources/streamdown/TransferError.swift @@ -7,12 +7,6 @@ enum TransferError: Error { case directoryError /// An error occurred while downloading a file from Amazon S3. case downloadError(_ message: String = "") - /// An error occurred moving the file to its final destination. - case fileMoveError - /// An error occurred while reading the file's contents. - case readError - /// An error occurred while uploading a file to Amazon S3. - case uploadError(_ message: String = "") /// An error occurred while writing the file's contents. case writeError @@ -22,12 +16,6 @@ enum TransferError: Error { return "The destination directory could not be located or created" case .downloadError(message: let message): return "An error occurred attempting to download the file: \(message)" - case .fileMoveError: - return "The file couldn't be moved to the destination directory" - case .readError: - return "An error occurred while reading the file data" - case .uploadError(message: let message): - return "An error occurred attempting to upload the file: \(message)" case .writeError: return "An error occurred while writing the file data" } diff --git a/swift/example_code/s3/binary-streaming/Sources/streamdown/streamdown.swift b/swift/example_code/s3/binary-streaming/Sources/streamdown/streamdown.swift new file mode 100644 index 00000000000..d2b06a18ba8 --- /dev/null +++ b/swift/example_code/s3/binary-streaming/Sources/streamdown/streamdown.swift @@ -0,0 +1,165 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +// +/// A simple example that shows how to use the AWS SDK for Swift to +/// upload files using binary streaming. + +// snippet-start:[swift.s3.streaming-down.imports] +import ArgumentParser +import AWSClientRuntime +import AWSS3 +import Foundation +import Smithy +import SmithyHTTPAPI +import SmithyStreams + +// snippet-end:[swift.s3.streaming-down.imports] + +// -MARK: - Async command line tool + +struct ExampleCommand: ParsableCommand { + // -MARK: Command arguments + @Option(help: "Name of the Amazon S3 bucket to download from") + var bucket: String + @Option(help: "Key of the file on Amazon S3 to download") + var key: String + @Option(help: "Local path to download the file to") + var dest: String? + @Option(help: "Name of the Amazon S3 Region to use (default: us-east-1)") + var region = "us-east-1" + + static var configuration = CommandConfiguration( + commandName: "streamdown", + abstract: """ + This example shows how to use binary data streaming to download a file + from Amazon S3. + """, + discussion: """ + """ + ) + + // snippet-start:[swift.s3.streaming-down] + /// Download a file from the specified bucket. + /// + /// - Parameters: + /// - bucket: The Amazon S3 bucket name to get the file from. + /// - key: The name (or path) of the file to download from the bucket. + /// - destPath: The pathname on the local filesystem at which to store + /// the downloaded file. + func downloadFile(bucket: String, key: String, destPath: String?) async throws { + let fileURL: URL + + // If no destination path was provided, use the key as the name to use + // for the file in the downloads folder. + + if destPath == nil { + do { + try fileURL = FileManager.default.url( + for: .downloadsDirectory, + in: .userDomainMask, + appropriateFor: URL(string: key), + create: true + ).appendingPathComponent(key) + } catch { + throw TransferError.directoryError + } + } else { + fileURL = URL(fileURLWithPath: destPath!) + } + + let config = try await S3Client.S3ClientConfiguration(region: region) + let s3Client = S3Client(config: config) + + // Create a `FileHandle` referencing the local destination. Then + // create a `ByteStream` from that. + + FileManager.default.createFile(atPath: fileURL.path, contents: nil, attributes: nil) + let fileHandle = try FileHandle(forWritingTo: fileURL) + + // Download the file using `GetObject`. + + let getInput = GetObjectInput( + bucket: bucket, + key: key + ) + + do { + let getOutput = try await s3Client.getObject(input: getInput) + + guard let body = getOutput.body else { + throw TransferError.downloadError("Error: No data returned for download") + } + + // If the body is returned as a `Data` object, write that to the + // file. If it's a stream, read the stream chunk by chunk, + // appending each chunk to the destination file. + + switch body { + case .data: + guard let data = try await body.readData() else { + throw TransferError.downloadError("Download error") + } + + // Write the `Data` to the file. + + do { + try data.write(to: fileURL) + } catch { + throw TransferError.writeError + } + break + + case .stream(let stream as ReadableStream): + while (true) { + let chunk = try await stream.readAsync(upToCount: 5 * 1024 * 1024) + guard let chunk = chunk else { + break + } + + // Write the chunk to the destination file. + + do { + try fileHandle.write(contentsOf: chunk) + } catch { + throw TransferError.writeError + } + } + + break + default: + throw TransferError.downloadError("Received data is unknown object type") + } + } catch { + throw TransferError.downloadError("Error downloading the file: \(error)") + } + + print("File downloaded to \(fileURL.path).") + } + // snippet-end:[swift.s3.streaming-down] + + // -MARK: - Asynchronous main code + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + try await downloadFile(bucket: bucket, key: key, destPath: dest) + } +} + +// -MARK: - Entry point + +/// The program's asynchronous entry point. +@main +struct Main { + static func main() async { + let args = Array(CommandLine.arguments.dropFirst()) + + do { + let command = try ExampleCommand.parse(args) + try await command.runAsync() + } catch let error as TransferError { + print("ERROR: \(error.errorDescription ?? "Unknown error")") + } catch { + ExampleCommand.exit(withError: error) + } + } +} diff --git a/swift/example_code/s3/binary-streaming/Sources/streamup/TransferError.swift b/swift/example_code/s3/binary-streaming/Sources/streamup/TransferError.swift new file mode 100644 index 00000000000..7142e3bc044 --- /dev/null +++ b/swift/example_code/s3/binary-streaming/Sources/streamup/TransferError.swift @@ -0,0 +1,19 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +/// Errors thrown by the example's functions. +enum TransferError: Error { + /// An error occurred while reading the file's contents. + case readError + /// An error occurred while uploading a file to Amazon S3. + case uploadError(_ message: String = "") + + var errorDescription: String? { + switch self { + case .readError: + return "An error occurred while reading the file data" + case .uploadError(message: let message): + return "An error occurred attempting to upload the file: \(message)" + } + } +} diff --git a/swift/example_code/s3/binary-streaming/Sources/entry.swift b/swift/example_code/s3/binary-streaming/Sources/streamup/streamup.swift similarity index 91% rename from swift/example_code/s3/binary-streaming/Sources/entry.swift rename to swift/example_code/s3/binary-streaming/Sources/streamup/streamup.swift index b20649ddd5c..adbcf051c75 100644 --- a/swift/example_code/s3/binary-streaming/Sources/entry.swift +++ b/swift/example_code/s3/binary-streaming/Sources/streamup/streamup.swift @@ -4,7 +4,7 @@ /// A simple example that shows how to use the AWS SDK for Swift to /// upload files using binary streaming. -// snippet-start:[swift.s3.binary-streaming.imports] +// snippet-start:[swift.s3.streaming-up.imports] import ArgumentParser import AWSClientRuntime import AWSS3 @@ -13,7 +13,7 @@ import Smithy import SmithyHTTPAPI import SmithyStreams -// snippet-end:[swift.s3.binary-streaming.imports] +// snippet-end:[swift.s3.streaming-up.imports] // -MARK: - Async command line tool @@ -38,14 +38,14 @@ struct ExampleCommand: ParsableCommand { """ ) - // snippet-start:[swift.s3.binary-streaming.upload-file] + // snippet-start:[swift.s3.streaming-up] /// Upload a file to the specified bucket. /// /// - Parameters: /// - bucket: The Amazon S3 bucket name to store the file into. /// - key: The name (or path) of the file to upload to in the `bucket`. - /// - sourcePath: The pathname on the local filesystem at which to store - /// the uploaded file. + /// - sourcePath: The pathname on the local filesystem of the file to + /// upload. func uploadFile(sourcePath: String, bucket: String, key: String?) async throws { let fileURL: URL = URL(fileURLWithPath: sourcePath) let fileName: String @@ -91,7 +91,7 @@ struct ExampleCommand: ParsableCommand { print("File uploaded to \(fileURL.path).") } - // snippet-end:[swift.s3.binary-streaming.upload-file] + // snippet-end:[swift.s3.streaming-up] // -MARK: - Asynchronous main code diff --git a/swift/example_code/s3/presigned-urls/Sources/presigned-download/entry.swift b/swift/example_code/s3/presigned-urls/Sources/presigned-download/entry.swift index abd01ad9ed3..77a32f78aeb 100644 --- a/swift/example_code/s3/presigned-urls/Sources/presigned-download/entry.swift +++ b/swift/example_code/s3/presigned-urls/Sources/presigned-download/entry.swift @@ -80,7 +80,7 @@ struct ExampleCommand: ParsableCommand { let s3Client = try await S3Client() - // Create a presigned URLRequest with the `GetObject` action. + // Download the file using `GetObject` and the stream's `readData()`. let getInput = GetObjectInput( bucket: bucket,