diff options
-rw-r--r-- | src/cowboy_rest.erl | 8 | ||||
-rw-r--r-- | test/handlers/provide_range_callback_h.erl | 20 | ||||
-rw-r--r-- | test/rest_handler_SUITE.erl | 64 |
3 files changed, 91 insertions, 1 deletions
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl index d908b40..b874692 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -1363,7 +1363,13 @@ set_ranged_body_callback(Req, State=#state{handler=Handler}, Callback) -> %% this for non-bytes units they can always return a single range with a binary %% content-range information. {Ranges, Req2, State2} when length(Ranges) > 1 -> - set_multipart_ranged_body(Req2, State2, Ranges) + %% We have to check whether there are sendfile tuples in the + %% ranges to be sent. If there are we must use stream_reply. + HasSendfile = [] =/= [true || {_, {sendfile, _, _, _}} <- Ranges], + case HasSendfile of + true -> send_multipart_ranged_body(Req2, State2, Ranges); + false -> set_multipart_ranged_body(Req2, State2, Ranges) + end end catch Class:{case_clause, no_call} -> error_terminate(Req, State, Class, {error, {missing_callback, {Handler, Callback, 2}}, 'A callback specified in ranges_provided/2 is not exported.'}) diff --git a/test/handlers/provide_range_callback_h.erl b/test/handlers/provide_range_callback_h.erl index 136e37e..5b23e07 100644 --- a/test/handlers/provide_range_callback_h.erl +++ b/test/handlers/provide_range_callback_h.erl @@ -43,11 +43,31 @@ get_text_plain(Req, State) -> get_text_plain_bytes(#{qs := <<"missing">>}, _) -> ct_helper_error_h:ignore(cowboy_rest, set_ranged_body_callback, 3), no_call; +get_text_plain_bytes(Req=#{qs := <<"sendfile">>, range := {_, [{From=0, infinity}]}}, State) -> + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + {[{{From, Size - 1, Size}, {sendfile, From, Size, Path}}], Req, State}; get_text_plain_bytes(Req=#{range := {_, [{From=0, infinity}]}}, State) -> %% We send everything in one part. Body = <<"This is ranged REST!">>, Total = byte_size(Body), {[{{From, Total - 1, Total}, Body}], Req, State}; +get_text_plain_bytes(Req=#{qs := <<"sendfile">>, range := {_, Range}}, State) -> + %% We check the range header we get and send everything hardcoded. + [ + {50, 99}, + {150, 199}, + {250, 299}, + -99 + ] = Range, + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + {[ + {{50, 99, Size}, {sendfile, 50, 50, Path}}, + {{150, 199, Size}, {sendfile, 150, 50, Path}}, + {{250, 299, Size}, {sendfile, 250, 50, Path}}, + {{Size - 99, Size - 1, Size}, {sendfile, Size - 99, 99, Path}} + ], Req, State}; get_text_plain_bytes(Req=#{range := {_, Range}}, State) -> %% We check the range header we get and send everything hardcoded. [ diff --git a/test/rest_handler_SUITE.erl b/test/rest_handler_SUITE.erl index 58898b2..363f789 100644 --- a/test/rest_handler_SUITE.erl +++ b/test/rest_handler_SUITE.erl @@ -413,6 +413,30 @@ provide_range_callback(Config) -> {ok, <<"This is ranged REST!">>} = gun:await_body(ConnPid, Ref), ok. +provide_range_callback_sendfile(Config) -> + doc("A successful request for a single range results in a " + "206 partial content response with content-range set. (RFC7233 4.1, RFC7233 4.2)"), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/provide_range_callback?sendfile", [ + {<<"accept-encoding">>, <<"gzip">>}, + {<<"range">>, <<"bytes=0-">>} + ]), + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + {ok, Body} = file:read_file(Path), + {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + {_, ContentRange} = lists:keyfind(<<"content-range">>, 1, Headers), + ContentRange = iolist_to_binary([ + <<"bytes 0-">>, + integer_to_binary(Size - 1), + <<"/">>, + integer_to_binary(Size) + ]), + {_, <<"text/plain">>} = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, Body} = gun:await_body(ConnPid, Ref), + ok. + provide_range_callback_multipart(Config) -> doc("A successful request for multiple ranges results in a " "206 partial content response using the multipart/byteranges " @@ -442,6 +466,46 @@ provide_range_callback_multipart(Config) -> <<"ThisisrangedREST!">> = BodyAcc, ok. +provide_range_callback_multipart_sendfile(Config) -> + doc("A successful request for multiple ranges results in a " + "206 partial content response using the multipart/byteranges " + "content-type and the content-range not being set. The real " + "content-type and content-range of the parts can be found in " + "the multipart headers. (RFC7233 4.1, RFC7233 A)"), + ConnPid = gun_open(Config), + Ref = gun:get(ConnPid, "/provide_range_callback?sendfile", [ + {<<"accept-encoding">>, <<"gzip">>}, + %% This range selects a few random chunks of the file. + {<<"range">>, <<"bytes=50-99, 150-199, 250-299, -99">>} + ]), + Path = code:lib_dir(cowboy) ++ "/ebin/cowboy.app", + Size = filelib:file_size(Path), + Skip = Size - 399, + {ok, << + _:50/binary, Body1:50/binary, + _:50/binary, Body2:50/binary, + _:50/binary, Body3:50/binary, + _:Skip/binary, Body4/bits>>} = file:read_file(Path), + {response, nofin, 206, Headers} = gun:await(ConnPid, Ref), + {_, <<"bytes">>} = lists:keyfind(<<"accept-ranges">>, 1, Headers), + false = lists:keyfind(<<"content-range">>, 1, Headers), + {_, <<"multipart/byteranges; boundary=", Boundary/bits>>} + = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, Body0} = gun:await_body(ConnPid, Ref), + Body = do_decode(Headers, Body0), + %% We will receive the ranges in the same order as requested. + {ContentRanges, BodyAcc} = do_provide_range_callback_multipart_body(Body, Boundary, [], <<>>), + LastFrom = 300 + Skip, + LastTo = Size - 1, + [ + {bytes, 50, 99, Size}, + {bytes, 150, 199, Size}, + {bytes, 250, 299, Size}, + {bytes, LastFrom, LastTo, Size} + ] = ContentRanges, + BodyAcc = <<Body1/binary, Body2/binary, Body3/binary, Body4/binary>>, + ok. + do_provide_range_callback_multipart_body(Rest, Boundary, ContentRangesAcc, BodyAcc) -> case cow_multipart:parse_headers(Rest, Boundary) of {ok, Headers, Rest1} -> |