diff --git a/package.json b/package.json index 8ca9d00..cedb820 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,12 @@ }, "dependencies": { "@hapi/content": "^4.1.0", - "it-multipart": "~0.0.2" + "it-multipart": "^1.0.1" }, "devDependencies": { "aegir": "^20.0.0", "chai": "^4.2.0", - "ipfs-http-client": "^35.1.0", + "ipfs-http-client": "ipfs/js-ipfs-http-client#support-unixfs-metadata", "request": "^2.88.0" }, "engines": { diff --git a/src/parser.js b/src/parser.js index 86ea02b..7b23d4b 100644 --- a/src/parser.js +++ b/src/parser.js @@ -12,22 +12,16 @@ const isDirectory = (mediatype) => mediatype === multipartFormdataType || mediat const parseDisposition = (disposition) => { const details = {} details.type = disposition.split(';')[0] - if (details.type === 'file' || details.type === 'form-data') { - const namePattern = / filename="(.[^"]+)"/ - const matches = disposition.match(namePattern) - details.name = matches ? matches[1] : '' - } - - return details -} -const parseHeader = (header) => { - const type = Content.type(header['content-type']) - const disposition = parseDisposition(header['content-disposition']) + if (details.type === 'file' || details.type === 'form-data') { + const filenamePattern = / filename="(.[^"]+)"/ + const filenameMatches = disposition.match(filenamePattern) + details.filename = filenameMatches ? filenameMatches[1] : '' - const details = type - details.name = decodeURIComponent(disposition.name) - details.type = disposition.type + const namePattern = / name="(.[^"]+)"/ + const nameMatches = disposition.match(namePattern) + details.name = nameMatches ? nameMatches[1] : '' + } return details } @@ -50,49 +44,86 @@ const ignore = async (stream) => { } } -async function * parser (stream, options) { - for await (const part of multipart(stream, options.boundary)) { - const partHeader = parseHeader(part.headers) +async function * parseEntry (stream, options) { + for await (const part of stream) { + if (!part.headers['content-type']) { + throw new Error('No content-type in multipart part') + } - if (isDirectory(partHeader.mime)) { - yield { - type: 'directory', - name: partHeader.name - } + const type = Content.type(part.headers['content-type']) - await ignore(part.body) + if (type.boundary) { + // recursively parse nested multiparts + yield * parser(part.body, { + ...options, + boundary: type.boundary + }) continue } - if (partHeader.mime === applicationSymlink) { - const target = await collect(part.body) + if (!part.headers['content-disposition']) { + throw new Error('No content disposition in multipart part') + } + + const entry = {} + + if (part.headers.mtime) { + entry.mtime = parseInt(part.headers.mtime, 10) + } + + if (part.headers.mode) { + entry.mode = parseInt(part.headers.mode, 8) + } + if (isDirectory(type.mime)) { + entry.type = 'directory' + } else if (type.mime === applicationSymlink) { + entry.type = 'symlink' + } else { + entry.type = 'file' + } + + const disposition = parseDisposition(part.headers['content-disposition']) + + entry.name = decodeURIComponent(disposition.filename) + entry.body = part.body + + yield entry + } +} + +async function * parser (stream, options) { + for await (const entry of parseEntry(multipart(stream, options.boundary), options)) { + if (entry.type === 'directory') { yield { - type: 'symlink', - name: partHeader.name, - target: target.toString('utf8') + type: 'directory', + name: entry.name, + mtime: entry.mtime, + mode: entry.mode } - continue + await ignore(entry.body) } - if (partHeader.boundary) { - // recursively parse nested multiparts - for await (const entry of parser(part, { - ...options, - boundary: partHeader.boundary - })) { - yield entry + if (entry.type === 'symlink') { + yield { + type: 'symlink', + name: entry.name, + target: (await collect(entry.body)).toString('utf8'), + mtime: entry.mtime, + mode: entry.mode } - - continue } - yield { - type: 'file', - name: partHeader.name, - content: part.body + if (entry.type === 'file') { + yield { + type: 'file', + name: entry.name, + content: entry.body, + mtime: entry.mtime, + mode: entry.mode + } } } } diff --git a/test/parser.spec.js b/test/parser.spec.js index 2214306..1a79072 100644 --- a/test/parser.spec.js +++ b/test/parser.spec.js @@ -14,7 +14,7 @@ const os = require('os') const isWindows = os.platform() === 'win32' -const readDir = (path, prefix, output = []) => { +const readDir = (path, prefix, includeMetadata, output = []) => { const entries = fs.readdirSync(path) entries.forEach(entry => { @@ -23,21 +23,25 @@ const readDir = (path, prefix, output = []) => { const type = fs.statSync(entryPath) if (type.isDirectory()) { - readDir(entryPath, `${prefix}/${entry}`, output) + readDir(entryPath, `${prefix}/${entry}`, includeMetadata, output) + + output.push({ + path: `${prefix}/${entry}`, + mtime: includeMetadata ? parseInt(type.mtimeMs / 1000) : undefined, + mode: includeMetadata ? type.mode : undefined + }) } if (type.isFile()) { output.push({ path: `${prefix}/${entry}`, - content: fs.createReadStream(entryPath) + content: fs.createReadStream(entryPath), + mtime: includeMetadata ? parseInt(type.mtimeMs / 1000) : undefined, + mode: includeMetadata ? type.mode : undefined }) } }) - output.push({ - path: prefix - }) - return output } @@ -75,6 +79,8 @@ describe('parser', () => { describe('single file', () => { const filePath = path.resolve(__dirname, 'fixtures/config') const fileContent = fs.readFileSync(filePath, 'utf8') + const fileMtime = parseInt(Date.now() / 1000) + const fileMode = parseInt('0777', 8) before(() => { handler = async (req) => { @@ -84,7 +90,7 @@ describe('parser', () => { for await (const entry of parser(req)) { if (entry.type === 'file') { - const file = { name: entry.name, content: '' } + const file = { ...entry, content: '' } for await (const data of entry.content) { file.content += data.toString() @@ -95,13 +101,12 @@ describe('parser', () => { } expect(files.length).to.equal(1) - expect(files[0].name).to.equal('config') - expect(files[0].content).to.equal(fileContent) + expect(JSON.parse(files[0].content)).to.deep.equal(JSON.parse(fileContent)) } }) it('parses ctl.config.replace correctly', async () => { - await ctl.config.replace(filePath) + await ctl.config.replace(JSON.parse(fileContent)) }) it('parses regular multipart requests correctly', (done) => { @@ -111,6 +116,22 @@ describe('parser', () => { request.post({ url: `http://localhost:${PORT}`, formData: formData }, (err) => done(err)) }) + + it('parses multipart requests with metatdata correctly', (done) => { + const formData = { + file: { + value: fileContent, + options: { + header: { + mtime: fileMtime, + mode: fileMode + } + } + } + } + + request.post({ url: `http://localhost:${PORT}`, formData }, (err) => done(err)) + }) }) describe('directory', () => { @@ -123,15 +144,15 @@ describe('parser', () => { expect(req.headers['content-type']).to.be.a('string') for await (const entry of parser(req)) { - if (entry.type === 'file') { - const file = { name: entry.name, content: '' } + const file = { ...entry, content: '' } + if (entry.content) { for await (const data of entry.content) { file.content += data.toString() } - - files.push(file) } + + files.push(file) } } }) @@ -149,12 +170,31 @@ describe('parser', () => { return } - expect(files.length).to.equal(5) - expect(files[0].name).to.equal('fixtures/config') - expect(files[1].name).to.equal('fixtures/folderlink/deepfile') - expect(files[2].name).to.equal('fixtures/link') - expect(files[3].name).to.equal('fixtures/otherfile') - expect(files[4].name).to.equal('fixtures/subfolder/deepfile') + expect(files).to.have.lengthOf(contents.length) + + for (let i = 0; i < contents.length; i++) { + expect(files[i].name).to.equal(contents[i].path) + expect(files[i].mode).to.be.undefined + expect(files[i].mtime).to.be.undefined + } + }) + + it('parses ctl.add with metadata correctly', async () => { + const contents = readDir(dirPath, 'fixtures', true) + + await ctl.add(contents, { recursive: true, followSymlinks: false }) + + if (isWindows) { + return + } + + expect(files).to.have.lengthOf(contents.length) + + for (let i = 0; i < contents.length; i++) { + expect(files[i].name).to.equal(contents[i].path) + expect(files[i].mode).to.equal(contents[i].mode) + expect(files[i].mtime).to.equal(contents[i].mtime) + } }) })