Skip to content
This repository was archived by the owner on Apr 29, 2020. It is now read-only.

Commit 7b5c85d

Browse files
committed
feat: support UnixFSv1.5 metadata
1 parent 34007f4 commit 7b5c85d

File tree

3 files changed

+126
-65
lines changed

3 files changed

+126
-65
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
},
2828
"dependencies": {
2929
"@hapi/content": "^4.1.0",
30-
"it-multipart": "~0.0.2"
30+
"it-multipart": "^1.0.1"
3131
},
3232
"devDependencies": {
3333
"aegir": "^20.0.0",
3434
"chai": "^4.2.0",
35-
"ipfs-http-client": "^35.1.0",
35+
"ipfs-http-client": "ipfs/js-ipfs-htt-client#support-unixfs-metadata",
3636
"request": "^2.88.0"
3737
},
3838
"engines": {

src/parser.js

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,16 @@ const isDirectory = (mediatype) => mediatype === multipartFormdataType || mediat
1212
const parseDisposition = (disposition) => {
1313
const details = {}
1414
details.type = disposition.split(';')[0]
15-
if (details.type === 'file' || details.type === 'form-data') {
16-
const namePattern = / filename="(.[^"]+)"/
17-
const matches = disposition.match(namePattern)
18-
details.name = matches ? matches[1] : ''
19-
}
20-
21-
return details
22-
}
2315

24-
const parseHeader = (header) => {
25-
const type = Content.type(header['content-type'])
26-
const disposition = parseDisposition(header['content-disposition'])
16+
if (details.type === 'file' || details.type === 'form-data') {
17+
const filenamePattern = / filename="(.[^"]+)"/
18+
const filenameMatches = disposition.match(filenamePattern)
19+
details.filename = filenameMatches ? filenameMatches[1] : ''
2720

28-
const details = type
29-
details.name = decodeURIComponent(disposition.name)
30-
details.type = disposition.type
21+
const namePattern = / name="(.[^"]+)"/
22+
const nameMatches = disposition.match(namePattern)
23+
details.name = nameMatches ? nameMatches[1] : ''
24+
}
3125

3226
return details
3327
}
@@ -50,49 +44,90 @@ const ignore = async (stream) => {
5044
}
5145
}
5246

53-
async function * parser (stream, options) {
54-
for await (const part of multipart(stream, options.boundary)) {
55-
const partHeader = parseHeader(part.headers)
47+
async function * parseEntry (stream, options) {
48+
let entry = {}
5649

57-
if (isDirectory(partHeader.mime)) {
58-
yield {
59-
type: 'directory',
60-
name: partHeader.name
50+
for await (const part of stream) {
51+
let type
52+
53+
if (part.headers['content-type']) {
54+
type = Content.type(part.headers['content-type'])
55+
56+
if (type.boundary) {
57+
// recursively parse nested multiparts
58+
yield * parser(part.body, {
59+
...options,
60+
boundary: type.boundary
61+
})
62+
63+
continue
6164
}
65+
}
66+
67+
if (!part.headers['content-disposition']) {
68+
throw new Error('No content disposition in multipart part')
69+
}
6270

63-
await ignore(part.body)
71+
const disposition = parseDisposition(part.headers['content-disposition'])
6472

65-
continue
73+
if (disposition.name.includes('mtime')) {
74+
entry.mtime = parseInt((await collect(part.body)).toString('utf8'), 10)
6675
}
6776

68-
if (partHeader.mime === applicationSymlink) {
69-
const target = await collect(part.body)
77+
if (disposition.name.includes('mode')) {
78+
entry.mode = parseInt((await collect(part.body)).toString('utf8'), 10)
79+
}
7080

71-
yield {
72-
type: 'symlink',
73-
name: partHeader.name,
74-
target: target.toString('utf8')
81+
if (type) {
82+
if (isDirectory(type.mime)) {
83+
entry.type = 'directory'
84+
} else if (type.mime === applicationSymlink) {
85+
entry.type = 'symlink'
86+
} else {
87+
entry.type = 'file'
7588
}
7689

77-
continue
90+
entry.name = decodeURIComponent(disposition.filename)
91+
entry.body = part.body
92+
93+
yield entry
94+
95+
entry = {}
7896
}
97+
}
98+
}
7999

80-
if (partHeader.boundary) {
81-
// recursively parse nested multiparts
82-
for await (const entry of parser(part, {
83-
...options,
84-
boundary: partHeader.boundary
85-
})) {
86-
yield entry
100+
async function * parser (stream, options) {
101+
for await (const entry of parseEntry(multipart(stream, options.boundary), options)) {
102+
if (entry.type === 'directory') {
103+
yield {
104+
type: 'directory',
105+
name: entry.name,
106+
mtime: entry.mtime,
107+
mode: entry.mode
87108
}
88109

89-
continue
110+
await ignore(entry.body)
111+
}
112+
113+
if (entry.type === 'symlink') {
114+
yield {
115+
type: 'symlink',
116+
name: entry.name,
117+
target: (await collect(entry.body)).toString('utf8'),
118+
mtime: entry.mtime,
119+
mode: entry.mode
120+
}
90121
}
91122

92-
yield {
93-
type: 'file',
94-
name: partHeader.name,
95-
content: part.body
123+
if (entry.type === 'file') {
124+
yield {
125+
type: 'file',
126+
name: entry.name,
127+
content: entry.body,
128+
mtime: entry.mtime,
129+
mode: entry.mode
130+
}
96131
}
97132
}
98133
}

test/parser.spec.js

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const os = require('os')
1414

1515
const isWindows = os.platform() === 'win32'
1616

17-
const readDir = (path, prefix, output = []) => {
17+
const readDir = (path, prefix, includeMetadata, output = []) => {
1818
const entries = fs.readdirSync(path)
1919

2020
entries.forEach(entry => {
@@ -23,21 +23,25 @@ const readDir = (path, prefix, output = []) => {
2323
const type = fs.statSync(entryPath)
2424

2525
if (type.isDirectory()) {
26-
readDir(entryPath, `${prefix}/${entry}`, output)
26+
readDir(entryPath, `${prefix}/${entry}`, includeMetadata, output)
27+
28+
output.push({
29+
path: `${prefix}/${entry}`,
30+
mtime: includeMetadata ? parseInt(type.mtimeMs / 1000) : undefined,
31+
mode: includeMetadata ? type.mode : undefined
32+
})
2733
}
2834

2935
if (type.isFile()) {
3036
output.push({
3137
path: `${prefix}/${entry}`,
32-
content: fs.createReadStream(entryPath)
38+
content: fs.createReadStream(entryPath),
39+
mtime: includeMetadata ? parseInt(type.mtimeMs / 1000) : undefined,
40+
mode: includeMetadata ? type.mode : undefined
3341
})
3442
}
3543
})
3644

37-
output.push({
38-
path: prefix
39-
})
40-
4145
return output
4246
}
4347

@@ -75,6 +79,8 @@ describe('parser', () => {
7579
describe('single file', () => {
7680
const filePath = path.resolve(__dirname, 'fixtures/config')
7781
const fileContent = fs.readFileSync(filePath, 'utf8')
82+
const fileMtime = parseInt(Date.now() / 1000)
83+
const fileMode = parseInt('0777', 8)
7884

7985
before(() => {
8086
handler = async (req) => {
@@ -84,7 +90,7 @@ describe('parser', () => {
8490

8591
for await (const entry of parser(req)) {
8692
if (entry.type === 'file') {
87-
const file = { name: entry.name, content: '' }
93+
const file = { ...entry, content: '' }
8894

8995
for await (const data of entry.content) {
9096
file.content += data.toString()
@@ -95,17 +101,18 @@ describe('parser', () => {
95101
}
96102

97103
expect(files.length).to.equal(1)
98-
expect(files[0].name).to.equal('config')
99-
expect(files[0].content).to.equal(fileContent)
104+
expect(JSON.parse(files[0].content)).to.deep.equal(JSON.parse(fileContent))
100105
}
101106
})
102107

103108
it('parses ctl.config.replace correctly', async () => {
104-
await ctl.config.replace(filePath)
109+
await ctl.config.replace(JSON.parse(fileContent))
105110
})
106111

107112
it('parses regular multipart requests correctly', (done) => {
108113
const formData = {
114+
mtime: fileMtime,
115+
mode: fileMode,
109116
file: fs.createReadStream(filePath)
110117
}
111118

@@ -123,15 +130,15 @@ describe('parser', () => {
123130
expect(req.headers['content-type']).to.be.a('string')
124131

125132
for await (const entry of parser(req)) {
126-
if (entry.type === 'file') {
127-
const file = { name: entry.name, content: '' }
133+
const file = { ...entry, content: '' }
128134

135+
if (entry.content) {
129136
for await (const data of entry.content) {
130137
file.content += data.toString()
131138
}
132-
133-
files.push(file)
134139
}
140+
141+
files.push(file)
135142
}
136143
}
137144
})
@@ -149,12 +156,31 @@ describe('parser', () => {
149156
return
150157
}
151158

152-
expect(files.length).to.equal(5)
153-
expect(files[0].name).to.equal('fixtures/config')
154-
expect(files[1].name).to.equal('fixtures/folderlink/deepfile')
155-
expect(files[2].name).to.equal('fixtures/link')
156-
expect(files[3].name).to.equal('fixtures/otherfile')
157-
expect(files[4].name).to.equal('fixtures/subfolder/deepfile')
159+
expect(files).to.have.lengthOf(contents.length)
160+
161+
for (let i = 0; i < contents.length; i++) {
162+
expect(files[i].name).to.equal(contents[i].path)
163+
expect(files[i].mode).to.be.undefined
164+
expect(files[i].mtime).to.be.undefined
165+
}
166+
})
167+
168+
it('parses ctl.add with metadata correctly', async () => {
169+
const contents = readDir(dirPath, 'fixtures', true)
170+
171+
await ctl.add(contents, { recursive: true, followSymlinks: false })
172+
173+
if (isWindows) {
174+
return
175+
}
176+
177+
expect(files).to.have.lengthOf(contents.length)
178+
179+
for (let i = 0; i < contents.length; i++) {
180+
expect(files[i].name).to.equal(contents[i].path)
181+
expect(files[i].mode).to.equal(contents[i].mode)
182+
expect(files[i].mtime).to.equal(contents[i].mtime)
183+
}
158184
})
159185
})
160186

0 commit comments

Comments
 (0)