-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
27
swift/example_code/s3/multipart-upload/Sources/TransferError.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
296
swift/example_code/s3/multipart-upload/Sources/entry.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |