diff options
author | Damien F. Katz <damien@apache.org> | 2008-10-02 16:14:45 +0000 |
---|---|---|
committer | Damien F. Katz <damien@apache.org> | 2008-10-02 16:14:45 +0000 |
commit | eb5264f55d5b594d7082918e9e94b64bf75d25a1 (patch) | |
tree | b21d6d06dffcaabcd777c8859c33168c4aa362d6 | |
parent | 82d31aa2671ac3ffc7b1dbf4c6c9b6c38f0d9f2e (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.erl | 577 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_misc_handlers.erl | 142 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_view.erl | 340 |
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 |