summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJan Lehnardt <jan@apache.org>2008-08-12 14:48:06 +0000
committerJan Lehnardt <jan@apache.org>2008-08-12 14:48:06 +0000
commit5907899532a9bff896d25cbbd9651f2fe88d2300 (patch)
tree1155e3624e383b84d22a43a2150910c059488a1c
parenta80e18ffb5ab0636b08152a7d5c22bac82ea706a (diff)
HTTP COPY & MOVE for documents with tests
git-svn-id: https://svn.apache.org/repos/asf/incubator/couchdb/trunk@685171 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--share/www/script/couch_tests.js51
-rw-r--r--src/couchdb/couch_httpd.erl124
2 files changed, 149 insertions, 26 deletions
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index 08c1bbe9..728aad33 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -115,6 +115,53 @@ var tests = {
// 1 less document should now be in the results.
T(results.total_rows == 2);
T(db.info().doc_count == 5);
+
+ // copy a doc
+ T(db.save({_id:"doc_to_be_copied",v:1}).ok);
+ var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_that_was_copied"}
+ });
+
+ T(xhr.status == 201);
+ T(db.open("doc_that_was_copied").v == 1);
+
+ // move a doc
+
+ // test error condition
+ var xhr = CouchDB.request("MOVE", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_that_was_moved"}
+ });
+ T(xhr.status == 400); // bad request, MOVE requires source rev.
+
+ var rev = db.open("doc_to_be_copied")._rev;
+ var xhr = CouchDB.request("MOVE", "/test_suite_db/doc_to_be_copied?rev=" + rev, {
+ headers: {"Destination":"doc_that_was_moved"}
+ });
+
+ T(xhr.status == 201);
+ T(db.open("doc_that_was_moved").v == 1);
+ T(db.open("doc_to_be_copied") == null);
+
+ // COPY with existing target
+ T(db.save({_id:"doc_to_be_copied",v:1}).ok);
+ var doc = db.save({_id:"doc_to_be_overwritten",v:1});
+ T(doc.ok);
+
+ // error condition
+ var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_to_be_overwritten"}
+ });
+ T(xhr.status == 412); // conflict
+
+ var rev = db.open("doc_to_be_overwritten")._rev;
+ var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
+ headers: {"Destination":"doc_to_be_overwritten?rev=" + rev}
+ });
+ T(xhr.status == 201);
+
+ var newRev = db.open("doc_to_be_overwritten")._rev;
+ T(rev != newRev);
+
},
// Do some edit conflict detection tests
@@ -414,8 +461,8 @@ var tests = {
// This is the reduce phase, we are reducing over emitted values from
// the map functions.
for(var i in values) {
- total = total + values[i]
- sqrTotal = sqrTotal + (values[i] * values[i])
+ total = total + values[i];
+ sqrTotal = sqrTotal + (values[i] * values[i]);
}
count = values.length;
}
diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
index 7b926019..82b09c72 100644
--- a/src/couchdb/couch_httpd.erl
+++ b/src/couchdb/couch_httpd.erl
@@ -54,15 +54,21 @@ handle_request(Req, DocumentRoot) ->
% alias HEAD to GET as mochiweb takes care of stripping the body
Method = case Req:get(method) of
'HEAD' -> 'GET';
- Other -> Other
+ Other ->
+ % handling of non standard HTTP verbs. Should be fixe din gen_tcp:recv()
+ case Other of
+ "COPY" -> 'COPY';
+ "MOVE" -> 'MOVE';
+ StandardMethod -> StandardMethod
+ end
end,
% for the path, use the raw path with the query string and fragment
% removed, but URL quoting left intact
{Path, _, _} = mochiweb_util:urlsplit_path(Req:get(raw_path)),
- ?LOG_DEBUG("~s ~s ~p~nHeaders: ~p", [
- atom_to_list(Req:get(method)),
+ ?LOG_DEBUG("~p ~s ~p~nHeaders: ~p", [
+ Method,
Path,
Req:get(version),
mochiweb_headers:to_list(Req:get(headers))
@@ -75,9 +81,10 @@ handle_request(Req, DocumentRoot) ->
send_error(Req, Error)
end,
- ?LOG_INFO("~s - - ~p ~B", [
+ ?LOG_INFO("~s - - ~p ~s ~B", [
Req:get(peer),
- atom_to_list(Req:get(method)) ++ " " ++ Path,
+ Method,
+ Path,
Resp:get(code)
]).
@@ -545,24 +552,7 @@ handle_doc_request(Req, 'GET', _DbName, Db, DocId) ->
} = parse_doc_query(Req),
case Revs of
[] ->
- case Rev of
- "" -> % open most recent rev
- case couch_db:open_doc(Db, DocId, Options) of
- {ok, #doc{revs=[DocRev|_]}=Doc} ->
- true;
- Error ->
- Doc = DocRev = undefined,
- 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}]} ->
- DocRev = Rev;
- {ok, [Else]} ->
- Doc = DocRev = undefined,
- throw(Else)
- end
- end,
+ {Doc, DocRev} = couch_doc_open(Db, DocId, Rev, Options),
Etag = none_match(Req, DocRev),
AdditionalHeaders = case Doc#doc.meta of
[] -> [{"Etag", Etag}]; % output etag when we have no meta
@@ -625,8 +615,94 @@ handle_doc_request(Req, 'PUT', _DbName, Db, DocId) ->
{rev, NewRev}
]});
+handle_doc_request(Req, 'COPY', _DbName, Db, SourceDocId) ->
+ SourceRev = case extract_header_rev(Req) of
+ missing_rev -> [];
+ Rev -> Rev
+ end,
+
+ {TargetDocId, TargetRev} = parse_copy_destination_header(Req),
+
+ % open revision Rev or Current
+ {Doc, _DocRev} = 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", "\"" ++ NewTargetRev ++ "\""}], {obj, [
+ {ok, true},
+ {id, TargetDocId},
+ {rev, NewTargetRev}
+ ]});
+
+handle_doc_request(Req, 'MOVE', _DbName, Db, SourceDocId) ->
+ SourceRev = case extract_header_rev(Req) 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, _DocRev} = 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) ->
+ {obj, [{"id", FDoc#doc.id}, {"rev", NewRev}]}
+ end,
+ Docs, ResultRevs),
+ send_json(Req, 201, {obj, [
+ {ok, true},
+ {new_revs, list_to_tuple(DocResults)}
+ ]});
+
handle_doc_request(_Req, _Method, _DbName, _Db, _DocId) ->
- throw({method_not_allowed, "DELETE,GET,HEAD,PUT"}).
+ throw({method_not_allowed, "DELETE,GET,HEAD,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{revs=[DocRev|_]}=Doc} ->
+ {Doc, DocRev};
+ 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, Rev};
+ {ok, [Else]} ->
+ throw(Else)
+ end
+ end.
+
+parse_copy_destination_header(Req) ->
+ Destination = Req:get_header_value("Destination"),
+ case regexp:match(Destination, "\\?") of
+ nomatch ->
+ {Destination, []};
+ {match, _, _} ->
+ {ok, [DocId, RevQueryOptions]} = regexp:split(Destination, "\\?"),
+ {ok, [_RevQueryKey, Rev]} = regexp:split(RevQueryOptions, "="),
+ {DocId, [Rev]}
+ end.
% Attachment request handlers