Skip to content

Commit 6b0bbc1

Browse files
authored
fix: send blobs when running ipfs-http-client in the browser (#3184)
To support streaming of native types with no buffering, normalise add input to blobs and upload using native FormData when the http client is run in the browser. That is, if the user passes a blob to the http client in the browser leave it alone as enumerating blob contents cause the file data to be read. Browser FormData objects do not allow you to specify headers for each multipart part which means we can't pass UnixFS metadata via the headers so we turn the metadata into a querystring and append it to the field name for each multipart part as a workaround. Fixes #3138 BREAKING CHANGES: - Removes the `mode`, `mtime` and `mtime-nsec` headers from multipart requests - Passes `mode`, `mtime` and `mtime-nsec` as querystring parameters appended to the field name of multipart requests
1 parent b44dd1a commit 6b0bbc1

File tree

6 files changed

+112
-38
lines changed

6 files changed

+112
-38
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@
1515
],
1616
"main": "src/index.js",
1717
"browser": {
18-
"./src/lib/to-stream.js": "./src/lib/to-stream.browser.js",
18+
"./src/lib/multipart-request.js": "./src/lib/multipart-request.browser.js",
1919
"ipfs-utils/src/files/glob-source": false,
20-
"go-ipfs": false
20+
"go-ipfs": false,
21+
"ipfs-core-utils/src/files/normalise-input": "ipfs-core-utils/src/files/normalise-input/index.browser.js"
2122
},
2223
"repository": {
2324
"type": "git",

src/lib/multipart-request.browser.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict'
2+
3+
const normaliseInput = require('ipfs-core-utils/src/files/normalise-input')
4+
const modeToString = require('./mode-to-string')
5+
const mtimeToObject = require('./mtime-to-object')
6+
const { File, FormData } = require('ipfs-utils/src/globalthis')
7+
8+
async function multipartRequest (source = '', abortController, headers = {}) {
9+
const formData = new FormData()
10+
let index = 0
11+
12+
for await (const { content, path, mode, mtime } of normaliseInput(source)) {
13+
let fileSuffix = ''
14+
const type = content ? 'file' : 'dir'
15+
16+
if (index > 0) {
17+
fileSuffix = `-${index}`
18+
}
19+
20+
let fieldName = type + fileSuffix
21+
const qs = []
22+
23+
if (mode !== null && mode !== undefined) {
24+
qs.push(`mode=${modeToString(mode)}`)
25+
}
26+
27+
if (mtime != null) {
28+
const {
29+
secs, nsecs
30+
} = mtimeToObject(mtime)
31+
32+
qs.push(`mtime=${secs}`)
33+
34+
if (nsecs != null) {
35+
qs.push(`mtime-nsecs=${nsecs}`)
36+
}
37+
}
38+
39+
if (qs.length) {
40+
fieldName = `${fieldName}?${qs.join('&')}`
41+
}
42+
43+
if (content) {
44+
formData.set(fieldName, content, encodeURIComponent(path))
45+
} else {
46+
formData.set(fieldName, new File([''], encodeURIComponent(path), { type: 'application/x-directory' }))
47+
}
48+
49+
index++
50+
}
51+
52+
return {
53+
headers,
54+
body: formData
55+
}
56+
}
57+
58+
module.exports = multipartRequest

src/lib/multipart-request.js

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
'use strict'
22

33
const normaliseInput = require('ipfs-core-utils/src/files/normalise-input')
4-
const toStream = require('./to-stream')
54
const { nanoid } = require('nanoid')
65
const modeToString = require('../lib/mode-to-string')
76
const mtimeToObject = require('../lib/mtime-to-object')
87
const merge = require('merge-options').bind({ ignoreUndefined: true })
8+
const toStream = require('it-to-stream')
99

1010
async function multipartRequest (source = '', abortController, headers = {}, boundary = `-----------------------------${nanoid()}`) {
1111
async function * streamFiles (source) {
@@ -22,26 +22,32 @@ async function multipartRequest (source = '', abortController, headers = {}, bou
2222
fileSuffix = `-${index}`
2323
}
2424

25-
yield `--${boundary}\r\n`
26-
yield `Content-Disposition: form-data; name="${type}${fileSuffix}"; filename="${encodeURIComponent(path)}"\r\n`
27-
yield `Content-Type: ${content ? 'application/octet-stream' : 'application/x-directory'}\r\n`
25+
let fieldName = type + fileSuffix
26+
const qs = []
2827

2928
if (mode !== null && mode !== undefined) {
30-
yield `mode: ${modeToString(mode)}\r\n`
29+
qs.push(`mode=${modeToString(mode)}`)
3130
}
3231

3332
if (mtime != null) {
3433
const {
3534
secs, nsecs
3635
} = mtimeToObject(mtime)
3736

38-
yield `mtime: ${secs}\r\n`
37+
qs.push(`mtime=${secs}`)
3938

4039
if (nsecs != null) {
41-
yield `mtime-nsecs: ${nsecs}\r\n`
40+
qs.push(`mtime-nsecs=${nsecs}`)
4241
}
4342
}
4443

44+
if (qs.length) {
45+
fieldName = `${fieldName}?${qs.join('&')}`
46+
}
47+
48+
yield `--${boundary}\r\n`
49+
yield `Content-Disposition: form-data; name="${fieldName}"; filename="${encodeURIComponent(path)}"\r\n`
50+
yield `Content-Type: ${content ? 'application/octet-stream' : 'application/x-directory'}\r\n`
4551
yield '\r\n'
4652

4753
if (content) {

src/lib/to-stream.browser.js

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/lib/to-stream.js

Lines changed: 0 additions & 7 deletions
This file was deleted.

test/files.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* eslint-env mocha */
2+
3+
'use strict'
4+
5+
const { Buffer } = require('buffer')
6+
const { expect } = require('interface-ipfs-core/src/utils/mocha')
7+
const f = require('./utils/factory')()
8+
9+
describe('.add', function () {
10+
this.timeout(20 * 1000)
11+
12+
let ipfs
13+
14+
before(async function () {
15+
ipfs = (await f.spawn()).api
16+
})
17+
18+
after(() => f.clean())
19+
20+
it('should ignore metadata until https://github.com/ipfs/go-ipfs/issues/6920 is implemented', async () => {
21+
const data = Buffer.from('some data')
22+
const result = await ipfs.add(data, {
23+
mode: 0o600,
24+
mtime: {
25+
secs: 1000,
26+
nsecs: 0
27+
}
28+
})
29+
30+
expect(result).to.not.have.property('mode')
31+
expect(result).to.not.have.property('mtime')
32+
expect(result).to.have.property('cid')
33+
34+
const { cid } = result
35+
expect(cid).to.have.property('codec', 'dag-pb')
36+
expect(cid.toString()).to.equal('QmVv4Wz46JaZJeH5PMV4LGbRiiMKEmszPYY3g6fjGnVXBS')
37+
})
38+
})

0 commit comments

Comments
 (0)