| // 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 |
| |
| 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({ |
| "body":"This is a body.", |
| "_attachments":{ |
| "foo.txt": { |
| "follows":true, |
| "content_type":"application/test", |
| "length":21 |
| }, |
| "bar.txt": { |
| "follows":true, |
| "content_type":"application/test", |
| "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--epilogue" |
| }); |
| |
| 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", {att_encoding_info: true}); |
| var firstrev = doc._rev; |
| |
| T(doc._attachments["foo.txt"].stub == true); |
| T(doc._attachments["bar.txt"].stub == true); |
| T(doc._attachments["baz.txt"].stub == true); |
| TEquals("undefined", typeof doc._attachments["foo.txt"].encoding); |
| TEquals("undefined", typeof doc._attachments["bar.txt"].encoding); |
| TEquals("gzip", doc._attachments["baz.txt"].encoding); |
| |
| //lets change attachment bar |
| delete doc._attachments["bar.txt"].stub; // remove stub member (or could set to false) |
| delete doc._attachments["bar.txt"].digest; // remove the digest (it's for the gzip form) |
| 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--" |
| }); |
| TEquals(201, xhr.status); |
| |
| 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); |
| |
| // now test receiving multipart docs |
| |
| function getBoundary(xhr) { |
| var ctype = CouchDB.xhrheader(xhr, "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); |
| } |
| } |
| } |
| return boundary; |
| } |
| |
| function parseMultipart(xhr) { |
| var boundary = getBoundary(xhr); |
| var mimetext = CouchDB.xhrbody(xhr); |
| // 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; |
| } |
| |
| |
| xhr = CouchDB.request("GET", "/test_suite_db/multipart?attachments=true", |
| {headers:{"accept": "multipart/related,*/*;"}}); |
| |
| T(xhr.status == 200); |
| |
| // parse out the multipart |
| var sections = parseMultipart(xhr); |
| TEquals("790", xhr.getResponseHeader("Content-Length"), |
| "Content-Length should be correct"); |
| T(sections.length == 3); |
| // The first section is the json doc. Check it's content-type. |
| // Each part carries their own meta data. |
| TEquals("application/json", sections[0].headers['Content-Type'], |
| "Content-Type should be application/json for section[0]"); |
| TEquals("application/test", sections[1].headers['Content-Type'], |
| "Content-Type should be application/test for section[1]"); |
| TEquals("application/test", sections[2].headers['Content-Type'], |
| "Content-Type should be application/test for section[2]"); |
| |
| TEquals("21", sections[1].headers['Content-Length'], |
| "Content-Length should be 21 section[1]"); |
| TEquals("18", sections[2].headers['Content-Length'], |
| "Content-Length should be 18 section[2]"); |
| |
| TEquals('attachment; filename="foo.txt"', sections[1].headers['Content-Disposition'], |
| "Content-Disposition should be foo.txt section[1]"); |
| TEquals('attachment; filename="bar.txt"', sections[2].headers['Content-Disposition'], |
| "Content-Disposition should be bar.txt section[2]"); |
| |
| 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"); |
| TEquals("this is 18 chars l", sections[2].body, "should be 18 chars long"); |
| |
| // now get attachments incrementally (only the attachments changes since |
| // a certain rev). |
| |
| xhr = CouchDB.request("GET", "/test_suite_db/multipart?atts_since=[\"" + firstrev + "\"]", |
| {headers:{"accept": "multipart/related, */*"}}); |
| |
| T(xhr.status == 200); |
| |
| var sections = parseMultipart(xhr); |
| |
| T(sections.length == 2); |
| |
| var doc = JSON.parse(sections[0].body); |
| |
| T(doc._attachments['foo.txt'].stub == true); |
| T(doc._attachments['bar.txt'].follows == true); |
| |
| TEquals("this is 18 chars l", sections[1].body, "should be 18 chars long 2"); |
| |
| // try the atts_since parameter together with the open_revs parameter |
| xhr = CouchDB.request( |
| "GET", |
| '/test_suite_db/multipart?open_revs=["' + |
| doc._rev + '"]&atts_since=["' + firstrev + '"]', |
| {headers: {"accept": "multipart/mixed"}} |
| ); |
| |
| T(xhr.status === 200); |
| |
| sections = parseMultipart(xhr); |
| // 1 section, with a multipart/related Content-Type |
| T(sections.length === 1); |
| T(sections[0].headers['Content-Type'].indexOf('multipart/related;') === 0); |
| |
| var innerSections = parseMultipart(sections[0]); |
| // 2 inner sections: a document body section plus an attachment data section |
| T(innerSections.length === 2); |
| T(innerSections[0].headers['Content-Type'] === 'application/json'); |
| |
| doc = JSON.parse(innerSections[0].body); |
| |
| T(doc._attachments['foo.txt'].stub === true); |
| T(doc._attachments['bar.txt'].follows === true); |
| |
| T(innerSections[1].body === "this is 18 chars l"); |
| |
| // try it with a rev that doesn't exist (should get all attachments) |
| |
| xhr = CouchDB.request("GET", "/test_suite_db/multipart?atts_since=[\"1-2897589\"]", |
| {headers:{"accept": "multipart/related,*/*;"}}); |
| |
| T(xhr.status == 200); |
| |
| var sections = parseMultipart(xhr); |
| |
| T(sections.length == 3); |
| |
| 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"); |
| TEquals("this is 18 chars l", sections[2].body, "should be 18 chars long 3"); |
| // try it with a rev that doesn't exist, and one that does |
| |
| xhr = CouchDB.request("GET", "/test_suite_db/multipart?atts_since=[\"1-2897589\",\"" + firstrev + "\"]", |
| {headers:{"accept": "multipart/related,*/*;"}}); |
| |
| T(xhr.status == 200); |
| |
| var sections = parseMultipart(xhr); |
| |
| T(sections.length == 2); |
| |
| var doc = JSON.parse(sections[0].body); |
| |
| T(doc._attachments['foo.txt'].stub == true); |
| T(doc._attachments['bar.txt'].follows == true); |
| |
| TEquals("this is 18 chars l", sections[1].body, "should be 18 chars long 4"); |
| |
| // check that with the document multipart/mixed API it's possible to receive |
| // attachments in compressed form (if they're stored in compressed form) |
| |
| var server_config = [ |
| { |
| section: "attachments", |
| key: "compression_level", |
| value: "8" |
| }, |
| { |
| section: "attachments", |
| key: "compressible_types", |
| value: "text/plain" |
| } |
| ]; |
| |
| function testMultipartAttCompression() { |
| var doc = { _id: "foobar" }; |
| var lorem = |
| CouchDB.request("GET", "/_utils/script/test/lorem.txt").responseText; |
| var helloData = "hello world"; |
| |
| TEquals(true, db.save(doc).ok); |
| |
| var firstRev = doc._rev; |
| var xhr = CouchDB.request( |
| "PUT", |
| "/" + db.name + "/" + doc._id + "/data.bin?rev=" + firstRev, |
| { |
| body: helloData, |
| headers: {"Content-Type": "application/binary"} |
| } |
| ); |
| TEquals(201, xhr.status); |
| |
| var secondRev = db.open(doc._id)._rev; |
| xhr = CouchDB.request( |
| "PUT", |
| "/" + db.name + "/" + doc._id + "/lorem.txt?rev=" + secondRev, |
| { |
| body: lorem, |
| headers: {"Content-Type": "text/plain"} |
| } |
| ); |
| TEquals(201, xhr.status); |
| |
| var thirdRev = db.open(doc._id)._rev; |
| |
| xhr = CouchDB.request( |
| "GET", |
| '/' + db.name + '/' + doc._id + '?open_revs=["' + thirdRev + '"]', |
| { |
| headers: { |
| "Accept": "multipart/mixed", |
| "X-CouchDB-Send-Encoded-Atts": "true" |
| } |
| } |
| ); |
| TEquals(200, xhr.status); |
| |
| var sections = parseMultipart(xhr); |
| // 1 section, with a multipart/related Content-Type |
| TEquals(1, sections.length); |
| TEquals(0, |
| sections[0].headers['Content-Type'].indexOf('multipart/related;')); |
| |
| var innerSections = parseMultipart(sections[0]); |
| // 3 inner sections: a document body section plus 2 attachment data sections |
| TEquals(3, innerSections.length); |
| TEquals('application/json', innerSections[0].headers['Content-Type']); |
| |
| doc = JSON.parse(innerSections[0].body); |
| |
| TEquals(true, doc._attachments['lorem.txt'].follows); |
| TEquals("gzip", doc._attachments['lorem.txt'].encoding); |
| TEquals(true, doc._attachments['data.bin'].follows); |
| T(doc._attachments['data.bin'] !== "gzip"); |
| |
| if (innerSections[1].body === helloData) { |
| T(innerSections[2].body !== lorem); |
| } else if (innerSections[2].body === helloData) { |
| T(innerSections[1].body !== lorem); |
| } else { |
| T(false, "Could not found data.bin attachment data"); |
| } |
| |
| // now test that it works together with the atts_since parameter |
| |
| xhr = CouchDB.request( |
| "GET", |
| '/' + db.name + '/' + doc._id + '?open_revs=["' + thirdRev + '"]' + |
| '&atts_since=["' + secondRev + '"]', |
| { |
| headers: { |
| "Accept": "multipart/mixed", |
| "X-CouchDB-Send-Encoded-Atts": "true" |
| } |
| } |
| ); |
| TEquals(200, xhr.status); |
| |
| sections = parseMultipart(xhr); |
| // 1 section, with a multipart/related Content-Type |
| TEquals(1, sections.length); |
| TEquals(0, |
| sections[0].headers['Content-Type'].indexOf('multipart/related;')); |
| |
| innerSections = parseMultipart(sections[0]); |
| // 2 inner sections: a document body section plus 1 attachment data section |
| TEquals(2, innerSections.length); |
| TEquals('application/json', innerSections[0].headers['Content-Type']); |
| |
| doc = JSON.parse(innerSections[0].body); |
| |
| TEquals(true, doc._attachments['lorem.txt'].follows); |
| TEquals("gzip", doc._attachments['lorem.txt'].encoding); |
| TEquals("undefined", typeof doc._attachments['data.bin'].follows); |
| TEquals(true, doc._attachments['data.bin'].stub); |
| T(innerSections[1].body !== lorem); |
| } |
| |
| run_on_modified_server(server_config, testMultipartAttCompression); |
| |
| // cleanup |
| db.deleteDb(); |
| }; |