src/cowmachine_response.erl

%% @author Justin Sheehy <justin@basho.com>
%% @author Andy Gross <andy@basho.com>
%% @copyright 2007-2009 Basho Technologies, 2018-2022 Marc Worrell.
%% Based on mochiweb_request.erl, which is Copyright 2007 Mochi Media, Inc.
%% @doc Response functions, generate the response inclusive body and headers.
%% The body can be sourced from multiple sources. These sources include files,
%% binary files and functions.
%% The response body function handles range requests.
%% @reference <a href="https://github.com/mochi/mochiweb/blob/main/src/mochiweb_request.erl">mochiweb_request.erl</a>
%% @end
%%
%%    Licensed under the Apache License, Version 2.0 (the "License");
%%    you may not use this file except in compliance with the License.
%%    You may obtain a copy of the License at
%%
%%        http://www.apache.org/licenses/LICENSE-2.0
%%
%%    Unless required by applicable law or agreed to in writing, software
%%    distributed under the License is distributed on an "AS IS" BASIS,
%%    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%    See the License for the specific language governing permissions and
%%    limitations under the License.


-module(cowmachine_response).
-author('Marc Worrell <marc@worrell.nl>').

-export([
     send_response/1,
     server_header/0
     ]).

% Debugging
-export([
    send_stream_body/2
]).

-define(FILE_CHUNK_LENGTH, 16#80000). % 512KB

%% @doc Returns server header.
-spec server_header() -> Result when
	Result :: binary().
server_header() ->
    case application:get_env(cowmachine, server_header) of
        {ok, Server} -> z_convert:to_binary(Server);
        undefined ->
            case application:get_key(cowmachine, vsn) of
                {ok, Version} -> <<"CowMachine/", (z_convert:to_binary(Version))/binary>>;
                undefined -> <<"CowMachine">>
            end
    end.

%% @doc Send responce.
-spec send_response(Context) -> Result
    when 
		Context :: cowmachine_req:context(),
		Result :: {ok, Req, Env} | {stop, Req},
		Req :: cowboy_req:req(),
        Env :: cowboy_middleware:env().
send_response(Context) ->
    ControllerOptions = cowmachine_req:controller_options(Context),
    HttpStatusCode = case proplists:get_value(http_status_code, ControllerOptions) of
                         Code when is_integer(Code) -> Code;
                         _ -> cowmachine_req:response_code(Context)
                     end,
    Context1 = send_response_range(HttpStatusCode, Context),
    {ok, cowmachine_req:req(Context1), cowmachine_req:env(Context1)}.

%% --------------------------------------------------------------------------------------------------
%% Internal functions
%% --------------------------------------------------------------------------------------------------

-spec send_response_range(Code, Context) -> Result when
	Code :: integer(),
	Context :: cowmachine_req:context(),
	Result :: cowmachine_req:context().
send_response_range(200, Context) ->
    {Range, Context1} = get_range(Context),
    case Range of
        undefined ->
            send_response_code(200, all, Context1);
        Ranges ->
            case get_resp_body_size(cowmachine_req:resp_body(Context1)) of
                {ok, Size, Body1} ->
                    Context2 = cowmachine_req:set_resp_body(Body1, Context1),
                    case range_parts(Ranges, Size) of
                        [] ->
                            % no valid ranges
                            % could be 416, for now we'll just return 200 and the whole body
                            send_response_code(200, all, Context2);
                        PartList ->
                            % error_logger:info_msg("PARTS ~p (~p)", [PartList, wrq:path(ReqData)]),
                            ContentType = cowmachine_req:get_resp_header(<<"content-type">>, Context2),
                            {RangeHeaders, Boundary} = make_range_headers(PartList, Size, ContentType),
                            Context3 = cowmachine_req:set_resp_headers(RangeHeaders, Context2),
                            send_response_code(206, {PartList, Size, Boundary, ContentType}, Context3)
                    end;
                {error, nosize} ->
                    send_response_code(200, all, Context1)
            end
    end;
send_response_range(Code, Context) ->
    send_response_code(Code, all, Context).

-spec send_response_code(Code, Parts, Context) -> Result when
	Code :: integer(), 
	Parts :: cowmachine_req:parts(),
	Context :: cowmachine_req:context(),
	Result :: cowmachine_req:context().
send_response_code(Code, Parts, Context) ->
    send_response_bodyfun(cowmachine_req:resp_body(Context), Code, Parts, Context).

-spec send_response_bodyfun(RespBody, Code, Parts, Context) -> Result when
	RespBody :: cowmachine_req:resp_body(),
	Code :: integer(), 
	Parts :: cowmachine_req:parts(),
	Context :: cowmachine_req:context(),
	Result :: cowmachine_req:context().
% send_response_bodyfun(undefined, Code, Parts, Context) ->
%     send_response_bodyfun(<<>>, Code, Parts, Context);
send_response_bodyfun({device, IO}, Code, Parts, Context) ->
    Length = iodevice_size(IO),
    send_response_bodyfun({device, Length, IO}, Code, Parts, Context);
send_response_bodyfun({device, Length, IO}, Code, all, Context) ->
    Writer = fun(FunContext) ->
                send_device_body(FunContext, Length, IO),
                _ = file:close(IO),
                FunContext
             end,
    start_response_stream(Code, Length, Writer, all, Context);
send_response_bodyfun({device, _Length, IO}, Code, Parts, Context) ->
    Writer = fun(FunContext, WParts) ->
                FunContext1 = send_device_body_parts(FunContext, WParts, IO),
                _ = file:close(IO),
                FunContext1
             end,
    start_response_stream(Code, undefined, Writer, Parts, Context);
% File
send_response_bodyfun({file, Filename}, Code, Parts, Context) ->
    Length = filelib:file_size(Filename),
    send_response_bodyfun({file, Length, Filename}, Code, Parts, Context);
send_response_bodyfun({file, Length, Filename}, Code, all, Context) ->
    Writer = fun(FunContext) ->
        send_file_body(FunContext, Length, Filename, fin)
    end,
    start_response_stream(Code, Length, Writer, all, Context);
send_response_bodyfun({file, _Length, Filename}, Code, Parts, Context) ->
    Writer = fun(FunContext, WParts) ->
        send_file_body_parts(FunContext, WParts, Filename)
    end,
    start_response_stream(Code, undefined, Writer, Parts, Context);
% Stream functions with continuation
send_response_bodyfun({stream, {_, _} = InitialStream}, Code, Parts, Context) ->
    start_response_stream(Code, undefined, InitialStream, Parts, Context);
send_response_bodyfun({stream, Size, {_, _} = InitialStream}, Code, Parts, Context) ->
    start_response_stream(Code, Size, InitialStream, Parts, Context);
send_response_bodyfun({stream, StreamFun}, Code, Parts, Context) when is_function(StreamFun) ->
    start_response_stream(Code, undefined, {<<>>, StreamFun}, Parts, Context);
send_response_bodyfun({stream, Size, StreamFun}, Code, Parts, Context) when is_function(StreamFun) ->
    start_response_stream(Code, Size, {<<>>, StreamFun}, Parts, Context);
% send_response_bodyfun({stream, {_Data, Fun} = Initial, Code, Parts, Context) when is_function(Fun, 1) ->
%     Writer = fun(FunContext) -> send_stream_body(FunContext, Fun(Parts)) end,
%     start_response_stream(Code, undefined, Writer, Context);
% % Stream without parts - remove parts response header
% send_response_bodyfun({stream, Fun}, Code, Parts, Context) when is_function(Fun, 0) ->
%     Writer = fun(FunContext) -> send_stream_body(FunContext, Fun()) end,
%     start_response_stream(Code, undefined, Writer, Context);
% send_response_bodyfun({stream, Size, Fun}, Code, all, Context) when is_function(Fun, 0) ->
%     Writer = fun(FunContext) -> send_stream_body(FunContext, Fun()) end,
%     start_response_stream(Code, undefined, Writer, Context);
% % Stream with parts (managed by the streaming function)
% send_response_bodyfun({stream, Fun}, Code, Parts, Context) when is_function(Fun, 1) ->
%     Writer = fun(FunContext) -> send_stream_body(FunContext, Fun(Parts)) end,
%     start_response_stream(Code, undefined, Writer, Context);
% send_response_bodyfun({stream, Size, Fun}, Code, Parts, Context) when is_function(Fun, 1) ->
%     Writer = fun(FunContext) -> send_stream_body(FunContext, Fun(Parts) end,
%     start_response_stream(Code, undefined, Writer, Context);
% Writer
send_response_bodyfun({writer, WriterFun}, Code, all, Context) ->
    Writer = fun(FunContext) ->
        send_writer_body(FunContext, WriterFun)
    end,
    start_response_stream(Code, undefined, Writer, all, Context);
% Data
send_response_bodyfun(undefined, Code, Parts, Context) ->
    send_response_bodyfun(<<>>, Code, Parts, Context);
send_response_bodyfun(Body, Code, all, Context) ->
    Length = iolist_size(Body),
    Headers = response_headers(Code, Length, Context),
    Req = cowmachine_req:req(Context),
    Req1 = cowboy_req:reply(Code, Headers, Body, Req),
    cowmachine_req:set_req(Req1, Context);
send_response_bodyfun(Body, Code, Parts, Context) ->
    Headers = response_headers(Context),
    Req = cowmachine_req:req(Context),
    Req1 = cowboy_req:stream_reply(Code, Headers, Req),
    Context1 = cowmachine_req:set_req(Req1, Context),
    send_parts(Context1, Parts, iolist_to_binary(Body)).

-spec start_response_stream(Code, Length, InitialStream, Parts, Context) -> Result when
	Code :: integer(),
	Length :: non_neg_integer() | undefined,
	InitialStream :: function() | tuple(),
	Parts :: cowmachine_req:parts(),
	Context :: cowmachine_req:context(),
	Result :: cowmachine_req:context().
start_response_stream(Code, Length, InitialStream, Parts, Context) ->
    {Code1, Context1, Parts1} = case is_streaming_range(InitialStream) of
        false when Parts =/= all ->
            % Drop range response header
            C1 = cowmachine_req:remove_resp_header(<<"accept-ranges">>, Context),
            C2 = cowmachine_req:remove_resp_header(<<"content-range">>, C1),
            C3 = cowmachine_req:remove_resp_header(<<"content-length">>, C2),
            {200, C3, all};
        false ->
            {Code, Context, all};
        true ->
            {Code, Context, Parts}
    end,
    Headers = response_headers(Code1, Length, Context1),
    Req = cowmachine_req:req(Context1),
    Req1 = cowboy_req:stream_reply(Code, Headers, Req),
    Context2 = cowmachine_req:set_req(Req1, Context1),
    FirstHunk = case InitialStream of
        {InitialData, InitialFun} ->
            {InitialData, stream_initial_fun(InitialFun, Parts1)};
        InitialFun ->
            stream_initial_fun(InitialFun, Parts1)
    end,
    send_stream_body(FirstHunk, Context2).

-spec stream_initial_fun(Fun, Parts) -> Result when 
	Fun :: function(), 
	Parts :: cowmachine_req:parts(), 
	Result :: done | function().
stream_initial_fun(F, Parts) when is_function(F, 2) ->
    fun(Ctx) -> F(Ctx, Parts) end;
stream_initial_fun(F, _Parts) when is_function(F) ->
    F;
stream_initial_fun(done, _Parts) ->
    done.


%% @doc Check if we support ranges on the data stream (body or function)
-spec is_streaming_range(Stream) -> Result when
	Stream :: Fun | {binary(), Fun} | {any(), Fun} | {any, done},
	Fun :: function(),
	Result :: boolean().
is_streaming_range(Fun) when is_function(Fun, 2) -> true;
is_streaming_range(Fun) when is_function(Fun) -> false;
is_streaming_range({<<>>, Fun}) when is_function(Fun, 2) ->
    % Only support ranges if the initial binary is a range, until we
    % add functionality to stream the data from the binary in accordance
    % with the requested ranges (and update the range given to the range function)
    true;
is_streaming_range({_, Fun}) when is_function(Fun) -> false;
is_streaming_range({_, done}) -> false.


%% @todo Add the cookies!

-spec response_headers(Code, Length, Context) -> Result when
	Code :: integer(), 
	Length :: non_neg_integer() | undefined, 
	Context :: cowmachine_req:context(),
	Result :: map().
response_headers(Code, Length, Context) ->
    Hdrs = cowmachine_req:get_resp_headers(Context),
    Hdrs1 = case Code of
        304 ->
            Hdrs;
        _ when is_integer(Length) ->
            Hdrs#{<<"content-length">> => integer_to_binary(Length)};
        _ ->
            Hdrs
    end,
    Hdrs1#{
        <<"server">> => server_header(),
        <<"date">> => cowboy_clock:rfc1123()
    }.

-spec response_headers(Context) -> Result when
	Context :: cowmachine_req:context(),
	Result :: map().
response_headers(Context) ->
    Hdrs = cowmachine_req:get_resp_headers(Context),
    Hdrs#{
        <<"server">> => server_header(),
        <<"date">> => cowboy_clock:rfc1123()
    }.

% With continuation
%% @doc Send stream body.

-spec send_stream_body(FunContext, Context) -> Result when	
	FunContext :: {InitialData, InitialFun} | InitialFun,
	InitialData :: binary() | {file, Filename} | {file, Size, Filename} | done | WriterFun, 
	Filename :: file:filename_all(),
	WriterFun :: function(),
	Size :: non_neg_integer(),
	InitialFun :: function(),
	Context :: cowmachine_req:context(),
	Result :: cowmachine_req:context().
send_stream_body({<<>>, done}, Context) ->
    % @TODO: in cowboy this give a wrong termination with two 0 size chunks
    send_chunk(Context, <<>>, fin);
send_stream_body({<<>>, Next}, Context) ->
    next(Next, Context);
send_stream_body({Data, Next}, Context) when is_binary(Data); is_list(Data) ->
    send_chunk(Context, Data, fin(Next)),
    next(Next, Context);
send_stream_body({{file, Filename}, Next}, Context) ->
    Length = filelib:file_size(Filename),
    send_stream_body({{file, Length, Filename}, Next}, Context);
send_stream_body({{file, 0, _Filename}, Next}, Context) ->
    send_stream_body(Context, {<<>>, Next});
send_stream_body({{file, Size, Filename}, Next}, Context) ->
    Context1 = send_file_body(Context, Size, Filename, fin(Next)),
    next(Next, Context1);
% Without continuation
send_stream_body(done, Context) ->
    send_chunk(Context, <<>>, fin);
send_stream_body(WriterFun, Context) when is_function(WriterFun, 1) ->
    WriterFun(Context);
send_stream_body(WriterFun, Context) when is_function(WriterFun, 0) ->
    _ = WriterFun(),
    Context.

-spec next(Fun, Context) -> Result when 
	Fun :: fun((any()) -> any()),
	Context :: cowmachine_req:context(),
	Result :: Context. 
next(Fun, Context) when is_function(Fun, 1) ->
    send_stream_body(Fun(Context), Context);
next(Fun, Context) when is_function(Fun, 0) ->
    send_stream_body(Fun(), Context);
next(done, Context) ->
    Context.

-spec fin(Next) -> Result when
	Next :: done | any(),
	Result :: fin | nofin.
fin(done) -> fin;
fin(_) -> nofin.

%%@equiv send_file_body_loop(Context, 0, Length, IO, fin)

-spec send_device_body(Context, Length, IO) -> Result when
	Context :: cowmachine_req:context(),
	Length :: non_neg_integer(), 
	IO :: file:io_device(), 
	Result :: cowmachine_req:context().
send_device_body(Context, Length, IO) ->
    send_file_body_loop(Context, 0, Length, IO, fin).

-spec send_file_body(Context, Length, File, FinNoFin) -> Result when
	Context :: cowmachine_req:context(),
	Length :: non_neg_integer(), 
	File :: Filename | file:iodata(),
	Filename :: file:name_all(),
	FinNoFin :: fin | nofin,
	Result :: cowmachine_req:context().
send_file_body(Context, Length, Filename, FinNoFin) ->
    {ok, FD} = file:open(Filename, [read,raw,binary]),
    try
        send_file_body_loop(Context, 0, Length, FD, FinNoFin)
    after
        file:close(FD)
    end.

-spec send_device_body_parts(Context, Parts, IO) -> Result when
	Context :: cowmachine_req:context(),
	Parts :: cowmachine_req:parts(),
	IO :: file:io_device(),
	Result :: cowmachine_req:context().
send_device_body_parts(Context, {[{From,Length}], _Size, _Boundary, _ContentType}, IO) ->
    {ok, _} = file:position(IO, From), 
    send_file_body_loop(Context, 0, Length, IO, fin);
send_device_body_parts(Context, {Parts, Size, Boundary, ContentType}, IO) ->
    lists:foreach(
        fun({From,Length}) ->
            {ok, _} = file:position(IO, From), 
            send_chunk(Context, part_preamble(Boundary, ContentType, From, Length, Size), nofin),
            send_file_body_loop(Context, 0, Length, IO, nofin),
            send_chunk(Context, <<"\r\n">>, nofin)
        end,
        Parts),
    send_chunk(Context, end_boundary(Boundary), fin).

-spec send_file_body_parts(Context, Parts, Filename) -> Result when
	Context :: cowmachine_req:context(),
	Parts :: cowmachine_req:parts(),
	Filename :: file:filename_all(),
	Result :: ok | {error, term()}.
send_file_body_parts(Context, Parts, Filename) ->
    {ok, FD} = file:open(Filename, [raw,binary]),
    try
        send_device_body_parts(Context, Parts, FD)
    after
        file:close(FD)
    end.

-spec send_parts(Context, Parts, Bin) -> Result when
	Context :: cowmachine_req:context(),
	Parts :: cowmachine_req:parts(),
	Bin :: binary(),
	Result :: cowmachine_req:context().
send_parts(Context, {[{From,Length}], _Size, _Boundary, _ContentType}, Bin) ->
    send_chunk(Context, binary:part(Bin,From,Length), fin);
send_parts(Context, {Parts, Size, Boundary, ContentType}, Bin) ->
    lists:foreach(
        fun({From,Length}) ->
            Part = [
                part_preamble(Boundary, ContentType, From, Length, Size),
                binary:part(Bin,From,Length),
                <<"\r\n">>
            ],
            send_chunk(Context, Part, nofin)
        end,
        Parts),
    send_chunk(Context, end_boundary(Boundary), fin).

-spec send_file_body_loop(Context, Offset, Size, Device, FinNoFin) -> Result when
	Context :: cowmachine_req:context(),
	Offset :: integer(), 
	Size :: non_neg_integer(), 
	Device :: file:io_device(), 
	FinNoFin :: fin | nofin, 
	Result :: cowmachine_req:context().
send_file_body_loop(Context, Offset, Size, _Device, FinNoFin) when Offset =:= Size ->
    send_chunk(Context, <<>>, FinNoFin);
send_file_body_loop(Context, Offset, Size, Device, FinNoFin) when Size - Offset =< ?FILE_CHUNK_LENGTH ->
    {ok, Data} = file:read(Device, Size - Offset),
    send_chunk(Context, Data, FinNoFin);
send_file_body_loop(Context, Offset, Size, Device, FinNoFin) ->
    {ok, Data} = file:read(Device, ?FILE_CHUNK_LENGTH),
    send_chunk(Context, Data, nofin),
    send_file_body_loop(Context, Offset+iolist_size(Data), Size, Device, FinNoFin).

-spec send_writer_body(Context, BodyFun) -> Result when
	Context :: cowmachine_req:context(),
	BodyFun :: function(), 
	Result :: any().
send_writer_body(Context, BodyFun) ->
    BodyFun(fun(Data, false, ContextW) ->
                    send_chunk(ContextW, Data, nofin);
               (Data, true, ContextW) ->
                    send_chunk(ContextW, Data, fin)
            end,
            Context).

-spec send_chunk(Context, Data, IsFin) -> Result when
	Context :: cowmachine_req:context(),
	Data :: iolist() | binary(),
	IsFin :: fin | nofin,
	Result :: Context.
send_chunk(Context, <<>>, nofin) ->
    Context;
send_chunk(Context, [], nofin) ->
    Context;
send_chunk(Context, Data, FinNoFin) ->
    Data1 = iolist_to_binary(Data),
    Req = cowmachine_req:req(Context),
    ok = cowboy_req:stream_body(Data1, FinNoFin, Req),
    Context.

-spec get_range(Context) -> Result when
    Context :: cowmachine_req:context(),
	Result :: {Range, cowmachine_req:context()},
	Range :: undefined | [ {integer()|none, integer()|none} ].
get_range(Context) ->
    Env = cowmachine_req:env(Context),
    Range = case maps:get(cowmachine_range_ok, Env) of
        false ->
            undefined;
        true ->
            Req = cowmachine_req:req(Context),
            case cowboy_req:header(<<"range">>, Req) of
                undefined ->
                    undefined;
                RawRange ->
                    parse_range_request(RawRange)
            end
    end,
    Env1 = Env#{ cowmachine_range => Range },
    {Range, cowmachine_req:set_env(Env1, Context)}.

-spec parse_range_request(RangeRequest) -> Result when 
	RangeRequest :: undefined | binary(),
	Result :: undefined | [{integer()|none,integer()|none}].
parse_range_request(<<"bytes=", RangeString/binary>>) ->
    try
        Ranges = binary:split(binary:replace(RangeString, <<" ">>, <<>>, [global]), <<",">>, [global]),
        lists:map(
            fun (<<"-", V/binary>>)  ->
                   {none, binary_to_integer(V)};
                (R) ->
                    case binary:split(R, <<"-">>) of
                        [S1, <<>>] -> {binary_to_integer(S1), none};
                        [<<>>, S2] -> {none, binary_to_integer(S2)};
                        [S1, S2] -> {binary_to_integer(S1), binary_to_integer(S2)};
                        [S1] -> {binary_to_integer(S1), none}
                    end
          end,
          Ranges)
    catch
        _:_ ->
            % Invalid range header, ignore silently
            undefined
    end;
parse_range_request(_) ->
    undefined.

% Map request ranges to byte ranges, taking the total body length into account.

-spec range_parts(Ranges, Size) -> Result when
	Ranges :: [{integer() | none, integer() | none}], 
	Size :: integer(),
	Result :: [{integer(),integer()}].
range_parts(Ranges, Size) ->
    Ranges1 = [ range_skip_length(Spec, Size) || Spec <- Ranges ],
    [ R || R <- Ranges1, R =/= invalid_range ].

-spec range_skip_length(Spec, Size) -> Result when
	Spec :: {integer() | none, integer() | none},
	Size :: integer(),
	Result :: invalid_range | {integer(),integer()}.
range_skip_length({none, R}, Size) when R =< Size, R >= 0 ->
    {Size - R, R};
range_skip_length({none, _OutOfRange}, Size) ->
    {0, Size};
range_skip_length({R, none}, Size) when R >= 0, R < Size ->
    {R, Size - R};
range_skip_length({_OutOfRange, none}, _Size) ->
    invalid_range;
range_skip_length({Start, End}, Size) when 0 =< Start, Start =< End, End < Size ->
    {Start, End - Start + 1};
range_skip_length({_OutOfRange, _End}, _Size) ->
    invalid_range.

-spec get_resp_body_size(RespBody) -> Result when
	RespBody :: cowmachine_req:resp_body(),
	Result :: {ok, integer(), cowmachine_req:resp_body()}
        | {error, nosize}.
get_resp_body_size({device, Size, _} = Body) ->
    {ok, Size, Body};
get_resp_body_size({device, IO}) ->
    Length = iodevice_size(IO),
    {ok, Length, {device, Length, IO}};
get_resp_body_size({file, Size, _} = Body) ->
    {ok, Size, Body};
get_resp_body_size({file, Filename}) ->
    Length = filelib:file_size(Filename),
    {ok, Length, {file, Length, Filename}};
get_resp_body_size(B) when is_binary(B) ->
    {ok, size(B), B};
get_resp_body_size(L) when is_list(L) ->
    B = iolist_to_binary(L),
    {ok, size(B), B};
get_resp_body_size(_) ->
    {error, nosize}.

-spec make_range_headers(Parts, Size, ContentType) -> Result when
	Parts :: list(), 
	Size :: non_neg_integer(),
	ContentType :: binary() | undefined,
	Result :: {list(), none} | {list(), binary()}.
make_range_headers([{Start, Length}], Size, _ContentType) ->
    HeaderList = [{<<"accept-ranges">>, <<"bytes">>},
                  {<<"content-range">>, iolist_to_binary([
                            "bytes ", make_io(Start), "-", make_io(Start+Length-1), "/", make_io(Size)])},
                  {<<"content-length">>, integer_to_binary(Length)}],
    {HeaderList, none};
make_range_headers(Parts, Size, ContentType) when is_list(Parts) ->
    Boundary = boundary(),
    Lengths = [
        iolist_size(part_preamble(Boundary, ContentType, Start, Length, Size)) + Length + 2
        || {Start,Length} <- Parts
    ],
    TotalLength = lists:sum(Lengths) + iolist_size(end_boundary(Boundary)), 
    HeaderList = [{<<"accept-ranges">>, <<"bytes">>},
                  {<<"content-type">>, <<"multipart/byteranges; boundary=", Boundary/binary>>},
                  {<<"content-length">>, integer_to_binary(TotalLength)}],
    {HeaderList, Boundary}.

-spec part_preamble(Boundary, CType, Start, Length, Size) -> Result when
	Boundary :: binary(), 
	CType :: binary(), 
	Start :: non_neg_integer(), 
	Length :: non_neg_integer(), 
	Size :: non_neg_integer(), 
	Result :: [binary()].
part_preamble(Boundary, CType, Start, Length, Size) ->
    [boundary(Boundary),
     <<"content-type: ">>, CType,
     <<"\r\ncontent-range: bytes ">>, 
        integer_to_binary(Start), <<"-">>, integer_to_binary(Start+Length-1), 
        <<"/">>, integer_to_binary(Size),
     <<"\r\n\r\n">>].

-spec boundary() -> Result when
	Result :: binary().
boundary() ->
    A = rand:uniform(100000000),
    B = rand:uniform(100000000),
    <<(integer_to_binary(A))/binary, $_, (integer_to_binary(B))/binary>>.

-spec boundary(B) -> Result when
	B :: binary(),
	Result :: binary().
boundary(B)     -> <<"--", B/binary, "\r\n">>.

-spec end_boundary(B) -> Result when
	B :: binary(),
	Result :: binary().
end_boundary(B) -> <<"--", B/binary, "--\r\n">>.

-spec make_io(Integer) -> Result when
	Integer :: integer(),
	Result :: list().
make_io(Integer) when is_integer(Integer) ->
    integer_to_list(Integer).
% make_io(Atom) when is_atom(Atom) ->
%     atom_to_list(Atom);
% make_io(Io) when is_list(Io); is_binary(Io) ->
%     Io.

-spec iodevice_size(IoDevice) -> Result when
	IoDevice :: file:io_device(),
	Size :: non_neg_integer(),
	Result :: Size.
iodevice_size(IoDevice) ->
    {ok, Size} = file:position(IoDevice, eof),
    {ok, 0} = file:position(IoDevice, bof),
    Size.