あおみかんのブログ

フリーランスのIT系エンジニア。ゲーム制作スタジオ4th cluster代表。

Refile+S3のpresignにSwiftからダイレクトにアップロードする

画像等のファイルをS3(とかのクラウドストレージ)に置くのが当たり前になってきた昨今、クライアントから画像をアップロードするときにアプリケーションサーバーを経由するのは、そこからさらにS3に挙げるという点ではムダ。

この改善方法として、クライアントサイド(ブラウザやネイティブアプリ)からS3に直接アップロードして、アプリケーションサーバーには「ここに置いといたよ」とだけ伝える方法がある。 このアプローチは、特にherokuのようなアプリケーションサーバーに負荷をかけたくない環境では高い効果を発揮する。

で、このエントリの対象読者は、上の文で何が言いたいか分かってくれる人。

そうじゃない人は、諦めてアプリケーションサーバーに頼ってCarrierWaveかPaperclipの安定版を使う事を強くオススメする。

さて本題。

CarrierWaveの後継と噂される(同じ人が開発している)Rails向けの画像取り扱いgemのRefileというのがある。まだ安定バージョンではないし、Rails5系に組み込もうとするとSinatraとrakeのバージョンがぶつかってしまったり面倒が多いが、設計はCarrierWaveよりもさらに洗練されているっぽい。

GitHub - refile/refile: Ruby file uploads, take 3

Refileにはdirect uploadとPresigned uploadsという機能がある。(大文字小文字はREADMEに倣った)

direct uploadはフォームをsubmitしようとしたときにファイルを1個ずつRails側に送って、それが完了してからフォームをsubmitする方式。

Presigned uploadsは、上記で言ったS3に直接上げる方式。

Refileでは、Javascriptからのアップロードのみ実装されているので、これをSwift側で実装する。

ざっくり言えば refile/refile.js at master · refile/refile · GitHub の移植。

// まずはpresign情報を取得する
request("https://example.com/attachments/cache/presign").validate().responseSwiftyJSON { (presignResponse) in
    // ※エラー処理は自分で書いて下さいね。 switchで .Success / .Failure 分岐するのが個人的には好き。
    let url = presignResponse.value!["url"].stringValue
    let fields = presignResponse.value!["fields"].dictionaryValue
    let asName = presignResponse.value!["as"].stringValue
    
    Alamofire.upload(multipartFormData: { (multipartFormData) in
        
        for (name, value) in fields {
            multipartFormData.append(value.stringValue.data(using: .utf8)!, withName: name)
        }
        
        // 以下のNSDataを用意する部分もお好みで。 UIImagePNGRepresentation を使っても良いし、拡張子などで分岐しても良い。
        let image = UIImage(named: "image1.jpg")!
        let imageData = UIImageJPEGRepresentation(image, 1.0)!
        
        multipartFormData.append(imageData, withName: asName)
        
    }, to: url, encodingCompletion: { (encodingResult) in
        // ここにもAlamofireのドキュメントを参照してエラー分岐等を書く必要があると思います。
        print("uploaded")
    })
}

ここで得たデータをPOSTやPUTしてruby側でActiveRecordに渡してやる必要があるけれど、そこはまだやってないので追記するつもり。

ちなみに現状のGemfileの中身はこんな感じ。

tagすら切られてない中途半端なバージョンなので、不用意にバージョンが上がってバグらないように一応refを固定。

gem 'rails', '~> 5.0.1'

# Refile (最新版, unstableなので依存解決のために無理して入れてます)
gem "refile", require: "refile/rails", github: 'refile/refile', ref: 'd7a42dcd7c'
gem "refile-mini_magick"
gem "refile-s3"
# Refileのために以下が必要っぽい
gem "sinatra", github: "sinatra/sinatra", tag: "v2.0.0.beta2"

そういえば、全然関係ないんですが、最近Skyrimにハマってます。めっちゃ面白いですね。