summaryrefslogtreecommitdiff
path: root/src/couchdb/couch_httpd_db.erl
diff options
context:
space:
mode:
authorJohn Christopher Anderson <jchris@apache.org>2009-03-13 22:15:34 +0000
committerJohn Christopher Anderson <jchris@apache.org>2009-03-13 22:15:34 +0000
commit9007e2d21dea8b0185c0096b30364a8ee40a3867 (patch)
tree7d8dacb2c8cd619f18dfab8fdb40d146ac28c85a /src/couchdb/couch_httpd_db.erl
parent65608e14e8911b33c30178d717d745edc9f66c17 (diff)
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
Diffstat (limited to 'src/couchdb/couch_httpd_db.erl')
-rw-r--r--src/couchdb/couch_httpd_db.erl285
1 files changed, 149 insertions, 136 deletions
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) ->