---
title: "Files with S3"
description: "Upload, list, and download objects from an S3 bucket."
seo:
  noindex: true
---

{/* Copyright (c) 2026 Betide Studio. All Rights Reserved. */}

Finish [AWS setup](/integrations/aws/setup), create a bucket in the selected region, and allow the caller to access only its intended object keys. An object key is the full name inside the bucket, such as `players/123/save.bin`.

## Download an object

**Blueprint**

<BlueprintViewer title="Download an object" src="/blueprints/crossplay-aws-s3-download.txt" />

**C++**

Game module dependencies: `AWSIKS3`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Generated/Objects/Nodes/AWSIKS3ObjectsNodes02.h"

void DownloadSaveBytes(UGameInstance &GameInstance, const FString &Bucket, const FString &ObjectKey,
                       const FScriptDelegate &OnComplete)
{
    FAWSIKS3GetObjectRequest Request;
    Request.Bucket = Bucket;
    Request.Key = ObjectKey;
    auto *Action = UAWSIKS3GetObject::GetObject(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}
```

Bind `OnComplete` to a `UFUNCTION` taking `const FAWSIKS3GetObjectResult&` and `const FAWSIKS3Error&`. Check `Error.IsError()` first and display `Error.Common.Message` on failure. On success, `Result.Body` contains the bytes; `Result.Body.Num()` gives their count.

**Bucket** is the bucket name, not a URL; **Object Key** is the full key. Download returns bytes in memory for your game to decode—it does not write a local file. S3 is a separate bucket/object API, not an automatic replacement for [Crossplay user files](/crossplay/cloud-saves).

## Upload an object

Pass your serialized save as a byte array. Keep **Body** and **Content Type** enabled on Make Request; C++ sets their `bSet...` flags. An empty byte array uploads an empty object.

**Blueprint**

<BlueprintViewer title="Upload an object" src="/blueprints/crossplay-aws-s3-upload.txt" />

**C++**

Game module dependencies: `AWSIKS3`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Generated/Objects/Nodes/AWSIKS3ObjectsNodes04.h"

void UploadSaveBytes(UGameInstance &GameInstance, const FString &Bucket, const FString &ObjectKey,
                     const TArray<uint8> &Bytes, const FScriptDelegate &OnComplete)
{
    FAWSIKS3PutObjectRequest Request;
    Request.Bucket = Bucket;
    Request.Key = ObjectKey;
    Request.bSetBody = true;
    Request.Body = Bytes;
    Request.bSetContentType = true;
    Request.ContentType = TEXT("application/octet-stream");
    auto *Action = UAWSIKS3PutObject::PutObject(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}
```

Bind `OnComplete` to a `UFUNCTION` taking `const FAWSIKS3PutObjectResult&` and `const FAWSIKS3Error&`. Report completion only when `Error.IsError()` is false; otherwise display `Error.Common.Message`.

Writing the same key replaces its current object. Keep ownership checks in your access policy; a client-supplied player ID alone does not protect another player's files.

<Accordion>
  <AccordionItem title="Listing, metadata, and larger transfers">

Use **Amazon S3 List Objects V2** with the desired prefix. Follow `NextContinuationToken` while the response is truncated. The C++ helper `List(Bucket, Prefix, Callback)` follows pages and returns `TArray<FUCIKCloudObject>` with key, size, ETag, and modification time. It fails if listing exceeds 100 pages or 10,000 objects; use raw pagination for larger listings.

The helper uploads in 8 MiB parts for larger payloads. A failed multipart result can carry a `ResumeToken` in `Result.Value`; preserve it with the same payload and destination before retrying. Progress is reported at completed operations/parts, not continuously for every byte. The helper has no cancellation handle; use the generated multipart operations when your game needs explicit upload/abort control.

Use generated request metadata fields for content type and custom metadata. `GetSignedUrl(Bucket, Key, bUpload, Lifetime, Callback)` produces a temporary URL using the caller's permissions; do not log or publicly share it. Both the helper and these raw Get/Put nodes keep transfer data in memory, so size files for your target device. Raw Put Object is a single request; it does not use the helper's multipart flow.

  </AccordionItem>
</Accordion>

## List player objects

Pass a prefix such as `players/123/`, including the trailing slash. The graph prints each object's key and byte count, follows continuation tokens, and rejects overlapping listings. Add Boolean **ObjectListInProgress**, default `false`.

**Blueprint**

<BlueprintViewer title="List player objects" src="/blueprints/crossplay-aws-s3-list.txt" />

**C++**

Game module dependencies: `AWSIKS3`, `BetideCore`, `Engine`. Add these members to your Game Instance, or use this class as the project's Game Instance.

```cpp
// S3ListGameInstance.h
#pragma once

#include "Engine/GameInstance.h"
#include "Generated/Objects/Nodes/AWSIKS3ObjectsNodes04.h"
#include "S3ListGameInstance.generated.h"

UCLASS()
class US3ListGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void ListPlayerObjects(const FString &Bucket, const FString &Prefix);

  private:
    bool bObjectListInProgress = false;
    FAWSIKS3ListObjectsV2Request CurrentRequest;
    void ReadObjectPage();
    UFUNCTION()
    void OnObjectPage(const FAWSIKS3ListObjectsV2Result &Result, const FAWSIKS3Error &Error);
};
```

```cpp
// S3ListGameInstance.cpp
#include "S3ListGameInstance.h"
#include "AWSIKS3.h"

void US3ListGameInstance::ListPlayerObjects(const FString &Bucket, const FString &Prefix)
{
    if (bObjectListInProgress)
    {
        UE_LOG(LogAWSIKS3, Warning, TEXT("Object list already running"));
        return;
    }
    bObjectListInProgress = true;
    CurrentRequest = FAWSIKS3ListObjectsV2Request();
    CurrentRequest.Bucket = Bucket;
    CurrentRequest.bSetPrefix = true;
    CurrentRequest.Prefix = Prefix;
    CurrentRequest.bSetMaxKeys = true;
    CurrentRequest.MaxKeys = 100;
    ReadObjectPage();
}

void US3ListGameInstance::ReadObjectPage()
{
    auto *Action = UAWSIKS3ListObjectsV2::ListObjectsV2(this, CurrentRequest);
    Action->OnSuccess.AddDynamic(this, &US3ListGameInstance::OnObjectPage);
    Action->OnFailure.AddDynamic(this, &US3ListGameInstance::OnObjectPage);
    Action->Activate();
}

void US3ListGameInstance::OnObjectPage(const FAWSIKS3ListObjectsV2Result &Result,
                                       const FAWSIKS3Error &Error)
{
    if (Error.IsError())
    {
        bObjectListInProgress = false;
        UE_LOG(LogAWSIKS3, Warning, TEXT("%s"), *Error.Common.Message);
        return;
    }
    for (const FAWSIKS3Object &Object : Result.Contents)
    {
        UE_LOG(LogAWSIKS3, Log, TEXT("%s (%lld bytes)"), *Object.Key, Object.Size);
    }
    if (!Result.IsTruncated)
    {
        bObjectListInProgress = false;
        UE_LOG(LogAWSIKS3, Log, TEXT("All objects listed"));
        return;
    }
    if (Result.NextContinuationToken.IsEmpty() ||
        Result.NextContinuationToken.Equals(CurrentRequest.ContinuationToken,
                                            ESearchCase::CaseSensitive))
    {
        bObjectListInProgress = false;
        UE_LOG(LogAWSIKS3, Warning, TEXT("Truncated listing returned an empty or repeated token"));
        return;
    }
    CurrentRequest.bSetContinuationToken = true;
    CurrentRequest.ContinuationToken = Result.NextContinuationToken;
    ReadObjectPage();
}
```

A prefix filters names; it does not grant access. Scope `s3:ListBucket` in your access policy. Keep the continuation token unchanged and use **Is Truncated**, not the page's item count, to decide whether to continue. [List Objects V2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html).

## Delete one object

Call this after the player chooses the object to remove. It deletes exactly the supplied key and does not set **Version Id**.

**Blueprint**

<BlueprintViewer title="Delete one object" src="/blueprints/crossplay-aws-s3-delete.txt" />

**C++**

Game module dependencies: `AWSIKS3`, `BetideCore`, `Engine`.

```cpp
#include "Engine/GameInstance.h"
#include "Generated/Objects/Nodes/AWSIKS3ObjectsNodes01.h"

void DeleteSelectedObject(UGameInstance &GameInstance, const FString &Bucket,
                          const FString &ObjectKey, const FScriptDelegate &OnComplete)
{
    FAWSIKS3DeleteObjectRequest Request;
    Request.Bucket = Bucket;
    Request.Key = ObjectKey;
    auto *Action = UAWSIKS3DeleteObject::DeleteObject(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}
```

Bind `OnComplete` to a `UFUNCTION` taking `const FAWSIKS3DeleteObjectResult&` and `const FAWSIKS3Error&`. Report success only when `Error.IsError()` is false; otherwise display `Error.Common.Message`.

Without versioning, deletion is permanent. With versioning enabled, this request adds a delete marker; older versions remain. Versioning-suspended buckets can also lose the null version. To remove a specific version permanently, enable **Version Id** and supply it, with `s3:DeleteObjectVersion` permission. [Delete Object behavior](https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObject.html).

## Multipart uploads

This example uploads two consecutive byte arrays as one object. **First Part** must contain at least 5 MiB; **Final Part** must be nonempty. Both chunks stay in memory. For more parts, keep consecutive numbers starting at `1` and complete only after every part succeeds. [Part limits](https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html).

**Blueprint**

Use a Game Instance Blueprint with these variables:

| Variables | Type | Default |
| --- | --- | --- |
| MultipartBusy | Boolean | false |
| MultipartBucket, MultipartKey, MultipartUploadId | String | empty |
| FirstPartData, FinalPartData | Byte Array | empty |

<BlueprintViewer title="Upload two parts with failure cleanup" src="/blueprints/crossplay-aws-s3-multipart.txt" />

**C++**

Game module dependencies: `AWSIKS3`, `BetideCore`, `Engine`. Add this flow to your Game Instance, or use the class below.

```cpp
// S3MultipartGameInstance.h
#pragma once

#include "Engine/GameInstance.h"
#include "Generated/Objects/Nodes/AWSIKS3ObjectsNodes01.h"
#include "Generated/Objects/Nodes/AWSIKS3ObjectsNodes06.h"
#include "S3MultipartGameInstance.generated.h"

UCLASS()
class US3MultipartGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void UploadTwoParts(const FString &Bucket, const FString &ObjectKey,
                        const TArray<uint8> &FirstPart, const TArray<uint8> &FinalPart);
    UFUNCTION(BlueprintCallable)
    void RetryMultipartCleanup();

  private:
    bool bBusy = false;
    FString UploadBucket, UploadKey, UploadId;
    TArray<uint8> FirstData, FinalData;
    TArray<FAWSIKS3CompletedPart> CompletedParts;
    int32 PartNumber = 1;
    void SendPart();
    void CompleteUpload();
    void AbortUpload();
    void ReleaseBuffers();
    UFUNCTION()
    void OnCreated(const FAWSIKS3CreateMultipartUploadResult &Result, const FAWSIKS3Error &Error);
    UFUNCTION()
    void OnPart(const FAWSIKS3UploadPartResult &Result, const FAWSIKS3Error &Error);
    UFUNCTION()
    void OnCompleted(const FAWSIKS3CompleteMultipartUploadResult &Result,
                     const FAWSIKS3Error &Error);
    UFUNCTION()
    void OnAborted(const FAWSIKS3AbortMultipartUploadResult &Result, const FAWSIKS3Error &Error);
};
```

```cpp
// S3MultipartGameInstance.cpp
#include "S3MultipartGameInstance.h"
#include "AWSIKS3.h"

void US3MultipartGameInstance::UploadTwoParts(const FString &Bucket, const FString &ObjectKey,
                                              const TArray<uint8> &FirstPart,
                                              const TArray<uint8> &FinalPart)
{
    if (bBusy || !UploadId.IsEmpty())
    {
        UE_LOG(LogAWSIKS3, Warning, TEXT("Upload active or cleanup required"));
        return;
    }
    if (FirstPart.Num() < 5 * 1024 * 1024 || FinalPart.IsEmpty())
    {
        UE_LOG(LogAWSIKS3, Warning,
               TEXT("First Part needs at least 5 MiB; Final Part must not be empty"));
        return;
    }
    bBusy = true;
    UploadBucket = Bucket;
    UploadKey = ObjectKey;
    FirstData = FirstPart;
    FinalData = FinalPart;
    CompletedParts.Reset(2);
    PartNumber = 1;
    FAWSIKS3CreateMultipartUploadRequest Request;
    Request.Bucket = UploadBucket;
    Request.Key = UploadKey;
    Request.bSetContentType = true;
    Request.ContentType = TEXT("application/octet-stream");
    Request.bSetChecksumAlgorithm = true;
    Request.ChecksumAlgorithm = EAWSIKS3ChecksumAlgorithm::SHA256;
    Request.bSetChecksumType = true;
    Request.ChecksumType = EAWSIKS3ChecksumType::COMPOSITE;
    auto *Action = UAWSIKS3CreateMultipartUpload::CreateMultipartUpload(this, Request);
    Action->OnSuccess.AddDynamic(this, &US3MultipartGameInstance::OnCreated);
    Action->OnFailure.AddDynamic(this, &US3MultipartGameInstance::OnCreated);
    Action->Activate();
}

void US3MultipartGameInstance::OnCreated(const FAWSIKS3CreateMultipartUploadResult &Result,
                                         const FAWSIKS3Error &Error)
{
    if (Error.IsError())
    {
        UE_LOG(LogAWSIKS3, Warning, TEXT("%s"), *Error.Common.Message);
        ReleaseBuffers();
        return;
    }
    UploadId = Result.UploadId;
    SendPart();
}

void US3MultipartGameInstance::SendPart()
{
    FAWSIKS3UploadPartRequest Request;
    Request.Bucket = UploadBucket;
    Request.Key = UploadKey;
    Request.UploadId = UploadId;
    Request.PartNumber = PartNumber;
    Request.bSetBody = true;
    Request.Body = PartNumber == 1 ? FirstData : FinalData;
    Request.bSetChecksumAlgorithm = true;
    Request.ChecksumAlgorithm = EAWSIKS3ChecksumAlgorithm::SHA256;
    auto *Action = UAWSIKS3UploadPart::UploadPart(this, Request);
    Action->OnSuccess.AddDynamic(this, &US3MultipartGameInstance::OnPart);
    Action->OnFailure.AddDynamic(this, &US3MultipartGameInstance::OnPart);
    Action->Activate();
}

void US3MultipartGameInstance::OnPart(const FAWSIKS3UploadPartResult &Result,
                                      const FAWSIKS3Error &Error)
{
    if (Error.IsError())
    {
        UE_LOG(LogAWSIKS3, Warning, TEXT("%s"), *Error.Common.Message);
        AbortUpload();
        return;
    }
    FAWSIKS3CompletedPart Part;
    Part.bSetPartNumber = true;
    Part.PartNumber = PartNumber;
    Part.bSetETag = true;
    Part.ETag = Result.ETag;
    Part.bSetChecksumSHA256 = true;
    Part.ChecksumSHA256 = Result.ChecksumSHA256;
    CompletedParts.Add(MoveTemp(Part));
    if (PartNumber == 1)
    {
        PartNumber = 2;
        SendPart();
        return;
    }
    CompleteUpload();
}

void US3MultipartGameInstance::CompleteUpload()
{
    FAWSIKS3CompleteMultipartUploadRequest Request;
    Request.Bucket = UploadBucket;
    Request.Key = UploadKey;
    Request.UploadId = UploadId;
    Request.bSetMultipartUpload = true;
    Request.MultipartUpload.bSetParts = true;
    Request.MultipartUpload.Parts = CompletedParts;
    Request.bSetChecksumType = true;
    Request.ChecksumType = EAWSIKS3ChecksumType::COMPOSITE;
    auto *Action = UAWSIKS3CompleteMultipartUpload::CompleteMultipartUpload(this, Request);
    Action->OnSuccess.AddDynamic(this, &US3MultipartGameInstance::OnCompleted);
    Action->OnFailure.AddDynamic(this, &US3MultipartGameInstance::OnCompleted);
    Action->Activate();
}

void US3MultipartGameInstance::OnCompleted(const FAWSIKS3CompleteMultipartUploadResult &Result,
                                           const FAWSIKS3Error &Error)
{
    if (Error.IsError())
    {
        UE_LOG(LogAWSIKS3, Warning, TEXT("%s"), *Error.Common.Message);
        AbortUpload();
        return;
    }
    UploadId.Reset();
    ReleaseBuffers();
    UE_LOG(LogAWSIKS3, Log, TEXT("Multipart upload complete"));
}

void US3MultipartGameInstance::AbortUpload()
{
    bBusy = true;
    FAWSIKS3AbortMultipartUploadRequest Request;
    Request.Bucket = UploadBucket;
    Request.Key = UploadKey;
    Request.UploadId = UploadId;
    auto *Action = UAWSIKS3AbortMultipartUpload::AbortMultipartUpload(this, Request);
    Action->OnSuccess.AddDynamic(this, &US3MultipartGameInstance::OnAborted);
    Action->OnFailure.AddDynamic(this, &US3MultipartGameInstance::OnAborted);
    Action->Activate();
}

void US3MultipartGameInstance::OnAborted(const FAWSIKS3AbortMultipartUploadResult &Result,
                                         const FAWSIKS3Error &Error)
{
    ReleaseBuffers();
    if (Error.IsError())
    {
        UE_LOG(LogAWSIKS3, Warning, TEXT("%s"), *Error.Common.Message);
        if (Error.ExceptionName.Equals(TEXT("NoSuchUpload"), ESearchCase::CaseSensitive))
        {
            UploadId.Reset();
            UE_LOG(LogAWSIKS3, Warning,
                   TEXT("Upload no longer active; verify the object if completion failed"));
        }
        else
        {
            UE_LOG(LogAWSIKS3, Warning,
                   TEXT("Cleanup failed; keep the upload ID and retry cleanup"));
        }
        return;
    }
    UploadId.Reset();
    UE_LOG(LogAWSIKS3, Log, TEXT("Multipart upload aborted"));
}

void US3MultipartGameInstance::RetryMultipartCleanup()
{
    if (bBusy)
    {
        UE_LOG(LogAWSIKS3, Warning, TEXT("Wait for the active request to finish"));
        return;
    }
    if (UploadId.IsEmpty())
    {
        UE_LOG(LogAWSIKS3, Log, TEXT("No multipart upload needs cleanup"));
        return;
    }
    AbortUpload();
}

void US3MultipartGameInstance::ReleaseBuffers()
{
    FirstData.Empty();
    FinalData.Empty();
    CompletedParts.Empty();
    bBusy = false;
}
```

Keep the Make nodes' checksum and manifest fields enabled. The flow requests **SHA256 / COMPOSITE** and forwards each part's returned **ETag** and **Checksum SHA256**, unchanged, into the ordered completion list. [Multipart checksums](https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity-upload.html).

A part or completion failure triggers abort. If cleanup fails, the bucket, key, and upload ID remain available; call **Retry Multipart Cleanup** after the active request finishes. It retries cleanup, not the data transfer. `NoSuchUpload` clears the inactive ID. If the completion response was lost, verify the destination object before starting another upload. [Abort behavior](https://docs.aws.amazon.com/AmazonS3/latest/API/API_AbortMultipartUpload.html).

This state lives in the Game Instance. Configure an **AbortIncompleteMultipartUpload** bucket lifecycle rule for uploads abandoned by a crash or shutdown. Aborting removes uploaded parts, not an already completed object. [Multipart lifecycle](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html).
