Skip to content

Commit 14e720e

Browse files
shepazonford-at-aws
authored andcommitted
New example for checksum with multipart upload
Added a new multipart-upload example, based on the existing one from S3. This example merges some functions together and displays each part's checksum after uploading it. Also did minor cleanup on the S3 multipart upload example.
1 parent 9353cb9 commit 14e720e

File tree

7 files changed

+324
-9
lines changed

7 files changed

+324
-9
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// swift-tools-version: 5.9
2+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// The swift-tools-version declares the minimum version of Swift required to
6+
// build this package.
7+
8+
import PackageDescription
9+
10+
let package = Package(
11+
name: "mpchecksums",
12+
// Let Xcode know the minimum Apple platforms supported.
13+
platforms: [
14+
.macOS(.v13),
15+
.iOS(.v15)
16+
],
17+
dependencies: [
18+
// Dependencies declare other packages that this package depends on.
19+
.package(
20+
url: "https://github.com/awslabs/aws-sdk-swift",
21+
from: "1.0.0"),
22+
.package(
23+
url: "https://github.com/apple/swift-argument-parser.git",
24+
branch: "main"
25+
)
26+
],
27+
targets: [
28+
// Targets are the basic building blocks of a package, defining a module or a test suite.
29+
// Targets can depend on other targets in this package and products
30+
// from dependencies.
31+
.executableTarget(
32+
name: "mpchecksums",
33+
dependencies: [
34+
.product(name: "AWSS3", package: "aws-sdk-swift"),
35+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
36+
],
37+
path: "Sources")
38+
39+
]
40+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/// Errors thrown by the example's functions.
5+
enum TransferError: Error {
6+
/// The checksum is missing or erroneous.
7+
case checksumError
8+
/// An error occurred when completing a multi-part upload to Amazon S3.
9+
case multipartFinishError(_ message: String = "")
10+
/// An error occurred when starting a multi-part upload to Amazon S3.
11+
case multipartStartError
12+
/// An error occurred while uploading a file to Amazon S3.
13+
case uploadError(_ message: String = "")
14+
/// An error occurred while reading the file's contents.
15+
case readError
16+
17+
var errorDescription: String? {
18+
switch self {
19+
case .checksumError:
20+
return "The checksum is missing or incorrect"
21+
case .multipartFinishError(message: let message):
22+
return "An error occurred when completing a multi-part upload to Amazon S3. \(message)"
23+
case .multipartStartError:
24+
return "An error occurred when starting a multi-part upload to Amazon S3."
25+
case .uploadError(message: let message):
26+
return "An error occurred attempting to upload the file: \(message)"
27+
case .readError:
28+
return "An error occurred while reading the file data"
29+
}
30+
}
31+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
//
4+
/// An example demonstrating how to perform multi-part uploads to Amazon S3
5+
/// using the AWS SDK for Swift.
6+
7+
// snippet-start:[swift.s3.mp-checksums.imports]
8+
import ArgumentParser
9+
import AWSClientRuntime
10+
import AWSS3
11+
import Foundation
12+
import Smithy
13+
// snippet-end:[swift.s3.mp-checksums.imports]
14+
15+
// -MARK: - Async command line tool
16+
17+
struct ExampleCommand: ParsableCommand {
18+
// -MARK: Command arguments
19+
@Option(help: "Path of local file to upload to Amazon S3")
20+
var file: String
21+
@Option(help: "Name of the Amazon S3 bucket to upload to")
22+
var bucket: String
23+
@Option(help: "Key name to give the file on Amazon S3")
24+
var key: String?
25+
@Option(help: "Name of the Amazon S3 Region to use")
26+
var region = "us-east-1"
27+
28+
static var configuration = CommandConfiguration(
29+
commandName: "mpchecksums",
30+
abstract: """
31+
This example shows how to use checksums with multi-part uploads.
32+
""",
33+
discussion: """
34+
"""
35+
)
36+
37+
// -MARK: - File uploading
38+
39+
// snippet-start:[swift.s3.mp-checksums.uploadfile]
40+
/// Upload a file to Amazon S3.
41+
///
42+
/// - Parameters:
43+
/// - file: The path of the local file to upload to Amazon S3.
44+
/// - bucket: The name of the bucket to upload the file into.
45+
/// - key: The key (name) to give the object on Amazon S3.
46+
///
47+
/// - Throws: Errors from `TransferError`
48+
func uploadFile(file: String, bucket: String, key: String?) async throws {
49+
let fileURL = URL(fileURLWithPath: file)
50+
let fileName: String
51+
52+
// If no key was provided, use the last component of the filename.
53+
54+
if key == nil {
55+
fileName = fileURL.lastPathComponent
56+
} else {
57+
fileName = key!
58+
}
59+
60+
// Create an Amazon S3 client in the desired Region.
61+
62+
let config = try await S3Client.S3ClientConfiguration(region: region)
63+
let s3Client = S3Client(config: config)
64+
65+
print("Uploading file from \(fileURL.path) to \(bucket)/\(fileName).")
66+
67+
let multiPartUploadOutput: CreateMultipartUploadOutput
68+
69+
// First, create the multi-part upload, using SHA256 checksums.
70+
71+
do {
72+
multiPartUploadOutput = try await s3Client.createMultipartUpload(
73+
input: CreateMultipartUploadInput(
74+
bucket: bucket,
75+
checksumAlgorithm: .sha256,
76+
key: key
77+
)
78+
)
79+
} catch {
80+
throw TransferError.multipartStartError
81+
}
82+
83+
// Get the upload ID. This needs to be included with each part sent.
84+
85+
guard let uploadID = multiPartUploadOutput.uploadId else {
86+
throw TransferError.uploadError("Unable to get the upload ID")
87+
}
88+
89+
// Open a file handle and prepare to send the file in chunks. Each chunk
90+
// is 5 MB, which is the minimum size allowed by Amazon S3.
91+
92+
do {
93+
let blockSize = Int(5 * 1024 * 1024)
94+
let fileHandle = try FileHandle(forReadingFrom: fileURL)
95+
let fileSize = try getFileSize(file: fileHandle)
96+
let blockCount = Int(ceil(Double(fileSize) / Double(blockSize)))
97+
var completedParts: [S3ClientTypes.CompletedPart] = []
98+
99+
// Upload the blocks one at as Amazon S3 object parts.
100+
101+
print("Uploading...")
102+
103+
for partNumber in 1...blockCount {
104+
let data: Data
105+
let startIndex = UInt64(partNumber - 1) * UInt64(blockSize)
106+
107+
// Read the block from the file.
108+
109+
data = try readFileBlock(file: fileHandle, startIndex: startIndex, size: blockSize)
110+
111+
let uploadPartInput = UploadPartInput(
112+
body: ByteStream.data(data),
113+
bucket: bucket,
114+
checksumAlgorithm: .sha256,
115+
key: key,
116+
partNumber: partNumber,
117+
uploadId: uploadID
118+
)
119+
120+
// Upload the part with a SHA256 checksum.
121+
122+
do {
123+
let uploadPartOutput = try await s3Client.uploadPart(input: uploadPartInput)
124+
125+
guard let eTag = uploadPartOutput.eTag else {
126+
throw TransferError.uploadError("Missing eTag")
127+
}
128+
guard let checksum = uploadPartOutput.checksumSHA256 else {
129+
throw TransferError.checksumError
130+
}
131+
print("Part \(partNumber) checksum: \(checksum)")
132+
133+
// Append the completed part description (including its
134+
// checksum, ETag, and part number) to the
135+
// `completedParts` array.
136+
137+
completedParts.append(
138+
S3ClientTypes.CompletedPart(
139+
checksumSHA256: checksum,
140+
eTag: eTag,
141+
partNumber: partNumber
142+
)
143+
)
144+
} catch {
145+
throw TransferError.uploadError(error.localizedDescription)
146+
}
147+
}
148+
149+
// Tell Amazon S3 that all parts have been uploaded.
150+
151+
do {
152+
let partInfo = S3ClientTypes.CompletedMultipartUpload(parts: completedParts)
153+
let multiPartCompleteInput = CompleteMultipartUploadInput(
154+
bucket: bucket,
155+
key: key,
156+
multipartUpload: partInfo,
157+
uploadId: uploadID
158+
)
159+
_ = try await s3Client.completeMultipartUpload(input: multiPartCompleteInput)
160+
} catch {
161+
throw TransferError.multipartFinishError(error.localizedDescription)
162+
}
163+
} catch {
164+
throw TransferError.uploadError("Error uploading the file: \(error)")
165+
}
166+
167+
print("Done. Uploaded as \(fileName) in bucket \(bucket).")
168+
}
169+
// snippet-end:[swift.s3.mp-checksums.uploadfile]
170+
171+
// -MARK: - File access
172+
173+
/// Get the size of a file in bytes.
174+
///
175+
/// - Parameter file: `FileHandle` identifying the file to return the size of.
176+
///
177+
/// - Returns: The number of bytes in the file.
178+
func getFileSize(file: FileHandle) throws -> UInt64 {
179+
let fileSize: UInt64
180+
181+
// Get the total size of the file in bytes, then compute the number
182+
// of blocks it will take to transfer the whole file.
183+
184+
do {
185+
try file.seekToEnd()
186+
fileSize = try file.offset()
187+
} catch {
188+
throw TransferError.readError
189+
}
190+
return fileSize
191+
}
192+
193+
/// Read the specified range of bytes from a file and return them in a
194+
/// new `Data` object.
195+
///
196+
/// - Parameters:
197+
/// - file: The `FileHandle` to read from.
198+
/// - startIndex: The index of the first byte to read.
199+
/// - size: The number of bytes to read.
200+
///
201+
/// - Returns: A new `Data` object containing the specified range of bytes.
202+
///
203+
/// - Throws: `TransferError.readError` if the read fails.
204+
func readFileBlock(file: FileHandle, startIndex: UInt64, size: Int) throws -> Data {
205+
file.seek(toFileOffset: startIndex)
206+
do {
207+
let data = try file.read(upToCount: size)
208+
guard let data else {
209+
throw TransferError.readError
210+
}
211+
return data
212+
} catch {
213+
throw TransferError.readError
214+
}
215+
}
216+
217+
// -MARK: - Asynchronous main code
218+
219+
/// Called by ``main()`` to run the bulk of the example.
220+
func runAsync() async throws {
221+
try await uploadFile(file: file, bucket: bucket,
222+
key: key)
223+
}
224+
}
225+
226+
// -MARK: - Entry point
227+
228+
/// The program's asynchronous entry point.
229+
@main
230+
struct Main {
231+
static func main() async {
232+
let args = Array(CommandLine.arguments.dropFirst())
233+
234+
do {
235+
let command = try ExampleCommand.parse(args)
236+
try await command.runAsync()
237+
} catch let error as TransferError {
238+
print("ERROR: \(error.errorDescription ?? "Unknown error")")
239+
} catch {
240+
ExampleCommand.exit(withError: error)
241+
}
242+
}
243+
}

swift/example_code/s3/multipart-upload/Sources/entry.swift

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
// snippet-start:[swift.s3.multipart-upload.imports]
88
import ArgumentParser
9-
import AsyncHTTPClient
109
import AWSClientRuntime
1110
import AWSS3
1211
import Foundation
@@ -38,6 +37,14 @@ struct ExampleCommand: ParsableCommand {
3837

3938
// -MARK: - File uploading
4039

40+
/// Upload a file to Amazon S3.
41+
///
42+
/// - Parameters:
43+
/// - file: The path of the local file to upload to Amazon S3.
44+
/// - bucket: The name of the bucket to upload the file into.
45+
/// - key: The key (name) to give the object on Amazon S3.
46+
///
47+
/// - Throws: Errors from `TransferError`
4148
func uploadFile(file: String, bucket: String, key: String?) async throws {
4249
let fileURL = URL(fileURLWithPath: file)
4350
let fileName: String
@@ -126,13 +133,12 @@ struct ExampleCommand: ParsableCommand {
126133
func startMultipartUpload(client: S3Client, bucket: String, key: String) async throws -> String {
127134
let multiPartUploadOutput: CreateMultipartUploadOutput
128135

129-
// First, create the multi-part upload, using SHA256 checksums.
136+
// First, create the multi-part upload.
130137

131138
do {
132139
multiPartUploadOutput = try await client.createMultipartUpload(
133140
input: CreateMultipartUploadInput(
134141
bucket: bucket,
135-
checksumAlgorithm: .sha256,
136142
key: key
137143
)
138144
)
@@ -171,25 +177,20 @@ struct ExampleCommand: ParsableCommand {
171177
let uploadPartInput = UploadPartInput(
172178
body: ByteStream.data(data),
173179
bucket: bucket,
174-
checksumAlgorithm: .sha256,
175180
key: key,
176181
partNumber: partNumber,
177182
uploadId: uploadID
178183
)
179184

180-
// Upload the part with a SHA256 checksum.
185+
// Upload the part.
181186
do {
182187
let uploadPartOutput = try await client.uploadPart(input: uploadPartInput)
183188

184189
guard let eTag = uploadPartOutput.eTag else {
185190
throw TransferError.uploadError("Missing eTag")
186191
}
187-
guard let checksum = uploadPartOutput.checksumSHA256 else {
188-
throw TransferError.checksumError
189-
}
190192

191193
return S3ClientTypes.CompletedPart(
192-
checksumSHA256: checksum,
193194
eTag: eTag,
194195
partNumber: partNumber
195196
)

0 commit comments

Comments
 (0)