Skip to content

Commit 8f1411d

Browse files
authored
Checksums in S3 example updates (#7217)
* Checksums in S3 example updates Added a SHA256 checksum to the multipart upload example. Added a new basic example showing the use of a checksum while doing a basic PutObject. Other cleanup. * 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 8b17f93 commit 8f1411d

File tree

9 files changed

+527
-31
lines changed

9 files changed

+527
-31
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+
}
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: "checksums",
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: "checksums",
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+
)

0 commit comments

Comments
 (0)