multipart/form-dataによるファイルのアップロード

Alamofireとかのライブラリを使った方が幸せになれると思うが) HTMLのフォームからの送信と同じ様なmultipartによるアップロードをiOSからする方法

要件

HTMLの場合

上記要件でかつアップロードと同じ画面内で結果を表示させたい場合、 jQueryなどを全く使わずに素のHTMLで書くと

もし、ajaxではなく送信結果は次の画面で表示させる様な場合だと<form>タグは <form name="upload" enctype="multipart/form-data" action="upload.php" method="post"> とする必要がある

iOSの場合

もし単一のファイルのアップロードだけなら、URLSessionuploadTask(with:〜でOKだが、 他のデータやファイルも付加したい場合は自分でmultipartのリクエストを生成する必要がある

とりあえずHTMLと同じものをベタ書き(+エラー省略)すると、

boundary文字列

multipartの場合、データの区切りを表すためにデータ内に含まれない様なバウンダリ文字列が必要となる

let boundary = String(format: "----iOSURLSessionBoundary.%08x%08x", arc4random(), arc4random())

今回はAlamofireを参考に送信元のプログラムとランダムな数字を組合せた文字列を生成している

bodyの生成

フォームのデータの場合の構造は

--(バウンダリ文字列)[CRLF]
Content-Disposition: form-data; name="フォームの名前"[CRLF]
[CRLF]
(フォームのデータ)

となる

ファイルの場合の構造は

--(バウンダリ文字列)[CRLF]
Content-Disposition: form-data; name="フォームの名前"; filename="ファイル名"[CRLF]
Content-Type: "ファイルのタイプ"[CRLF]
[CRLF]
(ファイルのデータ)

となる

各構造をバイナリ(Data型)にしたものを必要な分だけ組合せて、最後に

--(バウンダリ文字列)--[CRLF]

をつけたものがbody部分のデータとなる

今回の例だとフォームデータが一つとファイルデータが一つなので、 それぞれ一つずつを追加し最後にフッタを付けたものをbodyに入れている

headerの生成

フィールドにセットすべきなのは以下の2つ

  1. “Content-Type”
  2. “Content-Length”

Content-Typeにはタイプとバウンダリ文字列を以下のように指定する

multipart/form-data; boundary=(バウンダリ文字列)

Content-Lengthは普通にbodyのサイズを入れればOK

送信

送信はuploadTaskだとmultipart指定ができないのでdataTaskで行う。 それ以外は通常のdataTaskのやり方でOK

参考:サーバの処理(PHP)

今回サーバ側で検証用に使ったのは以下のソース
(送信されてきたファイルはそのまま専用のディレクトリに格納し結果をJSONで返すだけ)

// "upload"のディレクトリに書き込み権限が必要
$dir = __DIR__ . '/upload/';
$path = $dir . basename($_FILES['filename']['name']);

$data['result'] = 'アップロード失敗';
if (move_uploaded_file($_FILES['filename']['tmp_name'], $path)) {
    chmod($path, 0666);
    $data['result'] = date("H:i:s") . ' ' . $_POST['title'] . ' アップロード成功';
}

header('Content-Type: application/json');
echo json_encode($data);

参考リンク

開発環境