summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamien F. Katz <damien@apache.org>2008-10-02 16:14:45 +0000
committerDamien F. Katz <damien@apache.org>2008-10-02 16:14:45 +0000
commiteb5264f55d5b594d7082918e9e94b64bf75d25a1 (patch)
treeb21d6d06dffcaabcd777c8859c33168c4aa362d6
parent82d31aa2671ac3ffc7b1dbf4c6c9b6c38f0d9f2e (diff)
Added files forgotten in the httpd refactoring checkin.
git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@701174 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--src/couchdb/couch_httpd_db.erl577
-rw-r--r--src/couchdb/couch_httpd_misc_handlers.erl142
-rw-r--r--src/couchdb/couch_httpd_view.erl340
3 files changed, 1059 insertions, 0 deletions
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
new file mode 100644
index 00000000..ff3d8125
--- /dev/null
+++ b/src/couchdb/couch_httpd_db.erl
@@ -0,0 +1,577 @@
+% 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, db_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]).
+
+-record(doc_query_args, {
+ options = [],
+ rev = "",
+ open_revs = ""
+}).
+
+% 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.
+
+create_db_req(Req, DbName) ->
+ case couch_server:create(DbName, []) of
+ {ok, Db} ->
+ couch_db:close(Db),
+ send_json(Req, 201, {[{ok, true}]});
+ Error ->
+ throw(Error)
+ end.
+
+delete_db_req(Req, DbName) ->
+ case couch_server:delete(DbName) of
+ ok ->
+ send_json(Req, 200, {[{ok, true}]});
+ Error ->
+ throw(Error)
+ end.
+
+do_db_req(#httpd{path_parts=[DbName|_]}=Req, Fun) ->
+ case couch_db:open(DbName, []) of
+ {ok, Db} ->
+ try
+ Fun(Req, Db)
+ after
+ 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)),
+ DocId = couch_util:new_uuid(),
+ {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=[]}, []),
+ send_json(Req, 201, {[
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]});
+
+db_req(#httpd{path_parts=[_DbName]}=Req, _Db) ->
+ send_method_not_allowed(Req, "DELETE,GET,HEAD,POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) ->
+ {JsonProps} = couch_httpd:json_body(Req),
+ DocsArray = proplists:get_value(<<"docs">>, JsonProps),
+ % convert all the doc elements to native docs
+ case proplists:get_value(<<"new_edits">>, JsonProps, true) of
+ true ->
+ Docs = lists:map(
+ fun({ObjProps} = JsonObj) ->
+ Doc = couch_doc:from_json_obj(JsonObj),
+ Id = case Doc#doc.id of
+ <<>> -> couch_util:new_uuid();
+ Id0 -> Id0
+ end,
+ Revs = case proplists:get_value(<<"_rev">>, ObjProps) of
+ undefined -> [];
+ Rev -> [Rev]
+ end,
+ Doc#doc{id=Id,revs=Revs}
+ end,
+ DocsArray),
+ {ok, ResultRevs} = couch_db:update_docs(Db, Docs, []),
+
+ % output the results
+ DocResults = lists:zipwith(
+ fun(Doc, NewRev) ->
+ {[{"id", Doc#doc.id}, {"rev", NewRev}]}
+ end,
+ Docs, ResultRevs),
+ send_json(Req, 201, {[
+ {ok, true},
+ {new_revs, DocResults}
+ ]});
+
+ false ->
+ Options =
+ case proplists:get_value(<<"new_edits">>, JsonProps, true) of
+ true -> [new_edits];
+ _ -> []
+ end,
+ Docs = [couch_doc:from_json_obj(JsonObj) || JsonObj <- DocsArray],
+ ok = couch_db:save_docs(Db, Docs, Options),
+ send_json(Req, 201, {[
+ {ok, true}
+ ]})
+ end;
+db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_compact">>]}=Req, Db) ->
+ ok = couch_db:start_compact(Db),
+ send_json(Req, 202, {[{ok, true}]});
+
+db_req(#httpd{path_parts=[_,<<"_compact">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[_,<<"_purge">>]}=Req, Db) ->
+ {IdsRevs} = couch_httpd:json_body(Req),
+ % validate the json input
+ [{_Id, [_|_]=_Revs} = IdRevs || IdRevs <- IdsRevs],
+
+ case couch_db:purge_docs(Db, IdsRevs) of
+ {ok, PurgeSeq, PurgedIdsRevs} ->
+ send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs}}]});
+ 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) ->
+ #view_query_args{
+ start_key = StartKey,
+ start_docid = StartDocId,
+ count = Count,
+ skip = SkipCount,
+ direction = Dir
+ } = QueryArgs = couch_httpd_view:parse_view_query(Req),
+ {ok, Info} = couch_db:get_db_info(Db),
+ TotalRowCount = proplists:get_value(doc_count, Info),
+
+ StartId = if is_binary(StartKey) -> StartKey;
+ true -> StartDocId
+ end,
+
+ FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, TotalRowCount,
+ fun couch_db:enum_docs_reduce_to_count/1),
+ AdapterFun = fun(#full_doc_info{id=Id}=FullDocInfo, Offset, Acc) ->
+ case couch_doc:to_doc_info(FullDocInfo) of
+ #doc_info{deleted=false, rev=Rev} ->
+ FoldlFun({{Id, Id}, {[{rev, Rev}]}}, Offset, Acc);
+ #doc_info{deleted=true} ->
+ {ok, Acc}
+ end
+ end,
+ {ok, FoldResult} = couch_db:enum_docs(Db, StartId, Dir, AdapterFun,
+ {Count, SkipCount, undefined, []}),
+ couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult});
+
+db_req(#httpd{path_parts=[_,<<"_all_docs">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "GET,HEAD");
+
+db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) ->
+ #view_query_args{
+ start_key = StartKey,
+ count = Count,
+ skip = SkipCount,
+ direction = Dir
+ } = QueryArgs = couch_httpd_view:parse_view_query(Req),
+
+ {ok, Info} = couch_db:get_db_info(Db),
+ TotalRowCount = proplists:get_value(doc_count, Info),
+
+ FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, TotalRowCount,
+ fun couch_db:enum_docs_since_reduce_to_count/1),
+ StartKey2 = case StartKey of
+ nil -> 0;
+ <<>> -> 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,
+ rev=Rev,
+ update_seq=UpdateSeq,
+ deleted=Deleted,
+ conflict_revs=ConflictRevs,
+ deleted_conflict_revs=DelConflictRevs
+ } = DocInfo,
+ Json = {
+ [{"rev", Rev}] ++
+ case ConflictRevs of
+ [] -> [];
+ _ -> [{"conflicts", ConflictRevs}]
+ end ++
+ case DelConflictRevs of
+ [] -> [];
+ _ -> [{"deleted_conflicts", DelConflictRevs}]
+ end ++
+ case Deleted of
+ true -> [{"deleted", true}];
+ false -> []
+ end
+ },
+ FoldlFun({{UpdateSeq, Id}, Json}, Offset, Acc)
+ end, {Count, SkipCount, undefined, []}),
+ couch_httpd_view:finish_view_fold(Req, TotalRowCount, {ok, FoldResult});
+
+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(Req),
+ {ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs),
+ send_json(Req, {[
+ {missing_revs, {Results}}
+ ]});
+
+db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) ->
+ send_method_not_allowed(Req, "POST");
+
+db_req(#httpd{method='POST',path_parts=[DbName,<<"_design">>,Name|Rest]}=Req,
+ Db) ->
+ % Special case to enable using an unencoded in the URL of design docs, as
+ % slashes in document IDs must otherwise be URL encoded
+ db_req(Req#httpd{path_parts=[DbName,<<"_design/",Name/binary>>|Rest]}, Db);
+
+db_req(#httpd{path_parts=[_, DocId]}=Req, Db) ->
+ db_doc_req(Req, Db, DocId);
+
+db_req(#httpd{path_parts=[_, DocId, FileName]}=Req, Db) ->
+ db_attachment_req(Req, Db, DocId, FileName).
+
+
+
+db_doc_req(#httpd{method='DELETE'}=Req, Db, DocId) ->
+ case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev ->
+ couch_httpd:send_error(Req, 412, <<"missing_rev">>,
+ <<"Document rev/etag must be specified to delete">>);
+ RevToDelete ->
+ {ok, NewRev} = couch_db:delete_doc(Db, DocId, [RevToDelete]),
+ send_json(Req, 200, {[
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]})
+ end;
+
+db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
+ #doc_query_args{
+ rev = Rev,
+ open_revs = Revs,
+ options = Options
+ } = parse_doc_query(Req),
+ case Revs of
+ [] ->
+ Doc = couch_doc_open(Db, DocId, Rev, Options),
+ DiskEtag = couch_httpd:doc_etag(Doc),
+ EtagsToMatch = string:tokens(
+ couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
+ case lists:member(DiskEtag, EtagsToMatch) of
+ true ->
+ % the client has this in their cache.
+ couch_httpd:send_response(Req, 304, [{"Etag", DiskEtag}], <<>>);
+ false ->
+ Headers =
+ case Doc#doc.meta of
+ [] -> [{"Etag", DiskEtag}]; % output etag only when we have no meta
+ _ -> []
+ end,
+ send_json(Req, 200, Headers, 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} ->
+ Json = ?JSON_ENCODE({[{"missing", RevId}]}),
+ send_chunk(Resp, AccSeparator ++ Json)
+ end,
+ "," % AccSeparator now has a comma
+ end,
+ "", Results),
+ send_chunk(Resp, "]"),
+ end_json_response(Resp)
+ end;
+
+db_doc_req(#httpd{method='POST'}=Req, Db, DocId) ->
+ Form = couch_httpd:parse_form(Req),
+ Rev = list_to_binary(proplists:get_value("_rev", Form)),
+ Doc = case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
+ {ok, [{ok, Doc0}]} -> Doc0#doc{revs=[Rev]};
+ {ok, [Error]} -> throw(Error)
+ end,
+
+ NewAttachments = [
+ {list_to_binary(Name), {list_to_binary(ContentType), Content}} ||
+ {Name, {ContentType, _}, Content} <-
+ proplists:get_all_values("_attachments", Form)
+ ],
+ #doc{attachments=Attachments} = Doc,
+ NewDoc = Doc#doc{
+ attachments = Attachments ++ NewAttachments
+ },
+ {ok, NewRev} = couch_db:update_doc(Db, NewDoc, []),
+
+ send_json(Req, 201, [{"Etag", "\"" ++ NewRev ++ "\""}], {obj, [
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]});
+
+db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
+ Json = couch_httpd:json_body(Req),
+ Doc = couch_doc:from_json_obj(Json),
+ ExplicitRev =
+ case Doc#doc.revs of
+ [Rev0|_] -> Rev0;
+ [] -> undefined
+ end,
+ case extract_header_rev(Req, ExplicitRev) of
+ missing_rev ->
+ Revs = [];
+ Rev ->
+ Revs = [Rev]
+ end,
+ {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=Revs}, []),
+ send_json(Req, 201, [{"Etag", <<"\"", NewRev/binary, "\"">>}], {[
+ {ok, true},
+ {id, DocId},
+ {rev, NewRev}
+ ]});
+
+db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) ->
+ SourceRev =
+ case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev -> [];
+ Rev -> Rev
+ end,
+
+ {TargetDocId, TargetRev} = parse_copy_destination_header(Req),
+
+ % open revision Rev or Current
+ Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
+
+ % save new doc
+ {ok, NewTargetRev} = couch_db:update_doc(Db, Doc#doc{id=TargetDocId, revs=TargetRev}, []),
+
+ send_json(Req, 201, [{"Etag", "\"" ++ binary_to_list(NewTargetRev) ++ "\""}], {[
+ {ok, true},
+ {id, TargetDocId},
+ {rev, NewTargetRev}
+ ]});
+
+db_doc_req(#httpd{method='MOVE'}=Req, Db, SourceDocId) ->
+ SourceRev =
+ case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev ->
+ throw({bad_request, "MOVE requires a specified rev parameter"
+ "for the origin resource."});
+ Rev -> Rev
+ end,
+
+ {TargetDocId, TargetRev} = parse_copy_destination_header(Req),
+ % open revision Rev or Current
+ Doc = couch_doc_open(Db, SourceDocId, SourceRev, []),
+
+ % save new doc & delete old doc in one operation
+ Docs = [
+ Doc#doc{id=TargetDocId, revs=TargetRev},
+ #doc{id=SourceDocId, revs=[SourceRev], deleted=true}
+ ],
+
+ {ok, ResultRevs} = couch_db:update_docs(Db, Docs, []),
+
+ DocResults = lists:zipwith(
+ fun(FDoc, NewRev) ->
+ {[{id, FDoc#doc.id}, {rev, NewRev}]}
+ end,
+ Docs, ResultRevs),
+ send_json(Req, 201, {[
+ {ok, true},
+ {new_revs, DocResults}
+ ]});
+
+db_doc_req(Req, _Db, _DocId) ->
+ send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY,MOVE").
+
+% Useful for debugging
+% couch_doc_open(Db, DocId) ->
+% couch_doc_open(Db, DocId, [], []).
+
+couch_doc_open(Db, DocId, Rev, Options) ->
+ case Rev of
+ "" -> % 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, [Else]} ->
+ throw(Else)
+ end
+ end.
+
+% Attachment request handlers
+
+db_attachment_req(#httpd{method='GET'}=Req, Db, DocId, FileName) ->
+ case couch_db:open_doc(Db, DocId, []) of
+ {ok, #doc{attachments=Attachments}} ->
+ case proplists:get_value(FileName, Attachments) of
+ undefined ->
+ throw({not_found, "Document is missing attachment"});
+ {Type, Bin} ->
+ {ok, Resp} = start_chunked_response(Req, 200, [
+ {"Cache-Control", "must-revalidate"},
+ {"Content-Type", binary_to_list(Type)},
+ {"Content-Length", integer_to_list(couch_doc:bin_size(Bin))}]),
+ couch_doc:bin_foldl(Bin,
+ fun(BinSegment, []) ->
+ send_chunk(Resp, BinSegment),
+ {ok, []}
+ end,
+ []
+ ),
+ send_chunk(Resp, "")
+ end;
+ Error ->
+ throw(Error)
+ end;
+
+db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileName)
+ when (Method == 'PUT') or (Method == 'DELETE') ->
+
+ NewAttachment = case Method of
+ 'DELETE' ->
+ [];
+ _ ->
+ [{FileName, {
+ list_to_binary(couch_httpd:header_value(Req,"Content-Type")),
+ couch_httpd:body(Req)
+ }}]
+ end,
+
+ Doc = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of
+ missing_rev -> % make the new doc
+ #doc{id=DocId};
+ Rev ->
+ case couch_db:open_doc_revs(Db, DocId, [Rev], []) of
+ {ok, [{ok, Doc0}]} -> Doc0#doc{revs=[Rev]};
+ {ok, [Error]} -> throw(Error)
+ end
+ end,
+
+ #doc{attachments=Attachments} = Doc,
+ DocEdited = Doc#doc{
+ attachments = NewAttachment ++ proplists:delete(FileName, Attachments)
+ },
+ {ok, UpdatedRev} = couch_db:update_doc(Db, DocEdited, []),
+ send_json(Req, case Method of 'DELETE' -> 200; _ -> 201 end, {[
+ {ok, true},
+ {id, DocId},
+ {rev, UpdatedRev}
+ ]});
+
+db_attachment_req(Req, _Db, _DocId, _FileName) ->
+ send_method_not_allowed(Req, "DELETE,GET,HEAD,PUT").
+
+
+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};
+ {"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=list_to_binary(Rev)};
+ {"open_revs", "all"} ->
+ Args#doc_query_args{open_revs=all};
+ {"open_revs", RevsJsonStr} ->
+ JsonArray = ?JSON_DECODE(RevsJsonStr),
+ Args#doc_query_args{open_revs=JsonArray};
+ _Else -> % unknown key value pair, ignore.
+ Args
+ end
+ end, #doc_query_args{}, couch_httpd:qs(Req)).
+
+
+
+extract_header_rev(Req, ExplictRev) when is_list(ExplictRev)->
+ extract_header_rev(Req, list_to_binary(ExplictRev));
+extract_header_rev(Req, ExplictRev) ->
+ Etag = case couch_httpd:header_value(Req, "If-Match") of
+ undefined -> undefined;
+ Tag -> string:strip(Tag, both, $")
+ end,
+ case {ExplictRev, Etag} of
+ {undefined, undefined} -> missing_rev;
+ {_, undefined} -> ExplictRev;
+ {undefined, _} -> list_to_binary(Etag);
+ _ when ExplictRev == Etag -> list_to_binary(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 regexp:match(Destination, "\\?") of
+ nomatch ->
+ {list_to_binary(Destination), []};
+ {match, _, _} ->
+ {ok, [DocId, RevQueryOptions]} = regexp:split(Destination, "\\?"),
+ {ok, [_RevQueryKey, Rev]} = regexp:split(RevQueryOptions, "="),
+ {list_to_binary(DocId), [list_to_binary(Rev)]}
+ end.
+
diff --git a/src/couchdb/couch_httpd_misc_handlers.erl b/src/couchdb/couch_httpd_misc_handlers.erl
new file mode 100644
index 00000000..040e34de
--- /dev/null
+++ b/src/couchdb/couch_httpd_misc_handlers.erl
@@ -0,0 +1,142 @@
+% 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_misc_handlers).
+
+-export([handle_welcome_req/2,handle_utils_dir_req/2,handle_all_dbs_req/1,
+ handle_replicate_req/1,handle_restart_req/1,handle_uuids_req/1,
+ handle_config_req/1]).
+
+-export([increment_update_seq_req/2]).
+
+
+-include("couch_db.hrl").
+
+-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]).
+
+% httpd global handlers
+
+handle_welcome_req(#httpd{method='GET'}=Req, WelcomeMessage) ->
+ send_json(Req, {[
+ {couchdb, WelcomeMessage},
+ {version, list_to_binary(couch_server:get_version())}
+ ]});
+handle_welcome_req(Req, _) ->
+ send_method_not_allowed(Req, "GET,HEAD").
+
+
+handle_utils_dir_req(#httpd{method='GET'}=Req, DocumentRoot) ->
+ "/" ++ UrlPath = couch_httpd:path(Req),
+ case couch_httpd:partition(UrlPath) of
+ {_ActionKey, "/", RelativePath} ->
+ % GET /_utils/path or GET /_utils/
+ couch_httpd:serve_file(Req, RelativePath, DocumentRoot);
+ {_ActionKey, "", _RelativePath} ->
+ % GET /_utils
+ couch_httpd:send_response(Req, 301, [{"Location", "/_utils/"}], <<>>)
+ end;
+handle_utils_dir_req(Req, _) ->
+ send_method_not_allowed(Req, "GET,HEAD").
+
+
+handle_all_dbs_req(#httpd{method='GET'}=Req) ->
+ {ok, DbNames} = couch_server:all_databases(),
+ send_json(Req, DbNames);
+handle_all_dbs_req(Req) ->
+ send_method_not_allowed(Req, "GET,HEAD").
+
+
+handle_replicate_req(#httpd{method='POST'}=Req) ->
+ {Props} = couch_httpd:json_body(Req),
+ Source = proplists:get_value(<<"source">>, Props),
+ Target = proplists:get_value(<<"target">>, Props),
+ {Options} = proplists:get_value(<<"options">>, Props, {[]}),
+ {ok, {JsonResults}} = couch_rep:replicate(Source, Target, Options),
+ send_json(Req, {[{ok, true} | JsonResults]});
+handle_replicate_req(Req) ->
+ send_method_not_allowed(Req, "POST").
+
+
+handle_restart_req(#httpd{method='POST'}=Req) ->
+ Response = send_json(Req, {[{ok, true}]}),
+ spawn(fun() -> couch_server:remote_restart() end),
+ Response;
+handle_restart_req(Req) ->
+ send_method_not_allowed(Req, "POST").
+
+
+handle_uuids_req(#httpd{method='POST'}=Req) ->
+ Count = list_to_integer(couch_httpd:qs_value(Req, "count", "1")),
+ % generate the uuids
+ UUIDs = [ couch_util:new_uuid() || _ <- lists:seq(1,Count)],
+ % send a JSON response
+ send_json(Req, {[{"uuids", UUIDs}]});
+handle_uuids_req(Req) ->
+ send_method_not_allowed(Req, "POST").
+
+
+% Config request handler
+
+
+% GET /_config/
+% GET /_config
+handle_config_req(#httpd{method='GET', path_parts=[_]}=Req) ->
+ KVs = [{list_to_binary(Key), list_to_binary(Value)}
+ || {Key, Value} <- couch_config:all()],
+ send_json(Req, 200, {KVs});
+% GET /_config/Section
+handle_config_req(#httpd{method='GET', path_parts=[_,Section]}=Req) ->
+ KVs = [{list_to_binary(Key), list_to_binary(Value)}
+ || {Key, Value} <- couch_config:get(Section)],
+ send_json(Req, 200, {KVs});
+% PUT /_config/Section/Key
+% "value"
+handle_config_req(#httpd{method='PUT', path_parts=[_, Section, Key]}=Req) ->
+ Value = binary_to_list(couch_httpd:body(Req)),
+ ok = couch_config:set(Section, Key, Value),
+ send_json(Req, 200, {[
+ {ok, true}
+ ]});
+% GET /_config/Section/Key
+handle_config_req(#httpd{method='GET', path_parts=[_, Section, Key]}=Req) ->
+ case couch_config:get(Section, Key, null) of
+ null ->
+ throw({not_found, unknown_config_value});
+ Value ->
+ send_json(Req, 200, list_to_binary(Value))
+ end;
+% DELETE /_config/Section/Key
+handle_config_req(#httpd{method='DELETE',path_parts=[_,Section,Key]}=Req) ->
+ case couch_config:get(Section, Key, null) of
+ null ->
+ throw({not_found, unknown_config_value});
+ OldValue ->
+ couch_config:delete(Section, Key),
+ send_json(Req, 200, list_to_binary(OldValue))
+ end;
+handle_config_req(Req) ->
+ send_method_not_allowed(Req, "GET,PUT,DELETE").
+
+
+% httpd db handlers
+
+increment_update_seq_req(#httpd{method='POST'}=Req, Db) ->
+ {ok, NewSeq} = couch_db:increment_update_seq(Db),
+ send_json(Req, {[{ok, true},
+ {update_seq, NewSeq}
+ ]});
+increment_update_seq_req(Req, _Db) ->
+ send_method_not_allowed(Req, "GET,PUT,DELETE").
+
diff --git a/src/couchdb/couch_httpd_view.erl b/src/couchdb/couch_httpd_view.erl
new file mode 100644
index 00000000..0f3dd144
--- /dev/null
+++ b/src/couchdb/couch_httpd_view.erl
@@ -0,0 +1,340 @@
+% 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_view).
+-include("couch_db.hrl").
+
+-export([handle_view_req/2,handle_temp_view_req/2]).
+
+-export([parse_view_query/1,make_view_fold_fun/4,finish_view_fold/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,end_json_response/1]).
+
+
+handle_view_req(#httpd{method='GET',path_parts=[_,_, Id, ViewName]}=Req, Db) ->
+ #view_query_args{
+ start_key = StartKey,
+ count = Count,
+ skip = SkipCount,
+ direction = Dir,
+ start_docid = StartDocId,
+ reduce = Reduce
+ } = QueryArgs = parse_view_query(Req),
+
+ case couch_view:get_map_view({couch_db:name(Db),
+ <<"_design/", Id/binary>>, ViewName}) of
+ {ok, View} ->
+ {ok, RowCount} = couch_view:get_row_count(View),
+ Start = {StartKey, StartDocId},
+ FoldlFun = make_view_fold_fun(Req, QueryArgs, RowCount,
+ fun couch_view:reduce_to_count/1),
+ FoldAccInit = {Count, SkipCount, undefined, []},
+ FoldResult = couch_view:fold(View, Start, Dir, FoldlFun, FoldAccInit),
+ finish_view_fold(Req, RowCount, FoldResult);
+ {not_found, Reason} ->
+ case couch_view:get_reduce_view({couch_db:name(Db),
+ <<"_design/", Id/binary>>, ViewName}) of
+ {ok, View} ->
+ case Reduce of
+ false ->
+ {reduce, _N, _Lang, MapView} = View,
+ {ok, RowCount} = couch_view:get_row_count(MapView),
+ Start = {StartKey, StartDocId},
+ FoldlFun = make_view_fold_fun(Req, QueryArgs, RowCount,
+ fun couch_view:reduce_to_count/1),
+ FoldAccInit = {Count, SkipCount, undefined, []},
+ FoldResult = couch_view:fold(MapView, Start, Dir, FoldlFun, FoldAccInit),
+ finish_view_fold(Req, RowCount, FoldResult);
+ _ ->
+ output_reduce_view(Req, View)
+ end;
+ _ ->
+ throw({not_found, Reason})
+ end
+ end;
+
+handle_view_req(Req, _Db) ->
+ send_method_not_allowed(Req, "GET,HEAD").
+
+handle_temp_view_req(#httpd{method='POST'}=Req, Db) ->
+ #view_query_args{
+ start_key = StartKey,
+ count = Count,
+ skip = SkipCount,
+ direction = Dir,
+ start_docid = StartDocId
+ } = QueryArgs = parse_view_query(Req),
+
+ case couch_httpd:primary_header_value(Req, "content-type") of
+ undefined -> ok;
+ "application/json" -> ok;
+ Else -> throw({incorrect_mime_type, Else})
+ end,
+ {Props} = couch_httpd:json_body(Req),
+ Language = proplists:get_value(<<"language">>, Props, <<"javascript">>),
+ MapSrc = proplists:get_value(<<"map">>, Props),
+ case proplists:get_value(<<"reduce">>, Props, null) of
+ null ->
+ {ok, View} = couch_view:get_map_view({temp, couch_db:name(Db), Language, MapSrc}),
+ Start = {StartKey, StartDocId},
+
+ {ok, TotalRows} = couch_view:get_row_count(View),
+
+ FoldlFun = make_view_fold_fun(Req, QueryArgs, TotalRows,
+ fun couch_view:reduce_to_count/1),
+ FoldAccInit = {Count, SkipCount, undefined, []},
+ FoldResult = couch_view:fold(View, Start, Dir, fun(A, B, C) ->
+ FoldlFun(A, B, C)
+ end, FoldAccInit),
+ finish_view_fold(Req, TotalRows, FoldResult);
+
+ RedSrc ->
+ {ok, View} = couch_view:get_reduce_view(
+ {temp, couch_db:name(Db), Language, MapSrc, RedSrc}),
+ output_reduce_view(Req, View)
+ end;
+
+handle_temp_view_req(Req, _Db) ->
+ send_method_not_allowed(Req, "POST").
+
+
+output_reduce_view(Req, View) ->
+ #view_query_args{
+ start_key = StartKey,
+ end_key = EndKey,
+ count = Count,
+ skip = Skip,
+ direction = Dir,
+ start_docid = StartDocId,
+ end_docid = EndDocId,
+ group_level = GroupLevel
+ } = parse_view_query(Req),
+ GroupRowsFun =
+ fun({_Key1,_}, {_Key2,_}) when GroupLevel == 0 ->
+ true;
+ ({Key1,_}, {Key2,_})
+ when is_integer(GroupLevel) and is_list(Key1) and is_list(Key2) ->
+ lists:sublist(Key1, GroupLevel) == lists:sublist(Key2, GroupLevel);
+ ({Key1,_}, {Key2,_}) ->
+ Key1 == Key2
+ end,
+ {ok, Resp} = start_json_response(Req, 200),
+ send_chunk(Resp, "{\"rows\":["),
+ {ok, _} = couch_view:fold_reduce(View, Dir, {StartKey, StartDocId}, {EndKey, EndDocId},
+ GroupRowsFun,
+ fun(_Key, _Red, {AccSeparator,AccSkip,AccCount}) when AccSkip > 0 ->
+ {ok, {AccSeparator,AccSkip-1,AccCount}};
+ (_Key, _Red, {AccSeparator,0,AccCount}) when AccCount == 0 ->
+ {stop, {AccSeparator,0,AccCount}};
+ (_Key, Red, {AccSeparator,0,AccCount}) when GroupLevel == 0 ->
+ Json = ?JSON_ENCODE({[{key, null}, {value, Red}]}),
+ send_chunk(Resp, AccSeparator ++ Json),
+ {ok, {",",0,AccCount-1}};
+ (Key, Red, {AccSeparator,0,AccCount})
+ when is_integer(GroupLevel)
+ andalso is_list(Key) ->
+ Json = ?JSON_ENCODE(
+ {[{key, lists:sublist(Key, GroupLevel)},{value, Red}]}),
+ send_chunk(Resp, AccSeparator ++ Json),
+ {ok, {",",0,AccCount-1}};
+ (Key, Red, {AccSeparator,0,AccCount}) ->
+ Json = ?JSON_ENCODE({[{key, Key}, {value, Red}]}),
+ send_chunk(Resp, AccSeparator ++ Json),
+ {ok, {",",0,AccCount-1}}
+ end, {"", Skip, Count}),
+ send_chunk(Resp, "]}"),
+ end_json_response(Resp).
+
+
+reverse_key_default(nil) -> {};
+reverse_key_default({}) -> nil;
+reverse_key_default(Key) -> Key.
+
+parse_view_query(Req) ->
+ QueryList = couch_httpd:qs(Req),
+ lists:foldl(fun({Key,Value}, Args) ->
+ case {Key, Value} of
+ {"", _} ->
+ Args;
+ {"key", Value} ->
+ JsonKey = ?JSON_DECODE(Value),
+ Args#view_query_args{start_key=JsonKey,end_key=JsonKey};
+ {"startkey_docid", DocId} ->
+ Args#view_query_args{start_docid=list_to_binary(DocId)};
+ {"endkey_docid", DocId} ->
+ Args#view_query_args{end_docid=list_to_binary(DocId)};
+ {"startkey", Value} ->
+ Args#view_query_args{start_key=?JSON_DECODE(Value)};
+ {"endkey", Value} ->
+ Args#view_query_args{end_key=?JSON_DECODE(Value)};
+ {"count", Value} ->
+ case (catch list_to_integer(Value)) of
+ Count when is_integer(Count) ->
+ if Count < 0 ->
+ Args#view_query_args {
+ direction =
+ if Args#view_query_args.direction == rev -> fwd;
+ true -> rev
+ end,
+ count=Count,
+ start_key = reverse_key_default(Args#view_query_args.start_key),
+ start_docid = reverse_key_default(Args#view_query_args.start_docid),
+ end_key = reverse_key_default(Args#view_query_args.end_key),
+ end_docid = reverse_key_default(Args#view_query_args.end_docid)};
+ true ->
+ Args#view_query_args{count=Count}
+ end;
+ _Error ->
+ Msg = io_lib:format("Bad URL query value, number expected: count=~s", [Value]),
+ throw({query_parse_error, Msg})
+ end;
+ {"update", "false"} ->
+ Args#view_query_args{update=false};
+ {"descending", "true"} ->
+ case Args#view_query_args.direction of
+ fwd ->
+ Args#view_query_args {
+ direction = rev,
+ start_key = reverse_key_default(Args#view_query_args.start_key),
+ start_docid = reverse_key_default(Args#view_query_args.start_docid),
+ end_key = reverse_key_default(Args#view_query_args.end_key),
+ end_docid = reverse_key_default(Args#view_query_args.end_docid)};
+ _ ->
+ Args %already reversed
+ end;
+ {"descending", "false"} ->
+ % The descending=false behaviour is the default behaviour, so we
+ % simpply ignore it. This is only for convenience when playing with
+ % the HTTP API, so that a user doesn't get served an error when
+ % flipping true to false in the descending option.
+ Args;
+ {"skip", Value} ->
+ case (catch list_to_integer(Value)) of
+ Count when is_integer(Count) ->
+ Args#view_query_args{skip=Count};
+ _Error ->
+ Msg = lists:flatten(io_lib:format(
+ "Bad URL query value, number expected: skip=~s", [Value])),
+ throw({query_parse_error, Msg})
+ end;
+ {"group", "true"} ->
+ Args#view_query_args{group_level=exact};
+ {"group_level", LevelStr} ->
+ Args#view_query_args{group_level=list_to_integer(LevelStr)};
+ {"reduce", "true"} ->
+ Args#view_query_args{reduce=true};
+ {"reduce", "false"} ->
+ Args#view_query_args{reduce=false};
+ _ -> % unknown key
+ Msg = lists:flatten(io_lib:format(
+ "Bad URL query key:~s", [Key])),
+ throw({query_parse_error, Msg})
+ end
+ end, #view_query_args{}, QueryList).
+
+
+make_view_fold_fun(Req, QueryArgs, TotalViewCount, ReduceCountFun) ->
+ #view_query_args{
+ end_key = EndKey,
+ end_docid = EndDocId,
+ direction = Dir,
+ count = Count
+ } = QueryArgs,
+
+ PassedEndFun =
+ case Dir of
+ fwd ->
+ fun(ViewKey, ViewId) ->
+ couch_view:less_json([EndKey, EndDocId], [ViewKey, ViewId])
+ end;
+ rev->
+ fun(ViewKey, ViewId) ->
+ couch_view:less_json([ViewKey, ViewId], [EndKey, EndDocId])
+ end
+ end,
+
+ NegCountFun = fun({{Key, DocId}, Value}, OffsetReds,
+ {AccCount, AccSkip, Resp, AccRevRows}) ->
+ Offset = ReduceCountFun(OffsetReds),
+ PassedEnd = PassedEndFun(Key, DocId),
+ case {PassedEnd, AccCount, AccSkip, Resp} of
+ {true, _, _, _} -> % The stop key has been passed, stop looping.
+ {stop, {AccCount, AccSkip, Resp, AccRevRows}};
+ {_, 0, _, _} -> % we've done "count" rows, stop foldling
+ {stop, {0, 0, Resp, AccRevRows}};
+ {_, _, AccSkip, _} when AccSkip > 0 ->
+ {ok, {AccCount, AccSkip - 1, Resp, AccRevRows}};
+ {_, _, _, undefined} ->
+ {ok, Resp2} = start_json_response(Req, 200),
+ Offset2 = TotalViewCount - Offset -
+ lists:min([TotalViewCount - Offset, - AccCount]),
+ JsonBegin = io_lib:format("{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n",
+ [TotalViewCount, Offset2]),
+ send_chunk(Resp2, JsonBegin),
+ JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+ {ok, {AccCount + 1, 0, Resp2, [?JSON_ENCODE(JsonObj) | AccRevRows]}};
+ {_, AccCount, _, Resp} ->
+ JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+ {ok, {AccCount + 1, 0, Resp, [?JSON_ENCODE(JsonObj), ",\r\n" | AccRevRows]}}
+ end
+ end,
+
+ PosCountFun = fun({{Key, DocId}, Value}, OffsetReds,
+ {AccCount, AccSkip, Resp, AccRevRows}) ->
+ Offset = ReduceCountFun(OffsetReds), % I think we only need this call once per view
+ PassedEnd = PassedEndFun(Key, DocId),
+ case {PassedEnd, AccCount, AccSkip, Resp} of
+ {true, _, _, _} ->
+ % The stop key has been passed, stop looping.
+ {stop, {AccCount, AccSkip, Resp, AccRevRows}};
+ {_, 0, _, _} ->
+ % we've done "count" rows, stop foldling
+ {stop, {0, 0, Resp, AccRevRows}};
+ {_, _, AccSkip, _} when AccSkip > 0 ->
+ {ok, {AccCount, AccSkip - 1, Resp, AccRevRows}};
+ {_, _, _, undefined} ->
+ {ok, Resp2} = start_json_response(Req, 200),
+ JsonBegin = io_lib:format("{\"total_rows\":~w,\"offset\":~w,\"rows\":[\r\n",
+ [TotalViewCount, Offset]),
+ JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+
+ send_chunk(Resp2, JsonBegin ++ ?JSON_ENCODE(JsonObj)),
+ {ok, {AccCount - 1, 0, Resp2, AccRevRows}};
+ {_, AccCount, _, Resp} when (AccCount > 0) ->
+ JsonObj = {[{id, DocId}, {key, Key}, {value, Value}]},
+ send_chunk(Resp, ",\r\n" ++ ?JSON_ENCODE(JsonObj)),
+ {ok, {AccCount - 1, 0, Resp, AccRevRows}}
+ end
+ end,
+ case Count > 0 of
+ true -> PosCountFun;
+ false -> NegCountFun
+ end.
+
+finish_view_fold(Req, TotalRows, FoldResult) ->
+ case FoldResult of
+ {ok, {_, _, undefined, _}} ->
+ % nothing found in the view, nothing has been returned
+ % send empty view
+ send_json(Req, 200, {[
+ {total_rows, TotalRows},
+ {rows, []}
+ ]});
+ {ok, {_, _, Resp, AccRevRows}} ->
+ % end the view
+ send_chunk(Resp, AccRevRows ++ "\r\n]}"),
+ end_json_response(Resp);
+ Error ->
+ throw(Error)
+ end. \ No newline at end of file