Permalink
Browse files

return part headers from yaws_multipart:read_multipart_form

Each part of a multipart/form-data message can have part-specific
headers such as Content-Type and Content-Transfer-Encoding. This
changes adds these headers to the parameter lists within the dict
returned from yaws_multipart:read_multipart_form and to the parameter
list returned from yaws_api:parse_multipart_post.

Modify the documentation to reflect the fact that these headers are
now returned if present. Also fixed a coding error in the existing
multipart example in yaws.tex.

Add a new unit test to multipart_post_parsing.erl to verify the return
value of yaws_multipart:read_multipart_form. The unit test previously
checked only yaws_api:parse_multipart_post.

This work is based on a patch submitted by Dilshod Temirkhodjaev.
  • Loading branch information...
1 parent 144ac13 commit 641939aab2d93747313acab6ba4ac5c3005a3a77 @vinoski vinoski committed Jun 27, 2010
Showing with 88 additions and 41 deletions.
  1. +22 −9 doc/yaws.tex
  2. +14 −1 src/yaws_api.erl
  3. +30 −23 src/yaws_multipart.erl
  4. +22 −8 test/eunit/multipart_post_parsing.erl
View
@@ -848,7 +848,8 @@ \section{POSTing files}
capabilities:
\begin{enumerate}
-\item It reads all parameters --- files uploaded and other simple parameters.
+\item It reads all parameters --- files uploaded and other simple
+ parameters.
\item It takes a few options to help file uploads. Specifically:
\begin{enumerate}
\item \verb+{max_file_size, MaxBytes}+: if the file size in bytes
@@ -884,18 +885,30 @@ \section{POSTing files}
\begin{verbatim}
[{filename, "name of the uploaded file as entered on the form"},
- {value, Contents_of_the_file_all_in_memory}]
+ {value, Contents_of_the_file_all_in_memory} | _T]
\end{verbatim}
or:
\begin{verbatim}
[{filename, "name of the uploaded file as entered on the form"},
- {temp_file, "full pathname of the temp file"}]
+ {temp_file, "full pathname of the temp file"} | _T]
\end{verbatim}
-For the temporary file case, it's your responsibility to delete the
-file when you're done with it.
+Some multipart/form messages also include headers such as
+\verb+Content-Type+ and \verb+Content-Transfer-Encoding+ for different
+subparts of the message. If these headers are present in any subpart
+of a multipart/form message, they're also included in that subpart's
+parameter list, like this:
+
+\begin{verbatim}
+[{filename, "name of the uploaded file as entered on the form"},
+ {value, Contents_of_the_file_all_in_memory},
+ {content_type, "image/png"} | _T]
+\end{verbatim}
+
+Note that for the temporary file case, it's your responsibility to
+delete the file when you're done with it.
Here's an example:
@@ -906,14 +919,14 @@ \section{POSTing files}
out(Arg) ->
Options = [no_temp_file],
case yaws_multipart:read_multipart_form(Arg, Options) of
- {done, Params} ->
- io:format("Params : ~p", [Params]),
- [{filename, FileName},{value,FileContent}] =
+ {done, Params} ->
+ io:format("Params : ~p~n", [Params]),
+ {ok, [{filename, FileName},{value,FileContent}|_]} =
dict:find("my_file", Params),
AnotherParam = dict:find("another_param", Params);
%% do something with FileName, FileContent and AnotherParam
{error, Reason} ->
- io:format("Error reading multipart form: ~s", [Reason]);
+ io:format("Error reading multipart form: ~s~n", [Reason]);
Other -> Other
end.
\end{verbatim}
View
@@ -360,7 +360,20 @@ do_header(Head) ->
{value, {_,"form-data"++Line}} ->
Parameters = parse_arg_line(Line),
{value, {_,Name}} = lists:keysearch(name, 1, Parameters),
- {Name, Parameters};
+ {Name,
+ lists:map(fun({"content-type", Val}) ->
+ {content_type, Val};
+ ({"content-transfer-encoding", Val}) ->
+ {content_transfer_encoding, Val};
+ ({"content-id", Val}) ->
+ {content_id, Val};
+ ({"content-description", Val}) ->
+ {content_description, Val};
+ (KV) ->
+ KV
+ end,
+ Parameters ++ lists:keydelete("content-disposition", 1,
+ Header))};
_ ->
{Header}
end.
View
@@ -16,7 +16,8 @@
max_file_size,
no_temp_file,
temp_dir = yaws:tmpdir("/tmp"),
- temp_file
+ temp_file,
+ headers = []
}).
read_multipart_form(A, Options) when A#arg.state == undefined ->
@@ -45,7 +46,7 @@ multipart(A, State) ->
Parse = yaws_api:parse_multipart_post(A),
case Parse of
{cont, Cont, Res} ->
- case addFileChunk(A, Res, State) of
+ case add_file_chunk(A, Res, State) of
{done, NewState} ->
{done, NewState#upload.params};
{cont, NewState} ->
@@ -54,7 +55,7 @@ multipart(A, State) ->
Error
end;
{result, Res} ->
- case addFileChunk(A, Res, State#upload{last=true}) of
+ case add_file_chunk(A, Res, State#upload{last=true}) of
{done, S2} ->
{done,S2#upload.params};
Error={error, _Reason} ->
@@ -63,16 +64,16 @@ multipart(A, State) ->
end.
-addFileChunk(A, [{part_body, Data}|Res], State) ->
- addFileChunk(A, [{body, Data}|Res], State);
+add_file_chunk(A, [{part_body, Data}|Res], State) ->
+ add_file_chunk(A, [{body, Data}|Res], State);
-addFileChunk(_A, [], State) when State#upload.last == true ->
+add_file_chunk(_A, [], State) when State#upload.last == true ->
{done, close_previous_param(State)};
-addFileChunk(_A, [], State) ->
+add_file_chunk(_A, [], State) ->
{cont, State};
-addFileChunk(A, [{head, {_Name, Opts}}|Res], State ) ->
+add_file_chunk(A, [{head, {_Name, Opts}}|Res], State ) ->
S1 = close_previous_param(State),
S2 = lists:foldl(
fun({filename, Fname0}, RunningState) ->
@@ -95,26 +96,29 @@ addFileChunk(A, [{head, {_Name, Opts}}|Res], State ) ->
({name, ParamName}, RunningState) ->
RunningState#upload{
param_name = ParamName,
- param_running_value = undefined}
+ param_running_value = undefined};
+ (HdrVal, RunningState) ->
+ RunningState#upload{
+ headers = [HdrVal | RunningState#upload.headers]}
end,
S1,
Opts),
- addFileChunk(A,Res,S2);
+ add_file_chunk(A,Res,S2);
-addFileChunk(A, [{body, Data}|Res], State) when State#upload.fd /= undefined ->
+add_file_chunk(A, [{body, Data}|Res], State) when State#upload.fd /= undefined ->
NewSize = compute_new_size(State,Data),
- Check = check_param_size(State, NewSize),
+ Check = check_param_size(State, NewSize),
case Check of
ok ->
ok = file:write(State#upload.fd, Data),
- addFileChunk(A, Res, State#upload{running_file_size = NewSize});
+ add_file_chunk(A, Res, State#upload{running_file_size = NewSize});
Error={error, _Reason} ->
Error
end;
-addFileChunk(A, [{body, Data}|Res], State) ->
+add_file_chunk(A, [{body, Data}|Res], State) ->
NewSize = compute_new_size(State,Data),
- Check = check_param_size(State, NewSize),
+ Check = check_param_size(State, NewSize),
case Check of
ok ->
NewState =
@@ -125,7 +129,7 @@ addFileChunk(A, [{body, Data}|Res], State) ->
NewData = compute_new_value(PrevValue, Data),
State#upload{param_running_value = NewData}
end,
- addFileChunk(A, Res, NewState#upload{running_file_size = NewSize});
+ add_file_chunk(A, Res, NewState#upload{running_file_size = NewSize});
Error={error, _Reason} ->
Error
end.
@@ -137,9 +141,10 @@ create_temp_file(State) ->
case State#upload.fixed_filename of
undefined ->
{A, B, C} = now(),
- FileName = "yaws_"++ integer_to_list(A) ++
- "_" ++ integer_to_list(B) ++
- "_" ++ integer_to_list(C),
+ FileName = yaws:join_sep(["yaws",
+ integer_to_list(A),
+ integer_to_list(B),
+ integer_to_list(C)], "_"),
filename:join([State#upload.temp_dir, FileName]);
Filename ->
Filename
@@ -151,9 +156,9 @@ create_temp_file(State) ->
end
.
-close_previous_param(State = #upload{param_name = undefined}) ->
+close_previous_param(#upload{param_name = undefined} = State) ->
State;
-close_previous_param(State = #upload{param_name = ParamName}) ->
+close_previous_param(#upload{param_name = ParamName} = State) ->
S2 = case State#upload.filename of
undefined ->
ParamValue = State#upload.param_running_value,
@@ -174,17 +179,19 @@ close_previous_param(State = #upload{param_name = ParamName}) ->
[{temp_file,
State#upload.temp_file}])
end,
+ ParamInfo3 = lists:append(ParamInfo2, State#upload.headers),
State#upload{
filename = undefined,
fd = undefined,
temp_file= undefined,
running_file_size = 0,
- params = dict:store(ParamName, ParamInfo2,
+ params = dict:store(ParamName, ParamInfo3,
State#upload.params)
}
end,
S2#upload{param_name = undefined,
- param_running_value = undefined}.
+ param_running_value = undefined,
+ headers = []}.
compute_new_size(State, Data) ->
case Data of
@@ -3,17 +3,23 @@
-include("../../include/yaws_api.hrl").
-include_lib("eunit/include/eunit.hrl").
+data_to_parse() ->
+ list_to_binary(
+ ["--!!!\r\n",
+ "Content-Disposition: form-data; name=\"abc123\"; "
+ ++ "filename=\"abc123\"\r\n"
+ ++ "Content-Type: text/plain\r\n"
+ ++ "Test-Header: sampledata\r\n\r\n",
+ "sometext\n\r\n--!!!--\r\n"]).
+
complete_parse() ->
- Data = list_to_binary(
- ["--!!!\r\n",
- "Content-Disposition: form-data; name=\"abc123\"; "
- ++ "filename=\"abc123\"\r\n\r\n",
- "sometext\n\r\n--!!!--\r\n"]),
- yaws_api:parse_multipart_post(mk_arg(Data)).
+ yaws_api:parse_multipart_post(mk_arg(data_to_parse())).
complete_parse_test() ->
- {result,[{head,{"abc123", [{filename,"abc123"},{name,"abc123"}]}},
- {body,"sometext\n"}]} = complete_parse().
+ {result,[{head,{"abc123", [{filename,"abc123"},{name,"abc123"},
+ {content_type,"text/plain"},
+ {"test-header","sampledata"}]}},
+ {body,"sometext\n"}]} = complete_parse().
incomplete_body_test() ->
@@ -57,6 +63,14 @@ incomplete_head_test() ->
[{head,{"ghi789",[{filename,"ghi789"},{name,"ghi789"}]}},
{body,"sometext\n"}]} = {Res1, Res2}.
+read_multipart_form_test() ->
+ {done, Dict} = yaws_multipart:read_multipart_form(mk_arg(data_to_parse()),
+ [no_temp_file]),
+ {ok, Params} = dict:find("abc123", Dict),
+ "abc123" = proplists:get_value(filename, Params),
+ "sometext\n" = proplists:get_value(value, Params),
+ "text/plain" = proplists:get_value(content_type, Params),
+ "sampledata" = proplists:get_value("test-header", Params).
mk_arg(Data) ->
ContentType = "multipart/form-data; boundary=!!!",

0 comments on commit 641939a

Please sign in to comment.