Skip to content

Commit

Permalink
Streamed binary downloading example for Swift
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
shepazon authored and rlhagerm committed Dec 10, 2024
1 parent cfce46d commit 576c0c5
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 21 deletions.
13 changes: 11 additions & 2 deletions swift/example_code/s3/binary-streaming/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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"
),
]
)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 576c0c5

Please sign in to comment.