diff options
-rw-r--r-- | share/www/script/couch_tests.js | 51 | ||||
-rw-r--r-- | src/couchdb/couch_httpd.erl | 124 |
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 |