diff --git a/swift/example_code/s3/presigned-urls/Package.swift b/swift/example_code/s3/presigned-urls/Package.swift index 3e22234df99..f99409cecbe 100644 --- a/swift/example_code/s3/presigned-urls/Package.swift +++ b/swift/example_code/s3/presigned-urls/Package.swift @@ -22,17 +22,31 @@ let package = Package( .package( url: "https://github.com/apple/swift-argument-parser.git", branch: "main" + ), + .package( + url: "https://github.com/swift-server/async-http-client.git", + from: "1.9.0" ) ], 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. + // Targets can depend on other targets in this package and products + // from dependencies. .executableTarget( - name: "presigned", + name: "presigned-download", dependencies: [ .product(name: "AWSS3", package: "aws-sdk-swift"), .product(name: "ArgumentParser", package: "swift-argument-parser"), ], - path: "Sources") + path: "Sources/presigned-download"), + .executableTarget( + name: "presigned-upload", + dependencies: [ + .product(name: "AWSS3", package: "aws-sdk-swift"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "AsyncHTTPClient", package: "async-http-client") + ], + path: "Sources/presigned-upload") + ] ) diff --git a/swift/example_code/s3/presigned-urls/Sources/TransferDirection.swift b/swift/example_code/s3/presigned-urls/Sources/TransferDirection.swift deleted file mode 100644 index b73ab07f1ae..00000000000 --- a/swift/example_code/s3/presigned-urls/Sources/TransferDirection.swift +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -import Foundation -import ArgumentParser - -/// Flags used to identify whether the file is to be uploaded or downloaded. -enum TransferDirection: String, EnumerableFlag { - /// The file transfer is an upload. - case up - /// The file transfer is a download. - case down -} diff --git a/swift/example_code/s3/presigned-urls/Sources/entry.swift b/swift/example_code/s3/presigned-urls/Sources/entry.swift deleted file mode 100644 index 1c108a009fe..00000000000 --- a/swift/example_code/s3/presigned-urls/Sources/entry.swift +++ /dev/null @@ -1,500 +0,0 @@ -// 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 -/// perform file uploads and downloads to Amazon S3, both with and -/// without presigned requests. Also included is code to perform -/// multi-part uploads. - -// snippet-start:[swift.s3.presigned.imports] -import ArgumentParser -import AWSClientRuntime -import AWSS3 -import Foundation -import Smithy -import SmithyHTTPAPI -// snippet-end:[swift.s3.presigned.imports] - -// -MARK: - Async command line tool - -struct ExampleCommand: ParsableCommand { - // -MARK: Command arguments - @Flag(help: "Transfer direction of the file transfer (up or down)") - var direction: TransferDirection - @Flag(help: "Enable multi-part upload (default: no)") - var multiPart: Bool = false - @Option(help: "Source path or Amazon S3 file path") - var source: String - @Option(help: "Destination path or Amazon S3 file path") - var dest: String? - @Option(help: "Name of the Amazon S3 bucket containing the file") - var bucket: 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: "presigned", - abstract: """ - This example shows how to use presigned requests when transferring files - using Amazon Simple Storage Service (Amazon S3). In addition, you can - optionally use a flag on the command line to use multi-part uploads. - """, - discussion: """ - """ - ) - - // -MARK: - File upload - - // snippet-start:[swift.s3.presigned.presign-upload-file] - /// Upload the specified file to the Amazon S3 bucket with the given name. - /// - /// - Parameters: - /// - sourcePath: The pathname of the source file on a local disk. - /// - bucket: The Amazon S3 bucket to store the file into. - /// - key: The key (name) to give the uploaded file. The filename of the - /// source is used by default. - /// - /// - Throws: `TransferError.uploadError` - func uploadFile(sourcePath: String, bucket: String, key: String?) async throws { - let fileURL = URL(fileURLWithPath: sourcePath) - let fileName: String - - // If no key was provided, use the last component of the filename. - - if key == nil { - fileName = fileURL.lastPathComponent - } else { - fileName = key! - } - - print("Uploading file from \(fileURL.absoluteString) to \(bucket)/\(fileName).") - - // Create the presigned request for the `PostObject` request. - - // snippet-start:[swift.s3.presigned.presign-PutObject] - let fileData = try Data(contentsOf: fileURL) - let dataStream = ByteStream.data(fileData) - - let config = try await S3Client.S3ClientConfiguration(region: region) - let s3Client = S3Client(config: config) - let putInput = PutObjectInput( - body: dataStream, - bucket: bucket, - key: fileName - ) - - // Presign the `PutObject` request with a 5-minute expiration. The - // presigned `URLRequest` can then be sent using the URL Loading System - // (https://developer.apple.com/documentation/foundation/url_loading_system/uploading_data_to_a_website). - - let presignedRequest: URLRequest - do { - presignedRequest = try await s3Client.presignedRequestForPutObject(input: putInput, expiration: TimeInterval(5 * 60)) - } catch { - throw TransferError.signingError - } - // snippet-end:[swift.s3.presigned.presign-PutObject] - - // Send the HTTP request and upload the file to Amazon S3. - - try await httpSendFileRequest(request: presignedRequest, source: fileURL) - } - // snippet-end:[swift.s3.presigned.presign-upload-file] - - // -MARK: Multi-part upload - - // snippet-start:[swift.s3.presigned.upload-multipart] - /// Upload the specified file into the given bucket using a multi-part upload. - /// - /// - Parameters: - /// - sourcePath: The pathname of the source file to upload. - /// - bucket: The name of the bucket to store the file into. - /// - key: The name to give the file in the bucket. If not specified, the - /// file's name is used. - /// - Throws: `TransferError.uploadError`, `TransferError.readError` - func uploadFileMultipart(sourcePath: String, bucket: String, key: String?) async throws { - let source = URL(fileURLWithPath: sourcePath) - let fileName: String - - // If the key isn't specified, use the last component of the path instead. - - if key == nil { - fileName = source.lastPathComponent - } else { - fileName = key! - } - - var completedParts: [S3ClientTypes.CompletedPart] = [] - - // First, create the multi-part upload. - - let uploadID = try await startMultipartUpload(bucket: bucket, key: fileName) - - // Open a file handle and prepare to send the file in chunks. Each chunk - // is 5 MB since that's the smallest chunk size allowed by Amazon S3. - - let blockSize = Int(5 * 1024 * 1024) - let fileHandle = try FileHandle(forReadingFrom: source) - let fileSize = try getFileSize(file: fileHandle) - let blockCount = Int(ceil(Double(fileSize) / Double(blockSize))) - - // Upload the blocks one at a time as Amazon S3 object parts. - - print("Uploading...") - - // snippet-start:[swift.s3.presigned.presign-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(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.presigned.presign-upload-loop] - - // Finish the upload. - - try await finishMultipartUpload(uploadId: uploadID, bucket: bucket, - key: fileName, parts: completedParts) - - print("Done. Uploaded as \(fileName) in bucket \(bucket).") - } - // snippet-end:[swift.s3.presigned.upload-multipart] - - // snippet-start:[swift.s3.presigned.presign-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(bucket: String, key: String) async throws -> String { - let multiPartUploadOutput: CreateMultipartUploadOutput - - // First, create the multi-part upload. - - do { - let config = try await S3Client.S3ClientConfiguration(region: region) - let s3Client = S3Client(config: config) - - multiPartUploadOutput = try await s3Client.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.presigned.presign-upload-create] - - // snippet-start:[swift.s3.presigned.presign-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: - /// - 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(uploadId: String, bucket: String, key: String, - parts: [S3ClientTypes.CompletedPart]) async throws { - do { - let config = try await S3Client.S3ClientConfiguration(region: region) - let s3Client = S3Client(config: config) - - let partInfo = S3ClientTypes.CompletedMultipartUpload(parts: parts) - let multiPartCompleteInput = CompleteMultipartUploadInput( - bucket: bucket, - key: key, - multipartUpload: partInfo, - uploadId: uploadId - ) - _ = try await s3Client.completeMultipartUpload(input: multiPartCompleteInput) - } catch { - throw TransferError.multipartFinishError - } - } - // snippet-end:[swift.s3.presigned.presign-upload-complete] - - // snippet-start:[swift.s3.presigned.presign-upload-part] - /// Upload the specified data as part of an Amazon S3 multi-part upload. - /// - /// - Parameters: - /// - 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(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 - ) - - let request: URLRequest - do { - let config = try await S3Client.S3ClientConfiguration() - let s3Client = S3Client(config: config) - - request = try await s3Client.presignedRequestForUploadPart(input: uploadPartInput, expiration: 2 * 60) - } catch { - throw TransferError.signingError - } - - let (_, response) = try await URLSession.shared.upload(for: request, from: data) - guard let response = response as? HTTPURLResponse else { - throw TransferError.uploadError("No response from Amazon S3") - } - - if response.statusCode != 200 { - throw TransferError.uploadError( - "Upload of part \(partNumber) failed with status code \(response.statusCode)" - ) - } else { - let eTag = response.value(forHTTPHeaderField: "ETag") - return S3ClientTypes.CompletedPart(eTag: eTag, partNumber: partNumber) - } - } - // snippet-end:[swift.s3.presigned.presign-upload-part] - - // MARK: Support - - /// 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: - File download - - // snippet-start:[swift.s3.presigned.download-file] - /// Download a file from the specified bucket and store it in the local - /// filesystem. - /// - /// - Parameters: - /// - bucket: The Amazon S3 bucket name from which to retrieve the file. - /// - 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 the destination path is `nil`, create a file URL that will save - // the file with the same name in the user's Downloads directory. - // Otherwise create the file URL directly from the specified destination - // path. - - if destPath == nil { - do { - try fileURL = FileManager.default.url( - for: .downloadsDirectory, - in: .userDomainMask, - appropriateFor: URL(string: key), - create: true - ).appending(component: key) - } catch { - throw TransferError.directoryError - } - } else { - fileURL = URL(fileURLWithPath: destPath!) - } - - let s3Client = try await S3Client() - - // Create a presigned URLRequest with the `GetObject` action. - - // snippet-start:[swift.s3.presigned.getobject] - let getInput = GetObjectInput( - bucket: bucket, - key: key - ) - - let presignedRequest: URLRequest - do { - presignedRequest = try await s3Client.presignedRequestForGetObject( - input: getInput, - expiration: TimeInterval(5 * 60) - ) - } catch { - throw TransferError.signingError - } - // snippet-end:[swift.s3.presigned.getobject] - - // Use the presigned request to fetch the file from Amazon S3 and - // store it at the location given by the `destPath` parameter. - - try await httpFetchRequest(request: presignedRequest, dest: fileURL) - } - // snippet-end:[swift.s3.presigned.download-file] - - // -MARK: - HTTP file transfers - - /// Send a file to S3 using the specified presigned `URLRequest`. - /// - /// - Parameters: - /// - request: A presigned Amazon S3 `URLRequest`. - /// - source: A `URL` indicating the location of the source file. - /// - /// - Throws: `TransferError.uploadError` - func httpSendFileRequest(request: URLRequest, source: URL) async throws { - let (_, response) = try await URLSession.shared.upload(for: request, fromFile: source) - guard let response = response as? HTTPURLResponse else { - throw TransferError.uploadError("No response from Amazon S3") - } - - if response.statusCode != 200 { - throw TransferError.uploadError( - "Upload failed with status code: \(response.statusCode)" - ) - } else { - print("File uploaded to \(source.absoluteString)") - } - } - - /// Use the specified `URLRequest` to download a file. - /// - /// - Parameters: - /// - request: The presigned URLRequest to perform. - /// - dest: The file system URL to relocated the fully downloaded file to. - /// - /// - Throws: `TransferError.downloadError`, `TransferError.writeError`, - /// `TransferError.fileMoveError` - /// - /// The file is first downloaded to the user's Downloads directory, then - /// it's copied to the destination URL. - func httpFetchRequest(request: URLRequest, dest: URL) async throws { - let (fileURL, response) = try await URLSession.shared.download(for: request) - guard let response = response as? HTTPURLResponse else { - throw TransferError.downloadError("No response from Amazon S3") - } - - // If the download was successful, move the file to the destination. - - if response.statusCode == 200 { - do { - try FileManager.default.moveItem(at: fileURL, to: dest) - print("File saved as \(dest.lastPathComponent)") - } catch { - throw TransferError.fileMoveError - } - } else { - print("ERROR: Download failed with HTTP status code: \(response.statusCode)") - throw TransferError.downloadError( - "Download failed with HTTP status code: \(response.statusCode)" - ) - } - } - - // -MARK: - Asynchronous main code - - /// Called by ``main()`` to run the bulk of the example. - func runAsync() async throws { - switch direction { - case .up: - if multiPart == false { - try await uploadFile(sourcePath: source, bucket: bucket, - key: dest) - } else { - try await uploadFileMultipart(sourcePath: source, bucket: bucket, - key: dest) - } - case .down: - try await downloadFile(bucket: bucket, key: source, - 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) - } - } -} diff --git a/swift/example_code/s3/presigned-urls/Sources/TransferError.swift b/swift/example_code/s3/presigned-urls/Sources/presigned-download/TransferError.swift similarity index 100% rename from swift/example_code/s3/presigned-urls/Sources/TransferError.swift rename to swift/example_code/s3/presigned-urls/Sources/presigned-download/TransferError.swift diff --git a/swift/example_code/s3/presigned-urls/Sources/presigned-download/entry.swift b/swift/example_code/s3/presigned-urls/Sources/presigned-download/entry.swift new file mode 100644 index 00000000000..abd01ad9ed3 --- /dev/null +++ b/swift/example_code/s3/presigned-urls/Sources/presigned-download/entry.swift @@ -0,0 +1,238 @@ +// 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 +/// download files, both with and without presigning the requests. + +// snippet-start:[swift.s3.presigned.imports] +import ArgumentParser +import AWSClientRuntime +import AWSS3 +import Foundation +import Smithy +import SmithyHTTPAPI + +// Include FoundationNetworking on non-Apple platforms. + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif +// snippet-end:[swift.s3.presigned.imports] + +// -MARK: - Async command line tool + +struct ExampleCommand: ParsableCommand { + // -MARK: Command arguments + @Flag(help: "Presign the file download request") + var presign = false + @Option(help: "Amazon S3 file path of file to download") + var source: String + @Option(help: "Destination file path") + var dest: String? + @Option(help: "Name of the Amazon S3 bucket to download from") + var bucket: 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: "presigned-download", + abstract: """ + This example shows how to download files from Amazon S3, optionally + with presigning. + """, + discussion: """ + """ + ) + + // snippet-start:[swift.s3.download-file] + /// Download a file from the specified bucket and store it in the local + /// filesystem. + /// + /// - Parameters: + /// - bucket: The Amazon S3 bucket name from which to retrieve the file. + /// - 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 + + print("Downloading the file \(bucket)/\(key)") + + // If the destination path is `nil`, create a file URL that will save + // the file with the same name in the user's Downloads directory. + // Otherwise create the file URL directly from the specified destination + // path. + + 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 s3Client = try await S3Client() + + // Create a presigned URLRequest with the `GetObject` action. + + 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") + } + + guard let data = try await body.readData() else { + throw TransferError.downloadError("Download error") + } + + try data.write(to: fileURL) + } catch { + throw TransferError.downloadError("Error downloading the file: \(error)") + } + + print("File downloaded to \(fileURL.path).") + } + // snippet-end:[swift.s3.download-file] + + // snippet-start:[swift.s3.presigned.download-file] + /// Download a file from the specified bucket and store it in the local + /// filesystem. Demonstrates using a custom configuration when presigning + /// a request. + /// + /// - Parameters: + /// - bucket: The Amazon S3 bucket name from which to retrieve the file. + /// - 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 downloadFilePresigned(bucket: String, key: String, destPath: String?) async throws { + let fileURL: URL + + print("Downloading (with presigning) the file \(bucket)/\(key)") + + // If the destination path is `nil`, create a file URL that will save + // the file with the same name in the user's Downloads directory. + // Otherwise create the file URL directly from the specified destination + // path. + + 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 s3Client = try await S3Client() + + // Create a presigned URLRequest with the `GetObject` action. + + // snippet-start:[swift.s3.presigned.getobject] + let getInput = GetObjectInput( + bucket: bucket, + key: key + ) + + let presignedRequest: URLRequest + do { + presignedRequest = try await s3Client.presignedRequestForGetObject( + input: getInput, + expiration: TimeInterval(5 * 60) + ) + } catch { + throw TransferError.signingError + } + // snippet-end:[swift.s3.presigned.getobject] + + // Use the presigned request to fetch the file from Amazon S3 and + // store it at the location given by the `destPath` parameter. + + let downloadTask = URLSession.shared.downloadTask(with: presignedRequest) { + localURL, downloadResponse, error in + guard let localURL else { + print("Error: no file downloaded") + return + } + + if error != nil { + print("Error downloading file: \(error.debugDescription)") + return + } + + do { + try FileManager.default.moveItem(at: localURL, to: fileURL) + } catch { + print("Error moving file to final location") + return + } + } + downloadTask.resume() + + // Wait for the file to finish downloading. Since there isn't a way to + // cancel provided by this example, the .canceling state isn't + // checked. + + while downloadTask.state != .completed { + sleep(1) + } + + // The download is complete, or has failed. If it's failed, display an + // appropriate error message. + if downloadTask.error != nil { + throw TransferError.downloadError("Error downloading file: \(downloadTask.error.debugDescription)") + } + print("File downloaded to \(fileURL.path).") + } + // snippet-end:[swift.s3.presigned.download-file] + + // -MARK: - Asynchronous main code + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + if presign { + try await downloadFilePresigned(bucket: bucket, key: source, + destPath: dest) + + } else { + try await downloadFile(bucket: bucket, key: source, 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) + } + } +} diff --git a/swift/example_code/s3/presigned-urls/Sources/presigned-upload/TransferError.swift b/swift/example_code/s3/presigned-urls/Sources/presigned-upload/TransferError.swift new file mode 100644 index 00000000000..f05b3640a39 --- /dev/null +++ b/swift/example_code/s3/presigned-urls/Sources/presigned-upload/TransferError.swift @@ -0,0 +1,47 @@ +// 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 { + /// The destination directory for a download is missing or inaccessible. + 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 when completing a multi-part upload to Amazon S3. + case multipartFinishError + /// 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 + /// An error occurred while presigning the URL. + case signingError + /// An error occurred while writing the file's contents. + case writeError + + var errorDescription: String? { + switch self { + case .directoryError: + 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 .multipartFinishError: + return "An error occurred when completing a multi-part upload to Amazon S3." + 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" + case .signingError: + return "An error occurred while pre-signing the URL" + case .writeError: + return "An error occurred while writing the file data" + } + } +} diff --git a/swift/example_code/s3/presigned-urls/Sources/presigned-upload/entry.swift b/swift/example_code/s3/presigned-urls/Sources/presigned-upload/entry.swift new file mode 100644 index 00000000000..824464398e1 --- /dev/null +++ b/swift/example_code/s3/presigned-urls/Sources/presigned-upload/entry.swift @@ -0,0 +1,219 @@ +// 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 +/// small files, both with and without presigning the requests. +/// +/// Note that this example does not support multi-part uploads, so it can only +/// upload small files. + +// snippet-start:[swift.s3.presigned-upload.imports] +import ArgumentParser +import AsyncHTTPClient +import AWSClientRuntime +import AWSS3 +import Foundation +import Smithy +// snippet-end:[swift.s3.presigned-upload.imports] + +// -MARK: - Async command line tool + +struct ExampleCommand: ParsableCommand { + // -MARK: Command arguments + @Flag(help: "Presign the file upload request") + var presign = false + @Option(help: "Path of local file to upload to Amazon S3") + var source: String + @Option(help: "Name of the Amazon S3 bucket to upload to") + var bucket: String + @Option(help: "Destination file path within the bucket") + 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: "presigned-upload", + abstract: """ + This example shows how to upload small files to Amazon S3, optionally + with presigning. + """, + discussion: """ + """ + ) + + // -MARK: - File upload + + // snippet-start:[swift.s3.presigned.upload-file] + func uploadFile(sourcePath: String, bucket: String, key: String?) async throws { + let fileURL = URL(fileURLWithPath: sourcePath) + 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).") + + let fileData = try Data(contentsOf: fileURL) + let dataStream = ByteStream.data(fileData) + + // Use PutObject to send the file to Amazon S3. + + do { + let putInput = PutObjectInput( + body: dataStream, + bucket: bucket, + key: fileName + ) + _ = try await s3Client.putObject(input: putInput) + } catch { + throw TransferError.uploadError("Error uploading file: \(error.localizedDescription)") + } + print("Uploaded \(sourcePath) to \(bucket)/\(fileName).") + } + // snippet-end:[swift.s3.presigned.upload-file] + + // -MARK: - Presigned upload + + // snippet-start:[swift.s3.presigned.presign-upload-file] + /// Upload the specified file to the Amazon S3 bucket with the given name. + /// The request is presigned. + /// + /// - Parameters: + /// - sourcePath: The pathname of the source file on a local disk. + /// - bucket: The Amazon S3 bucket to store the file into. + /// - key: The key (name) to give the uploaded file. The filename of the + /// source is used by default. + /// + /// - Throws: `TransferError.uploadError` + func uploadFilePresigned(sourcePath: String, bucket: String, key: String?) async throws { + let fileURL = URL(fileURLWithPath: sourcePath) + 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) + + // Look to see if the file already exists in the target bucket. + + do { + let headInput = HeadObjectInput(bucket: bucket, key: fileName) + let headResult = try await s3Client.headObject(input: headInput) + + // If HeadObject is successful, the file definitely exists. + + print("File exists with ETag \(headResult.eTag ?? ""). Skipping upload.") + return + } catch let error as AWSServiceError { + switch(error.errorCode) { + case "NotFound": + break + default: + throw TransferError.readError + } + } + + print("Uploading file from \(fileURL.path) to \(bucket)/\(fileName).") + + // Presign the `PutObject` request with a 10-minute expiration. The + // presigned request has a custom configuration that asks for up to + // six attempts to put the file. + // + // The presigned `URLRequest` can then be sent using the URL Loading System + // (https://developer.apple.com/documentation/foundation/url_loading_system/uploading_data_to_a_website). + + // snippet-start:[swift.s3.presigned.presign-PutObject-advanced] + let fileData = try Data(contentsOf: fileURL) + let dataStream = ByteStream.data(fileData) + let presignedURL: URL + + // Create a presigned URL representing the `PutObject` request that + // will upload the file to Amazon S3. If no URL is generated, a + // `TransferError.signingError` is thrown. + + let putConfig = try await S3Client.S3ClientConfiguration( + maxAttempts: 6, + region: region + ) + + do { + let url = try await PutObjectInput( + body: dataStream, + bucket: bucket, + key: fileName + ).presignURL( + config: putConfig, + expiration: TimeInterval(10 * 60) + ) + + guard let url = url else { + throw TransferError.signingError + } + presignedURL = url + } catch { + throw TransferError.signingError + } + // snippet-end:[swift.s3.presigned.presign-PutObject-advanced] + + // Send the HTTP request and upload the file to Amazon S3. + + var request = HTTPClientRequest(url: presignedURL.absoluteString) + request.method = .PUT + request.body = .bytes(fileData) + + _ = try await HTTPClient.shared.execute(request, timeout: .seconds(5*60)) + + print("Uploaded (presigned) \(sourcePath) to \(bucket)/\(fileName).") + } + // snippet-end:[swift.s3.presigned.presign-upload-file] + + // -MARK: - Asynchronous main code + + /// Called by ``main()`` to run the bulk of the example. + func runAsync() async throws { + if presign { + try await uploadFilePresigned(sourcePath: source, bucket: bucket, + key: dest) + } else { + try await uploadFile(sourcePath: source, bucket: bucket, + key: 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) + } + } +}