Skip to content

Commit

Permalink
Multi-part upload example for Swift
Browse files Browse the repository at this point in the history
  • Loading branch information
shepazon authored and DavidSouther committed Nov 25, 2024
1 parent a19c4d3 commit 836db03
Show file tree
Hide file tree
Showing 3 changed files with 363 additions and 0 deletions.
40 changes: 40 additions & 0 deletions swift/example_code/s3/multipart-upload/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// swift-tools-version: 5.9
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
// The swift-tools-version declares the minimum version of Swift required to
// build this package.

import PackageDescription

let package = Package(
name: "multipart",
// Let Xcode know the minimum Apple platforms supported.
platforms: [
.macOS(.v13),
.iOS(.v15)
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(
url: "https://github.com/awslabs/aws-sdk-swift",
from: "1.0.0"),
.package(
url: "https://github.com/apple/swift-argument-parser.git",
branch: "main"
)
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products
// from dependencies.
.executableTarget(
name: "mpupload",
dependencies: [
.product(name: "AWSS3", package: "aws-sdk-swift"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
path: "Sources")

]
)
27 changes: 27 additions & 0 deletions swift/example_code/s3/multipart-upload/Sources/TransferError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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 when completing a multi-part upload to Amazon S3.
case multipartFinishError(_ message: String = "")
/// An error occurred when starting a multi-part upload to Amazon S3.
case multipartStartError
/// An error occurred while uploading a file to Amazon S3.
case uploadError(_ message: String = "")
/// An error occurred while reading the file's contents.
case readError

var errorDescription: String? {
switch self {
case .multipartFinishError(message: let message):
return "An error occurred when completing a multi-part upload to Amazon S3. \(message)"
case .multipartStartError:
return "An error occurred when starting a multi-part upload to Amazon S3."
case .uploadError(message: let message):
return "An error occurred attempting to upload the file: \(message)"
case .readError:
return "An error occurred while reading the file data"
}
}
}
296 changes: 296 additions & 0 deletions swift/example_code/s3/multipart-upload/Sources/entry.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
//
/// An example demonstrating how to perform multi-part uploads to Amazon S3
/// using the AWS SDK for Swift.

// snippet-start:[swift.s3.multipart-upload-upload.imports]
import ArgumentParser
import AsyncHTTPClient
import AWSClientRuntime
import AWSS3
import Foundation
import Smithy
// snippet-end:[swift.s3.multipart-upload-upload.imports]

// -MARK: - Async command line tool

struct ExampleCommand: ParsableCommand {
// -MARK: Command arguments
@Option(help: "Path of local file to upload to Amazon S3")
var file: String
@Option(help: "Name of the Amazon S3 bucket to upload to")
var bucket: String
@Option(help: "Key name to give the file on Amazon S3")
var key: String?
@Option(help: "Name of the Amazon S3 Region to use")
var region = "us-east-1"

static var configuration = CommandConfiguration(
commandName: "mpupload",
abstract: """
This example shows how to upload files to Amazon S3 using multi-part
uploads.
""",
discussion: """
"""
)

// -MARK: - File uploading

func uploadFile(file: String, bucket: String, key: String?) async throws {
let fileURL = URL(fileURLWithPath: file)
let fileName: String

// If no key was provided, use the last component of the filename.

if key == nil {
fileName = fileURL.lastPathComponent
} else {
fileName = key!
}

// Create an Amazon S3 client in the desired Region.

let config = try await S3Client.S3ClientConfiguration(region: region)
let s3Client = S3Client(config: config)

print("Uploading file from \(fileURL.path) to \(bucket)/\(fileName).")

// Start the multi-part upload process and get the upload ID.

var completedParts: [S3ClientTypes.CompletedPart] = []

let uploadID = try await startMultipartUpload(client: s3Client,
bucket: bucket, key: fileName)

// Open a file handle and prepare to send the file in chunks. Each chunk
// is 5 MB, which is the minimum size allowed by Amazon S3.

do {
let blockSize = Int(5 * 1024 * 1024)
let fileHandle = try FileHandle(forReadingFrom: fileURL)
let fileSize = try getFileSize(file: fileHandle)
let blockCount = Int(ceil(Double(fileSize) / Double(blockSize)))

// Upload the blocks one at as Amazon S3 object parts.

print("Uploading...")

// snippet-start:[swift.s3.multipart-upload.upload-loop]
for partNumber in 1...blockCount {
let data: Data
let startIndex = UInt64(partNumber - 1) * UInt64(blockSize)

// Read the block from the file.

data = try readFileBlock(file: fileHandle, startIndex: startIndex, size: blockSize)

// Upload the part to Amazon S3 and append the `CompletedPart` to
// the array `completedParts` for use after all parts are uploaded.

let completedPart = try await uploadPart(
client: s3Client, uploadID: uploadID,
bucket: bucket, key: fileName,
partNumber: partNumber, data: data
)
completedParts.append(completedPart)

let percent = Double(partNumber) / Double(blockCount) * 100
print(String(format: " %.1f%%", percent))
}
// snippet-end:[swift.s3.multipart-upload.upload-loop]

// Finish the upload.

try await finishMultipartUpload(client: s3Client, uploadId: uploadID,
bucket: bucket, key: fileName,
parts: completedParts)
} catch {
throw TransferError.uploadError("Error uploading the file: \(error)")
}

print("Done. Uploaded as \(fileName) in bucket \(bucket).")
}

// snippet-start:[swift.s3.multipart-upload.create]
/// Start a multi-part upload to Amazon S3.
/// - Parameters:
/// - bucket: The name of the bucket to upload into.
/// - key: The name of the object to store in the bucket.
///
/// - Returns: A string containing the `uploadId` of the multi-part
/// upload job.
///
/// - Throws:
func startMultipartUpload(client: S3Client, bucket: String, key: String) async throws -> String {
let multiPartUploadOutput: CreateMultipartUploadOutput

// First, create the multi-part upload.

do {
multiPartUploadOutput = try await client.createMultipartUpload(
input: CreateMultipartUploadInput(
bucket: bucket,
key: key
)
)
} catch {
throw TransferError.multipartStartError
}

// Get the upload ID. This needs to be included with each part sent.

guard let uploadID = multiPartUploadOutput.uploadId else {
throw TransferError.uploadError("Unable to get the upload ID")
}

return uploadID
}
// snippet-end:[swift.s3.multipart-upload.create]

// snippet-start:[swift.s3.multipart-upload.upload-part]
/// Upload the specified data as part of an Amazon S3 multi-part upload.
///
/// - Parameters:
/// - client: The S3Client to use to upload the part.
/// - uploadID: The upload ID of the multi-part upload to add the part to.
/// - bucket: The name of the bucket the data is being written to.
/// - key: A string giving the key which names the Amazon S3 object the file is being added to.
/// - partNumber: The part number within the file that the specified data represents.
/// - data: The data to send as the specified object part number in the object.
///
/// - Throws: `TransferError.signingError`, `TransferError.uploadError`
///
/// - Returns: A `CompletedPart` object describing the part that was uploaded.
/// contains the part number as well as the ETag returned by Amazon S3.
func uploadPart(client: S3Client, uploadID: String, bucket: String,
key: String, partNumber: Int, data: Data)
async throws -> S3ClientTypes.CompletedPart {
let uploadPartInput = UploadPartInput(
body: ByteStream.data(data),
bucket: bucket,
key: key,
partNumber: partNumber,
uploadId: uploadID
)

do {
let uploadPartOutput = try await client.uploadPart(input: uploadPartInput)
guard let eTag = uploadPartOutput.eTag else {
throw TransferError.uploadError("Missing eTag")
}

return S3ClientTypes.CompletedPart(eTag: eTag, partNumber: partNumber)
} catch {
throw TransferError.uploadError(error.localizedDescription)
}
}
// snippet-end:[swift.s3.multipart-upload.upload-part]

// snippet-start:[swift.s3.multipart-upload.upload-complete]
/// Complete a multi-part upload by creating a `CompletedMultipartUpload`
/// with the array of completed part descriptions. This is used as the
/// value of the `multipartUpload` property when calling
/// `completeMultipartUpload(input:)`.
///
/// - Parameters:
/// - client: The S3Client to finish uploading with.
/// - uploadId: The multi-part upload's ID string.
/// - bucket: The name of the bucket the upload is targeting.
/// - key: The name of the object being written to the bucket.
/// - parts: An array of `CompletedPart` objects describing each part
/// of the upload.
///
/// - Throws: `TransferError.multipartFinishError`
func finishMultipartUpload(client: S3Client, uploadId: String, bucket: String, key: String,
parts: [S3ClientTypes.CompletedPart]) async throws {
do {
let partInfo = S3ClientTypes.CompletedMultipartUpload(parts: parts)
let multiPartCompleteInput = CompleteMultipartUploadInput(
bucket: bucket,
key: key,
multipartUpload: partInfo,
uploadId: uploadId
)
_ = try await client.completeMultipartUpload(input: multiPartCompleteInput)
} catch {
dump(error)
throw TransferError.multipartFinishError(error.localizedDescription)
}
}
// snippet-end:[swift.s3.multipart-upload.upload-complete]

// -MARK: - File access

/// Get the size of a file in bytes.
///
/// - Parameter file: `FileHandle` identifying the file to return the size of.
///
/// - Returns: The number of bytes in the file.
func getFileSize(file: FileHandle) throws -> UInt64 {
let fileSize: UInt64

// Get the total size of the file in bytes, then compute the number
// of blocks it will take to transfer the whole file.

do {
try file.seekToEnd()
fileSize = try file.offset()
} catch {
throw TransferError.readError
}
return fileSize
}

/// Read the specified range of bytes from a file and return them in a
/// new `Data` object.
///
/// - Parameters:
/// - file: The `FileHandle` to read from.
/// - startIndex: The index of the first byte to read.
/// - size: The number of bytes to read.
///
/// - Returns: A new `Data` object containing the specified range of bytes.
///
/// - Throws: `TransferError.readError` if the read fails.
func readFileBlock(file: FileHandle, startIndex: UInt64, size: Int) throws -> Data {
file.seek(toFileOffset: startIndex)
do {
let data = try file.read(upToCount: size)
guard let data else {
throw TransferError.readError
}
return data
} catch {
throw TransferError.readError
}
}

// -MARK: - Asynchronous main code

/// Called by ``main()`` to run the bulk of the example.
func runAsync() async throws {
try await uploadFile(file: file, bucket: bucket,
key: key)
}
}

// -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)
}
}
}

0 comments on commit 836db03

Please sign in to comment.