Skip to content

Commit

Permalink
Allow HTTP partial response (2600hz#3967)
Browse files Browse the repository at this point in the history
* Allow HTTP partial response

* Added documentation for range request
  • Loading branch information
kalda341 authored and k-anderson committed Jul 20, 2017
1 parent 531cacd commit 785b9af
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 1 deletion.
14 changes: 14 additions & 0 deletions applications/crossbar/doc/basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,17 @@ curl -v \
-H "Content-Type: application/json" \
http://{SERVER_URL}:8000/v2/accounts/{ACCOUNT_ID}?pretty_print=true
```

#### Requesting a range of binary data

It is useful to be able to get just a section of a file when streaming or
resuming a download. This can be accomplished with the range header, eg:

```shell
curl -v \
-H "X-Auth-Token: {AUTH_TOKEN}" \
-H "Content-Type: application/json" \
-H "Accept: audio/mpeg" \
-H "Range: bytes={START_BYTE}-{END_BYTE}" \
http://{SERVER_URL}:8000/v2/accounts/{ACCOUNT_ID}/vmboxes/{VMBOX_ID}/messages/{MESSAGE_ID}/raw
```
45 changes: 44 additions & 1 deletion applications/crossbar/src/api_resource.erl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
,multiple_choices/2
,generate_etag/2
,expires/2
,get_range/2
]).

-include("crossbar.hrl").
Expand Down Expand Up @@ -779,7 +780,28 @@ to_binary(Req, Context, 'undefined') ->
RespData = cb_context:resp_data(Context),
Event = api_util:create_event_name(Context, <<"to_binary">>),
_ = crossbar_bindings:map(Event, {Req, Context}),
{RespData, api_util:set_resp_headers(Req, Context), Context};
%% Handle HTTP range header
case cb_context:req_header(Context, <<"range">>) of
'undefined' ->
{RespData, api_util:set_resp_headers(Req, Context), Context};
RangeHeader ->
RangeData={Content, Start, End, Length, FileLength} = get_range(RespData, RangeHeader),
ErrorCode = resp_error_code_for_range(RangeData),
Setters = [{fun cb_context:set_resp_data/2, Content}
,{fun cb_context:set_resp_error_code/2, ErrorCode}
,{fun cb_context:add_resp_headers/2
,[{<<"Content-Length">>, kz_term:to_binary(Length)}
,{<<"Content-Range">>, kz_term:to_binary(io_lib:fwrite("bytes ~B-~B/~B", [Start, End, FileLength]))}
,{<<"Accept-Ranges">>, <<"bytes">>}
]
}
],
NewContext = cb_context:setters(Context, Setters),
%% Respond, possibly with 206
{'ok', Req1} = cowboy_req:reply(kz_term:to_binary(ErrorCode), cb_context:resp_headers(NewContext), Content, Req),
{'halt', Req1, NewContext}
end;

to_binary(Req, Context, Accept) ->
lager:debug("request has overridden accept header: ~s", [Accept]),
case to_fun(Context, Accept, 'to_binary') of
Expand All @@ -789,6 +811,27 @@ to_binary(Req, Context, Accept) ->
(?MODULE):Fun(Req, Context)
end.

-type range_response() :: {ne_binary(), pos_integer(), pos_integer(), pos_integer(), pos_integer()}.

-spec get_range(ne_binary(), ne_binary()) -> range_response().
get_range(Data, RangeHeader) ->
FileLength = size(Data),
lager:debug("recieved range header ~p for file size ~p", [RangeHeader, FileLength]),
{Start, End} = case cowboy_http:range(RangeHeader) of
{<<"bytes">>, [{S, 'infinity'}]} -> {S, FileLength};
{<<"bytes">>, [{S, E}]} when E < FileLength -> {S, E + 1};
_Thing ->
lager:info("invalid range specification ~p", [RangeHeader]),
{0, FileLength}
end,
ContentLength = End - Start,
<<_:Start/binary, Content:ContentLength/binary, _/binary>> = Data,
{Content, Start, End - 1, ContentLength, FileLength}.

-spec resp_error_code_for_range(range_response()) -> 200 | 206.
resp_error_code_for_range({_Content, _Start, _End, FileLength, FileLength}) -> 200;
resp_error_code_for_range({_Content, _Start, _End, _Length, _FileLength}) -> 206.

-spec send_file(cowboy_req:req(), cb_context:context()) -> api_util:pull_file_response_return().
send_file(Req, Context) ->
lager:debug("run: send_file"),
Expand Down
14 changes: 14 additions & 0 deletions applications/crossbar/test/api_resource_test.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-module(api_resource_test).

-include_lib("eunit/include/eunit.hrl").

get_range_test_() ->
FullBinary = <<"abcdefg">>,
[?_assertEqual({<<"a">>, 0, 0, 1, 7}, api_resource:get_range(FullBinary, <<"bytes=0-0">>))
,?_assertEqual({<<"bcd">>, 1, 3, 3, 7}, api_resource:get_range(FullBinary, <<"bytes=1-3">>))
,?_assertEqual({<<"g">>, 6, 6, 1, 7}, api_resource:get_range(FullBinary, <<"bytes=6-6">>))
,?_assertEqual({FullBinary, 0, 6, 7, 7}, api_resource:get_range(FullBinary, <<"bytes=0-6">>))
%% Invalid should give full size
,?_assertEqual({FullBinary, 0, 6, 7, 7}, api_resource:get_range(FullBinary, <<"bytes=0-9">>))
,?_assertEqual({FullBinary, 0, 6, 7, 7}, api_resource:get_range(FullBinary, <<"">>))
].

0 comments on commit 785b9af

Please sign in to comment.