Skip to content
UCIK Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Files with S3

Upload, list, and download objects from an S3 bucket.

Finish 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

Download an object
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: AWSIKS3, BetideCore, Engine.

#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.

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.

Upload an object
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: AWSIKS3, BetideCore, Engine.

#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.

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.

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.

List player objects
Preparing your graph…

Loading the interactive viewer.

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

// 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);
};
// 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.

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.

Delete one object
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: AWSIKS3, BetideCore, Engine.

#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.

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.

Use a Game Instance Blueprint with these variables:

Variables Type Default
MultipartBusy Boolean false
MultipartBucket, MultipartKey, MultipartUploadId String empty
FirstPartData, FinalPartData Byte Array empty
Upload two parts with failure cleanup
Preparing your graph…

Loading the interactive viewer.

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

// 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);
};
// 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.

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.

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.

Was this page helpful?