blob: 9e9657fdff4318b31bd8ef7cd70ffbdfff1d22b1 [file] [log] [blame]
% 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(couch_httpd_db).
-include("couch_db.hrl").
-export([handle_request/1, handle_compact_req/2, handle_design_req/2,
db_req/2, couch_doc_open/4,handle_changes_req/2,
update_doc_result_to_json/1, update_doc_result_to_json/2,
handle_design_info_req/2, handle_view_cleanup_req/2]).
-import(couch_httpd,
[send_json/2,send_json/3,send_json/4,send_method_not_allowed/2,
start_json_response/2,send_chunk/2,end_json_response/1,
start_chunked_response/3, absolute_uri/2, send/2,
start_response_length/4]).
-record(doc_query_args, {
options = [],
rev = nil,
open_revs = [],
show = nil
}).
% Database request handlers
handle_request(#httpd{path_parts=[DbName|RestParts],method=Method,
db_url_handlers=DbUrlHandlers}=Req)->
case {Method, RestParts} of
{'PUT', []} ->
create_db_req(Req, DbName);
{'DELETE', []} ->
delete_db_req(Req, DbName);
{_, []} ->
do_db_req(Req, fun db_req/2);
{_, [SecondPart|_]} ->
Handler = couch_util:dict_find(SecondPart, DbUrlHandlers, fun db_req/2),
do_db_req(Req, Handler)
end.
get_changes_timeout(Req, Resp) ->
DefaultTimeout = list_to_integer(
couch_config:get("httpd", "changes_timeout", "60000")),
case couch_httpd:qs_value(Req, "heartbeat") of
undefined ->
case couch_httpd:qs_value(Req, "timeout") of
undefined ->
{DefaultTimeout, fun() -> stop end};
TimeoutList ->
{lists:min([DefaultTimeout, list_to_integer(TimeoutList)]),
fun() -> stop end}
end;
"true" ->
{DefaultTimeout, fun() -> send_chunk(Resp, "\n"), ok end};
TimeoutList ->
{lists:min([DefaultTimeout, list_to_integer(TimeoutList)]),
fun() -> send_chunk(Resp, "\n"), ok end}
end.
start_sending_changes(_Resp, "continuous") ->
ok;
start_sending_changes(Resp, _Else) ->
send_chunk(Resp, "{\"results\":[\n").
handle_changes_req(#httpd{method='GET',path_parts=[DbName|_]}=Req, Db) ->
{FilterFun, EndFilterFun} = make_filter_funs(Req, Db),
StartSeq = list_to_integer(couch_httpd:qs_value(Req, "since", "0")),
{ok, Resp} = start_json_response(Req, 200),
ResponseType = couch_httpd:qs_value(Req, "feed", "normal"),
start_sending_changes(Resp, ResponseType),
if ResponseType == "continuous" orelse ResponseType == "longpoll" ->
Self = self(),
{ok, Notify} = couch_db_update_notifier:start_link(
fun({_, DbName0}) when DbName0 == DbName ->
Self ! db_updated;
(_) ->
ok
end),
{Timeout, TimeoutFun} = get_changes_timeout(Req, Resp),
couch_stats_collector:track_process_count(Self,
{httpd, clients_requesting_changes}),
try
keep_sending_changes(Req, Resp, Db, StartSeq, <<"">>, Timeout,
TimeoutFun, ResponseType, FilterFun, EndFilterFun)
after
couch_db_update_notifier:stop(Notify),
get_rest_db_updated() % clean out any remaining update messages
end;
true ->
{ok, {LastSeq, _Prepend, _, _, _}} =
send_changes(Req, Resp, Db, StartSeq, <<"">>, "normal",
FilterFun, EndFilterFun),
end_sending_changes(Resp, LastSeq, ResponseType)
end;
handle_changes_req(#httpd{path_parts=[_,<<"_changes">>]}=Req, _Db) ->
send_method_not_allowed(Req, "GET,HEAD").
% waits for a db_updated msg, if there are multiple msgs, collects them.
wait_db_updated(Timeout, TimeoutFun) ->
receive db_updated -> get_rest_db_updated()
after Timeout ->
case TimeoutFun() of
ok -> wait_db_updated(Timeout, TimeoutFun);
stop -> stop
end
end.
get_rest_db_updated() ->
receive db_updated -> get_rest_db_updated()
after 0 -> updated
end.
end_sending_changes(Resp, EndSeq, "continuous") ->
send_chunk(Resp, [?JSON_ENCODE({[{<<"last_seq">>, EndSeq}]}) | "\n"]),
end_json_response(Resp);
end_sending_changes(Resp, EndSeq, _Else) ->
send_chunk(Resp, io_lib:format("\n],\n\"last_seq\":~w}\n", [EndSeq])),
end_json_response(Resp).
keep_sending_changes(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Resp,
Db, StartSeq, Prepend, Timeout, TimeoutFun, ResponseType, Filter, End) ->
{ok, {EndSeq, Prepend2, _, _, _}} = send_changes(Req, Resp, Db, StartSeq,
Prepend, ResponseType, Filter, End),
couch_db:close(Db),
if
EndSeq > StartSeq, ResponseType == "longpoll" ->
end_sending_changes(Resp, EndSeq, ResponseType);
true ->
case wait_db_updated(Timeout, TimeoutFun) of
updated ->
case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
{ok, Db2} ->
keep_sending_changes(Req, Resp, Db2, EndSeq, Prepend2, Timeout,
TimeoutFun, ResponseType, Filter, End);
_Else ->
end_sending_changes(Resp, EndSeq, ResponseType)
end;
stop ->
end_sending_changes(Resp, EndSeq, ResponseType)
end
end.
changes_enumerator(DocInfos, {_, _, FilterFun, Resp, "continuous"}) ->
[#doc_info{id=Id, high_seq=Seq, revs=[#rev_info{deleted=Del}|_]}|_] = DocInfos,
Results0 = [FilterFun(DocInfo) || DocInfo <- DocInfos],
Results = [Result || Result <- Results0, Result /= null],
case Results of
[] ->
{ok, {Seq, nil, FilterFun, Resp, "continuous"}};
_ ->
send_chunk(Resp, [?JSON_ENCODE(changes_row(Seq, Id, Del, Results)) |"\n"]),
{ok, {Seq, nil, FilterFun, Resp, "continuous"}}
end;
changes_enumerator(DocInfos, {_, Prepend, FilterFun, Resp, _}) ->
[#doc_info{id=Id, high_seq=Seq, revs=[#rev_info{deleted=Del}|_]}|_] = DocInfos,
Results0 = [FilterFun(DocInfo) || DocInfo <- DocInfos],
Results = [Result || Result <- Results0, Result /= null],
case Results of
[] ->
{ok, {Seq, Prepend, FilterFun, Resp, nil}};
_ ->
send_chunk(Resp, [Prepend, ?JSON_ENCODE(changes_row(Seq, Id, Del, Results))]),
{ok, {Seq, <<",\n">>, FilterFun, Resp, nil}}
end.
changes_row(Seq, Id, Del, Results) ->
{[{seq,Seq},{id,Id},{changes,Results}] ++ deleted_item(Del)}.
deleted_item(true) -> [{deleted,true}];
deleted_item(_) -> [].
send_changes(Req, Resp, Db, StartSeq, Prepend, ResponseType, FilterFun, End) ->
Style = list_to_existing_atom(
couch_httpd:qs_value(Req, "style", "main_only")),
try
couch_db:changes_since(Db, Style, StartSeq, fun changes_enumerator/2,
{StartSeq, Prepend, FilterFun, Resp, ResponseType})
after
End()
end.
make_filter_funs(Req, Db) ->
Filter = couch_httpd:qs_value(Req, "filter", ""),
case [list_to_binary(couch_httpd:unquote(Part))
|| Part <- string:tokens(Filter, "/")] of
[] ->
{fun(#doc_info{revs=[#rev_info{rev=Rev}|_]}) ->
{[{rev, couch_doc:rev_to_str(Rev)}]}
end,
fun() -> ok end};
[DName, FName] ->
DesignId = <<"_design/", DName/binary>>,
case couch_db:open_doc(Db, DesignId) of
{ok, #doc{body={Props}}} ->
FilterSrc = try couch_util:get_nested_json_value({Props},
[<<"filters">>, FName])
catch
throw:{not_found, _} ->
throw({bad_request, "invalid filter function"})
end,
Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>),
{ok, Pid} = couch_query_servers:start_filter(Lang, FilterSrc),
FilterFun = fun(DInfo = #doc_info{revs=[#rev_info{rev=Rev}|_]}) ->
{ok, Doc} = couch_db:open_doc(Db, DInfo, [deleted]),
{ok, Pass} = couch_query_servers:filter_doc(Pid, Doc, Req, Db),
case Pass of
true ->
{[{rev, couch_doc:rev_to_str(Rev)}]};
false ->
null
end
end,
EndFilterFun = fun() ->
couch_query_servers:end_filter(Pid)
end,
{FilterFun, EndFilterFun};
_Error ->
throw({bad_request, "invalid design doc"})
end;
_Else ->
throw({bad_request,
"filter parameter must be of the form `designname/filtername`"})
end.
handle_compact_req(#httpd{method='POST',path_parts=[DbName,_,Id|_]}=Req, _Db) ->
ok = couch_view_compactor:start_compact(DbName, Id),
send_json(Req, 202, {[{ok, true}]});
handle_compact_req(#httpd{method='POST'}=Req, Db) ->
ok = couch_db:start_compact(Db),
send_json(Req, 202, {[{ok, true}]});
handle_compact_req(Req, _Db) ->
send_method_not_allowed(Req, "POST").
handle_view_cleanup_req(#httpd{method='POST'}=Req, Db) ->
% delete unreferenced index files
ok = couch_view:cleanup_index_files(Db),
send_json(Req, 202, {[{ok, true}]});
handle_view_cleanup_req(Req, _Db) ->
send_method_not_allowed(Req, "POST").
handle_design_req(#httpd{
path_parts=[_DbName,_Design,_DesName, <<"_",_/binary>> = Action | _Rest],
design_url_handlers = DesignUrlHandlers
}=Req, Db) ->
Handler = couch_util:dict_find(Action, DesignUrlHandlers, fun db_req/2),
Handler(Req, Db);
handle_design_req(Req, Db) ->
db_req(Req, Db).
handle_design_info_req(#httpd{
method='GET',
path_parts=[_DbName, _Design, DesignName, _]
}=Req, Db) ->
DesignId = <<"_design/", DesignName/binary>>,
{ok, GroupInfoList} = couch_view:get_group_info(Db, DesignId),
send_json(Req, 200, {[
{name, DesignName},
{view_index, {GroupInfoList}}
]});
handle_design_info_req(Req, _Db) ->
send_method_not_allowed(Req, "GET").
create_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
ok = couch_httpd:verify_is_server_admin(Req),
case couch_server:create(DbName, [{user_ctx, UserCtx}]) of
{ok, Db} ->
couch_db:close(Db),
DocUrl = absolute_uri(Req, "/" ++ couch_util:url_encode(DbName)),
send_json(Req, 201, [{"Location", DocUrl}], {[{ok, true}]});
Error ->
throw(Error)
end.
delete_db_req(#httpd{user_ctx=UserCtx}=Req, DbName) ->
ok = couch_httpd:verify_is_server_admin(Req),
case couch_server:delete(DbName, [{user_ctx, UserCtx}]) of
ok ->
send_json(Req, 200, {[{ok, true}]});
Error ->
throw(Error)
end.
do_db_req(#httpd{user_ctx=UserCtx,path_parts=[DbName|_]}=Req, Fun) ->
case couch_db:open(DbName, [{user_ctx, UserCtx}]) of
{ok, Db} ->
try
Fun(Req, Db)
after
catch couch_db:close(Db)
end;
Error ->
throw(Error)
end.
db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) ->
{ok, DbInfo} = couch_db:get_db_info(Db),
send_json(Req, {DbInfo});
db_req(#httpd{method='POST',path_parts=[DbName]}=Req, Db) ->
Doc = couch_doc:from_json_obj(couch_httpd:json_body(Req)),
Doc2 = case Doc#doc.id of
<<"">> ->
Doc#doc{id=couch_util:new_uuid(), revs={0, []}};
_ ->
Doc
end,
DocId = Doc2#doc.id,
case couch_httpd:qs_value(Req, "batch") of
"ok" ->
% batch
ok = couch_batch_save:eventually_save_doc(
Db#db.name, Doc2, Db#db.user_ctx),
send_json(Req, 202, [], {[
{ok, true},
{id, DocId}
]});
_Normal ->
% normal
{ok, NewRev} = couch_db:update_doc(Db, Doc2, []),
DocUrl = absolute_uri(
Req, binary_to_list(<<"/",DbName/binary,"/", DocId/binary>>)),
send_json(Req, 201, [{"Location", DocUrl}], {[
{ok, true},
{id, DocId},
{rev, couch_doc:rev_to_str(NewRev)}
]})
end;
db_req(#httpd{path_parts=[_DbName]}=Req, _Db) ->
send_method_not_allowed(Req, "DELETE,GET,HEAD,POST");
db_req(#httpd{method='POST',path_parts=[_,<<"_ensure_full_commit">>]}=Req, Db) ->
UpdateSeq = couch_db:get_update_seq(Db),
CommittedSeq = couch_db:get_committed_update_seq(Db),
{ok, StartTime} =
case couch_httpd:qs_value(Req, "seq") of
undefined ->
committed = couch_batch_save:commit_now(Db#db.name, Db#db.user_ctx),
couch_db:ensure_full_commit(Db);
RequiredStr ->
RequiredSeq = list_to_integer(RequiredStr),
if RequiredSeq > UpdateSeq ->
throw({bad_request,
"can't do a full commit ahead of current update_seq"});
RequiredSeq > CommittedSeq ->
% user asked for an explicit sequence, don't commit any batches
couch_db:ensure_full_commit(Db);
true ->
{ok, Db#db.instance_start_time}
end
end,
send_json(Req, 201, {[
{ok, true},
{instance_start_time, StartTime}
]});
db_req(#httpd{path_parts=[_,<<"_ensure_full_commit">>]}=Req, _Db) ->
send_method_not_allowed(Req, "POST");
db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
couch_stats_collector:increment({httpd, bulk_requests}),
{JsonProps} = couch_httpd:json_body_obj(Req),
DocsArray = proplists:get_value(<<"docs">>, JsonProps),
case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of
"true" ->
Options = [full_commit];
"false" ->
Options = [delay_commit];
_ ->
Options = []
end,
case proplists:get_value(<<"new_edits">>, JsonProps, true) of
true ->
Docs = lists:map(
fun({ObjProps} = JsonObj) ->
Doc = couch_doc:from_json_obj(JsonObj),
validate_attachment_names(Doc),
Id = case Doc#doc.id of
<<>> -> couch_util:new_uuid();
Id0 -> Id0
end,
case proplists:get_value(<<"_rev">>, ObjProps) of
undefined ->
Revs = {0, []};
Rev ->
{Pos, RevId} = couch_doc:parse_rev(Rev),
Revs = {Pos, [RevId]}
end,
Doc#doc{id=Id,revs=Revs}
end,
DocsArray),
Options2 =
case proplists:get_value(<<"all_or_nothing">>, JsonProps) of
true -> [all_or_nothing|Options];
_ -> Options
end,
case couch_db:update_docs(Db, Docs, Options2) of
{ok, Results} ->
% output the results
DocResults = lists:zipwith(fun update_doc_result_to_json/2,
Docs, Results),
send_json(Req, 201, DocResults);
{aborted, Errors} ->
ErrorsJson =
lists:map(fun update_doc_result_to_json/1, Errors),
send_json(Req, 417, ErrorsJson)
end;
false ->
Docs = [couch_doc:from_json_obj(JsonObj) || JsonObj <- DocsArray],
{ok, Errors} = couch_db:update_docs(Db, Docs, Options, replicated_changes),
ErrorsJson =
lists:map(fun update_doc_result_to_json/1, Errors),
send_json(Req, 201, ErrorsJson)
end;
db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->
send_method_not_allowed(Req, "POST");
db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) ->
{IdsRevs} = couch_httpd:json_body_obj(Req),
IdsRevs2 = [{Id, couch_doc:parse_revs(Revs)} || {Id, Revs} <- IdsRevs],
case couch_db:purge_docs(Db, IdsRevs2) of
{ok, PurgeSeq, PurgedIdsRevs} ->
PurgedIdsRevs2 = [{Id, couch_doc:rev_to_strs(Revs)} || {Id, Revs} <- PurgedIdsRevs],
send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs2}}]});
Error ->
throw(Error)
end;
db_req(#httpd{path_parts=[_,<<"_purge">>]}=Req, _Db) ->
send_method_not_allowed(Req, "POST");
db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs">>]}=Req, Db) ->
all_docs_view(Req, Db, nil);
db_req(#httpd{method='POST',path_parts=[_,<<"_all_docs">>]}=Req, Db) ->
{Fields} = couch_httpd:json_body_obj(Req),
case proplists:get_value(<<"keys">>, Fields, nil) of
nil ->
?LOG_DEBUG("POST to _all_docs with no keys member.", []),
all_docs_view(Req, Db, nil);
Keys when is_list(Keys) ->
all_docs_view(Req, Db, Keys);
_ ->
throw({bad_request, "`keys` member must be a array."})
end;
db_req(#httpd{path_parts=[_,<<"_all_docs">>]}=Req, _Db) ->
send_method_not_allowed(Req, "GET,HEAD,POST");
db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) ->
#view_query_args{
start_key = StartKey,
limit = Limit,
skip = SkipCount,
direction = Dir
} = QueryArgs = couch_httpd_view:parse_view_params(Req, nil, map),
{ok, Info} = couch_db:get_db_info(Db),
CurrentEtag = couch_httpd:make_etag(Info),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
TotalRowCount = proplists:get_value(doc_count, Info),
FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db,
TotalRowCount, #view_fold_helper_funs{
reduce_count = fun couch_db:enum_docs_since_reduce_to_count/1
}),
StartKey2 = case StartKey of
nil -> 0;
<<>> -> 100000000000;
{} -> 100000000000;
StartKey when is_integer(StartKey) -> StartKey
end,
{ok, FoldResult} = couch_db:enum_docs_since(Db, StartKey2, Dir,
fun(DocInfo, Offset, Acc) ->
#doc_info{
id=Id,
high_seq=Seq,
revs=[#rev_info{rev=Rev,deleted=Deleted} | RestInfo]
} = DocInfo,
ConflictRevs = couch_doc:rev_to_strs(
[Rev1 || #rev_info{deleted=false, rev=Rev1} <- RestInfo]),
DelConflictRevs = couch_doc:rev_to_strs(
[Rev1 || #rev_info{deleted=true, rev=Rev1} <- RestInfo]),
Json = {
[{<<"rev">>, couch_doc:rev_to_str(Rev)}] ++
case ConflictRevs of
[] -> [];
_ -> [{<<"conflicts">>, ConflictRevs}]
end ++
case DelConflictRevs of
[] -> [];
_ -> [{<<"deleted_conflicts">>, DelConflictRevs}]
end ++
case Deleted of
true -> [{<<"deleted">>, true}];
false -> []
end
},
FoldlFun({{Seq, Id}, Json}, Offset, Acc)
end, {Limit, SkipCount, undefined, [], nil}),
couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult})
end);
db_req(#httpd{path_parts=[_,<<"_all_docs_by_seq">>]}=Req, _Db) ->
send_method_not_allowed(Req, "GET,HEAD");
db_req(#httpd{method='POST',path_parts=[_,<<"_missing_revs">>]}=Req, Db) ->
{JsonDocIdRevs} = couch_httpd:json_body_obj(Req),
JsonDocIdRevs2 = [{Id, [couch_doc:parse_rev(RevStr) || RevStr <- RevStrs]} || {Id, RevStrs} <- JsonDocIdRevs],
{ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs2),
Results2 = [{Id, [couch_doc:rev_to_str(Rev) || Rev <- Revs]} || {Id, Revs} <- Results],
send_json(Req, {[
{missing_revs, {Results2}}
]});
db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->
send_method_not_allowed(Req, "POST");
db_req(#httpd{method='PUT',path_parts=[_,<<"_admins">>]}=Req,
Db) ->
Admins = couch_httpd:json_body(Req),
ok = couch_db:set_admins(Db, Admins),
send_json(Req, {[{<<"ok">>, true}]});
db_req(#httpd{method='GET',path_parts=[_,<<"_admins">>]}=Req, Db) ->
send_json(Req, couch_db:get_admins(Db));
db_req(#httpd{path_parts=[_,<<"_admins">>]}=Req, _Db) ->
send_method_not_allowed(Req, "PUT,GET");
db_req(#httpd{method='PUT',path_parts=[_,<<"_revs_limit">>]}=Req,
Db) ->
Limit = couch_httpd:json_body(Req),
ok = couch_db:set_revs_limit(Db, Limit),
send_json(Req, {[{<<"ok">>, true}]});
db_req(#httpd{method='GET',path_parts=[_,<<"_revs_limit">>]}=Req, Db) ->
send_json(Req, couch_db:get_revs_limit(Db));
db_req(#httpd{path_parts=[_,<<"_revs_limit">>]}=Req, _Db) ->
send_method_not_allowed(Req, "PUT,GET");
% Special case to enable using an unencoded slash in the URL of design docs,
% as slashes in document IDs must otherwise be URL encoded.
db_req(#httpd{method='GET',mochi_req=MochiReq, path_parts=[DbName,<<"_design/",_/binary>>|_]}=Req, _Db) ->
PathFront = "/" ++ couch_httpd:quote(binary_to_list(DbName)) ++ "/",
[PathFront|PathTail] = re:split(MochiReq:get(raw_path), "_design%2F",
[{return, list}]),
couch_httpd:send_redirect(Req, PathFront ++ "_design/" ++
mochiweb_util:join(PathTail, "_design%2F"));
db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name]}=Req, Db) ->
db_doc_req(Req, Db, <<"_design/",Name/binary>>);
db_req(#httpd{path_parts=[_DbName,<<"_design">>,Name|FileNameParts]}=Req, Db) ->
db_attachment_req(Req, Db, <<"_design/",Name/binary>>, FileNameParts);
% Special case to allow for accessing local documents without %2F
% encoding the docid. Throws out requests that don't have the second
% path part or that specify an attachment name.
db_req(#httpd{path_parts=[_DbName, <<"_local">>]}, _Db) ->
throw({bad_request, <<"Invalid _local document id.">>});
db_req(#httpd{path_parts=[_DbName, <<"_local/">>]}, _Db) ->
throw({bad_request, <<"Invalid _local document id.">>});
db_req(#httpd{path_parts=[_DbName, <<"_local">>, Name]}=Req, Db) ->
db_doc_req(Req, Db, <<"_local/", Name/binary>>);
db_req(#httpd{path_parts=[_DbName, <<"_local">> | _Rest]}, _Db) ->
throw({bad_request, <<"_local documents do not accept attachments.">>});
db_req(#httpd{path_parts=[_, DocId]}=Req, Db) ->
db_doc_req(Req, Db, DocId);
db_req(#httpd{path_parts=[_, DocId | FileNameParts]}=Req, Db) ->
db_attachment_req(Req, Db, DocId, FileNameParts).
all_docs_view(Req, Db, Keys) ->
#view_query_args{
start_key = StartKey,
start_docid = StartDocId,
end_key = EndKey,
limit = Limit,
skip = SkipCount,
direction = Dir
} = QueryArgs = couch_httpd_view:parse_view_params(Req, Keys, map),
{ok, Info} = couch_db:get_db_info(Db),
CurrentEtag = couch_httpd:make_etag(Info),
couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
TotalRowCount = proplists:get_value(doc_count, Info),
StartId = if is_binary(StartKey) -> StartKey;
true -> StartDocId
end,
FoldAccInit = {Limit, SkipCount, undefined, [], nil},
case Keys of
nil ->
PassedEndFun =
case Dir of
fwd ->
fun(ViewKey, _ViewId) ->
couch_db_updater:less_docid(EndKey, ViewKey)
end;
rev->
fun(ViewKey, _ViewId) ->
couch_db_updater:less_docid(ViewKey, EndKey)
end
end,
FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db,
TotalRowCount, #view_fold_helper_funs{
reduce_count = fun couch_db:enum_docs_reduce_to_count/1,
passed_end = PassedEndFun
}),
AdapterFun = fun(#full_doc_info{id=Id}=FullDocInfo, Offset, Acc) ->
case couch_doc:to_doc_info(FullDocInfo) of
#doc_info{revs=[#rev_info{deleted=false, rev=Rev}|_]} ->
FoldlFun({{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}]}}, Offset, Acc);
#doc_info{revs=[#rev_info{deleted=true}|_]} ->
{ok, Acc}
end
end,
{ok, FoldResult} = couch_db:enum_docs(Db, StartId, Dir,
AdapterFun, FoldAccInit),
couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult});
_ ->
FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db,
TotalRowCount, #view_fold_helper_funs{
reduce_count = fun(Offset) -> Offset end
}),
KeyFoldFun = case Dir of
fwd ->
fun lists:foldl/3;
rev ->
fun lists:foldr/3
end,
{ok, FoldResult} = KeyFoldFun(
fun(Key, {ok, FoldAcc}) ->
DocInfo = (catch couch_db:get_doc_info(Db, Key)),
Doc = case DocInfo of
{ok, #doc_info{id=Id, revs=[#rev_info{deleted=false, rev=Rev}|_]}} ->
{{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}]}};
{ok, #doc_info{id=Id, revs=[#rev_info{deleted=true, rev=Rev}|_]}} ->
{{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}, {deleted, true}]}};
not_found ->
{{Key, error}, not_found};
_ ->
?LOG_ERROR("Invalid DocInfo: ~p", [DocInfo]),
throw({error, invalid_doc_info})
end,
Acc = (catch FoldlFun(Doc, 0, FoldAcc)),
case Acc of
{stop, Acc2} ->
{ok, Acc2};
_ ->
Acc
end
end, {ok, FoldAccInit}, Keys),
couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult})
end
end).
db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) ->
% check for the existence of the doc to handle the 404 case.
couch_doc_open(Db, DocId, nil, []),
case couch_httpd:qs_value(Req, "rev") of
undefined ->
update_doc(Req, Db, DocId, {[{<<"_deleted">>,true}]});
Rev ->
update_doc(Req, Db, DocId, {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]})
end;
db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
#doc_query_args{
show = Format,
rev = Rev,
open_revs = Revs,
options = Options
} = parse_doc_query(Req),
case Format of
nil ->
case Revs of
[] ->
Doc = couch_doc_open(Db, DocId, Rev, Options),
DiskEtag = couch_httpd:doc_etag(Doc),
case Doc#doc.meta of
[] ->
% output etag only when we have no meta
couch_httpd:etag_respond(Req, DiskEtag, fun() ->
send_json(Req, 200, [{"Etag", DiskEtag}], couch_doc:to_json_obj(Doc, Options))
end);
_ ->
send_json(Req, 200, [], couch_doc:to_json_obj(Doc, Options))
end;
_ ->
{ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options),
{ok, Resp} = start_json_response(Req, 200),
send_chunk(Resp, "["),
% We loop through the docs. The first time through the separator
% is whitespace, then a comma on subsequent iterations.
lists:foldl(
fun(Result, AccSeparator) ->
case Result of
{ok, Doc} ->
JsonDoc = couch_doc:to_json_obj(Doc, Options),
Json = ?JSON_ENCODE({[{ok, JsonDoc}]}),
send_chunk(Resp, AccSeparator ++ Json);
{{not_found, missing}, RevId} ->
RevStr = couch_doc:rev_to_str(RevId),
Json = ?JSON_ENCODE({[{"missing", RevStr}]}),
send_chunk(Resp, AccSeparator ++ Json)
end,
"," % AccSeparator now has a comma
end,
"", Results),
send_chunk(Resp, "]"),
end_json_response(Resp)
end;
_ ->
{DesignName, ShowName} = Format,
couch_httpd_show:handle_doc_show(Req, DesignName, ShowName, DocId, Db)
end;
db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
couch_doc:validate_docid(DocId),
case couch_httpd:header_value(Req, "content-type") of
"multipart/form-data" ++ _Rest ->
ok;
_Else ->
throw({bad_ctype, <<"Invalid Content-Type header for form upload">>})
end,
Form = couch_httpd:parse_form(Req),
Rev = couch_doc:parse_rev(list_to_binary(proplists:get_value("_rev", Form))),
{ok, [{ok, Doc}]} = couch_db:open_doc_revs(Db, DocId, [Rev], []),
UpdatedAtts = [
#att{name=validate_attachment_name(Name),
type=list_to_binary(ContentType),
data=Content} ||
{Name, {ContentType, _}, Content} <-
proplists:get_all_values("_attachments", Form)
],
#doc{atts=OldAtts} = Doc,
OldAtts2 = lists:flatmap(
fun(#att{name=OldName}=Att) ->
case [1 || A <- UpdatedAtts, A#att.name == OldName] of
[] -> [Att]; % the attachment wasn't in the UpdatedAtts, return it
_ -> [] % the attachment was in the UpdatedAtts, drop it
end
end, OldAtts),
NewDoc = Doc#doc{
atts = UpdatedAtts ++ OldAtts2
},
{ok, NewRev} = couch_db:update_doc(Db, NewDoc, []),
send_json(Req, 201, [{"Etag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewRev)) ++ "\""}], {[
{ok, true},
{id, DocId},
{rev, couch_doc:rev_to_str(NewRev)}
]});
db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
couch_doc:validate_docid(DocId),
Json = couch_httpd:json_body(Req),
case couch_httpd:qs_value(Req, "batch") of
"ok" ->
% batch
Doc = couch_doc_from_req(Req, DocId, Json),
ok = couch_batch_save:eventually_save_doc(Db#db.name, Doc, Db#db.user_ctx),
send_json(Req, 202, [], {[
{ok, true},
{id, DocId}
]});
_Normal ->
% normal
Location = absolute_uri(Req, "/" ++ ?b2l(Db#db.name) ++ "/" ++ ?b2l(DocId)),
update_doc(Req, Db, DocId, Json, [{"Location", Location}])
end;
db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) ->
SourceRev =
case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
missing_rev -> nil;
Rev -> Rev
end,
{TargetDocId, TargetRevs} = parse_copy_destination_header(Req),
% open old doc
Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
% save new doc
{ok, NewTargetRev} = couch_db:update_doc(Db,
Doc#doc{id=TargetDocId, revs=TargetRevs}, []),
% respond
send_json(Req, 201,
[{"Etag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewTargetRev)) ++ "\""}],
update_doc_result_to_json(TargetDocId, {ok, NewTargetRev}));
db_doc_req(Req, _Db, _DocId) ->
send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY").
update_doc_result_to_json({{Id, Rev}, Error}) ->
{_Code, Err, Msg} = couch_httpd:error_info(Error),
{[{id, Id}, {rev, couch_doc:rev_to_str(Rev)},
{error, Err}, {reason, Msg}]}.
update_doc_result_to_json(#doc{id=DocId}, Result) ->
update_doc_result_to_json(DocId, Result);
update_doc_result_to_json(DocId, {ok, NewRev}) ->
{[{id, DocId}, {rev, couch_doc:rev_to_str(NewRev)}]};
update_doc_result_to_json(DocId, Error) ->
{_Code, ErrorStr, Reason} = couch_httpd:error_info(Error),
{[{id, DocId}, {error, ErrorStr}, {reason, Reason}]}.
update_doc(Req, Db, DocId, Json) ->
update_doc(Req, Db, DocId, Json, []).
update_doc(Req, Db, DocId, Json, Headers) ->
#doc{deleted=Deleted} = Doc = couch_doc_from_req(Req, DocId, Json),
case couch_httpd:header_value(Req, "X-Couch-Full-Commit") of
"true" ->
Options = [full_commit];
"false" ->
Options = [delay_commit];
_ ->
Options = []
end,
{ok, NewRev} = couch_db:update_doc(Db, Doc, Options),
NewRevStr = couch_doc:rev_to_str(NewRev),
ResponseHeaders = [{"Etag", <<"\"", NewRevStr/binary, "\"">>}] ++ Headers,
send_json(Req, if Deleted -> 200; true -> 201 end,
ResponseHeaders, {[
{ok, true},
{id, DocId},
{rev, NewRevStr}]}).
couch_doc_from_req(Req, DocId, Json) ->
Doc = couch_doc:from_json_obj(Json),
validate_attachment_names(Doc),
ExplicitDocRev =
case Doc#doc.revs of
{Start,[RevId|_]} -> {Start, RevId};
_ -> undefined
end,
case extract_header_rev(Req, ExplicitDocRev) of
missing_rev ->
Revs = {0, []};
{Pos, Rev} ->
Revs = {Pos, [Rev]}
end,
Doc#doc{id=DocId, revs=Revs}.
% Useful for debugging
% couch_doc_open(Db, DocId) ->
% couch_doc_open(Db, DocId, nil, []).
couch_doc_open(Db, DocId, Rev, Options) ->
case Rev of
nil -> % open most recent rev
case couch_db:open_doc(Db, DocId, Options) of
{ok, Doc} ->
Doc;
Error ->
throw(Error)
end;
_ -> % open a specific rev (deletions come back as stubs)
case couch_db:open_doc_revs(Db, DocId, [Rev], Options) of
{ok, [{ok, Doc}]} ->
Doc;
{ok, [{{not_found, missing}, Rev}]} ->
throw({not_found, missing});
{ok, [Else]} ->
throw(Else)
end
end.
% Attachment request handlers
db_attachment_req(#httpd{method='GET'}=Req, Db, DocId, FileNameParts) ->
FileName = list_to_binary(mochiweb_util:join(lists:map(fun binary_to_list/1, FileNameParts),"/")),
#doc_query_args{
rev=Rev,
options=Options
} = parse_doc_query(Req),
#doc{
atts=Atts
} = Doc = couch_doc_open(Db, DocId, Rev, Options),
case [A || A <- Atts, A#att.name == FileName] of
[] ->
throw({not_found, "Document is missing attachment"});
[#att{type=Type, len=Len}=Att] ->
Etag = couch_httpd:doc_etag(Doc),
couch_httpd:etag_respond(Req, Etag, fun() ->
{ok, Resp} = start_response_length(Req, 200, [
{"ETag", Etag},
{"Cache-Control", "must-revalidate"},
{"Content-Type", binary_to_list(Type)}
], integer_to_list(Len)),
couch_doc:att_foldl(Att,
fun(BinSegment, _) -> send(Resp, BinSegment) end,[])
end)
end;
db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileNameParts)
when (Method == 'PUT') or (Method == 'DELETE') ->
FileName = validate_attachment_name(
mochiweb_util:join(
lists:map(fun binary_to_list/1,
FileNameParts),"/")),
NewAtt = case Method of
'DELETE' ->
[];
_ ->
[#att{
name=FileName,
type = case couch_httpd:header_value(Req,"Content-Type") of
undefined ->
% We could throw an error here or guess by the FileName.
% Currently, just giving it a default.
<<"application/octet-stream">>;
CType ->
list_to_binary(CType)
end,
data = case couch_httpd:body_length(Req) of
undefined ->
<<"">>;
{unknown_transfer_encoding, Unknown} ->
exit({unknown_transfer_encoding, Unknown});
chunked ->
fun(MaxChunkSize, ChunkFun, InitState) ->
couch_httpd:recv_chunked(Req, MaxChunkSize,
ChunkFun, InitState)
end;
0 ->
<<"">>;
Length when is_integer(Length) ->
fun() -> couch_httpd:recv(Req, 0) end;
Length ->
exit({length_not_integer, Length})
end,
len = case couch_httpd:header_value(Req,"Content-Length") of
undefined ->
undefined;
Length ->
list_to_integer(Length)
end
}]
end,
Doc = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
missing_rev -> % make the new doc
couch_doc:validate_docid(DocId),
#doc{id=DocId};
Rev ->
case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
{ok, [{ok, Doc0}]} -> Doc0;
{ok, [Error]} -> throw(Error)
end
end,
#doc{atts=Atts} = Doc,
DocEdited = Doc#doc{
atts = NewAtt ++ [A || A <- Atts, A#att.name /= FileName]
},
{ok, UpdatedRev} = couch_db:update_doc(Db, DocEdited, []),
#db{name=DbName} = Db,
{Status, Headers} = case Method of
'DELETE' ->
{200, []};
_ ->
{201, [{"Location", absolute_uri(Req, "/" ++
binary_to_list(DbName) ++ "/" ++
binary_to_list(DocId) ++ "/" ++
binary_to_list(FileName)
)}]}
end,
send_json(Req,Status, Headers, {[
{ok, true},
{id, DocId},
{rev, couch_doc:rev_to_str(UpdatedRev)}
]});
db_attachment_req(Req, _Db, _DocId, _FileNameParts) ->
send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT").
parse_doc_format(FormatStr) when is_binary(FormatStr) ->
parse_doc_format(?b2l(FormatStr));
parse_doc_format(FormatStr) when is_list(FormatStr) ->
SplitFormat = lists:splitwith(fun($/) -> false; (_) -> true end, FormatStr),
case SplitFormat of
{DesignName, [$/ | ShowName]} -> {?l2b(DesignName), ?l2b(ShowName)};
_Else -> throw({bad_request, <<"Invalid doc format">>})
end;
parse_doc_format(_BadFormatStr) ->
throw({bad_request, <<"Invalid doc format">>}).
parse_doc_query(Req) ->
lists:foldl(fun({Key,Value}, Args) ->
case {Key, Value} of
{"attachments", "true"} ->
Options = [attachments | Args#doc_query_args.options],
Args#doc_query_args{options=Options};
{"meta", "true"} ->
Options = [revs_info, conflicts, deleted_conflicts | Args#doc_query_args.options],
Args#doc_query_args{options=Options};
{"revs", "true"} ->
Options = [revs | Args#doc_query_args.options],
Args#doc_query_args{options=Options};
{"local_seq", "true"} ->
Options = [local_seq | Args#doc_query_args.options],
Args#doc_query_args{options=Options};
{"revs_info", "true"} ->
Options = [revs_info | Args#doc_query_args.options],
Args#doc_query_args{options=Options};
{"conflicts", "true"} ->
Options = [conflicts | Args#doc_query_args.options],
Args#doc_query_args{options=Options};
{"deleted_conflicts", "true"} ->
Options = [deleted_conflicts | Args#doc_query_args.options],
Args#doc_query_args{options=Options};
{"rev", Rev} ->
Args#doc_query_args{rev=couch_doc:parse_rev(Rev)};
{"open_revs", "all"} ->
Args#doc_query_args{open_revs=all};
{"open_revs", RevsJsonStr} ->
JsonArray = ?JSON_DECODE(RevsJsonStr),
Args#doc_query_args{open_revs=[couch_doc:parse_rev(Rev) || Rev <- JsonArray]};
{"show", FormatStr} ->
Args#doc_query_args{show=parse_doc_format(FormatStr)};
_Else -> % unknown key value pair, ignore.
Args
end
end, #doc_query_args{}, couch_httpd:qs(Req)).
extract_header_rev(Req, ExplicitRev) when is_binary(ExplicitRev) or is_list(ExplicitRev)->
extract_header_rev(Req, couch_doc:parse_rev(ExplicitRev));
extract_header_rev(Req, ExplicitRev) ->
Etag = case couch_httpd:header_value(Req, "If-Match") of
undefined -> undefined;
Value -> couch_doc:parse_rev(string:strip(Value, both, $"))
end,
case {ExplicitRev, Etag} of
{undefined, undefined} -> missing_rev;
{_, undefined} -> ExplicitRev;
{undefined, _} -> Etag;
_ when ExplicitRev == Etag -> Etag;
_ ->
throw({bad_request, "Document rev and etag have different values"})
end.
parse_copy_destination_header(Req) ->
Destination = couch_httpd:header_value(Req, "Destination"),
case re:run(Destination, "\\?", [{capture, none}]) of
nomatch ->
{list_to_binary(Destination), {0, []}};
match ->
[DocId, RevQs] = re:split(Destination, "\\?", [{return, list}]),
[_RevQueryKey, Rev] = re:split(RevQs, "=", [{return, list}]),
{Pos, RevId} = couch_doc:parse_rev(Rev),
{list_to_binary(DocId), {Pos, [RevId]}}
end.
validate_attachment_names(Doc) ->
lists:foreach(fun(#att{name=Name}) ->
validate_attachment_name(Name)
end, Doc#doc.atts).
validate_attachment_name(Name) when is_list(Name) ->
validate_attachment_name(list_to_binary(Name));
validate_attachment_name(<<"_",_/binary>>) ->
throw({bad_request, <<"Attachment name can't start with '_'">>});
validate_attachment_name(Name) ->
case is_valid_utf8(Name) of
true -> Name;
false -> throw({bad_request, <<"Attachment name is not UTF-8 encoded">>})
end.
%% borrowed from mochijson2:json_bin_is_safe()
is_valid_utf8(<<>>) ->
true;
is_valid_utf8(<<C, Rest/binary>>) ->
case C of
$\" ->
false;
$\\ ->
false;
$\b ->
false;
$\f ->
false;
$\n ->
false;
$\r ->
false;
$\t ->
false;
C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF ->
false;
C when C < 16#7f ->
is_valid_utf8(Rest);
_ ->
false
end.