因為 cowboy 在目前 git master 版本跟最新的 0.9 release 兩個版本,在實作 http multi-part request 的處理上,給了兩個不相容的處理方式,而且 cowboy 又在 1.0 開發的過程中,作者也沒有時間給出很多範例以及參考的資料,再加上網路上搜尋到的解決方案,幾乎都是舊版的 cowboy 支援的方式,因此,我們試著研究把 multi-part http request 的範例程式寫出來。
我們參考了 cowboy multipart 這一份唯一的官方文件,另外在 cowboy source code 裡面,有一個 http_multipart 測試程式,由這兩個資料,我們可以組合出一個可以運作的 multi-part http request 的範例程式。
project
這個範例專案已經放在 github 了,可以直接由clone 這個 project cowboy_fileupload source code in github 。
project settings
這個範例遵循 cowboy 範例的作法,使用了 erlang.mk 以及 relx 這兩個工具。
專案要寫 Makefile 與 relx.config 兩個設定檔,Makefile 要將 cowboy master branch 設定為 dependent library,接下來在 make 時,才會自動下載這些 libraries。
-
Makefile
PROJECT = upload DEPS = cowboy dep_cowboy = pkg://cowboy master include erlang.mk
-
relx.config
{release, {upload_example, "1"}, [upload]}. {extended_start_script, true}.
static html index.html
靜態網頁要放在 priv 的目錄中,index.html 裡面寫了三個 html form,以下只列出最多的第三個 form,這些 form 的 action 都設定為 /upload 這個網址,cowboy 的 routing 要設定 /upload 的網址,由 multipart 的程式碼處理。
這個 form 有兩個 text 欄位,另外還有兩個 file 的欄位,而且是一個 text 與一個 file 間隔的順序,接下來我們作的 cowboy mutipart handler 必須要能根據欄位資料的型態,自動判斷是不是 text 或是檔案,而有不同的對應處理方式。
<form id="uploadForm3" action="/upload" method="POST" enctype="multipart/form-data">
<h1>Upload Form 3</h1>
description 3.1: <input type="text" id="desc3_1" name="desc3_1" /><br/>
file 3.1: <input type="file" name="file3_1" /><br/>
description 3.2: <input type="text" id="desc3_2" name="desc3_2"><br>
file 3.2: <input type="file" name="file3_2" /><br/>
<button type="submit">Submit</button>
</form>
source codes
-
upload.app.src
relx 工具會自動根據這個檔案,產生 OTP upload.app 設定檔,兩個檔案的差異只有 modules 欄位。upload.app.src填寫為 [] 空的 list,而 upload.app 自動由 relx 把相關的 modules 填寫上去了。{modules, [upload_app, upload_sup, upload_handler]},
-
upload_sup.erl
這是 OTP 的 supervisor 程式,基本上內容就跟其他 cowboy samples 一樣。 -
upload_app.erl
重點是 cowboy 的 routing 部份,/ 指定為靜態檔案,路徑在 upload 的 priv 路徑裡面的 index.html。而 /upload 路徑指派由 upload_handler 處理。
Dispatch = cowboy_router:compile([ {'_', [ {"/", cowboy_static, {priv_file, upload, "index.html"}}, {"/upload", upload_handler, []} ]} ]), {ok, _} = cowboy:start_http(http, 100, [{port, 8000}], [ {env, [{dispatch, Dispatch}]} ]),
-
uplaod_handler.erl
這個檔案是 multipart 程式的處理重點,首先我們定義這個 handler 的 behaviour。
-behaviour(cowboy_http_handler).
接下來是實作三個 callback functions
init/3, handle/2, terminate/3
重點是 handle,acc_multipart 是處理 multipart的遞迴程式,最後取得的結果 Result,是所有 multipart 資料的 header 與 body 的 list,但因為有些欄位是檔案,我們沒有必要把檔案內容放到 body 裡面傳回來這裡,所以在 acc_multipart 裡面有特別把檔案的 body 改寫為固定的文字內容 filecontent。
handle(Req, State) -> {Result, Req2} = acc_multipart(Req, []), io:format( "Result= ~p~n", [Result] ), {ok, Req3} = cowboy_req:reply(200, [ {<<"content-type">>, <<"text/plain; charset=UTF-8">>} ], <<"OK">>, Req2), %%writeToFile(term_to_binary(Result)), {ok, Req3, State}.
這裡把測試時取得的 Result 資料記錄下來。
%% Result= [{[{<<"content-disposition">>,<<"form-data; name=\"desc3_1\"">>}], %% <<"desc1">>}, %% {[{<<"content-type">>,<<"text/plain">>}, %% {<<"content-disposition">>, %% <<"form-data; name=\"file3_1\"; filename=\"userlist1.txt\"">>}], %% <<"filecontent\r\n">>}, %% {[{<<"content-disposition">>,<<"form-data; name=\"desc3_2\"">>}], %% <<"desc2">>}, %% {[{<<"content-type">>,<<"text/plain">>}, %% {<<"content-disposition">>, %% <<"form-data; name=\"file3_2\"; filename=\"userlist2.txt\"">>}], %% <<"filecontent\r\n">>}]
參考 cowboy multipart 裡面 Reading a multipart message 這一段的內容,我們可以用 cow_multipart:form_data 回傳的資料內容的不同,直接將 text 與 file 兩個區分開來。file 的部份可直接取得 content type: CType 與檔名 Filename。
因為要把檔案寫入磁碟中,但當檔案超過 8MB 的時候,cowboy 不能一次把所有資料都傳給 stream_file 處理,因此搭配 stream_file 的檔案寫入的處理,我們把檔案開啟 file:open 跟關閉檔案 file:close 分別寫在 stream_file 的前面與後面。
acc_multipart(Req, Acc) -> case cowboy_req:part(Req) of {ok, Headers, Req2} -> [Req4, Body] = case cow_multipart:form_data(Headers) of {data, _FieldName} -> {ok, MyBody, Req3} = cowboy_req:part_body(Req2), [Req3, MyBody]; {file, _FieldName, Filename, CType, _CTransferEncoding} -> io:format("stream_file filename=~p content_type=~p~n", [Filename, CType]), {ok, IoDevice} = file:open( Filename, [raw, write, binary]), Req5=stream_file(Req2, IoDevice), file:close(IoDevice), [Req5, <<"skip printing file content">>] end, acc_multipart(Req4, [{Headers, Body}|Acc]); {done, Req2} -> {lists:reverse(Acc), Req2} end.
參考 cowboy multipart 裡面 Skipping unwanted parts 這一段的內容,我們知道 cowboy 在還沒取得所有上傳檔案的資料時,cowboy_req:part_body 就會先回傳給呼叫端,並用 more 為信號告訴 client 還需要再呼叫一次,取得檔案後面的資料。
stream_file(Req, IoDevice) -> case cowboy_req:part_body(Req) of {ok, Body, Req2} -> io:format("part_body ok~n", []), file:write(IoDevice, Body), Req2; {more, Body, Req2} -> io:format("part_body more~n", []), file:write(IoDevice, Body), stream_file(Req2, IoDevice) end.
編譯與測試
-
編譯
> make
它會自動取得所有需要的 libraries 包含了 cowboy, cowlib, ranch 還有封裝工具 relx。
-
啟動
> _rel/bin/upload_example console
-
測試