Soto icon

Soto

Getting Started

Before you start you are going to need an Amazon Web Services (AWS) account, an Identity and Access Management (IAM) user account and access key credentials. The credentials identify the user or role trying to access AWS resources. AWS provide enough documentation on creating users and credentials so I won't repeat that here.

Creating an AWSClient

Once you have your credentials (accessKeyId and secretAccessKey) you are ready to use Soto. First we need to create an AWSClient. The AWSClient does all the communication with AWS. In this situation we are going to supply the credentials directly to the client, but there are various methods for supplying credentials. You can read more about them here.

let awsClient = AWSClient(
    credentialProvider: .static(accessKeyId: myAccessKeyId, secretAccessKey: mySecretAccessKey),
    httpClientProvider: .createNew
)

You will notice the second parameter httpClientProvider. This indicates where the client should acquire the HTTP client it uses. In this example we have asked that it create its own HTTP client.

Create a service object

In our sample we are going to create an S3 bucket, upload some text to a file in the bucket, download it and then delete the file and bucket. To work with S3 we need to import the SotoS3 library and create an S3 service object. While the AWSClient does the actual communication with AWS, the service object provides the configuration and APIs for communicating with the service, in this case S3.

import SotoS3

let s3 = S3(client: awsClient, region: .euwest1)

The S3 object is initialized with the AWSClient we created earlier and the AWS region you want to work in.

Creating an S3 Bucket

The first action is to create the S3 bucket. You create a request object first and then call the createBucket function with that request object. This is the standard pattern for all the Soto functions.

let bucketName = "soto-getting-started-bucket"
let createBucketRequest = S3.CreateBucketRequest(bucket: bucketName)
let createBucketFuture = s3.createBucket(createBucketRequest)

The S3.createBucket function returns immediately with a SwiftNIO EventLoopFuture. This is not the result of the createBucket operation. It is a structure that will be updated with the result when it is available. A simple way to get the result is to call wait() on this EventLoopFuture. This removes all the advantages of the asynchronous features of SwiftNIO but will simplify this article. The wait function is throwing, as the operation you are waiting on may throw an error. The error you are most likely to hit is S3ErrorType.bucketAlreadyExists. You need to think of a unique name for your S3 bucket.

_ = try createBucketFuture.wait()

In this example we don't need to know the contents of the response, just the fact that it has finished.

Upload an object to your bucket

We now upload some text to a file in your S3 bucket. To upload to a file called lorum.txt in our S3 bucket we use S3.putObject.

let lorumText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed..."
let putObjectRequest = S3.PutObjectRequest(
    body: .string(lorumText), 
    bucket: bucketName, 
    key: "lorum.txt"
)
_ = try s3.putObject(putObjectRequest).wait()

Here we use a String as source for the object, but you can upload a Foundation Data with .data(), a SwiftNIO ByteBuffer with .byteBuffer(), stream data for an upload using .stream() or stream data from a file handle using .fileHandle().

Download your object

To download the object from your S3 bucket we use S3.getObject.

let getObjectRequest = S3.GetObjectRequest(bucket: bucketName, key: "lorum.txt")
let response = try s3.getObject(getObjectRequest).wait()
let body = response.body?.asString()
assert(body == lorumText)

This time we do care about the response from the S3.getObject operation so we store it in response. The response object is of type S3.GetObjectOutput. The contents of the file downloaded can be found in S3.GetObjectOutput.body. This is an AWSPayload which can be converted to the format you require. Above we use asString() to convert it back to a string and then we compare it with the original string to ensure they are the same.

Delete everything

At the end we want to delete the bucket. Before we delete the bucket we have to delete its contents ie the object we uploaded to it.

let deleteObjectRequest = S3.DeleteObjectRequest(bucket: bucketName, key: "lorum.txt")
_ = try s3.deleteObject(putObjectRequest).wait()
let deleteBucketRequest = S3.DeleteBucketRequest(bucket: bucketName)
try s3.deleteBucket(deleteBucketRequest).wait()

And there you have it, a simple, although fairly useless operation. It should demonstrate calling AWS service operations with Soto is easy.

Asynchronous operations

The above example is a very inefficient way to use the Soto libraries. After each call we are blocking the thread waiting on the results of that call. There are a couple of ways to improve on this. You can either chain all your operations together using a number of methods from EventLoopFuture, or you can use the new Swift concurrency implementations of the API functions.

Chaining

If you are already running within a framework using SwiftNIO, for instance Vapor, and are running on an EventLoop then you will not be able to use wait().

The code below is an asynchronous version of what we wrote above.

let bucketName = "soto-getting-started-bucket"
let lorumText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed..."

let request = S3.CreateBucketRequest(bucket: bucketName)
let futureResponse = s3.createBucket(request)
    .flatMap { _ -> EventLoopFuture<S3.PutObjectOutput> in
        let request = S3.PutObjectRequest(
            body: .string(lorumText),
            bucket: bucketName,
            key: "lorum.txt"
        )
        return s3.putObject(request)
    }
    .flatMap { _ -> EventLoopFuture<S3.GetObjectOutput> in
        let request = S3.GetObjectRequest(bucket: bucketName, key: "lorum.txt")
        return s3.getObject(request)
    }
    .map { response -> Void in
        // we have the contents of the file, verify it
        // against the original string
        let body = response.body?.asString()
        assert(body == lorumText)
    }
    .flatMap { _ -> EventLoopFuture<S3.DeleteObjectOutput> in
        let request = S3.DeleteObjectRequest(bucket: bucketName, key: "lorum.txt")
        return s3.deleteObject(request)
    }
    .flatMap { _ -> EventLoopFuture<Void> in
        let request = S3.DeleteBucketRequest(bucket: bucketName)
        return s3.deleteBucket(request)
    }
// don't block thread, instead run closure when operation is
// complete
futureResponse.whenComplete { result in
    switch result {
    case .failure(let error):
        print("Failed \(error)")
    case .success:
        print("Success!")
    }
}

Above we use

  • EventLoopFuture.flatMap to provide a new EventLoopFuture when an EventLoopFuture is fulfilled. For instance when the putObject operation finishes we return a new EventLoopFuture for the getObject operation.
  • EventLoopFuture.map to process and transform the result of a fulfilled EventLoopFuture. Above we use map to verify the result of the getObject operation and transform it into Void as we don't need it after this.

If you are going to use the Swift NIO EventLoopFuture interfaces then to get the most out of Soto, it is recommended you spend some time learning how Swift NIO works. You can find documentation for it here.

Async/Await

With the release of Soto v5.9.0 we have added support for Swift concurrency. This makes working with the Soto commands considerably easier. The above Swift NIO code can be replaced with the following

let bucketName = "soto-getting-started-bucket"
let lorumText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed..."
do {
    let request = S3.CreateBucketRequest(bucket: bucketName)
    _ = try await s3.createBucket(request)
    
    let putRequest = S3.PutObjectRequest(
        body: .string(lorumText),
        bucket: bucketName,
        key: "lorum.txt"
    )
    _ = try await s3.putObject(putRequest)
    
    let getRequest = S3.GetObjectRequest(bucket: bucketName, key: "lorum.txt")
    let getResponse = try await s3.getObject(getRequest)
    
    let body = getResponse.body?.asString()
    assert(body == lorumText)
    
    let deleteObjectRequest = S3.DeleteObjectRequest(bucket: bucketName, key: "lorum.txt")
    _ = try await s3.deleteObject(deleteObjectRequest)
    
    let deleteBucketRequest = S3.DeleteBucketRequest(bucket: bucketName)
    _ = try await s3.deleteBucket(deleteBucketRequest)

    print("Success!")
} catch {
    print("Failed \(error)")
}