Current section
Files
Jump to
Current section
Files
src/couchbeam_httpc.erl
%%% -*- erlang -*-
%%%
%%% This file is part of couchbeam released under the MIT license.
%%% See the NOTICE for more information.
-module(couchbeam_httpc).
-export([request/5,
db_request/5, db_request/6,
json_body/1,
db_resp/2,
make_headers/4,
maybe_oauth_header/4]).
%% urls utils
-export([server_url/1, db_url/1, doc_url/2]).
%% atts utols
-export([reply_att/1, wait_mp_doc/2, len_doc_to_mp_stream/3, send_mp_doc/5]).
-include("couchbeam.hrl").
request(Method, Url, Headers, Body, Options) ->
{FinalHeaders, FinalOpts} = make_headers(Method, Url, Headers,
Options),
hackney:request(Method, Url , FinalHeaders, Body, FinalOpts).
db_request(Method, Url, Headers, Body, Options) ->
db_request(Method, Url, Headers, Body, Options, []).
db_request(Method, Url, Headers, Body, Options, Expect) ->
Resp = request(Method, Url, Headers, Body, Options),
db_resp(Resp, Expect).
json_body(Ref) ->
{ok, Body} = hackney:body(Ref),
couchbeam_ejson:decode(Body).
make_headers(Method, Url, Headers, Options) ->
Headers1 = case couchbeam_util:get_value(<<"Accept">>, Headers) of
undefined ->
[{<<"Accept">>, <<"application/json, */*;q=0.9">>} | Headers];
_ ->
Headers
end,
{Headers2, Options1} = maybe_oauth_header(Method, Url, Headers1, Options),
maybe_proxyauth_header(Headers2, Options1).
maybe_oauth_header(Method, Url, Headers, Options) ->
case couchbeam_util:get_value(oauth, Options) of
undefined ->
{Headers, Options};
OauthProps ->
Hdr = couchbeam_util:oauth_header(Url, Method, OauthProps),
{[Hdr|Headers], proplists:delete(oauth, Options)}
end.
maybe_proxyauth_header(Headers, Options) ->
case couchbeam_util:get_value(proxyauth, Options) of
undefined ->
{Headers, Options};
ProxyauthProps ->
{lists:append([ProxyauthProps,Headers]), proplists:delete(proxyauth, Options)}
end.
db_resp({ok, Ref}=Resp, _Expect) when is_reference(Ref) ->
Resp;
db_resp({ok, 401, _}, _Expect) ->
{error, unauthenticated};
db_resp({ok, 403, _}, _Expect) ->
{error, forbidden};
db_resp({ok, 404, _}, _Expect) ->
{error, not_found};
db_resp({ok, 409, _}, _Expect) ->
{error, conflict};
db_resp({ok, 412, _}, _Expect) ->
{error, precondition_failed};
db_resp({ok, _, _}=Resp, []) ->
Resp;
db_resp({ok, Status, Headers}=Resp, Expect) ->
case lists:member(Status, Expect) of
true -> Resp;
false ->
{error, {bad_response, {Status, Headers, <<>>}}}
end;
db_resp({ok, 401, _, Ref}, _Expect) ->
hackney:skip_body(Ref),
{error, unauthenticated};
db_resp({ok, 403, _, Ref}, _Expect) ->
hackney:skip_body(Ref),
{error, forbidden};
db_resp({ok, 404, _, Ref}, _Expect) ->
hackney:skip_body(Ref),
{error, not_found};
db_resp({ok, 409, _, Ref}, _Expect) ->
hackney:skip_body(Ref),
{error, conflict};
db_resp({ok, 412, _, Ref}, _Expect) ->
hackney:skip_body(Ref),
{error, precondition_failed};
db_resp({ok, _, _, _}=Resp, []) ->
Resp;
db_resp({ok, Status, Headers, Ref}=Resp, Expect) ->
case lists:member(Status, Expect) of
true -> Resp;
false ->
{ok, Body} = hackney:body(Ref),
{error, {bad_response, {Status, Headers, Body}}}
end;
db_resp(Error, _Expect) ->
Error.
%% @doc Asemble the server URL for the given client
%% @spec server_url({Host, Port}) -> iolist()
server_url(#server{url=Url}) ->
Url.
db_url(#db{name=DbName}) ->
DbName.
doc_url(Db, DocId) ->
iolist_to_binary([db_url(Db), <<"/">>, DocId]).
%% attachments handling
%% @hidden
reply_att(ok) ->
ok;
reply_att(done) ->
done;
reply_att({ok, 404, _, Ref}) ->
hackney:skip_body(Ref),
{error, not_found};
reply_att({ok, 409, _, Ref}) ->
hackney:skip_body(Ref),
{error, conflict};
reply_att({ok, Status, _, Ref}) when Status =:= 200 orelse Status =:= 201 ->
{[{<<"ok">>, true}|R]} = couchbeam_httpc:json_body(Ref),
{ok, {R}};
reply_att({ok, Status, Headers, Ref}) ->
{ok, Body} = hackney:body(Ref),
{error, {bad_response, {Status, Headers, Body}}};
reply_att(Error) ->
Error.
%% @hidden
wait_mp_doc(Ref, Buffer) ->
%% we are always waiting for the full doc
case hackney:stream_multipart(Ref) of
{headers, _} ->
wait_mp_doc(Ref, Buffer);
{body, Data} ->
NBuffer = << Buffer/binary, Data/binary >>,
wait_mp_doc(Ref, NBuffer);
end_of_part when Buffer =:= <<>> ->
%% end of part in multipart/mixed
wait_mp_doc(Ref, Buffer);
end_of_part ->
%% decode the doc
{Props} = Doc = couchbeam_ejson:decode(Buffer),
case couchbeam_util:get_value(<<"_attachments">>, Props, {[]}) of
{[]} ->
%% not attachments wait for the eof or the next doc
NState = {Ref, fun() -> wait_mp_doc(Ref, <<>>) end},
{doc, Doc, NState};
{Atts} ->
%% start to receive the attachments
%% we get the list of attnames for the versions of
%% couchdb that don't provide the att name in the
%% header.
AttNames = [AttName || {AttName, _} <- Atts],
NState = {Ref, fun() ->
wait_mp_att(Ref, {nil, AttNames})
end},
{doc, Doc, NState}
end;
mp_mixed ->
%% we are starting a multipart/mixed (probably on revs)
%% continue
wait_mp_doc(Ref, Buffer);
mp_mixed_eof ->
%% end of multipar/mixed wait for the next doc
wait_mp_doc(Ref, Buffer);
eof ->
eof
end.
%% @hidden
wait_mp_att(Ref, {AttName, AttNames}) ->
case hackney:stream_multipart(Ref) of
{headers, Headers} ->
%% old couchdb api doesn't give the content-disposition
%% header so we have to use the list of att names given in
%% the doc. Hopefully the erlang parser keeps it in order.
case hackney_headers:get_value(<<"content-disposition">>,
hackney_headers:new(Headers)) of
undefined ->
[Name | Rest] = AttNames,
NState = {Ref, fun() ->
wait_mp_att(Ref, {Name, Rest})
end},
{att, Name, NState};
CDisp ->
{_, Props} = content_disposition(CDisp),
Name = proplists:get_value(<<"filename">>, Props),
[_ | Rest] = AttNames,
NState = {Ref, fun() ->
wait_mp_att(Ref, {Name, Rest})
end},
{att, Name, NState}
end;
{body, Data} ->
%% return the attachment par
NState = {Ref, fun() -> wait_mp_att(Ref, {AttName, AttNames}) end},
{att_body, AttName, Data, NState};
end_of_part ->
%% wait for the next attachment
NState = {Ref, fun() -> wait_mp_att(Ref, {nil, AttNames}) end},
{att_eof, AttName, NState};
mp_mixed_eof ->
%% wait for the next doc
wait_mp_doc(Ref, <<>>);
eof ->
%% we are done with the multipart request
eof
end.
%% @hidden
content_disposition(Data) ->
hackney_bstr:token_ci(Data, fun
(_Rest, <<>>) ->
{error, badarg};
(Rest, Disposition) ->
hackney_bstr:params(Rest, fun
(<<>>, Params) -> {Disposition, Params};
(_Rest2, _) -> {error, badarg}
end)
end).
%% @hidden
len_doc_to_mp_stream(Atts, Boundary, {Props}) ->
{AttsSize, Stubs} = lists:foldl(fun(Att, {AccSize, AccAtts}) ->
{AttLen, Name, Type, Encoding, _Msg} = att_info(Att),
AccSize1 = AccSize +
4 + %% \r\n\r\n
AttLen +
byte_size(hackney_bstr:to_binary(AttLen)) +
4 + %% "\r\n--"
byte_size(Boundary) +
byte_size(Name) +
byte_size(Type) +
byte_size(<<"\r\nContent-Disposition: attachment; filename=\"\"">> ) +
byte_size(<<"\r\nContent-Type: ">>) +
byte_size(<<"\r\nContent-Length: ">>) +
case Encoding of
<<"identity">> ->
0;
_ ->
byte_size(Encoding) +
byte_size(<<"\r\nContent-Encoding: ">>)
end,
AccAtts1 = [{Name, {[{<<"content_type">>, Type},
{<<"length">>, AttLen},
{<<"follows">>, true},
{<<"encoding">>, Encoding}]}}
| AccAtts],
{AccSize1, AccAtts1}
end, {0, []}, Atts),
Doc1 = case couchbeam_util:get_value(<<"_attachments">>, Props) of
undefined ->
{Props ++ [{<<"_attachments">>, {Stubs}}]};
{OldAtts} ->
%% remove updated attachments from the old list of
%% attachments
OldAtts1 = lists:foldl(fun({Name, AttProps}, Acc) ->
case couchbeam_util:get_value(Name, Stubs) of
undefined ->
[{Name, AttProps} | Acc];
_ ->
Acc
end
end, [], OldAtts),
%% update the list of the attachnebts with the attachments
%% that will be sent in the multipart
FinalAtts = lists:reverse(OldAtts1) ++ Stubs,
{lists:keyreplace(<<"_attachments">>, 1, Props,
{<<"_attachments">>, {FinalAtts}})}
end,
%% eencode the doc
JsonDoc = couchbeam_ejson:encode(Doc1),
%% calculate the final size with the doc part
FinalSize = 2 + % "--"
byte_size(Boundary) +
36 + % "\r\ncontent-type: application/json\r\n\r\n"
byte_size(JsonDoc) +
4 + % "\r\n--"
byte_size(Boundary) +
+ AttsSize +
2, % "--"
{FinalSize, JsonDoc, Doc1}.
%% @hidden
send_mp_doc(Atts, Ref, Boundary, JsonDoc, Doc) ->
%% send the doc
DocParts = [<<"--", Boundary/binary >>,
<<"Content-Type: application/json">>,
<<>>, JsonDoc, <<>>],
DocBin = hackney_bstr:join(DocParts, <<"\r\n">>),
case hackney:send_body(Ref, DocBin) of
ok ->
send_mp_doc_atts(Atts, Ref, Doc, Boundary);
Error ->
Error
end.
%% @hidden
send_mp_doc_atts([], Ref, Doc, Boundary) ->
%% no more attachments, send the final boundary (eof)
case hackney:send_body(Ref, <<"\r\n--", Boundary/binary, "--" >>) of
ok ->
%% collect the response.
mp_doc_reply(Ref, Doc);
Error ->
Error
end;
send_mp_doc_atts([Att | Rest], Ref, Doc, Boundary) ->
{AttLen, Name, Type, Encoding, Msg} = att_info(Att),
BinAttLen = hackney_bstr:to_binary(AttLen),
AttHeadersParts = [<<"--", Boundary/binary >>,
<<"Content-Disposition: attachment; filename=\"", Name/binary, "\"" >>,
<<"Content-Type: ", Type/binary >>,
<<"Content-Length: ", BinAttLen/binary >>],
AttHeadersParts1 = AttHeadersParts ++
case Encoding of
<<"identity">> ->
[<<>>, <<>>];
_ ->
[<<"Content-Encoding: ", Encoding/binary >>, <<>>,
<<>>]
end,
AttHeadersBin = hackney_bstr:join(AttHeadersParts1, <<"\r\n">>),
%% first send the att headers
case hackney:send_body(Ref, AttHeadersBin) of
ok ->
%% send the attachment by itself
case hackney:send_body(Ref, Msg) of
ok ->
%% everything is OK continue to the next attachment
send_mp_doc_atts(Rest, Ref, Doc, Boundary);
Error ->
Error
end;
Error ->
Error
end.
%% @hidden
mp_doc_reply(Ref, Doc) ->
Resp = hackney:start_response(Ref),
case couchbeam_httpc:db_resp(Resp, [200, 201]) of
{ok, _, _, Ref} ->
{JsonProp} = couchbeam_httpc:json_body(Ref),
NewRev = couchbeam_util:get_value(<<"rev">>, JsonProp),
NewDocId = couchbeam_util:get_value(<<"id">>, JsonProp),
%% set the new doc ID
Doc1 = couchbeam_doc:set_value(<<"_id">>, NewDocId, Doc),
%% set the new rev
FinalDoc = couchbeam_doc:set_value(<<"_rev">>, NewRev, Doc1),
%% return the updated document
{ok, FinalDoc};
Error ->
Error
end.
%% @hidden
att_info({Name, {file, Path}=Msg}) ->
CType = mimerl:filename(hackney_bstr:to_binary(Name)),
Len = filelib:file_size(Path),
{Len, Name, CType, <<"identity">>, Msg};
att_info({Name, Bin}) when is_list(Bin) ->
att_info({Name, iolist_to_binary(Bin)});
att_info({Name, Bin}) when is_binary(Bin) ->
CType = mimerl:filename(hackney_bstr:to_binary(Name)),
{byte_size(Bin), Name, CType, <<"identity">>, Bin};
att_info({Name, {file, Path}=Msg, Encoding}) ->
CType = mimerl:filename(hackney_bstr:to_binary(Name)),
Len = filelib:file_size(Path),
{Len, Name, CType, Encoding, Msg};
att_info({Name, {Fun, _Acc0}=Msg, Len}) when is_function(Fun) ->
{Len, Name, <<"application/octet-stream">>, <<"identity">>, Msg};
att_info({Name, Fun, Len}) when is_function(Fun) ->
{Len, Name, <<"application/octet-stream">>, <<"identity">>, Fun};
att_info({Name, Bin, Encoding}) when is_binary(Bin) ->
CType = mimerl:filename(hackney_bstr:to_binary(Name)),
{byte_size(Bin), Name, CType, Encoding, Bin};
att_info({Name, {Fun, _Acc0}=Msg, Len, Encoding}) when is_function(Fun) ->
{Len, Name, <<"application/octet-stream">>, Encoding, Msg};
att_info({Name, Fun, Len, Encoding}) when is_function(Fun) ->
{Len, Name, <<"application/octet-stream">>, Encoding, Fun};
att_info({Name, Bin, CType, Encoding}) when is_binary(Bin) ->
{byte_size(Bin), Name, CType, Encoding, Bin};
att_info({Name, {Fun, _Acc0}=Msg, Len, CType, Encoding})
when is_function(Fun) ->
{Len, Name, CType, Encoding, Msg};
att_info({Name, Fun, Len, CType, Encoding}) when is_function(Fun) ->
{Len, Name, CType, Encoding, Fun}.