AWS SDK for Swiftを使って新しくフォルダ(のようなもの)を作り、その直下の階層にファイルをアップロードする

# 概要
AWS SDK for Swiftを使ってS3上に新規フォルダを作ろうとしたときにS3の概念をよく知らずにフォルダを作成しようとしていたため、ハマりかけました。
概念を踏まえて思いついて方法を試したら無事作れたので記事として残しておきます。

なお、以下の項目についてはこの記事では触れません。
- プロジェクトに[SDK](https://github.com/awslabs/aws-sdk-swift)を追加する手順
- S3 Bucketの作成方法
- AWSのCredentialsの作成方法

# 詳細

[Add folder in Amazon s3 bucket](https://stackoverflow.com/questions/6791257/add-folder-in-amazon-s3-bucket)

こちらのStackoverflowの回答に
>There are no folders in Amazon S3. It just that most of the S3 browser tools available show part of the key name separated by slash as a folder.
If you really need that you can create an empty object with the slash at the end. e.g. "folder/" It will looks like a folder if you open it with a GUI tool and AWS Console.

とある通り、前提として**フォルダという概念はS3に存在しません。**


一方で、以下の画像のようにS3上でフォルダを作成した人もいると思います。

![スクリーンショット 2024-02-05 19.40.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/e4e7809d-1b87-66ad-3c4f-8ddb3624929c.png)

実はS3は、**キーバリューストアであり、単純なキーとそれに対応するバリューを保存する形式を採用しています。**
S3内では、フォルダとファイルは同じものとして扱われ、階層構造を持たず、実際には、フォルダは見せかけ上のものであり、ファイルのキー名にスラッシュを含めることで、AWSコンソール上でフォルダのように表示されるだけです。

したがって、S3上でフォルダを作成する際には、空のオブジェクトをスラッシュで終了させることで、フォルダのように見えるものを作成できます。
例えば、「folder/」というキー名を持つオブジェクトは、AWSコンソールでフォルダとして表示されます。

そこで、今回はSwiftUIで定義したボタンをタップしたタイミングでフォルダ(のようなものを)をS3上に作成し、直下にJSONを保存します。
以下のようなフローを想像してみてください。

![Group 10615.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/1ffb2758-f5d8-23ac-3f06-1c1176aee5b6.png)


上記の流れをSwiftで実装したのが以下のようなコードです。



```Swift:ContentView
import SwiftUI

struct ContentView: View {
    @StateObject private var fileManager = FileManager()

    // テスト環境バケット
    @State var bucketName = "bucketName"
    @State var folderName = "SampleFolder"
    
    let jsonString = """
    {
        "name": "Ichiro Tanaka",
        "email": "Ichiro.Tanaka@example.com",
        "address":"234 Sub St Osaka Japan",
        "hobbies": "reading"
    }
    """
    
    var body: some View {
        VStack {
            Button(action: {
                guard let jsonData = jsonString.data(using: .utf8) else {
                    fatalError("Unable to convert string to data")
                }
                Task {
                    await fileManager.exportDataToS3(bucket: bucketName, fileData: jsonData, dataType: .json, folderName: folderName)
                }
            }, label: {
                Text("S3にJSON送信")
            })
        }
    }
}
```


```Swift:FileManager
import Foundation
import UIKit

enum DataType {
    case image
    case json
}

class FileManager: ObservableObject {
    /// データをS3にPOSTするための関数
    /// - Parameters:
    ///   - bucket: S3のバケット名
    ///   - fileData: 送信するデータ
    ///   - dataType: Image,JSONのどちらを送信するかを判断するためのenum
    func exportDataToS3(bucket: String,fileData: Data, dataType: DataType,folderName:String) async {
        let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? ""
        
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        let dateString = formatter.string(from: Date())
        
        let keyString = "\(folderName)"+"/"+"\(appName)" + "_" + "\(dateString).\(dataType)"

        do {
            try await AWSS3Service().createFile(
                bucket: bucket, key: keyString, withData: fileData)
            print("Upload successful.")
        } catch {
            print("Error uploading file: \(error)")
        }
    }
}
```



```Swift:AWSS3Service
import AWSClientRuntime
import AWSS3
import ClientRuntime
import Foundation

/// AWS SDK for Swiftを使用してAWSと対話するコードを含むクラス。
public class AWSS3Service {
    let client: S3Client
    let credentials = AWSS3Credentials()
    /// 新しい``ServiceHandler``オブジェクトを初期化して返します。これは、例で使用されるAWS呼び出しを実行するために使用されます。
    ///
    /// - Returns: 呼び出す準備ができた新しい``ServiceHandler``オブジェクトを返します。
    public init() async {
        do {
            let region = "regionName"
            let credentials = Credentials(
                accessKey: credentials.accessKey, secret: credentials.secretKey)
            let credentialsProvider = try StaticCredentialsProvider(credentials)
            let config = try S3Client.S3ClientConfiguration(
                region: region, credentialsProvider: credentialsProvider)
            client = S3Client(config: config)
        } catch {
            print("ERROR: ", dump(error, name: "S3クライアントの初期化中"))
            exit(1)
        }
    }

    /// 指定したバケットに、与えられた名前でファイルを作成します。新しいファイルの内容は`Data`オブジェクトからアップロードされます。
    ///
    /// - Parameters:
    ///   - bucket: ファイルを作成するバケットの名前。
    ///   - key: 作成するファイルの名前。
    ///   - data: 新しいファイルに書き込む`Data`オブジェクト。
    public func createFile(bucket: String, key: String, withData data: Data) async throws {
        let dataStream = ByteStream.data(data)

        let input = PutObjectInput(
            body: dataStream,
            bucket: bucket,
            key: key
        )
        _ = try await client.putObject(input: input)
    }
}
```


## 動作確認

1.バケット内にオブジェクトが存在しないことを確認します。
![スクリーンショット 2024-02-06 20.01.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/dbfdc971-6739-3e36-d830-de75a38f8030.png)


2.「S3にJSON送信」をタップします。
![スクリーンショット 2024-02-06 19.41 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/05b5cb47-0168-c74a-8662-44e4b2dcab5a.png)

3.バケット内に指定したフォルダ名である「SampleFolder」が作成されていることを確認します。
![スクリーンショット 2024-02-06 20.01.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/ad2e0757-a0ed-6747-ce55-852b5d190471.png)

4.「SampleFolder」直下に定義した名称、内容でJSONファイルが作成されていることを確認します。
![スクリーンショット 2024-02-06 20.01.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/34999a27-5bca-1428-4ad3-df9cfc0b14b1.png)
![スクリーンショット 2024-02-06 20.02.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/a20ba309-bad8-fdf4-d0f2-47eafc02db67.png)