はじめに
ファイルをアップロードする際、通常は userid など、ファイルと密接に関連しておりサーバー側で直接取得できない追加情報が必要になります。そのため、 multipart/form-data は柔軟な選択肢であり、フォームを通じてファイルと関連するユーザーデータを同時に送信できます。
1. Androidクライアント
1. HTTP通信プロセスのラップ
まず、ファイルのアップロードプロセスがUIの更新に影響を与えないようにしたいため、ここでは AsyncTask を使って実装します:
public class HttpUpload extends AsyncTask<Void, Integer, Void> {
public HttpUpload(Context context, String filePath) {
super();
this.context = context; // UIの更新(進捗表示)に使用
this.filePath = filePath;
}
@Override
protected void onPreExecute() {
// クライアントパラメータの設定
// プログレスバー/ダイアログの表示
}
@Override
protected Void doInBackground(Void... params) {
// POSTリクエストの作成
// フォームフィールドと値の入力
// 進捗の監視
// POSTリクエストの送信
// レスポンス結果の処理
}
@Override
protected void onProgressUpdate(Integer... progress) {
// 進捗の更新
}
@Override
protected void onPostExecute(Void result) {
// 進捗表示の非表示
}
}
2. HTTP通信の実装
HTTP通信は、以下のように Apache HttpClient を使用して実現できます:
//--- onPreExecute
// クライアントパラメータの設定
int timeout = 10000;
HttpParams httpParameters = new BasicHttpParams();
HttpConnectionParams.setConnectionTimeout(httpParameters, timeout);
HttpConnectionParams.setSoTimeout(httpParameters, timeout);
// クライアントの作成
client = new DefaultHttpClient(httpParameters);
// 進捗表示の作成と表示
System.out.println("upload start");
//--- doInBackground
try {
File file = new File(filePath);
// POSTリクエストの作成
HttpPost post = new HttpPost(url);
// multipartメッセージボディの作成
// および進捗の監視
MultipartEntity entity = new MyMultipartEntity(new ProgressListener() {
@Override
public void transferred(long num) {
// 完了した割合で onProgressUpdate メソッドを呼び出す
// publishProgress((int) ((num / (float) totalSize) * 100));
System.out.println(num + " - " + totalSize);
}
});
// ファイルの入力(複数ファイル可)
ContentBody cbFile = new FileBody(file, "image/png");
entity.addPart("source", cbFile); // <input type="file" name="source"> に相当
// フィールドの入力
entity.addPart("userid", new StringBody("u30018512", Charset.forName("UTF-8")));
entity.addPart("username", new StringBody("中文不乱码", Charset.forName("UTF-8")));
// メッセージボディの総長の初期化(進捗記述用)
int totalSize = entity.getContentLength();
// POSTリクエストのメッセージボディを設定
post.setEntity(entity);
// POSTリクエストの送信
HttpResponse response = client.execute(post);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == HttpStatus.SC_OK) {
// 200を返す
String fullRes = EntityUtils.toString(response.getEntity());
System.out.println("OK: " + fullRes);
} else {
// その他のエラーステータスコード
System.out.println("Error: " + statusCode);
}
} catch (ClientProtocolException e) {
// HTTPプロトコル関連のエラー(URLの形式不正など)
e.printStackTrace();
} catch (IOException e) {
// IOエラー(ファイルが見つからないなど)
e.printStackTrace();
}
//--- onProgressUpdate
// 進捗の更新
//--- onPostExecute
// 進捗表示の非表示
3. 進捗の記録
Apache HttpClient 自体は進捗データを提供していないため、ここでは MultipartEntity をオーバーライドして独自に実装する必要があります:
public class MyMultipartEntity extends MultipartEntity {
private final ProgressListener listener;
public MyMultipartEntity(final ProgressListener listener) {
super();
this.listener = listener;
}
public MyMultipartEntity(final HttpMultipartMode mode, final ProgressListener listener) {
super(mode);
this.listener = listener;
}
public MyMultipartEntity(HttpMultipartMode mode, final String boundary, final Charset charset,
final ProgressListener listener) {
super(mode, boundary, charset);
this.listener = listener;
}
@Override
public void writeTo(final OutputStream outstream) throws IOException {
super.writeTo(new CountingOutputStream(outstream, this.listener));
}
public static interface ProgressListener {
void transferred(long num);
}
public static class CountingOutputStream extends FilterOutputStream {
private final ProgressListener listener;
private long transferred;
public CountingOutputStream(final OutputStream out, final ProgressListener listener) {
super(out);
this.listener = listener;
this.transferred = 0;
}
public void write(byte[] b, int off, int len) throws IOException {
out.write(b, off, len);
this.transferred += len;
this.listener.transferred(this.transferred);
}
public void write(int b) throws IOException {
out.write(b);
this.transferred++;
this.listener.transferred(this.transferred);
}
}
}
内部で FilterOutputStream をオーバーラ���ドしてカウントをサポートすることで、進捗データを取得します。
4. HttpUploadの呼び出し
必要な場所で new HttpUpload(this, filePath).execute(); を呼び出すだけです。一般的な AsyncTask と大きな違いはありません。
2. Nodeサーバーサイド
1. サーバーの構築
express を使用して、以下のように素早くHTTPサーバーを構築します:
var app = express();
// ...ルーティング制御
app.listen(3000);
最も簡単な方法は、古典的な Hello World のような http.createServer().listen(3000) ですが、ここで express を使用するのは主にルーティング制御とミドルウェア管理を簡素化するためです。 express は成熟したルーティング制御とミドルウェア管理メカニズムを提供しており、自分で書くのは少し手間がかかります。
2. multipartリクエストのプリプロセス
リクエストのプリプロセスはミドルウェアの本来の役割です。ここではConnectミドルウェアの connect-multiparty を採用します:
// ミドルウェアによるプリプロセス
app.use('/upload', require('connect-multiparty')());
// multipart POSTリクエストの処理
app.post('/upload', function(req, res){
console.log(req.body.userid);
console.log(req.body.username);
console.log('Received file:\n' + JSON.stringify(req.files));
var imageDir = path.join(__dirname, 'images');
var imagePath = path.join(imageDir, req.files.source.name);
// 存在チェック
fs.stat(imagePath, function(err, stat) {
if (err && err.code !== 'ENOENT') {
res.writeHead(500);
res.end('fs.stat() error');
}
else {
// 既に存在する場合は新しい名前を生成
if (stat && stat.isFile()) {
imagePath = path.join(imageDir, new Date().getTime() + req.files.source.name);
}
// リネーム
fs.rename(
req.files.source.path,
imagePath,
function(err){
if(err !== null){
console.log(err);
res.send({error: 'Server Writting Failed'});
} else {
res.send('ok');
}
}
);
}
});
});
ここでは、単に rename を使って画像を目的のパスに配置するだけの処理を行っています。サムネイルの作成、クロップ、合成(ウォーターマーク)などのより複雑な操作は、 imagemagick などの関連するオープンソースモジュールを通じて実行できます。
connect-multiparty は req を受け取ると、まずシステムの一時フォルダにすべてのファイルをストリームとして受信します(もちろん、受信パスは uploadDir 属性で設定でき、 maxFields や maxFieldsSize などの制限パラメータも設定可能です。詳細は andrewrk/node-multiparty を参照してください)。同時にフォームフィールドを解析し、最終的に処理結果を req オブジェクトに紐付けます( req.body はフォームフィールドとその値のオブジェクト、 req.files は一時ファイルオブジェクトのオブジェクトです)。例:
u30018512
中文不乱码
Received file:
{"source":{"fieldName":"source","originalFilename":"activity.png","path":"C:\\Us
ers\\ay\\AppData\\Local\\Temp\\KjgxW_Rmz8XL1er1yIVhEqU9.png","headers":{"content
-disposition":"form-data; name=\"source\"; filename=\"activity.png\"","content-t
ype":"image/png","content-transfer-encoding":"binary"},"size":66148,"name":"acti
vity.png","type":"image/png"}}
一時ファイルのパスを取得した後、 rename を行うことでファイルをサーバーの目的の場所に配置します。
注意:Windows環境では一時フォルダは C:/Users/[username]/AppData/Local/Temp/ になります。目的のパスがCドライブ以外にある場合はエラーが発生します(ドライブをまたぐと rename が失敗するため)。以下の通りです:
{ [Error: EXDEV, rename 'C:\Users\ay\AppData\Local\Temp\KjgxW_Rmz8XL1er1yIVhEqU9
.png']
errno: -4037,
code: 'EXDEV',
path: 'C:\\Users\\ay\\AppData\\Local\\Temp\\KjgxW_Rmz8XL1er1yIVhEqU9.png' }
解決策についてですが、WindowsでNodeをいじるのはお勧めしません。様々な奇妙な問題に遭遇し、煩わしさが増すだけです。それに、VBoxで仮想マシンを構築するのもそれほど手間ではありません。
3. 静的ファイルのレスポンス
ファイルをアップロードした後、それがアクセス可能なもの(画像など)であれば、静的ファイルとしてレスポンスを返す必要があります。 express では静的ディレクトリを設定するだけで済むので、非常に楽です。以下の通りです:
// static files
app.use('/images', express.static(path.join(__dirname, 'images')));
3. プロジェクトのURL
GitHub: ayqy/MultipartUpload
P.S. そういえば、GitHubのプロフィールが空っぽなのは少し格好がつかないので、これからは積極的に何か公開していこうと思います。エンジニアのキャリアガイドの一つに「ポートフォリオを蓄積すること」がありますし、もう怠ける理由はありません。気合を入れて取り組んでいきます。
参考文献
-
danysantiago/NodeJS-Android-Photo-Upload
この記事はこのプロジェクトをフォークしたものですが、元のプロジェクトは長らくメンテナンスされていなかったため(最終コミットは2012年11月19日の 518c106)、筆者が修正を加えました。
-
より成熟し、人気のあるAndroid用アップロードコンポーネントです。NodeとPHPのサーバーサイド実装も提供されていますが、当然ながら規模も比較的大きくなります。
コメントはまだありません