From 9007e2d21dea8b0185c0096b30364a8ee40a3867 Mon Sep 17 00:00:00 2001 From: John Christopher Anderson Date: Fri, 13 Mar 2009 22:15:34 +0000 Subject: Commit Damien's rep_security branch to trunk. Changes bulk_docs conflict checking. Breaks file format, see mailing list for data upgrade procedure, or http://wiki.apache.org/couchdb/Breaking_changes git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@753448 13f79535-47bb-0310-9956-ffa450edef68 --- src/couchdb/couch_httpd_db.erl | 285 +++++++++++++++++++++-------------------- 1 file changed, 149 insertions(+), 136 deletions(-) (limited to 'src/couchdb/couch_httpd_db.erl') diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl index 75022cd3..3680d73b 100644 --- a/src/couchdb/couch_httpd_db.erl +++ b/src/couchdb/couch_httpd_db.erl @@ -22,8 +22,8 @@ -record(doc_query_args, { options = [], - rev = "", - open_revs = "" + rev = nil, + open_revs = [] }). % Database request handlers @@ -89,13 +89,13 @@ db_req(#httpd{method='GET',path_parts=[_DbName]}=Req, Db) -> 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=[]}, []), + {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId}, []), DocUrl = absolute_uri(Req, binary_to_list(<<"/",DbName/binary,"/",DocId/binary>>)), send_json(Req, 201, [{"Location", DocUrl}], {[ {ok, true}, {id, DocId}, - {rev, NewRev} + {rev, couch_doc:rev_to_str(NewRev)} ]}); db_req(#httpd{path_parts=[_DbName]}=Req, _Db) -> @@ -131,32 +131,59 @@ db_req(#httpd{method='POST',path_parts=[_,<<"_bulk_docs">>]}=Req, Db) -> <<>> -> couch_util:new_uuid(); Id0 -> Id0 end, - Revs = case proplists:get_value(<<"_rev">>, ObjProps) of - undefined -> []; - Rev -> [Rev] + case proplists:get_value(<<"_rev">>, ObjProps) of + undefined -> + Revs = {0, []}; + Rev -> + {Pos, RevId} = couch_doc:parse_rev(Rev), + Revs = {Pos, [RevId]} end, Doc#doc{id=Id,revs=Revs} end, DocsArray), - {ok, ResultRevs} = couch_db:update_docs(Db, Docs, Options), - - % 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} - ]}); - + Options2 = + case proplists:get_value(<<"all_or_nothing">>, JsonProps) of + true -> [all_or_nothing|Options]; + _ -> Options + end, + case couch_db:update_docs(Db, Docs, Options2) of + {ok, Results} -> + % output the results + DocResults = lists:zipwith( + fun(Doc, {ok, NewRev}) -> + {[{<<"id">>, Doc#doc.id}, {<<"rev">>, couch_doc:rev_to_str(NewRev)}]}; + (Doc, Error) -> + {_Code, Err, Msg} = couch_httpd:error_info(Error), + % maybe we should add the http error code to the json? + {[{<<"id">>, Doc#doc.id}, {<<"error">>, Err}, {"reason", Msg}]} + end, + Docs, Results), + send_json(Req, 201, DocResults); + {aborted, Errors} -> + ErrorsJson = + lists:map( + fun({{Id, Rev}, Error}) -> + {_Code, Err, Msg} = couch_httpd:error_info(Error), + {[{<<"id">>, Id}, + {<<"rev">>, couch_doc:rev_to_str(Rev)}, + {<<"error">>, Err}, + {"reason", Msg}]} + end, Errors), + send_json(Req, 417, ErrorsJson) + end; false -> Docs = [couch_doc:from_json_obj(JsonObj) || JsonObj <- DocsArray], - ok = couch_db:update_docs(Db, Docs, Options, false), - send_json(Req, 201, {[ - {ok, true} - ]}) + {ok, Errors} = couch_db:update_docs(Db, Docs, Options, replicated_changes), + ErrorsJson = + lists:map( + fun({{Id, Rev}, Error}) -> + {_Code, Err, Msg} = couch_httpd:error_info(Error), + {[{<<"id">>, Id}, + {<<"rev">>, couch_doc:rev_to_str(Rev)}, + {<<"error">>, Err}, + {"reason", Msg}]} + end, Errors), + send_json(Req, 201, ErrorsJson) end; db_req(#httpd{path_parts=[_,<<"_bulk_docs">>]}=Req, _Db) -> send_method_not_allowed(Req, "POST"); @@ -170,12 +197,12 @@ db_req(#httpd{path_parts=[_,<<"_compact">>]}=Req, _Db) -> 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], + IdsRevs2 = [{Id, couch_doc:parse_revs(Revs)} || {Id, Revs} <- IdsRevs], - case couch_db:purge_docs(Db, IdsRevs) of + case couch_db:purge_docs(Db, IdsRevs2) of {ok, PurgeSeq, PurgedIdsRevs} -> - send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs}}]}); + PurgedIdsRevs2 = [{Id, couch_doc:rev_to_strs(Revs)} || {Id, Revs} <- PurgedIdsRevs], + send_json(Req, 200, {[{<<"purge_seq">>, PurgeSeq}, {<<"purged">>, {PurgedIdsRevs2}}]}); Error -> throw(Error) end; @@ -204,7 +231,7 @@ db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) -> {ok, Info} = couch_db:get_db_info(Db), CurrentEtag = couch_httpd:make_etag(proplists:get_value(update_seq, Info)), - couch_httpd:etag_respond(Req, CurrentEtag, fun() -> + couch_httpd:etag_respond(Req, CurrentEtag, fun() -> TotalRowCount = proplists:get_value(doc_count, Info), FoldlFun = couch_httpd_view:make_view_fold_fun(Req, QueryArgs, CurrentEtag, Db, TotalRowCount, #view_fold_helper_funs{ @@ -227,14 +254,14 @@ db_req(#httpd{method='GET',path_parts=[_,<<"_all_docs_by_seq">>]}=Req, Db) -> deleted_conflict_revs=DelConflictRevs } = DocInfo, Json = { - [{<<"rev">>, Rev}] ++ + [{<<"rev">>, couch_doc:rev_to_str(Rev)}] ++ case ConflictRevs of [] -> []; - _ -> [{<<"conflicts">>, ConflictRevs}] + _ -> [{<<"conflicts">>, couch_doc:rev_to_strs(ConflictRevs)}] end ++ case DelConflictRevs of [] -> []; - _ -> [{<<"deleted_conflicts">>, DelConflictRevs}] + _ -> [{<<"deleted_conflicts">>, couch_doc:rev_to_strs(DelConflictRevs)}] end ++ case Deleted of true -> [{<<"deleted">>, true}]; @@ -251,9 +278,11 @@ db_req(#httpd{path_parts=[_,<<"_all_docs_by_seq">>]}=Req, _Db) -> 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), + JsonDocIdRevs2 = [{Id, [couch_doc:parse_rev(RevStr) || RevStr <- RevStrs]} || {Id, RevStrs} <- JsonDocIdRevs], + {ok, Results} = couch_db:get_missing_revs(Db, JsonDocIdRevs2), + Results2 = [{Id, [couch_doc:rev_to_str(Rev) || Rev <- Revs]} || {Id, Revs} <- Results], send_json(Req, {[ - {missing_revs, {Results}} + {missing_revs, {Results2}} ]}); db_req(#httpd{path_parts=[_,<<"_missing_revs">>]}=Req, _Db) -> @@ -271,6 +300,18 @@ db_req(#httpd{method='GET',path_parts=[_,<<"_admins">>]}=Req, Db) -> db_req(#httpd{path_parts=[_,<<"_admins">>]}=Req, _Db) -> send_method_not_allowed(Req, "PUT,GET"); +db_req(#httpd{method='PUT',path_parts=[_,<<"_revs_limit">>]}=Req, + Db) -> + Limit = couch_httpd:json_body(Req), + ok = couch_db:set_revs_limit(Db, Limit), + send_json(Req, {[{<<"ok">>, true}]}); + +db_req(#httpd{method='GET',path_parts=[_,<<"_revs_limit">>]}=Req, Db) -> + send_json(Req, couch_db:get_revs_limit(Db)); + +db_req(#httpd{path_parts=[_,<<"_revs_limit">>]}=Req, _Db) -> + send_method_not_allowed(Req, "PUT,GET"); + % Special case to enable using an unencoded slash in the URL of design docs, % as slashes in document IDs must otherwise be URL encoded. db_req(#httpd{method='GET',mochi_req=MochiReq, path_parts=[DbName,<<"_design/",_/binary>>|_]}=Req, _Db) -> @@ -334,7 +375,7 @@ all_docs_view(Req, Db, Keys) -> 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); + FoldlFun({{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}]}}, Offset, Acc); #doc_info{deleted=true} -> {ok, Acc} end @@ -358,9 +399,9 @@ all_docs_view(Req, Db, Keys) -> DocInfo = (catch couch_db:get_doc_info(Db, Key)), Doc = case DocInfo of {ok, #doc_info{id=Id, rev=Rev, deleted=false}} = DocInfo -> - {{Id, Id}, {[{rev, Rev}]}}; + {{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}]}}; {ok, #doc_info{id=Id, rev=Rev, deleted=true}} = DocInfo -> - {{Id, Id}, {[{rev, Rev}, {deleted, true}]}}; + {{Id, Id}, {[{rev, couch_doc:rev_to_str(Rev)}, {deleted, true}]}}; not_found -> {{Key, error}, not_found}; _ -> @@ -381,20 +422,12 @@ all_docs_view(Req, Db, Keys) -> - - 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, 409, <<"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} - ]}) + case couch_httpd:qs_value(Req, "rev") of + undefined -> + update_doc(Req, Db, DocId, {[{<<"_deleted">>,true}]}); + Rev -> + update_doc(Req, Db, DocId, {[{<<"_rev">>, ?l2b(Rev)},{<<"_deleted">>,true}]}) end; db_doc_req(#httpd{method='GET'}=Req, Db, DocId) -> @@ -438,82 +471,31 @@ db_doc_req(#httpd{method='GET'}=Req, Db, DocId) -> 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 = [ - {validate_attachment_name(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, - validate_attachment_names(Doc), - case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of - "true" -> - Options = [full_commit]; - _ -> - Options = [] - 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}, Options), - send_json(Req, 201, [{"Etag", <<"\"", NewRev/binary, "\"">>}], {[ - {ok, true}, - {id, DocId}, - {rev, NewRev} - ]}); + update_doc(Req, Db, DocId, couch_httpd:json_body(Req)); db_doc_req(#httpd{method='COPY'}=Req, Db, SourceDocId) -> SourceRev = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of - missing_rev -> []; + missing_rev -> nil; Rev -> Rev end, - {TargetDocId, TargetRev} = parse_copy_destination_header(Req), + {TargetDocId, TargetRevs} = 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} - ]}); + case couch_db:update_doc(Db, Doc#doc{id=TargetDocId, revs=TargetRevs}, []) of + {ok, NewTargetRev} -> + send_json(Req, 201, [{"Etag", "\"" ++ ?b2l(couch_doc:rev_to_str(NewTargetRev)) ++ "\""}], + update_result_to_json({ok, NewTargetRev})); + Error -> + throw(Error) + end; db_doc_req(#httpd{method='MOVE'}=Req, Db, SourceDocId) -> - SourceRev = + SourceRev = {SourceRevPos, SourceRevId} = case extract_header_rev(Req, couch_httpd:qs_value(Req, "rev")) of missing_rev -> throw({bad_request, "MOVE requires a specified rev parameter" @@ -521,37 +503,68 @@ db_doc_req(#httpd{method='MOVE'}=Req, Db, SourceDocId) -> Rev -> Rev end, - {TargetDocId, TargetRev} = parse_copy_destination_header(Req), + {TargetDocId, TargetRevs} = 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} + #doc{id=SourceDocId, revs={SourceRevPos, [SourceRevId]}, deleted=true}, + Doc#doc{id=TargetDocId, revs=TargetRevs} ], - {ok, ResultRevs} = couch_db:update_docs(Db, Docs, []), + {ok, [SourceResult, TargetResult]} = 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} + {SourceDocId, update_result_to_json(SourceResult)}, + {TargetDocId, update_result_to_json(TargetResult)} ]}); db_doc_req(Req, _Db, _DocId) -> send_method_not_allowed(Req, "DELETE,GET,HEAD,POST,PUT,COPY,MOVE"). +update_result_to_json({ok, NewRev}) -> + {[{rev, couch_doc:rev_to_str(NewRev)}]}; +update_result_to_json(Error) -> + {_Code, ErrorStr, Reason} = couch_httpd:error_info(Error), + {[{error, ErrorStr}, {reason, Reason}]}. + + +update_doc(Req, Db, DocId, Json) -> + #doc{deleted=Deleted} = Doc = couch_doc:from_json_obj(Json), + validate_attachment_names(Doc), + ExplicitDocRev = + case Doc#doc.revs of + {Start,[RevId|_]} -> {Start, RevId}; + _ -> undefined + end, + case extract_header_rev(Req, ExplicitDocRev) of + missing_rev -> + Revs = {0, []}; + {Pos, Rev} -> + Revs = {Pos, [Rev]} + end, + + case couch_httpd:header_value(Req, "X-Couch-Full-Commit", "false") of + "true" -> + Options = [full_commit]; + _ -> + Options = [] + end, + {ok, NewRev} = couch_db:update_doc(Db, Doc#doc{id=DocId, revs=Revs}, Options), + NewRevStr = couch_doc:rev_to_str(NewRev), + send_json(Req, if Deleted -> 200; true -> 201 end, + [{"Etag", <<"\"", NewRevStr/binary, "\"">>}], {[ + {ok, true}, + {id, DocId}, + {rev, NewRevStr}]}). + % 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 + nil -> % open most recent rev case couch_db:open_doc(Db, DocId, Options) of {ok, Doc} -> Doc; @@ -572,13 +585,13 @@ couch_doc_open(Db, DocId, Rev, Options) -> db_attachment_req(#httpd{method='GET'}=Req, Db, DocId, FileNameParts) -> FileName = list_to_binary(mochiweb_util:join(lists:map(fun binary_to_list/1, FileNameParts),"/")), case couch_db:open_doc(Db, DocId, []) of - {ok, #doc{attachments=Attachments, revs=[LastRev|_OldRevs]}} -> + {ok, #doc{attachments=Attachments}=Doc} -> case proplists:get_value(FileName, Attachments) of undefined -> throw({not_found, "Document is missing attachment"}); {Type, Bin} -> {ok, Resp} = start_chunked_response(Req, 200, [ - {"ETag", binary_to_list(LastRev)}, + {"ETag", couch_httpd:doc_etag(Doc)}, {"Cache-Control", "must-revalidate"}, {"Content-Type", binary_to_list(Type)}%, % My understanding of http://www.faqs.org/rfcs/rfc2616.html @@ -640,7 +653,7 @@ db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileNameParts) #doc{id=DocId}; Rev -> case couch_db:open_doc_revs(Db, DocId, [Rev], []) of - {ok, [{ok, Doc0}]} -> Doc0#doc{revs=[Rev]}; + {ok, [{ok, Doc0}]} -> Doc0; {ok, [Error]} -> throw(Error) end end, @@ -653,7 +666,7 @@ db_attachment_req(#httpd{method=Method}=Req, Db, DocId, FileNameParts) send_json(Req, case Method of 'DELETE' -> 200; _ -> 201 end, {[ {ok, true}, {id, DocId}, - {rev, UpdatedRev} + {rev, couch_doc:rev_to_str(UpdatedRev)} ]}); db_attachment_req(Req, _Db, _DocId, _FileNameParts) -> @@ -682,25 +695,24 @@ parse_doc_query(Req) -> 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)}; + Args#doc_query_args{rev=couch_doc:parse_rev(Rev)}; {"open_revs", "all"} -> Args#doc_query_args{open_revs=all}; {"open_revs", RevsJsonStr} -> JsonArray = ?JSON_DECODE(RevsJsonStr), - Args#doc_query_args{open_revs=JsonArray}; + Args#doc_query_args{open_revs=[couch_doc:parse_rev(Rev) || Rev <- JsonArray]}; _Else -> % unknown key value pair, ignore. Args end end, #doc_query_args{}, couch_httpd:qs(Req)). - -extract_header_rev(Req, ExplicitRev) when is_list(ExplicitRev)-> - extract_header_rev(Req, list_to_binary(ExplicitRev)); +extract_header_rev(Req, ExplicitRev) when is_binary(ExplicitRev) or is_list(ExplicitRev)-> + extract_header_rev(Req, couch_doc:parse_rev(ExplicitRev)); extract_header_rev(Req, ExplicitRev) -> Etag = case couch_httpd:header_value(Req, "If-Match") of undefined -> undefined; - Value -> list_to_binary(string:strip(Value, both, $")) + Value -> couch_doc:parse_rev(string:strip(Value, both, $")) end, case {ExplicitRev, Etag} of {undefined, undefined} -> missing_rev; @@ -716,11 +728,12 @@ parse_copy_destination_header(Req) -> Destination = couch_httpd:header_value(Req, "Destination"), case regexp:match(Destination, "\\?") of nomatch -> - {list_to_binary(Destination), []}; + {list_to_binary(Destination), {0, []}}; {match, _, _} -> {ok, [DocId, RevQueryOptions]} = regexp:split(Destination, "\\?"), {ok, [_RevQueryKey, Rev]} = regexp:split(RevQueryOptions, "="), - {list_to_binary(DocId), [list_to_binary(Rev)]} + {Pos, RevId} = couch_doc:parse_rev(Rev), + {list_to_binary(DocId), {Pos, [RevId]}} end. validate_attachment_names(Doc) -> -- cgit v1.2.3