なんかいろいろはまったのでメモ。
まずポイントとして、
- Alamofireは multipart/form-data に未対応(2014/12/21時点)
- AlamofireでファイルアップロードするためのAPIは二つあるが、NSDataを引数にとる方はどうもバグっている。NSURL(Filepath)を引数にとるほうを利用する。
- Grapeで multipart/form-data ではなくバイナリデータを受け取る場合はちょっと細工が必要
このあたりでしょうか。
Alamofire側のソース
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はマルチパート使えるようになるまで利用するのやめたほうが何かと楽。サーバもクライアントも両方ともめんどい