Swift で iOS アプリから GCS へ REST API でファイルを直接アップロードする方法

#はじめに
iOS アプリで GCS（Google Cloud Storage）にファイルを直接アップロードするための実装手順を説明します。

この仕組みには、次のようなメリットがあります。

- ウェブサーバ（Google Compute Engine 等を使って）を使わずに、（ある程度）セキュリティを意識したコンテンツサーバを立てることができる
- GCS のフルマネージドなサービスのメリット（拡張性、可用性）を享受することができる

GCS は AWS でいうところの S3（Amazon Simple Storage Service）の位置づけです。業界的にデファクトとなっている AWS S3 からの移行がしやすいように、Google は GCS に Interoperability API というものを提供しており、AWS S3 と同じ HTTPS リクエストで GCS のファイル操作が可能になっています。
この API を使って、Swift で書かれた iOS アプリから直接 GCS にファイルをアップロードする機能を実装します。

#実装方法
次のサイトを参考にしました。
[Google Cloud Storage に S3 互換の REST でアップロード](https://qiita.com/ngyuki/items/e84e895130de95a5ca7a)

PHP で同じことを実装しているので、そのまま Swift でなぞりました。
Swift 向けの AWS S3 用ライブラリは当然すでに存在している（GCS 用のライブラリもあるかもしれない）ため、それを流用すればすんなりできたのかもしれませんが、PHP のコードにならって素の HTTP リクエストで実装することにしました。

アクセスキー・シークレットキーを使っていくつかの HTTP ヘッダを構成し、リクエストボディにはアップロードしたいデータをそのまま埋め込みます。
Authorization HTTP ヘッダを作るという工程で、いくつかの項目を MD5 ハッシュ・BASE64 エンコード等しているところが特殊な仕掛けになっています。

#GCS 側の設定
GCS のバケット設定で、Interoperability API を使用できるようにします。
相互運用ストレージ アクセスキーが生成できるので、アクセスキーとシークレットの文字列を控えます。
これらの作業は GCS のコンソール画面で行います。
次の記事が参考になります。

[Google Cloud Storage に AWS CLI や AWS SDK for PHP でアップロード](https://qiita.com/ngyuki/items/fd3bb47cd4aeb2f61fec)

#Swift 側の準備
通常は API コールを行うときの肝となる MD5 ハッシュ、HMAC ハッシュができる関数が用意されていないので、Objective-C の関数をブリッジして呼べるように設定します。
ここが PHP と比較して厄介なところです。標準 API として実装しておいてほしかった・・・

まず、Bridging-Header.h をプロジェクトのトップレベルのフォルダ直下に作成します。

```
#ifndef Bridging_Header_h
#define Bridging_Header_h
 
#import "CommonCrypto/CommonHMAC.h"
 
#endif /* Bridging_Header_h */
```

次に、プロジェクト設定で、このヘッダファイルをブリッジ用のファイルとして認識させます。
XCode で設定しているビルドのスキームそれぞれに Build Settings を追加する必要があるので、注意が必要です。

#MD5 関数の実装
Objective-C の関数が呼べるようになったので、MD5 ハッシュを実行する関数を Swift 側で実装します。

次のサイトを参考にしました。
[Swift 4で HMAC-SHA256](https://dishware.sakura.ne.jp/swift/archives/181)

上のサイトで見つけたコードでは、変換結果を 16進数の文字列表現に変換しているため、PHP コードでいうところの `md5($body, true)` ではなく `md5($body, false)` の結果となってしまいます。
そこで、バイナリデータである Data クラスのオブジェクトを返すように手直ししています。

String クラス、Data クラスの extension として実装しています。

```swift
import Foundation
enum CryptoAlgorithm {
    case MD5, SHA1, SHA224, SHA256, SHA384, SHA512
    var HMACAlgorithm: CCHmacAlgorithm {
        var result: Int = 0
        switch self {
        case .MD5:      result = kCCHmacAlgMD5
        case .SHA1:     result = kCCHmacAlgSHA1
        case .SHA224:   result = kCCHmacAlgSHA224
        case .SHA256:   result = kCCHmacAlgSHA256
        case .SHA384:   result = kCCHmacAlgSHA384
        case .SHA512:   result = kCCHmacAlgSHA512
        }
        return CCHmacAlgorithm(result)
    }
    var digestLength: Int {
        var result: Int32 = 0
        switch self {
        case .MD5:      result = CC_MD5_DIGEST_LENGTH
        case .SHA1:     result = CC_SHA1_DIGEST_LENGTH
        case .SHA224:   result = CC_SHA224_DIGEST_LENGTH
        case .SHA256:   result = CC_SHA256_DIGEST_LENGTH
        case .SHA384:   result = CC_SHA384_DIGEST_LENGTH
        case .SHA512:   result = CC_SHA512_DIGEST_LENGTH
        }
        return Int(result)
    }
}
 
extension String {
    // String 型の変数を hmac ハッシュして Data 型で返す
    func hmacData(algorithm: CryptoAlgorithm, key: String) -> Data {
        var result: [CUnsignedChar]
        if let ckey = key.cString(using: String.Encoding.utf8), let cdata = self.cString(using: String.Encoding.utf8) {
            result = Array(repeating: 0, count: Int(algorithm.digestLength))
            CCHmac(algorithm.HMACAlgorithm, ckey, ckey.count - 1, cdata, cdata.count - 1, &result)
        } else {
            fatalError("Nil returned when processing input strings as UTF8")
        }
        return Data(bytes: result, count: result.count)
    }
}
 
extension Data {
    // Data 型の変数を md5 ハッシュして Data 型で返す
    public func md5Data() -> Data {
        var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
        _ =  self.withUnsafeBytes { bytes in
            CC_MD5(bytes, CC_LONG(self.count), &digest)
        }
        return Data(bytes: digest)
    }
}
```
ここの手直し方法を見出すまでに、だいぶ手間取りました。

#REST API コールの実装
さて、ようやく API コールをするところの実装です。
初めの参考サイトの PHP 実装にならってコーディングしていきます。

日付の文字列を作るときに、ロケールによって DateFormatter の生成する文字列が変わってしまうので、ロケールを `en_US_POSIX` に固定することで対処しています。

あとは、決まった手順で HTTP ヘッダを作成し、リクエストボディにアップロードしたい画像データを渡せば完了です。

```swift
let verb = "PUT"
let contentType = "image/jpeg"
let photoName = name + ".jpg"
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z"
let dt = dateFormatter.string(from: Date())
let md5:Data = imageData.md5Data()
let md5base64 = md5.base64EncodedString()
let resource = String(format: "/%@/%@", PhotoSharingServerBucket, photoName) 
let stringToSign = String(format: "%@\n%@\n%@\n%@\n%@", verb, md5base64, contentType, dt, resource)
let hmac = stringToSign.hmacData(algorithm: .SHA1, key: GCSSecretKey)
let hmacBase64 = hmac.base64EncodedString()
let authorization = String(format: "AWS %@:%@", GCSAccessKey, hmacBase64)
 
var request = URLRequest(url: PhotoSharingUploadURL(photoName),
                         cachePolicy: .reloadIgnoringLocalCacheData,
                         timeoutInterval: 60) 
request.httpMethod = verb
request.addValue(dt, forHTTPHeaderField: "Date")
request.addValue(contentType, forHTTPHeaderField: "Content-Type")
request.addValue(md5base64, forHTTPHeaderField: "Content-MD5")
request.addValue(authorization, forHTTPHeaderField: "Authorization")
 
// ボディデータの作成
request.httpBody = imageData
// レスポンスアクションの設定
let dataTask = URLSession.shared.dataTask(with: request) {[weak self]
    data, response, error in
    if let error = error {
        // エラー発生時
    }   
    let status = response.statusCode()
    if status != 200 {
        // HTTPステータスエラー
    }   
}
// 接続開始
dataTask.resume()
```

#まとめ
以前はこのアプリのために専用のサーバインスタンスを準備し、その API にならってリクエストを組んでいました。今回の実装によって GCS に直接アクセスすることができるようになったため、無駄なコスト削減と運用負荷の低減を実現しています。

かなりニッチなノウハウと思いますが、何かのお役に立てれば幸いです。