AlamofireでRails+GrapeなWebAPIに対してバイナリファイルを送信する


なんかいろいろはまったのでメモ。
まずポイントとして、

- Alamofireは multipart/form-data に未対応(2014/12/21時点)
- AlamofireでファイルアップロードするためのAPIは二つあるが、NSDataを引数にとる方はどうもバグっている。NSURL(Filepath)を引数にとるほうを利用する。
- Grapeで multipart/form-data ではなくバイナリデータを受け取る場合はちょっと細工が必要

このあたりでしょうか。

# Alamofire側のソース
[Alamofire-SwiftyJSON](https://github.com/SwiftyJSON/Alamofire-SwiftyJSON) 使ってます。

### Api.swift

```
//
//  Api.swift
//
//  Created by Takatomo Okitsu on 2014/12/09.
//  Copyright (c) 2014年 Takatomo Okitsu. All rights reserved.
//

import Foundation
import Alamofire

class Api: NSObject{
    
    class func upload(
        path: String,
        filePath: NSURL,
        success: (NSURLRequest, NSHTTPURLResponse?, JSON) -> Void,
        invalid: (NSURLRequest, NSHTTPURLResponse?, ApiError) -> Void,
        failure: (NSURLRequest, NSHTTPURLResponse?, NSError?) -> Void) -> Void
    {
        let method:Alamofire.Method = .POST
        var url:String = makeUrl(method, path: path, isBinary: true)
        
        Alamofire.upload(method, url, filePath)
            .responseSwiftyJSON { (request, response, json, error) in
                if error != nil {
                    failure(request, response, error)
                } else {
                    if json["error"] != nil {
                        var apiError = ApiError(error: json["error"].object, code: 0, response: json)
                        invalid(request, response, apiError)
                    } else {
                        success(request, response, json)
                    }
                }
        }
    }

    class func makeUrl(method:Alamofire.Method, path:String?, isBinary:Bool? = false) -> String{
        var urlString = "http://localhost:3000"
        if path != nil && !isBinary! {
            urlString += path! + ".json"
        } else if isBinary! {
            urlString += path! + ".binary" // バイナリファイルを送る場合は、URLの最後に.binaryを付与する
        }
        return urlString;
    }
}

```

### 呼び出し側
```
		let image:UIImage = ...
        // alamofire workaround...
		//　NSURLを生成するために、いったんファイルに書き出す。
        let data:NSData = NSData(data: UIImageJPEGRepresentation(image, 1))
        let tmpFilePath:String = NSTemporaryDirectory() + "/icon_tmp.jpg";
        data.writeToFile(tmpFilePath, atomically: true)
        Api.upload("/api/v1/images/image", filePath: NSURL(fileURLWithPath: tmpFilePath)!,
            success: { (request, response, json) -> Void in
                success(json)
            }, invalid: invalid, failure: failure)

```

# Rails+Grape側のコード

こんな感じでいけました。

### Images.rb
```
module V1
  class Images < Grape::API
	# ContentTypeがapplication/octet-streamの場合の定義
    content_type :binary, 'application/octet-stream'
    format :binary

    helpers do
      def uploaded_image
        data = env['api.request.input']

        temp_img_file = Tempfile.new('image')
        temp_img_file.binmode
        temp_img_file << data
        temp_img_file.rewind
        attachment = {
          :filename => 'image.jpg',
          :tempfile => temp_img_file
        }

        return ActionDispatch::Http::UploadedFile.new(attachment)
      end
    end

    resource :images do
      post :image do
        content_type "application/json"

        @user = current_user
        @user.image = uploaded_image //ここではCareerwWaveつかってる前提で書いてます。
        if @user.save
          return {
            image: {
              small: @user.image.small.url,
              medium: @user.image.medium.url,
              large: @user.image.large.url
            }
          }
        else
          return error!(@user.errors.full_messages, 200)
        end
      end
    end
  end
end

```

### v1.rb
```
module V1
  class V1 < Grape::API
    version 'v1', using: :path
    mount V1::Images
  end
end
```

### api.rb
```
module API
  class Base < Grape::API
    prefix 'api'
    format :json
    formatter :json, Grape::Formatter::Jbuilder
    rescue_from :all, :backtrace => true

    mount V1::V1
  end
end
```



# 結論
Alamofireはマルチパート使えるようになるまで利用するのやめたほうが何かと楽。サーバもクライアントも両方ともめんどい