0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

#はじめに
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 でアップロード

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 でアップロード

#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

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

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

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 ヘッダを作成し、リクエストボディにアップロードしたい画像データを渡せば完了です。

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 に直接アクセスすることができるようになったため、無駄なコスト削減と運用負荷の低減を実現しています。

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?