ご存じ iPhone / iPad のブラウザである Mobile Safari は、HTML5 の機能を多数実装してるわりに、<input type="file">
を使ったファイルのアップロードができない。
もちろん専用のネイティブアプリを作成すれば可能だけど、まあ、ほとんどの場合めんどくさいよね。
そこで、多少ユーザを選ぶけど、便利そうなのが William Lindmeier 氏制作の iOS アプリ Picup である。
ここでは、Picup の仕組みと、Rails アプリでの実装例を紹介する。
Picup の仕組み
iOS や MacOS (X?) では、URL スキームにアプリケーションを関連づけられる。Picup の場合 fileupload:// に続けて、アプリに渡したいパラメータをつけて Safari 等で開けば、Picup にパラメータが渡され起動する。
サーバ側では、Picup の Web サイトで配布しているスクリプト (picup.js) を利用して、 を、fileupload://... を開くボタンに書き換える (別にこのスクリプトを使わなくても fileupload://... へのリンクを自前で作ってもいい)。
このボタンがクリックされると、POST 先の URL (postURL)、フィールド名 (postImageParam) などのパラメータを渡して Picup が起動される。そして、Picup 上で、ユーザーが、カメラで撮影するか既存の写真を選択し、Done をタップすると、さきほど渡されたパラメータをもとに POST してくれる。
さらに、callbackURL を指定すると、POST 後に任意の URL を Safari で開くこともできる。その際に # 以降にいくつかパラメータが渡されるので、それを利用して、ユーザーを誘導することもできる。
Rails での作例
以下では、Ruby 1.9.2 + Rails 3.0.7 + carrierwave で Picup による写真のアップロードに対応した Rails アプリを作る。
とりあえず初期設定。
$ rails new picup_demo -d mysql -J
# Gemfile
- gem 'mysql2'
+ gem 'mysql2', '0.2.7'
+ gem 'carrierwave'
+ gem 'jquery-rails'
$ bundle$ rails g jquery:install
jquery や後述の picup.js をロードするようにする。
# app/views/layouts/application.html.erb
- <%= javascript_include_tag :defaults %>
+ <%= javascript_include_tag :all %>
写真のモデル・コントローラ・ビューを scaffold でつくる。
$ rails g scaffold photo file:string
$ rake db:create
$ rake db:migrate
アップローダをつくり、photos.file に設定する。
$ rails g uploader photo
# app/models/photo.rb
class Photo < ActiveRecord::Base
+ mount_uploader :file, PhotoUploader
end
フォームのフィールドを、type="file" にする。show で写真を表示する。
# app/views/photos/_form.html.erb
- <%= form_for(@photo) do |f| %>
+ <%= form_for(@photo, html: {multipart: true}) do |f| %>
- <%= f.text_field :file %>
+ <%= f.file_field :file %>
# app/views/photos/show.html/erb
- <%= @photo.file %>
+ <%= image_tag(@photo.file.url) %>
この時点で PC の Web ブラウザからアップロードできるか確認しておくとよい。
$ rails s
# http://localhost:3000/photos/new へ Web ブラウザでアクセス
Picup のヘルパースクリプトを導入。http://picupapp.com/picup.js.zip をダウンロード、展開し、picup.js を public/javascripts 下に移動する。
type="file" のフィールドを Picup を開くボタンに置き換える。
# app/views/photos/_form.html.erb
<script type="text/javascript" charset="utf-8">
$(function(){
window.name = "Picup_demo_new_photo";
// アップロード後表示されるページから遷移するためにウィンドウ名をつけておく
Picup.convertFileInput(
"photo_file", // <input type="file" /> の ID
{ 'referrername': escape('Picup Demo'), // Picup に表示されるアプリケーション名
'purpose': escape('Upload A Photo'), //Picup に表示されるメッセージ
'postImageParam': 'photo_file', // フィールド名、この場合、rails では params[:photo_file] でアクセスできる。
'postURL': 'http://192.168.1.10:3000/photos.json', // post先
'callbackURL': 'http://192.168.1.10:3000/uploaded' // アップロード後に開く URL
}
);
});
</script>
# app/controllers/photos_controller.rb
def create
- @photo = Photo.new(params[:photo])
+ @photo = Photo.new(file: params[:photo_file])
IPアドレスは適当に書き換えて。 ここまででとりあえずアップロードはできる。 (アップロード後表示されるページは RoutingError になっちゃうけど、/photos を見ると、正しくアップロードできているのが確認できる)
アップロード後、アップロードされた写真のページへ遷移させたいので、create のレスポンスに、そのページの URL を含ませる。
# app/controllers/photos_controller.rb
format.html { redirect_to(@photo, :notice => 'Photo was successfully created.') }
format.xml { render :xml => @photo, :status => :created, :location => @photo }
+ format.json {
+ imgur_like_response = {
+ rsp: {
+ stat: "ok",
+ image: {
+ original_image: photo_url(@photo)
+ }
+ }
+ }
+ render :json => imgur_like_response
+ }
{rsp: ...} となっているハッシュは、imgur に画像をアップロードした際のレスポンスを模したものである。いままで触れなかったが、Picup は postURL の指定がない場合、imgur.com に画像をアップロードする。その際、レスポンスに含まれる original_image の値は、アップロード後開かれる URL の # 以降に remoteImageURL=... の形で渡される (imgur.com からのレスポンスでは他にもいろいろな情報が付与されているが、無視されている)。
# public/uploaded.html
<!DOCTYPE html>
<html>
<head>
<title>Picup Demo</title>
<script>
<!--
var redirect = function() {
var keys_values = window.location.hash.substring(1).split("&");
var params = {};
for(var i = 0; i < keys_values.length; i += 1){
var key_value = keys_values[i].split("=");
params[key_value[0]] = key_value[1];
}
if (params.remoteImageURL) {
window.open(unescape(params.remoteImageURL), 'Picup_demo_new_photo');
window.close();
}
};
redirect();
//-->
</script>
</head>
<body>
<h1>アップロード完了</h1>
<a href="javascript:redirect();">自動的に移動しない場合はここをタップしてください。</a>
</body>
</html>
これが、アップロード後開かれるページである。URL の # 以降の remoteImageURL=... の値を読み出し、もともとフォームを表示していたタブ (Picup_demo_new_photo) で開き、このページ自身は閉じるようにしている。いちおうこれで、PC のブラウザからアップロードした際と似た挙動を実現できる。
なお、 の置き換えを、UA が Mobile Safari であることを確認した上で (Apple 公式の DetectingWebKit を使えば簡単) 行えば、PC のブラウザでも Mobile Safari でもほぼ同じように使える。
気になるところ
先述のとおり Picup.convertFileInput には postImageParam オプションで、送信されるファイルのフィールド名を指定できる。ここは、Rails の規約に従って photo[file] としたいところだが、実際に試してみると、Rails にはフィールド名が photo%5bfile%5d として渡されてしまい、params[:photo][:file] のようなかたちではアクセスできない。無用なエスケープがされちゃってるんですね。ちょっと残念。
あと、POST のレスポンスをもうすこし利用できるとうれしい。imgur のレスポンスに特化してるのがもったいない。