You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by gl...@apache.org on 2021/09/13 09:19:49 UTC
[couchdb-nano] 01/01: fix multipart/related requests when uploading
multiple attachments. Fixes issue#238
This is an automated email from the ASF dual-hosted git repository.
glynnbird pushed a commit to branch issue#238
in repository https://gitbox.apache.org/repos/asf/couchdb-nano.git
commit eb2c54dfbbdef288ebcafd2c3a2590678b151718
Author: Glynn Bird <gl...@apache.org>
AuthorDate: Mon Sep 13 10:18:24 2021 +0100
fix multipart/related requests when uploading multiple attachments. Fixes issue#238
---
lib/multipart.js | 53 ++++++++++++++++++++++++++++++++++++++++
lib/nano.js | 14 +++++++----
test/multipart.insert.test.js | 9 ++++---
test/multipart.test.js | 56 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 125 insertions(+), 7 deletions(-)
diff --git a/lib/multipart.js b/lib/multipart.js
new file mode 100644
index 0000000..cf7f946
--- /dev/null
+++ b/lib/multipart.js
@@ -0,0 +1,53 @@
+const CRLF = '\r\n'
+const DASHES = '--'
+
+// generate the payload, boundary and header for a multipart/related request
+// to upload binary attachments to CouchDB.
+// https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
+class MultiPartFactory {
+ // constructor
+ constructor (parts) {
+ // generate a unique id that forms the boundary between parts
+ this.boundary = this.uuid()
+ const bufferList = []
+
+ // for each part to be processed
+ for (const part of parts) {
+ // start with the boundary e.g. --0559337432997171\r\n
+ bufferList.push(Buffer.from(DASHES + this.boundary + CRLF))
+
+ // state the type and length of the following part
+ bufferList.push(Buffer.from(`content-type: ${part.content_type}${CRLF}`))
+ bufferList.push(Buffer.from(`content-length: ${part.data.length}${CRLF}`))
+
+ // two \r\n marks start of the part itself
+ bufferList.push(Buffer.from(CRLF))
+
+ // output the string/buffer
+ bufferList.push(typeof part.data === 'string' ? Buffer.from(part.data) : part.data)
+
+ // followed by /r/n
+ bufferList.push(Buffer.from(CRLF))
+ }
+
+ // right at the end we have an end marker e.g. --0559337432997171--\r\n
+ bufferList.push(Buffer.from(DASHES + this.boundary + DASHES + CRLF))
+
+ // buid up a single Buffer from the array of bits
+ this.data = Buffer.concat(bufferList)
+
+ // calculate the Content-Type header required to send with this request
+ this.header = `multipart/related; boundary=${this.boundary}`
+ }
+
+ uuid () {
+ const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
+ let retval = ''
+ for (let i = 0; i < 16; i++) {
+ retval += chars[Math.floor(Math.random() * chars.length)]
+ }
+ return retval
+ }
+}
+
+module.exports = MultiPartFactory
diff --git a/lib/nano.js b/lib/nano.js
index 1f58559..0b819ea 100644
--- a/lib/nano.js
+++ b/lib/nano.js
@@ -27,6 +27,7 @@ const SCRUBBED_STR = 'XXXXXX'
const defaultHttpAgent = new http.Agent(AGENT_DEFAULTS)
const defaultHttpsAgent = new https.Agent(AGENT_DEFAULTS)
const ChangesReader = require('./changesreader.js')
+const MultiPartFactory = require('./multipart.js')
function isEmpty (val) {
return val == null || !(Object.keys(val) || val).length
@@ -282,7 +283,11 @@ module.exports = exports = function dbScope (cfg) {
}
if (opts.multipart) {
- req.multipart = opts.multipart
+ // generate the multipart/related body, header and boundary to
+ // upload multiple binary attachments in one request
+ const mp = new MultiPartFactory(opts.multipart)
+ opts.contentType = mp.header
+ req.body = mp.data
}
req.headers = Object.assign(req.headers, opts.headers, cfg.defaultHeaders)
@@ -891,12 +896,13 @@ module.exports = exports = function dbScope (cfg) {
/* jscs:disable requireCamelCaseOrUpperCaseIdentifiers */
content_type: att.content_type
}
- multipart.push({ body: att.data })
+ multipart.push(att)
})
multipart.unshift({
- 'content-type': 'application/json',
- body: JSON.stringify(doc)
+ content_type: 'application/json',
+ data: JSON.stringify(doc),
+ name: 'document'
})
return relax({
diff --git a/test/multipart.insert.test.js b/test/multipart.insert.test.js
index 8c682f6..4f84462 100644
--- a/test/multipart.insert.test.js
+++ b/test/multipart.insert.test.js
@@ -53,7 +53,8 @@ afterEach(() => {
test('should be able to insert a document with attachments #1 - multipart PUT /db/id - db.multipart.insert', async () => {
// mocks
const response = { ok: true, id: '8s8g8h8h9', rev: '1-123' }
- const scope = nock(COUCH_URL, { reqheaders: { 'content-type': h => h.includes('multipart/related') } })
+ const scope = nock(COUCH_URL)
+ .matchHeader('content-type', h => h.includes('multipart/related'))
.put('/db/docid')
.reply(200, response)
@@ -66,7 +67,8 @@ test('should be able to insert a document with attachments #1 - multipart PUT /d
test('should be able to insert a document with attachments #2 - multipart PUT /db/id - db.multipart.insert', async () => {
const response = { ok: true, id: '8s8g8h8h9', rev: '1-123' }
- const scope = nock(COUCH_URL, { reqheaders: { 'content-type': h => h.includes('multipart/related') } })
+ const scope = nock(COUCH_URL)
+ .matchHeader('content-type', h => h.includes('multipart/related'))
.put('/db/docid')
.reply(200, response)
@@ -83,7 +85,8 @@ test('should be able to handle 404 - db.multipart.insert', async () => {
error: 'not_found',
reason: 'missing'
}
- const scope = nock(COUCH_URL, { reqheaders: { 'content-type': h => h.includes('multipart/related') } })
+ const scope = nock(COUCH_URL)
+ .matchHeader('content-type', h => h.includes('multipart/related'))
.put('/db/docid')
.reply(404, response)
diff --git a/test/multipart.test.js b/test/multipart.test.js
new file mode 100644
index 0000000..f1173f8
--- /dev/null
+++ b/test/multipart.test.js
@@ -0,0 +1,56 @@
+const MultiPartFactory = require('../lib/multipart.js')
+const textAttachment = { name: 'test.txt', data: 'Hello\r\nWorld!', content_type: 'text/plain' }
+const anotherTextAttachment = { name: 'test2.txt', data: 'the quick brown fox', content_type: 'text/plain' }
+
+test('should return different boundary each time', async () => {
+ const mf1 = new MultiPartFactory([])
+ const mf2 = new MultiPartFactory([])
+ const mf3 = new MultiPartFactory([])
+
+ expect(typeof mf1.boundary).toBe('string')
+ expect(typeof mf2.boundary).toBe('string')
+ expect(typeof mf3.boundary).toBe('string')
+ expect(mf1.boundary.length).toBe(16)
+ expect(mf2.boundary.length).toBe(16)
+ expect(mf3.boundary.length).toBe(16)
+ expect(mf1).not.toEqual(mf2)
+ expect(mf1).not.toEqual(mf3)
+ expect(mf2).not.toEqual(mf3)
+})
+
+test('should return boundary in header', async () => {
+ const mf1 = new MultiPartFactory([])
+ const boundary = mf1.boundary
+ const header = mf1.header
+ expect(header).toEqual(`multipart/related; boundary=${boundary}`)
+})
+
+test('should handle single attachments', async () => {
+ const mf1 = new MultiPartFactory([textAttachment])
+ expect(typeof mf1.data).toEqual('object')
+ expect(Buffer.isBuffer(mf1.data)).toEqual(true)
+ const lines = mf1.data.toString().split('\r\n')
+ expect(lines).toContain(`--${mf1.boundary}`)
+ expect(lines).toContain('content-type: text/plain')
+ expect(lines).toContain('content-length: 13')
+ expect(lines).toContain('')
+ expect(lines).toContain('Hello')
+ expect(lines).toContain('World!')
+ expect(lines).toContain(`--${mf1.boundary}--`)
+})
+
+test('should handle two attachments', async () => {
+ const mf1 = new MultiPartFactory([textAttachment, anotherTextAttachment])
+ expect(typeof mf1.data).toEqual('object')
+ expect(Buffer.isBuffer(mf1.data)).toEqual(true)
+ const lines = mf1.data.toString().split('\r\n')
+ expect(lines).toContain(`--${mf1.boundary}`)
+ expect(lines).toContain('content-type: text/plain')
+ expect(lines).toContain('content-length: 13')
+ expect(lines).toContain('')
+ expect(lines).toContain('Hello')
+ expect(lines).toContain('World!')
+ expect(lines).toContain('content-length: 19')
+ expect(lines).toContain('the quick brown fox')
+ expect(lines).toContain(`--${mf1.boundary}--`)
+})