From 2c783019993c1c5f0ddb5802a2e6763bb05dbe10 Mon Sep 17 00:00:00 2001 From: benoitc Date: Sun, 12 Jan 2014 13:38:25 +0100 Subject: [PATCH 01/16] unbreak js test and display a report --- src/couch_httpd_misc_handlers.erl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/couch_httpd_misc_handlers.erl b/src/couch_httpd_misc_handlers.erl index 1999e5e..57cc5ec 100644 --- a/src/couch_httpd_misc_handlers.erl +++ b/src/couch_httpd_misc_handlers.erl @@ -99,6 +99,7 @@ handle_restart_req(#httpd{method='POST'}=Req) -> ok = couch_httpd:verify_is_server_admin(Req), Result = send_json(Req, 202, {[{ok, true}]}), couch_server_sup:restart_core_server(), + couch_httpd_sup:reload_listeners(), Result; handle_restart_req(Req) -> send_method_not_allowed(Req, "POST"). From b97fac73c37056e4cbb2a8d584875e4c99da4504 Mon Sep 17 00:00:00 2001 From: benoitc Date: Sun, 12 Jan 2014 20:58:30 +0100 Subject: [PATCH 02/16] only display sasl reports on error. be less verbose in tests --- src/couch_httpd_misc_handlers.erl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/couch_httpd_misc_handlers.erl b/src/couch_httpd_misc_handlers.erl index 57cc5ec..1999e5e 100644 --- a/src/couch_httpd_misc_handlers.erl +++ b/src/couch_httpd_misc_handlers.erl @@ -99,7 +99,6 @@ handle_restart_req(#httpd{method='POST'}=Req) -> ok = couch_httpd:verify_is_server_admin(Req), Result = send_json(Req, 202, {[{ok, true}]}), couch_server_sup:restart_core_server(), - couch_httpd_sup:reload_listeners(), Result; handle_restart_req(Req) -> send_method_not_allowed(Req, "POST"). From 886f562cfb6f98e7aea080f19a049d1894e1414b Mon Sep 17 00:00:00 2001 From: benoitc Date: Mon, 13 Jan 2014 09:01:59 +0100 Subject: [PATCH 03/16] export couch_httpd:set_auth_handlers/0 This function wasn't exported so any changes to the `httpf/authentication_handlers` setting wasn't effective. --- src/couch_httpd.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/couch_httpd.erl b/src/couch_httpd.erl index ae28817..8cef53f 100644 --- a/src/couch_httpd.erl +++ b/src/couch_httpd.erl @@ -30,7 +30,7 @@ -export([send_json/2,send_json/3,send_json/4,last_chunk/1,parse_multipart_request/3]). -export([accepted_encodings/1,handle_request_int/5,validate_referer/1,validate_ctype/2]). -export([http_1_0_keep_alive/2]). - +-export([set_auth_handlers/0]). start_link(couch_http) -> Port = couch_config:get("httpd", "port", "5984"), @@ -157,7 +157,7 @@ set_auth_handlers() -> couch_config:get("httpd", "authentication_handlers", "")), AuthHandlers = lists:map( fun(A) -> {make_arity_1_fun(A), ?l2b(A)} end, AuthenticationSrcs), - ok = application:set_env(couch, auth_handlers, AuthHandlers). + ok = application:set_env(couch_httpd, auth_handlers, AuthHandlers). % SpecStr is a string like "{my_module, my_fun}" % or "{my_module, my_fun, <<"my_arg">>}" @@ -286,7 +286,7 @@ handle_request_int(MochiReq, DefaultFun, }, HandlerFun = couch_util:dict_find(HandlerKey, UrlHandlers, DefaultFun), - {ok, AuthHandlers} = application:get_env(couch, auth_handlers), + {ok, AuthHandlers} = application:get_env(couch_httpd, auth_handlers), {ok, Resp} = try From 9f994a55497ae308183a007a42819ecb604c9c13 Mon Sep 17 00:00:00 2001 From: benoitc Date: Mon, 13 Jan 2014 09:36:44 +0100 Subject: [PATCH 04/16] a more descriptive comment of the listener reloading flow --- src/couch_httpd_sup.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/couch_httpd_sup.erl b/src/couch_httpd_sup.erl index b430e45..1ce53f1 100644 --- a/src/couch_httpd_sup.erl +++ b/src/couch_httpd_sup.erl @@ -62,7 +62,7 @@ upgrade() -> %% @doc upgrade a listener -spec reload_listener(atom()) -> {ok, pid()} | {error, term()}. reload_listener(Id) -> - %% stop the listener + %% stop the listener and remove it from the supervision temporarely supervisor:terminate_child(?MODULE, Id), supervisor:delete_child(?MODULE, Id), From 898bb1e51e5ceff64ff1734d881360dcbe4e6b4e Mon Sep 17 00:00:00 2001 From: benoitc Date: Mon, 13 Jan 2014 23:06:26 +0100 Subject: [PATCH 05/16] couch_server_sup -> couch_sup --- src/couch_httpd.erl | 2 +- src/couch_httpd_misc_handlers.erl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/couch_httpd.erl b/src/couch_httpd.erl index 8cef53f..7be8a2a 100644 --- a/src/couch_httpd.erl +++ b/src/couch_httpd.erl @@ -92,7 +92,7 @@ start_link(couch_https) -> start_link(Name, Options) -> % read config and register for configuration changes - % just stop if one of the config settings change. couch_server_sup + % just stop if one of the config settings change. couch_sup % will restart us and then we will pick up the new settings. BindAddress = couch_config:get("httpd", "bind_address", any), diff --git a/src/couch_httpd_misc_handlers.erl b/src/couch_httpd_misc_handlers.erl index 1999e5e..293ef00 100644 --- a/src/couch_httpd_misc_handlers.erl +++ b/src/couch_httpd_misc_handlers.erl @@ -98,7 +98,7 @@ handle_restart_req(#httpd{method='POST'}=Req) -> couch_httpd:validate_ctype(Req, "application/json"), ok = couch_httpd:verify_is_server_admin(Req), Result = send_json(Req, 202, {[{ok, true}]}), - couch_server_sup:restart_core_server(), + couch_sup:restart_core_server(), Result; handle_restart_req(Req) -> send_method_not_allowed(Req, "POST"). From 1f1f46026f5f47bd88501a3083c8c566dd09e264 Mon Sep 17 00:00:00 2001 From: benoitc Date: Wed, 15 Jan 2014 21:10:45 +0100 Subject: [PATCH 06/16] fix rebarclean --- ebin/couch_httpd.app | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 ebin/couch_httpd.app diff --git a/ebin/couch_httpd.app b/ebin/couch_httpd.app deleted file mode 100644 index 4338b4e..0000000 --- a/ebin/couch_httpd.app +++ /dev/null @@ -1,14 +0,0 @@ -{application,couch_httpd, - [{description,"CouchDB HTTP API"}, - {vsn,"1.6.0"}, - {modules,[couch_httpd,couch_httpd_app,couch_httpd_auth, - couch_httpd_cors,couch_httpd_db,couch_httpd_external, - couch_httpd_misc_handlers,couch_httpd_oauth, - couch_httpd_proxy,couch_httpd_rewrite, - couch_httpd_stats_handlers,couch_httpd_sup, - couch_httpd_util,couch_httpd_vhost]}, - {registered,[couch_httpd_sup]}, - {applications,[kernel,stdlib,crypto,asn1,public_key,ssl,inets]}, - {included_applications,[mochiweb]}, - {mod,{couch_httpd_app,[]}}, - {env,[]}]}. From e41dc6ce105f8daf663104c36135157c6a26aacf Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Sat, 18 Jan 2014 00:31:49 -0500 Subject: [PATCH 07/16] Move addition of qs params after normalization This refactor executes normalize_path/1 before appending the bound query string parameters. COUCHDB-2031 --- src/couch_httpd_rewrite.erl | 58 +++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/src/couch_httpd_rewrite.erl b/src/couch_httpd_rewrite.erl index 997992f..011c3c8 100644 --- a/src/couch_httpd_rewrite.erl +++ b/src/couch_httpd_rewrite.erl @@ -143,36 +143,32 @@ handle_rewrite_req(#httpd{ DispatchList = [make_rule(Rule) || {Rule} <- Rules], Method1 = couch_util:to_binary(Method), - %% get raw path by matching url to a rule. - RawPath = case try_bind_path(DispatchList, Method1, - PathParts, QueryList) of - no_dispatch_path -> - throw(not_found); - {NewPathParts, Bindings} -> - Parts = [quote_plus(X) || X <- NewPathParts], - - % build new path, reencode query args, eventually convert - % them to json - Bindings1 = maybe_encode_bindings(Bindings), - Path = binary_to_list( - iolist_to_binary([ - string:join(Parts, [?SEPARATOR]), - [["?", mochiweb_util:urlencode(Bindings1)] - || Bindings1 =/= [] ] - ])), - - % if path is relative detect it and rewrite path - case mochiweb_util:safe_relative_path(Path) of - undefined -> - ?b2l(Prefix) ++ "/" ++ Path; - P1 -> - ?b2l(Prefix) ++ "/" ++ P1 - end - - end, - - % normalize final path (fix levels "." and "..") - RawPath1 = ?b2l(iolist_to_binary(normalize_path(RawPath))), + % get raw path by matching url to a rule. Throws not_found. + {NewPathParts0, Bindings0} = + try_bind_path(DispatchList, Method1, PathParts, QueryList), + NewPathParts = [quote_plus(X) || X <- NewPathParts0], + Bindings = maybe_encode_bindings(Bindings0), + + Path0 = string:join(NewPathParts, [?SEPARATOR]), + + % if path is relative detect it and rewrite path + Path1 = case mochiweb_util:safe_relative_path(Path0) of + undefined -> + ?b2l(Prefix) ++ "/" ++ Path0; + P1 -> + ?b2l(Prefix) ++ "/" ++ P1 + end, + + Path2 = normalize_path(Path1), + + Path3 = case Bindings of + [] -> + Path2; + _ -> + [Path2, "?", mochiweb_util:urlencode(Bindings)] + end, + + RawPath1 = ?b2l(iolist_to_binary(Path3)), % In order to do OAuth correctly, we have to save the % requested path. We use default so chained rewriting @@ -216,7 +212,7 @@ quote_plus(X) -> %% @doc Try to find a rule matching current url. If none is found %% 404 error not_found is raised try_bind_path([], _Method, _PathParts, _QueryList) -> - no_dispatch_path; + throw(not_found); try_bind_path([Dispatch|Rest], Method, PathParts, QueryList) -> [{PathParts1, Method1}, RedirectPath, QueryArgs, Formats] = Dispatch, case bind_method(Method1, Method) of From dc841e0492d960e1a19f6564a34076b9bbbd7d16 Mon Sep 17 00:00:00 2001 From: NickNorth Date: Tue, 3 Dec 2013 20:58:53 +0000 Subject: [PATCH 08/16] Speed up and move couch_httpd:find_in_binary. See https://issues.apache.org/jira/browse/COUCHDB-1953 --- src/couch_httpd.erl | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/couch_httpd.erl b/src/couch_httpd.erl index 7be8a2a..29f9db0 100644 --- a/src/couch_httpd.erl +++ b/src/couch_httpd.erl @@ -968,7 +968,7 @@ split_header(Line) -> mochiweb_util:parse_header(Value)}]. read_until(#mp{data_fun=DataFun, buffer=Buffer}=Mp, Pattern, Callback) -> - case find_in_binary(Pattern, Buffer) of + case couch_util:find_in_binary(Pattern, Buffer) of not_found -> Callback2 = Callback(Buffer), {Buffer2, DataFun2} = DataFun(), @@ -1044,34 +1044,6 @@ check_for_last(#mp{buffer=Buffer, data_fun=DataFun}=Mp) -> data_fun = DataFun2}) end. -find_in_binary(_B, <<>>) -> - not_found; - -find_in_binary(B, Data) -> - case binary:match(Data, [B], []) of - nomatch -> - partial_find(binary:part(B, {0, byte_size(B) - 1}), - binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), 1); - {Pos, _Len} -> - {exact, Pos} - end. - -partial_find(<<>>, _Data, _Pos) -> - not_found; - -partial_find(B, Data, N) when byte_size(Data) > 0 -> - case binary:match(Data, [B], []) of - nomatch -> - partial_find(binary:part(B, {0, byte_size(B) - 1}), - binary:part(Data, {byte_size(Data), -byte_size(Data) + 1}), N + 1); - {Pos, _Len} -> - {partial, N + Pos} - end; - -partial_find(_B, _Data, _N) -> - not_found. - - validate_bind_address(Address) -> case inet_parse:address(Address) of {ok, _} -> ok; From c629342a16925d0f9b9094016f4e91fecb39a774 Mon Sep 17 00:00:00 2001 From: benoitc Date: Sun, 2 Feb 2014 19:54:01 +0100 Subject: [PATCH 09/16] extract couch_httpd changes API in its own module --- src/couch_httpd_changes.erl | 174 ++++++++++++++++++++++++++++++++++++ src/couch_httpd_db.erl | 8 +- 2 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 src/couch_httpd_changes.erl diff --git a/src/couch_httpd_changes.erl b/src/couch_httpd_changes.erl new file mode 100644 index 0000000..1e431e9 --- /dev/null +++ b/src/couch_httpd_changes.erl @@ -0,0 +1,174 @@ +% 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_changes). + +-export([handle_changes_req/2]). + +-include_lib("couch/include/couch_db.hrl"). + +handle_changes_req(#httpd{method='POST'}=Req, Db) -> + couch_httpd:validate_ctype(Req, "application/json"), + handle_changes_req1(Req, Db); +handle_changes_req(#httpd{method='GET'}=Req, Db) -> + handle_changes_req1(Req, Db); +handle_changes_req(#httpd{path_parts=[_,<<"_changes">>]}=Req, _Db) -> + couch_httpd:send_method_not_allowed(Req, "GET,HEAD,POST"). + +handle_changes_req1(Req, #db{name=DbName}=Db) -> + AuthDbName = ?l2b(couch_config:get("couch_httpd_auth", "authentication_db")), + case AuthDbName of + DbName -> + % in the authentication database, _changes is admin-only. + ok = couch_db:check_is_admin(Db); + _Else -> + % on other databases, _changes is free for all. + ok + end, + handle_changes_req2(Req, Db). + +handle_changes_req2(Req, Db) -> + MakeCallback = fun(Resp) -> + fun({change, {ChangeProp}=Change, _}, "eventsource") -> + Seq = proplists:get_value(<<"seq">>, ChangeProp), + couch_httpd:send_chunk(Resp, ["data: ", ?JSON_ENCODE(Change), + "\n", "id: ", ?JSON_ENCODE(Seq), + "\n\n"]); + ({change, Change, _}, "continuous") -> + couch_httpd:send_chunk(Resp, [?JSON_ENCODE(Change) | "\n"]); + ({change, Change, Prepend}, _) -> + couch_httpd:send_chunk(Resp, [Prepend, ?JSON_ENCODE(Change)]); + (start, "eventsource") -> + ok; + (start, "continuous") -> + ok; + (start, _) -> + couch_httpd:send_chunk(Resp, "{\"results\":[\n"); + ({stop, _EndSeq}, "eventsource") -> + couch_httpd:end_json_response(Resp); + ({stop, EndSeq}, "continuous") -> + couch_httpd:send_chunk( + Resp, + [?JSON_ENCODE({[{<<"last_seq">>, EndSeq}]}) | "\n"] + ), + couch_httpd:end_json_response(Resp); + ({stop, EndSeq}, _) -> + couch_httpd:send_chunk( + Resp, + io_lib:format("\n],\n\"last_seq\":~w}\n", [EndSeq]) + ), + couch_httpd:end_json_response(Resp); + (timeout, _) -> + couch_httpd:send_chunk(Resp, "\n") + end + end, + ChangesArgs = parse_changes_query(Req, Db), + ChangesFun = couch_changes:handle_changes(ChangesArgs, Req, Db), + WrapperFun = case ChangesArgs#changes_args.feed of + "normal" -> + {ok, Info} = couch_db:get_db_info(Db), + CurrentEtag = couch_httpd:make_etag(Info), + fun(FeedChangesFun) -> + couch_httpd:etag_respond( + Req, + CurrentEtag, + fun() -> + {ok, Resp} = couch_httpd:start_json_response( + Req, 200, [{"ETag", CurrentEtag}] + ), + FeedChangesFun(MakeCallback(Resp)) + end + ) + end; + "eventsource" -> + Headers = [ + {"Content-Type", "text/event-stream"}, + {"Cache-Control", "no-cache"} + ], + {ok, Resp} = couch_httpd:start_chunked_response(Req, 200, Headers), + fun(FeedChangesFun) -> + FeedChangesFun(MakeCallback(Resp)) + end; + _ -> + % "longpoll" or "continuous" + {ok, Resp} = couch_httpd:start_json_response(Req, 200), + fun(FeedChangesFun) -> + FeedChangesFun(MakeCallback(Resp)) + end + end, + couch_stats_collector:increment( + {httpd, clients_requesting_changes} + ), + try + WrapperFun(ChangesFun) + after + couch_stats_collector:decrement( + {httpd, clients_requesting_changes} + ) + end. + +parse_changes_query(Req, Db) -> + ChangesArgs = lists:foldl(fun({Key, Value}, Args) -> + case {string:to_lower(Key), Value} of + {"feed", _} -> + Args#changes_args{feed=Value}; + {"descending", "true"} -> + Args#changes_args{dir=rev}; + {"since", "now"} -> + UpdateSeq = couch_util:with_db(Db#db.name, fun(WDb) -> + couch_db:get_update_seq(WDb) + end), + Args#changes_args{since=UpdateSeq}; + {"since", _} -> + Args#changes_args{since=list_to_integer(Value)}; + {"last-event-id", _} -> + Args#changes_args{since=list_to_integer(Value)}; + {"limit", _} -> + Args#changes_args{limit=list_to_integer(Value)}; + {"style", _} -> + Args#changes_args{style=list_to_existing_atom(Value)}; + {"heartbeat", "true"} -> + Args#changes_args{heartbeat=true}; + {"heartbeat", _} -> + Args#changes_args{heartbeat=list_to_integer(Value)}; + {"timeout", _} -> + Args#changes_args{timeout=list_to_integer(Value)}; + {"include_docs", "true"} -> + Args#changes_args{include_docs=true}; + {"attachments", "true"} -> + Opts = Args#changes_args.doc_options, + Args#changes_args{doc_options=[attachments|Opts]}; + {"att_encoding_info", "true"} -> + Opts = Args#changes_args.doc_options, + Args#changes_args{doc_options=[att_encoding_info|Opts]}; + {"conflicts", "true"} -> + Args#changes_args{conflicts=true}; + {"filter", _} -> + Args#changes_args{filter=Value}; + _Else -> % unknown key value pair, ignore. + Args + end + end, #changes_args{}, couch_httpd:qs(Req)), + %% if it's an EventSource request with a Last-event-ID header + %% that should override the `since` query string, since it's + %% probably the browser reconnecting. + case ChangesArgs#changes_args.feed of + "eventsource" -> + case couch_httpd:header_value(Req, "last-event-id") of + undefined -> + ChangesArgs; + Value -> + ChangesArgs#changes_args{since=list_to_integer(Value)} + end; + _ -> + ChangesArgs + end. diff --git a/src/couch_httpd_db.erl b/src/couch_httpd_db.erl index 45a6dd5..0d1e0f8 100644 --- a/src/couch_httpd_db.erl +++ b/src/couch_httpd_db.erl @@ -19,10 +19,10 @@ handle_design_info_req/3]). -import(couch_httpd, - [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, - start_json_response/2,send_chunk/2,last_chunk/1,end_json_response/1, - start_chunked_response/3, absolute_uri/2, send/2, - start_response_length/4, send_error/4]). + [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, + start_json_response/2,send_chunk/2,last_chunk/1,end_json_response/1, + start_chunked_response/3, absolute_uri/2, send/2, + start_response_length/4, send_error/4]). -record(doc_query_args, { options = [], From 70409b20e5be8f0fa5d125cd1e9ec2697c82d909 Mon Sep 17 00:00:00 2001 From: benoitc Date: Fri, 7 Feb 2014 15:38:34 +0100 Subject: [PATCH 10/16] add supports of view changes in the _changes API Now when the option `seq_indexed=true` is set in the design doc, the view filter in _changes will use it to retrieve the results. Compared to the current way, using a view index will be faster to retrieve changes. It also gives the possibility to filter changes by key or get changes in a key range. All the view options can be used. Note 1: if someone is trying to filter a changes with view options when the views are not indexed by sequence, a 400 error will be returned. Note 2: The changes will only be returned when the view is updated if seq_indexed=true --- src/couch_httpd_changes.erl | 250 +++++++++++++++++++++++++++++++++++- 1 file changed, 246 insertions(+), 4 deletions(-) diff --git a/src/couch_httpd_changes.erl b/src/couch_httpd_changes.erl index 1e431e9..56ce559 100644 --- a/src/couch_httpd_changes.erl +++ b/src/couch_httpd_changes.erl @@ -12,7 +12,9 @@ -module(couch_httpd_changes). --export([handle_changes_req/2]). +-export([handle_changes_req/2, + handle_changes/3, + handle_view_changes/3]). -include_lib("couch/include/couch_db.hrl"). @@ -34,9 +36,7 @@ handle_changes_req1(Req, #db{name=DbName}=Db) -> % on other databases, _changes is free for all. ok end, - handle_changes_req2(Req, Db). -handle_changes_req2(Req, Db) -> MakeCallback = fun(Resp) -> fun({change, {ChangeProp}=Change, _}, "eventsource") -> Seq = proplists:get_value(<<"seq">>, ChangeProp), @@ -72,7 +72,7 @@ handle_changes_req2(Req, Db) -> end end, ChangesArgs = parse_changes_query(Req, Db), - ChangesFun = couch_changes:handle_changes(ChangesArgs, Req, Db), + ChangesFun = handle_changes(ChangesArgs, Req, Db), WrapperFun = case ChangesArgs#changes_args.feed of "normal" -> {ok, Info} = couch_db:get_db_info(Db), @@ -116,6 +116,164 @@ handle_changes_req2(Req, Db) -> ) end. + +handle_changes(ChangesArgs, Req, Db) -> + case ChangesArgs#changes_args.filter of + "_view" -> + handle_view_changes(ChangesArgs, Req, Db); + _ -> + couch_changes:handle_changes(ChangesArgs, Req, Db) + end. + +%% wrapper around couch_mrview_changes. +%% This wrapper mimic couch_changes:handle_changes/3 and return a +%% Changefun that can be used by the handle_changes_req function. Also +%% while couch_mrview_changes:handle_changes/6 is returning tha view +%% changes this function return docs corresponding to the changes +%% instead so it can be used to replace the _view filter. +handle_view_changes(ChangesArgs, Req, Db) -> + %% parse view parameter + {DDocId, VName} = parse_view_param(Req), + + %% get view options + Query = case Req of + {json_req, {Props}} -> + {Q} = couch_util:get_value(<<"query">>, Props, {[]}), + Q; + _ -> + couch_httpd:qs(Req) + end, + ViewOptions = parse_view_options(Query, []), + + {ok, Infos} = couch_mrview:get_info(Db, DDocId), + case lists:member(<<"seq_indexed">>, + proplists:get_value(update_options, Infos, [])) of + true -> + handle_view_changes(Db, DDocId, VName, ViewOptions, ChangesArgs, + Req); + false when ViewOptions /= [] -> + ?LOG_ERROR("Tried to filter a non sequence indexed view~n",[]), + throw({bad_request, seqs_not_indexed}); + false -> + %% old method we are getting changes using the btree instead + %% which is not efficient, log it + ?LOG_WARN("Get view changes with seq_indexed=false.~n", []), + couch_changes:handle_changes(ChangesArgs, Req, Db) + end. + +handle_view_changes(#db{name=DbName}=Db0, DDocId, VName, ViewOptions, + ChangesArgs, Req) -> + #changes_args{ + feed = ResponseType, + since = Since, + db_open_options = DbOptions} = ChangesArgs, + + Options0 = [{since, Since}, + {view_options, ViewOptions}], + Options = case ResponseType of + "continuous" -> [stream | Options0]; + "eventsource" -> [stream | Options0]; + "longpoll" -> [{stream, once} | Options0]; + _ -> Options0 + end, + + %% reopen the db with the db options given to the changes args + couch_db:close(Db0), + DbOptions1 = [{user_ctx, Db0#db.user_ctx} | DbOptions], + {ok, Db} = couch_db:open(DbName, DbOptions1), + + + %% initialise the changes fun + ChangesFun = fun(Callback) -> + Callback(start, ResponseType), + + Acc0 = {"", 0, Db, Callback, ChangesArgs}, + couch_mrview_changes:handle_changes(DbName, DDocId, VName, + fun view_changes_cb/2, + Acc0, Options) + end, + ChangesFun. + + +view_changes_cb(stop, {LastSeq, {_, _, _, Callback, Args}}) -> + Callback({stop, LastSeq}, Args#changes_args.feed); + +view_changes_cb(heartbeat, {_, _, _, Callback, Args}=Acc) -> + Callback(timeout, Args#changes_args.feed), + {ok, Acc}; +view_changes_cb({{Seq, _Key, DocId}, _VAl}, + {Prepend, OldLimit, Db0, Callback, Args}=Acc) -> + + #changes_args{ + feed = ResponseType, + limit = Limit} = Args, + + %% if the doc sequence is > to the one in the db record, reopen the + %% database since it means we don't have the latest db value. + Db = case Db0#db.update_seq >= Seq of + true -> Db0; + false -> + {ok, Db1} = couch_db:reopen_db(Db0), + Db1 + end, + + case couch_db:get_doc_info(Db, DocId) of + {ok, DocInfo} -> + %% get change row + ChangeRow = view_change_row(Db, DocInfo, Args), + %% emit change row + Callback({change, ChangeRow, Prepend}, ResponseType), + + %% if we achieved the limit, stop here, else continue. + NewLimit = OldLimit + 1, + if Limit > NewLimit -> + {ok, {<<",\n">>, Db, NewLimit, Callback, Args}}; + true -> + {stop, {<<"">>, Db, NewLimit, Callback, Args}} + end; + {error, not_found} -> + %% doc not found, continue + {ok, Acc}; + Error -> + throw(Error) + end. + + +view_change_row(Db, DocInfo, Args) -> + #doc_info{id = Id, high_seq = Seq, revs = Revs} = DocInfo, + [#rev_info{rev=Rev, deleted=Del} | _] = Revs, + + #changes_args{style=Style, + include_docs=InDoc, + doc_options = DocOpts, + conflicts=Conflicts}=Args, + + Changes = case Style of + main_only -> + [{[{<<"rev">>, couch_doc:rev_to_str(Rev)}]}]; + all_docs -> + [{[{<<"rev">>, couch_doc:rev_to_str(R)}]} + || #rev_info{rev=R} <- Revs] + end, + + {[{<<"seq">>, Seq}, {<<"id">>, Id}, {<<"changes">>, Changes}] ++ + deleted_item(Del) ++ case InDoc of + true -> + Opts = case Conflicts of + true -> [deleted, conflicts]; + false -> [deleted] + end, + Doc = couch_index_util:load_doc(Db, DocInfo, Opts), + case Doc of + null -> + [{doc, null}]; + _ -> + [{doc, couch_doc:to_json_obj(Doc, DocOpts)}] + end; + false -> + [] + end}. + parse_changes_query(Req, Db) -> ChangesArgs = lists:foldl(fun({Key, Value}, Args) -> case {string:to_lower(Key), Value} of @@ -172,3 +330,87 @@ parse_changes_query(Req, Db) -> _ -> ChangesArgs end. + +parse_view_param({json_req, {Props}}) -> + {Query} = couch_util:get_value(<<"query">>, Props), + parse_view_param1(couch_util:get_value(<<"view">>, Query, <<"">>)); +parse_view_param(Req) -> + parse_view_param1(list_to_binary(couch_httpd:qs_value(Req, "view", ""))). + +parse_view_param1(ViewParam) -> + case re:split(ViewParam, <<"/">>) of + [DName, ViewName] -> + {<< "_design/", DName/binary >>, ViewName}; + _ -> + throw({bad_request, "Invalid `view` parameter."}) + end. + +parse_view_options([], Acc) -> + Acc; +parse_view_options([{K, V} | Rest], Acc) -> + Acc1 = case couch_util:to_binary(K) of + <<"reduce">> -> + [{reduce, couch_mrview_http:parse_boolean(V)}]; + <<"key">> -> + V1 = parse_json(V), + [{start_key, V1}, {end_key, V1} | Acc]; + <<"keys">> -> + [{keys, parse_json(V)} | Acc]; + <<"startkey">> -> + [{start_key, parse_json(V)} | Acc]; + <<"start_key">> -> + [{start_key, parse_json(V)} | Acc]; + <<"startkey_docid">> -> + [{start_key_docid, couch_util:to_binary(V)} | Acc]; + <<"start_key_docid">> -> + [{start_key_docid, couch_util:to_binary(V)} | Acc]; + <<"endkey">> -> + [{end_key, parse_json(V)} | Acc]; + <<"end_key">> -> + [{end_key, parse_json(V)} | Acc]; + <<"endkey_docid">> -> + [{start_key_docid, couch_util:to_binary(V)} | Acc]; + <<"end_key_docid">> -> + [{start_key_docid, couch_util:to_binary(V)} | Acc]; + <<"limit">> -> + [{limit, couch_mrview_http:parse_pos_int(V)} | Acc]; + <<"count">> -> + throw({query_parse_error, <<"QS param `count` is not `limit`">>}); + <<"stale">> when V =:= <<"ok">> orelse V =:= "ok" -> + [{stale, ok} | Acc]; + <<"stale">> when V =:= <<"update_after">> orelse V =:= "update_after" -> + [{stale, update_after} | Acc]; + <<"stale">> -> + throw({query_parse_error, <<"Invalid value for `stale`.">>}); + <<"descending">> -> + case couch_mrview_http:parse_boolean(V) of + true -> + [{direction, rev} | Acc]; + _ -> + [{direction, fwd} | Acc] + end; + <<"skip">> -> + [{skip, couch_mrview_http:parse_pos_int(V)} | Acc]; + <<"group">> -> + case couch_mrview_http:parse_booolean(V) of + true -> + [{group_level, exact} | Acc]; + _ -> + [{group_level, 0} | Acc] + end; + <<"group_level">> -> + [{group_level, couch_mrview_http:parse_pos_int(V)} | Acc]; + <<"inclusive_end">> -> + [{inclusive_end, couch_mrview_http:parse_boolean(V)}]; + _ -> + Acc + end, + parse_view_options(Rest, Acc1). + +parse_json(V) when is_list(V) -> + ?JSON_DECODE(V); +parse_json(V) -> + V. + +deleted_item(true) -> [{<<"deleted">>, true}]; +deleted_item(_) -> []. From f870b4babdc3081bc63d5ce15878ca2e0dd49cc3 Mon Sep 17 00:00:00 2001 From: benoitc Date: Fri, 7 Feb 2014 15:57:25 +0100 Subject: [PATCH 11/16] add the option use_index={no,yes} (yes by default) If use_index=no even if the view is indexed by sequence, the index won't be use. Instead it will fold the btree and return the changes each time the view map function can emit a value. (default behaviour). --- src/couch_httpd_changes.erl | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/couch_httpd_changes.erl b/src/couch_httpd_changes.erl index 56ce559..82d9fe0 100644 --- a/src/couch_httpd_changes.erl +++ b/src/couch_httpd_changes.erl @@ -118,6 +118,7 @@ handle_changes_req1(Req, #db{name=DbName}=Db) -> handle_changes(ChangesArgs, Req, Db) -> + case ChangesArgs#changes_args.filter of "_view" -> handle_view_changes(ChangesArgs, Req, Db); @@ -146,18 +147,26 @@ handle_view_changes(ChangesArgs, Req, Db) -> ViewOptions = parse_view_options(Query, []), {ok, Infos} = couch_mrview:get_info(Db, DDocId), - case lists:member(<<"seq_indexed">>, - proplists:get_value(update_options, Infos, [])) of - true -> + IsIndexed = lists:member(<<"seq_indexed">>, + proplists:get_value(update_options, Infos, + [])), + + NoIndex = couch_httpd:qs_value(Req, "use_index", "yes") =:= "no", + + case {IsIndexed, NoIndex} of + {true, false} -> handle_view_changes(Db, DDocId, VName, ViewOptions, ChangesArgs, Req); - false when ViewOptions /= [] -> + {true, true} when ViewOptions /= [] -> ?LOG_ERROR("Tried to filter a non sequence indexed view~n",[]), throw({bad_request, seqs_not_indexed}); - false -> + {false, _} when ViewOptions /= [] -> + ?LOG_ERROR("Tried to filter a non sequence indexed view~n",[]), + throw({bad_request, seqs_not_indexed}); + {_, _} -> %% old method we are getting changes using the btree instead %% which is not efficient, log it - ?LOG_WARN("Get view changes with seq_indexed=false.~n", []), + ?LOG_WARN("Filter without using a seq_indexed view.~n", []), couch_changes:handle_changes(ChangesArgs, Req, Db) end. From c1d2f8f0fe1af2159a8872629c965fcef4d7234e Mon Sep 17 00:00:00 2001 From: benoitc Date: Sat, 8 Feb 2014 19:55:40 +0100 Subject: [PATCH 12/16] couch_index: add background indexing facility This change add the possibility to trigger a view indexation in background. The indexation can only work in background if at least one process acquired it using the `couch_index_server:acquire_index/3` function. If all the process that acquired it are down or released it using `couch_index_server:release_indexer/3` then the background task is stopped. By default the background indexation will happen every 1s or when 200 docs has been saved in the database. These parameters can be changed using the options `threshold` and `refresh_interval` in the couch_index section. To use it with couch_mrview a new option {refresh, true} has been added to couch_mrview_changes:handle_changes Also the query parameter refresh=true is passsed in t the HTTP changes API. --- src/couch_httpd_changes.erl | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/couch_httpd_changes.erl b/src/couch_httpd_changes.erl index 82d9fe0..510e20a 100644 --- a/src/couch_httpd_changes.erl +++ b/src/couch_httpd_changes.erl @@ -177,8 +177,11 @@ handle_view_changes(#db{name=DbName}=Db0, DDocId, VName, ViewOptions, since = Since, db_open_options = DbOptions} = ChangesArgs, + Refresh = refresh_option(Req), + Options0 = [{since, Since}, - {view_options, ViewOptions}], + {view_options, ViewOptions}, + {refresh, Refresh}], Options = case ResponseType of "continuous" -> [stream | Options0]; "eventsource" -> [stream | Options0]; @@ -236,9 +239,9 @@ view_changes_cb({{Seq, _Key, DocId}, _VAl}, %% if we achieved the limit, stop here, else continue. NewLimit = OldLimit + 1, if Limit > NewLimit -> - {ok, {<<",\n">>, Db, NewLimit, Callback, Args}}; + {ok, {<<",\n">>, NewLimit, Db, Callback, Args}}; true -> - {stop, {<<"">>, Db, NewLimit, Callback, Args}} + {stop, {<<"">>, NewLimit, Db, Callback, Args}} end; {error, not_found} -> %% doc not found, continue @@ -416,6 +419,15 @@ parse_view_options([{K, V} | Rest], Acc) -> end, parse_view_options(Rest, Acc1). +refresh_option({json_req, {Props}}) -> + {Query} = couch_util:get_value(<<"query">>, Props), + couch_util:get_value(<<"refresh">>, Query, true); +refresh_option(Req) -> + case couch_httpd:qs_value(Req, "refresh", "true") of + "false" -> false; + _ -> true + end. + parse_json(V) when is_list(V) -> ?JSON_DECODE(V); parse_json(V) -> From 792797f394dfe95eb1195b2da37de57dc7dc0d56 Mon Sep 17 00:00:00 2001 From: benoitc Date: Sat, 8 Feb 2014 21:27:58 +0100 Subject: [PATCH 13/16] couch_httpd_changes: check removed keys from the view filter Make sure to only emit deleted document when a deleted key is passed to the view filter. --- src/couch_httpd_changes.erl | 40 +++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/couch_httpd_changes.erl b/src/couch_httpd_changes.erl index 510e20a..1c4a147 100644 --- a/src/couch_httpd_changes.erl +++ b/src/couch_httpd_changes.erl @@ -213,9 +213,15 @@ view_changes_cb(stop, {LastSeq, {_, _, _, Callback, Args}}) -> view_changes_cb(heartbeat, {_, _, _, Callback, Args}=Acc) -> Callback(timeout, Args#changes_args.feed), {ok, Acc}; -view_changes_cb({{Seq, _Key, DocId}, _VAl}, +view_changes_cb({{Seq, _Key, DocId}, Val}, {Prepend, OldLimit, Db0, Callback, Args}=Acc) -> + %% is the key removed from the index? + Removed = case Val of + {[{<<"_removed">>, true}]} -> true; + _ -> false + end, + #changes_args{ feed = ResponseType, limit = Limit} = Args, @@ -232,16 +238,24 @@ view_changes_cb({{Seq, _Key, DocId}, _VAl}, case couch_db:get_doc_info(Db, DocId) of {ok, DocInfo} -> %% get change row - ChangeRow = view_change_row(Db, DocInfo, Args), - %% emit change row - Callback({change, ChangeRow, Prepend}, ResponseType), - - %% if we achieved the limit, stop here, else continue. - NewLimit = OldLimit + 1, - if Limit > NewLimit -> - {ok, {<<",\n">>, NewLimit, Db, Callback, Args}}; - true -> - {stop, {<<"">>, NewLimit, Db, Callback, Args}} + {Deleted, ChangeRow} = view_change_row(Db, DocInfo, Args), + + case Removed of + true when Deleted /= true -> + %% the key has been removed from the view but the + %% document hasn't been deleted so ignore it. + {ok, Acc}; + _ -> + %% emit change row + Callback({change, ChangeRow, Prepend}, ResponseType), + + %% if we achieved the limit, stop here, else continue. + NewLimit = OldLimit + 1, + if Limit > NewLimit -> + {ok, {<<",\n">>, NewLimit, Db, Callback, Args}}; + true -> + {stop, {<<"">>, NewLimit, Db, Callback, Args}} + end end; {error, not_found} -> %% doc not found, continue @@ -268,7 +282,7 @@ view_change_row(Db, DocInfo, Args) -> || #rev_info{rev=R} <- Revs] end, - {[{<<"seq">>, Seq}, {<<"id">>, Id}, {<<"changes">>, Changes}] ++ + {Del, {[{<<"seq">>, Seq}, {<<"id">>, Id}, {<<"changes">>, Changes}] ++ deleted_item(Del) ++ case InDoc of true -> Opts = case Conflicts of @@ -284,7 +298,7 @@ view_change_row(Db, DocInfo, Args) -> end; false -> [] - end}. + end}}. parse_changes_query(Req, Db) -> ChangesArgs = lists:foldl(fun({Key, Value}, Args) -> From d936233c8e8c63681cf17c8f76b12f6540827eae Mon Sep 17 00:00:00 2001 From: benoitc Date: Sun, 9 Feb 2014 00:43:23 +0100 Subject: [PATCH 14/16] couch_replicator: add replication using changes in a view Instead of a database, the replicator can now filter the documents using a view index. All documents having a key emitted in the view can be replicated. View parameters can be used. Which means that you can replicate results corresponding to a key in a view or a range. --- src/couch_httpd_changes.erl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/couch_httpd_changes.erl b/src/couch_httpd_changes.erl index 1c4a147..6cb0b16 100644 --- a/src/couch_httpd_changes.erl +++ b/src/couch_httpd_changes.erl @@ -137,12 +137,15 @@ handle_view_changes(ChangesArgs, Req, Db) -> {DDocId, VName} = parse_view_param(Req), %% get view options - Query = case Req of + {Query, NoIndex} = case Req of {json_req, {Props}} -> {Q} = couch_util:get_value(<<"query">>, Props, {[]}), - Q; + NoIndex1 = (couch_util:get_value(<<"use_index">>, Q, + <<"yes">>) =:= <<"no">>), + {Q, NoIndex1}; _ -> - couch_httpd:qs(Req) + NoIndex1 = couch_httpd:qs_value(Req, "use_index", "yes") =:= "no", + {couch_httpd:qs(Req), NoIndex1} end, ViewOptions = parse_view_options(Query, []), @@ -151,8 +154,6 @@ handle_view_changes(ChangesArgs, Req, Db) -> proplists:get_value(update_options, Infos, [])), - NoIndex = couch_httpd:qs_value(Req, "use_index", "yes") =:= "no", - case {IsIndexed, NoIndex} of {true, false} -> handle_view_changes(Db, DDocId, VName, ViewOptions, ChangesArgs, From e0ba70feabac1c69aaa01402f45388210c213ef2 Mon Sep 17 00:00:00 2001 From: Peter Lemenkov Date: Fri, 10 Jan 2014 16:30:25 +0400 Subject: [PATCH 15/16] Adopt to the recent erlang-oauth (1.3+) Signed-off-by: Peter Lemenkov Signed-off-by: Alexander Shorin --- src/couch_httpd_oauth.erl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/couch_httpd_oauth.erl b/src/couch_httpd_oauth.erl index 07229d3..012a8ce 100644 --- a/src/couch_httpd_oauth.erl +++ b/src/couch_httpd_oauth.erl @@ -43,6 +43,7 @@ oauth_auth_callback(#httpd{mochi_req = MochiReq} = Req, CbParams) -> Method = atom_to_list(MochiReq:get(method)), #callback_params{ consumer = Consumer, + token = Token, token_secret = TokenSecret, url = Url, signature = Sig, @@ -61,7 +62,7 @@ oauth_auth_callback(#httpd{mochi_req = MochiReq} = Req, CbParams) -> "Consumer is `~p`, token secret is `~p`~n" "Expected signature was `~p`~n", [User, Sig, Method, Url, Params, Consumer, TokenSecret, - oauth:signature(Method, Url, Params, Consumer, TokenSecret)]), + oauth:sign(Method, Url, Params, Consumer, Token, TokenSecret)]), Req end. From 25a1b155940200b5ee7cdfd8792444f0b3c1fbfc Mon Sep 17 00:00:00 2001 From: benoitc Date: Mon, 17 Feb 2014 20:43:35 +0100 Subject: [PATCH 16/16] RCOUCH-7 :new _bulk_get handler implement the spec from couchbase https://github.com/couchbase/sync_gateway/wiki/Bulk-GET --- src/couch_httpd_bulk_get.erl | 140 +++++++++++++++++++++++++++++++++++ src/couch_httpd_db.erl | 15 ++-- 2 files changed, 145 insertions(+), 10 deletions(-) create mode 100644 src/couch_httpd_bulk_get.erl diff --git a/src/couch_httpd_bulk_get.erl b/src/couch_httpd_bulk_get.erl new file mode 100644 index 0000000..3e3d4f8 --- /dev/null +++ b/src/couch_httpd_bulk_get.erl @@ -0,0 +1,140 @@ +% 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_bulk_get). + +-include_lib("couch/include/couch_db.hrl"). +-include("couch_httpd.hrl"). + +-export([handle_req/2]). + +handle_req(#httpd{method='POST',path_parts=[_,<<"_bulk_get">>], + mochi_req=MochiReq}=Req, Db) -> + couch_httpd:validate_ctype(Req, "application/json"), + couch_httpd:validate_ctype(Req, "application/json"), + {JsonProps} = couch_httpd:json_body_obj(Req), + case couch_util:get_value(<<"docs">>, JsonProps) of + undefined -> + couch_httpd:send_error(Req, 400, + <<"bad_request">>, <<"Missing JSON list of + 'docs'">>); + DocsArray -> + #doc_query_args{ + options = Options + } = couch_httpd_db:parse_doc_query(Req), + + %% start the response + {Resp, Boundary} = case MochiReq:accepts_content_type("multipart/mixed") of + false -> + {ok, Resp1} = couch_httpd:start_json_response(Req, 200), + couch_httpd:send_chunk(Resp1, "["), + {Resp1, nil}; + true -> + Boundary1 = couch_uuids:random(), + CType = {"Content-Type", "multipart/mixed; boundary=\"" ++ + ?b2l(Boundary1) ++ "\""}, + {ok, Resp1} = couch_httpd:start_chunked_response(Req, 200, + [CType]), + {Resp1, Boundary1} + end, + + lists:foldr(fun({Props}, Acc) -> + DocId = couch_util:get_value(<<"id">>, Props), + Revs = [?b2l(couch_util:get_value(<<"rev">>, + Props, ""))], + Revs1 = couch_doc:parse_revs(Revs), + Options1 = case couch_util:get_value(<<"atts_since">>, + Props, []) of + [] -> + Options; + RevList when is_list(RevList) -> + RevList1 = couch_doc:parse_revs(RevList), + [{atts_since, RevList1}, attachments |Options] + end, + {ok, Results} = couch_db:open_doc_revs(Db, DocId, + Revs1, Options), + case Boundary of + nil -> + send_docs(Resp, DocId, Results, + Options1, Acc); + _ -> + send_docs_multipart(Resp, DocId, Results, + Boundary, Options1) + end, + "," + end, "", DocsArray), + + %% finish the response + case Boundary of + nil -> + couch_httpd:end_json_response(Resp); + _ -> + couch_httpd:send_chunk(Resp, <<"--">>), + couch_httpd:last_chunk(Resp) + end + end; +handle_req(#httpd{path_parts=[_,<<"_bulk_get">>]}=Req, _Db) -> + couch_httpd:send_method_not_allowed(Req, "POST"). + +send_docs(Resp, DocId, Results, Options, Sep) -> + couch_httpd:send_chunk(Resp, [Sep, "{ \"id\": \"", + ?JSON_ENCODE(DocId), "\", \"docs\": ["]), + lists:foldl( + fun(Result, AccSeparator) -> + case Result of + {ok, Doc} -> + JsonDoc = couch_doc:to_json_obj(Doc, Options), + Json = ?JSON_ENCODE({[{ok, JsonDoc}]}), + couch_httpd:send_chunk(Resp, AccSeparator ++ Json); + {{not_found, missing}, RevId} -> + RevStr = couch_doc:rev_to_str(RevId), + Json = ?JSON_ENCODE({[{"missing", RevStr}]}), + couch_httpd:send_chunk(Resp, AccSeparator ++ Json) + end, + "," % AccSeparator now has a comma + end, "", Results), + couch_httpd:send_chunk(Resp, "]}"). + +send_docs_multipart(Resp, DocId, Results, OuterBoundary, Options0) -> + Options = [attachments, follows, att_encoding_info | Options0], + InnerBoundary = couch_uuids:random(), + couch_httpd:send_chunk(Resp, <<"--", OuterBoundary/binary>>), + lists:foreach( + fun({ok, #doc{atts=Atts}=Doc}) -> + JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, Options)), + {ContentType, _Len} = couch_doc:len_doc_to_multi_part_stream( + InnerBoundary, JsonBytes, Atts, true), + Hdr = <<"\r\nContent-Type: ", ContentType/binary, "\r\n\r\n">>, + couch_httpd:send_chunk(Resp, Hdr), + couch_doc:doc_to_multi_part_stream(InnerBoundary, JsonBytes, + Atts, fun(Data) -> + couch_httpd:send_chunk(Resp, Data) + end, true), + couch_httpd:send_chunk(Resp, <<"\r\n--", OuterBoundary/binary>>); + ({{not_found, missing}, RevId}) -> + RevStr = couch_doc:rev_to_str(RevId), + Body = {[{<<"id">>, DocId}, + {<<"error">>, <<"not_found">>}, + {<<"reason">>, <<"missing">>}, + {<<"status">>, 400}]}, + Json = ?JSON_ENCODE(Body), + {ContentType, _Len} = couch_doc:len_doc_to_multi_part_stream( + InnerBoundary, Json, [], true), + + Hdr = <<"\r\nContent-Type: ", ContentType/binary, "\r\n\r\n">>, + couch_httpd:send_chunk(Resp, Hdr), + couch_doc:doc_to_multi_part_stream(InnerBoundary, Json, + [], fun(Data) -> + couch_httpd:send_chunk(Resp, Data) + end, true), + couch_httpd:send_chunk(Resp, <<"\r\n--", OuterBoundary/binary>>) + end, Results). diff --git a/src/couch_httpd_db.erl b/src/couch_httpd_db.erl index 0d1e0f8..d02f8df 100644 --- a/src/couch_httpd_db.erl +++ b/src/couch_httpd_db.erl @@ -12,11 +12,13 @@ -module(couch_httpd_db). -include_lib("couch/include/couch_db.hrl"). +-include("couch_httpd.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/3]). + 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/3]). +-export([parse_doc_query/1]). -import(couch_httpd, [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, @@ -24,13 +26,6 @@ start_chunked_response/3, absolute_uri/2, send/2, start_response_length/4, send_error/4]). --record(doc_query_args, { - options = [], - rev = nil, - open_revs = [], - update_type = interactive_edit, - atts_since = nil -}). % Database request handlers handle_request(#httpd{path_parts=[DbName|RestParts],method=Method,