summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--share/Makefile.am1
-rw-r--r--share/www/script/couch_tests.js1
-rw-r--r--share/www/script/test/attachments_multipart.js185
-rw-r--r--src/couchdb/couch_doc.erl13
-rw-r--r--src/couchdb/couch_httpd_db.erl12
5 files changed, 200 insertions, 12 deletions
diff --git a/share/Makefile.am b/share/Makefile.am
index 4adce5db..34711b11 100644
--- a/share/Makefile.am
+++ b/share/Makefile.am
@@ -97,6 +97,7 @@ nobase_dist_localdata_DATA = \
www/script/sha1.js \
www/script/test/all_docs.js \
www/script/test/attachments.js \
+ www/script/test/attachments_multipart.js \
www/script/test/attachment_names.js \
www/script/test/attachment_paths.js \
www/script/test/attachment_views.js \
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index 5c1e2bc3..d991c05d 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -30,6 +30,7 @@ loadTest("basics.js");
// keep sorted
loadTest("all_docs.js");
loadTest("attachments.js");
+loadTest("attachments_multipart.js");
loadTest("attachment_names.js");
loadTest("attachment_paths.js");
loadTest("attachment_views.js");
diff --git a/share/www/script/test/attachments_multipart.js b/share/www/script/test/attachments_multipart.js
new file mode 100644
index 00000000..dbe2cff6
--- /dev/null
+++ b/share/www/script/test/attachments_multipart.js
@@ -0,0 +1,185 @@
+// 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.
+
+couchTests.attachments_multipart= function(debug) {
+ var db = new CouchDB("test_suite_db", {"X-Couch-Full-Commit":"false"});
+ db.deleteDb();
+ db.createDb();
+ if (debug) debugger;
+
+ // mime multipart
+
+ xhr = CouchDB.request("PUT", "/test_suite_db/multipart", {
+ headers: {"Content-Type": "multipart/related;boundary=\"abc123\""},
+ body:
+ "--abc123\r\n" +
+ "content-type: application/json\r\n" +
+ "\r\n" +
+ JSON.stringify({
+ "body":"This is a body.",
+ "_attachments":{
+ "foo.txt": {
+ "follows":true,
+ "content_type":"text/plain",
+ "length":21
+ },
+ "bar.txt": {
+ "follows":true,
+ "content_type":"text/plain",
+ "length":20
+ },
+ "baz.txt": {
+ "follows":true,
+ "content_type":"text/plain",
+ "length":19
+ }
+ }
+ }) +
+ "\r\n--abc123\r\n" +
+ "\r\n" +
+ "this is 21 chars long" +
+ "\r\n--abc123\r\n" +
+ "\r\n" +
+ "this is 20 chars lon" +
+ "\r\n--abc123\r\n" +
+ "\r\n" +
+ "this is 19 chars lo" +
+ "\r\n--abc123--"
+ });
+
+ var result = JSON.parse(xhr.responseText);
+
+ T(result.ok)
+
+
+
+ TEquals(201, xhr.status, "should send 201 Accepted");
+
+ xhr = CouchDB.request("GET", "/test_suite_db/multipart/foo.txt");
+
+ T(xhr.responseText == "this is 21 chars long");
+
+ xhr = CouchDB.request("GET", "/test_suite_db/multipart/bar.txt");
+
+ T(xhr.responseText == "this is 20 chars lon");
+
+ xhr = CouchDB.request("GET", "/test_suite_db/multipart/baz.txt");
+
+ T(xhr.responseText == "this is 19 chars lo");
+
+ // now edit an attachment
+
+ var doc = db.open("multipart");
+
+ T(doc._attachments["foo.txt"].stub == true);
+ T(doc._attachments["bar.txt"].stub == true);
+ T(doc._attachments["baz.txt"].stub == true);
+
+ //lets change attachment bar
+ delete doc._attachments["bar.txt"].stub; // remove stub member (or could set to false)
+ doc._attachments["bar.txt"].length = 18;
+ doc._attachments["bar.txt"].follows = true;
+ //lets delete attachment baz:
+ delete doc._attachments["baz.txt"];
+
+ var xhr = CouchDB.request("PUT", "/test_suite_db/multipart", {
+ headers: {"Content-Type": "multipart/related;boundary=\"abc123\""},
+ body:
+ "--abc123\r\n" +
+ "content-type: application/json\r\n" +
+ "\r\n" +
+ JSON.stringify(doc) +
+ "\r\n--abc123\r\n" +
+ "\r\n" +
+ "this is 18 chars l" +
+ "\r\n--abc123--"
+ });
+
+ xhr = CouchDB.request("GET", "/test_suite_db/multipart/bar.txt");
+
+ T(xhr.responseText == "this is 18 chars l");
+
+ xhr = CouchDB.request("GET", "/test_suite_db/multipart/baz.txt");
+ T(xhr.status == 404);
+
+ xhr = CouchDB.request("GET", "/test_suite_db/multipart?attachments=true",
+ {headers:{"accept": "multipart/related,*/*;"}});
+
+ var headers = xhr.getAllResponseHeaders();
+
+ var ctype = xhr.getResponseHeader("Content-Type");
+
+ var ctypeArgs = ctype.split("; ").slice(1);
+ var boundary = null;
+ for(var i=0; i<ctypeArgs.length; i++) {
+ if (ctypeArgs[i].indexOf("boundary=") == 0) {
+ boundary = ctypeArgs[i].split("=")[1];
+ if (boundary.charAt(0) == '"') {
+ // stringified boundary, parse as json
+ // (will maybe not if there are escape quotes)
+ boundary = JSON.parse(boundary);
+ }
+ }
+ }
+
+ T(boundary != null);
+
+ function parseMime(boundary, mimetext) {
+ // strip off leading boundary
+ var leading = "--" + boundary + "\r\n";
+ var last = "\r\n--" + boundary + "--";
+
+ // strip off leading and trailing boundary
+ var leadingIdx = mimetext.indexOf(leading) + leading.length;
+ var trailingIdx = mimetext.indexOf(last);
+ mimetext = mimetext.slice(leadingIdx, trailingIdx);
+
+ // now split the sections
+ var sections = mimetext.split(new RegExp("\\r\\n--" + boundary));
+
+ // spilt out the headers for each section
+ for(var i=0; i < sections.length; i++) {
+ var section = sections[i];
+ var headerEndIdx = section.indexOf("\r\n\r\n");
+ var headersraw = section.slice(0, headerEndIdx).split(/\r\n/);
+ var body = section.slice(headerEndIdx + 4);
+ var headers = {};
+ for(var j=0; j<headersraw.length; j++) {
+ var tmp = headersraw[j].split(": ");
+ headers[tmp[0]] = tmp[1];
+ }
+ sections[i] = {"headers":headers, "body":body};
+ }
+
+ return sections;
+ }
+
+ // parse out the multipart
+
+ T(xhr.status == 200);
+
+ var sections = parseMime(boundary, xhr.responseText);
+
+ // The first section is the json doc. Check it's content-type. It contains
+ // the metadata for all the following attachments
+
+ T(sections[0].headers['content-type'] == "application/json");
+
+ var doc = JSON.parse(sections[0].body);
+
+ T(doc._attachments['foo.txt'].follows = true);
+ T(doc._attachments['bar.txt'].follows = true);
+
+ T(sections[1].body == "this is 21 chars long");
+ T(sections[2].body == "this is 18 chars l");
+
+};
diff --git a/src/couchdb/couch_doc.erl b/src/couchdb/couch_doc.erl
index 4cb20d6c..079d9dfa 100644
--- a/src/couchdb/couch_doc.erl
+++ b/src/couchdb/couch_doc.erl
@@ -340,14 +340,14 @@ fold_streamed_data(RcvFun, LenLeft, Fun, Acc) when LenLeft > 0->
len_doc_to_multi_part_stream(Boundary,JsonBytes,Atts,AttsSinceRevPos) ->
2 + % "--"
size(Boundary) +
- 34 + % "\r\ncontent-type: application/json\r\n"
+ 36 + % "\r\ncontent-type: application/json\r\n\r\n"
iolist_size(JsonBytes) +
4 + % "\r\n--"
size(Boundary) +
+ lists:foldl(fun(#att{revpos=RevPos,len=Len}, AccAttsSize) ->
if RevPos > AttsSinceRevPos ->
AccAttsSize +
- 2 + % "\r\n"
+ 4 + % "\r\n\r\n"
Len +
4 + % "\r\n--"
size(Boundary);
@@ -358,17 +358,18 @@ len_doc_to_multi_part_stream(Boundary,JsonBytes,Atts,AttsSinceRevPos) ->
2. % "--"
doc_to_multi_part_stream(Boundary,JsonBytes,Atts,AttsSinceRevPos,WriteFun) ->
- WriteFun([<<"--", Boundary, "\r\ncontent-type: application/json\r\n">>,
- JsonBytes, <<"\r\n--", Boundary>>]),
+ WriteFun([<<"--", Boundary/binary,
+ "\r\ncontent-type: application/json\r\n\r\n">>,
+ JsonBytes, <<"\r\n--", Boundary/binary>>]),
atts_to_mp(Atts, Boundary, WriteFun, AttsSinceRevPos).
atts_to_mp([], _Boundary, WriteFun, _AttsSinceRevPos) ->
WriteFun(<<"--">>);
atts_to_mp([#att{revpos=RevPos} = Att | RestAtts], Boundary, WriteFun,
AttsSinceRevPos) when RevPos > AttsSinceRevPos ->
- WriteFun(<<"\r\n">>),
+ WriteFun(<<"\r\n\r\n">>),
att_foldl(Att, fun(Data, ok) -> WriteFun(Data) end, ok),
- WriteFun(<<"\r\n--", Boundary>>),
+ WriteFun(<<"\r\n--", Boundary/binary>>),
atts_to_mp(RestAtts, Boundary, WriteFun, AttsSinceRevPos);
atts_to_mp([_ | RestAtts], Boundary, WriteFun, AttsSinceRevPos) ->
atts_to_mp(RestAtts, Boundary, WriteFun, AttsSinceRevPos).
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
index 7be10d42..4138db7d 100644
--- a/src/couchdb/couch_httpd_db.erl
+++ b/src/couchdb/couch_httpd_db.erl
@@ -784,7 +784,7 @@ db_doc_req(#httpd{method='PUT'}=Req, Db, DocId) ->
Loc = absolute_uri(Req, "/" ++ ?b2l(Db#db.name) ++ "/" ++ ?b2l(DocId)),
RespHeaders = [{"Location", Loc}],
- case couch_httpd:header_value(Req, "content-type") of
+ case couch_httpd:header_value(Req, "Content-Type") of
("multipart/related" ++ _Rest) = ContentType->
Doc0 = couch_doc:doc_from_multi_part_stream(ContentType,
fun() -> receive_request_data(Req) end),
@@ -869,18 +869,18 @@ send_doc_efficiently(Req, #doc{atts=Atts}=Doc, Headers, Options) ->
undefined -> [];
AcceptHeader -> string:tokens(AcceptHeader, ", ")
end,
- case lists:member(AcceptedTypes, "multipart/related") of
+ case lists:member("multipart/related", AcceptedTypes) of
false ->
send_json(Req, 200, [], couch_doc:to_json_obj(Doc, Options));
true ->
Boundary = couch_uuids:random(),
- JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, Options)),
+ JsonBytes = ?JSON_ENCODE(couch_doc:to_json_obj(Doc, [follows|Options])),
AttsSinceRevPos = proplists:get_value(atts_after_revpos, Options, 0),
Len = couch_doc:len_doc_to_multi_part_stream(Boundary,JsonBytes,Atts,
AttsSinceRevPos),
- CType = {<<"content-type">>,
- <<"multipart/related; boundary=", Boundary/binary>>},
- Resp = start_response_length(Req, 200, [CType | Headers], Len),
+ CType = {<<"Content-Type">>,
+ <<"multipart/related; boundary=\"", Boundary/binary, "\"">>},
+ {ok, Resp} = start_response_length(Req, 200, [CType|Headers], Len),
couch_doc:doc_to_multi_part_stream(Boundary,JsonBytes,Atts,
AttsSinceRevPos,
fun(Data) -> couch_httpd:send(Resp, Data) end)