diff --git a/package.json b/package.json index d0ec757..72dbf25 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "debug": "^3.1.0", "file-type": "^8.0.0", "filesize": "^3.6.1", + "get-stream": "^3.0.0", "ipfs-unixfs": "^0.1.14", "mime-types": "^2.1.18", "multihashes": "^0.4.13", diff --git a/src/index.js b/src/index.js index e792a87..92ac407 100644 --- a/src/index.js +++ b/src/index.js @@ -2,13 +2,12 @@ 'use strict' -const fileType = require('file-type') -const mimeTypes = require('mime-types') const stream = require('stream') const toBlob = require('stream-to-blob') const resolver = require('./resolver') const pathUtils = require('./utils/path') +const detectContentType = require('./utils/content-type') const header = (status = 200, statusText = 'OK', headers = {}) => ({ status, @@ -73,21 +72,20 @@ const response = (ipfsNode, ipfsPath) => { }) // return only after first chunk being checked - let filetypeChecked = false + let contentTypeDetected = false readableStream.on('data', (chunk) => { // check mime on first chunk - if (filetypeChecked) { + if (contentTypeDetected) { return } - filetypeChecked = true + contentTypeDetected = true // return Response with mime type - const fileSignature = fileType(chunk) - const mimeType = mimeTypes.lookup(fileSignature ? fileSignature.ext : null) + const contentType = detectContentType(ipfsPath, chunk) if (typeof Blob === 'undefined') { - if (mimeType) { - resolve(new Response(responseStream, header(200, 'OK', { 'Content-Type': mimeTypes.contentType(mimeType) }))) + if (contentType) { + resolve(new Response(responseStream, header(200, 'OK', { 'Content-Type': contentType }))) } else { resolve(new Response(responseStream, header())) } @@ -97,8 +95,8 @@ const response = (ipfsNode, ipfsPath) => { resolve(new Response(err.toString(), header(500, 'Error fetching the file'))) } - if (mimeType) { - resolve(new Response(blob, header(200, 'OK', { 'Content-Type': mimeTypes.contentType(mimeType) }))) + if (contentType) { + resolve(new Response(blob, header(200, 'OK', { 'Content-Type': contentType }))) } else { resolve(new Response(blob, header())) } diff --git a/src/utils/content-type.js b/src/utils/content-type.js new file mode 100644 index 0000000..f4074f3 --- /dev/null +++ b/src/utils/content-type.js @@ -0,0 +1,21 @@ +'use strict' + +const fileType = require('file-type') +const mime = require('mime-types') + +const detectContentType = (path, chunk) => { + let fileSignature + + // try to guess the filetype based on the first bytes + // note that `file-type` doesn't support svgs, therefore we assume it's a svg if path looks like it + if (!path.endsWith('.svg')) { + fileSignature = fileType(chunk) + } + + // if we were unable to, fallback to the `path` which might contain the extension + const mimeType = mime.lookup(fileSignature ? fileSignature.ext : path) + + return mime.contentType(mimeType) +} + +module.exports = detectContentType diff --git a/test/fixtures/test-mime-types/cat.jpg b/test/fixtures/test-mime-types/cat.jpg new file mode 100644 index 0000000..8d8cd22 Binary files /dev/null and b/test/fixtures/test-mime-types/cat.jpg differ diff --git a/test/fixtures/test-mime-types/hexagons-xml.svg b/test/fixtures/test-mime-types/hexagons-xml.svg new file mode 100644 index 0000000..fe6b79d --- /dev/null +++ b/test/fixtures/test-mime-types/hexagons-xml.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/test-mime-types/hexagons.svg b/test/fixtures/test-mime-types/hexagons.svg new file mode 100644 index 0000000..557d927 --- /dev/null +++ b/test/fixtures/test-mime-types/hexagons.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/test-mime-types/index.html b/test/fixtures/test-mime-types/index.html new file mode 100644 index 0000000..b7350c2 --- /dev/null +++ b/test/fixtures/test-mime-types/index.html @@ -0,0 +1,5 @@ + + + Website + + diff --git a/test/fixtures/test-mime-types/pp.txt b/test/fixtures/test-mime-types/pp.txt new file mode 100644 index 0000000..f192290 --- /dev/null +++ b/test/fixtures/test-mime-types/pp.txt @@ -0,0 +1,120 @@ +PRIDE AND PREJUDICE + +By Jane Austen + + + +Chapter 1 + + +It is a truth universally acknowledged, that a single man in possession +of a good fortune, must be in want of a wife. + +However little known the feelings or views of such a man may be on his +first entering a neighbourhood, this truth is so well fixed in the minds +of the surrounding families, that he is considered the rightful property +of some one or other of their daughters. + +"My dear Mr. Bennet," said his lady to him one day, "have you heard that +Netherfield Park is let at last?" + +Mr. Bennet replied that he had not. + +"But it is," returned she; "for Mrs. Long has just been here, and she +told me all about it." + +Mr. Bennet made no answer. + +"Do you not want to know who has taken it?" cried his wife impatiently. + +"_You_ want to tell me, and I have no objection to hearing it." + +This was invitation enough. + +"Why, my dear, you must know, Mrs. Long says that Netherfield is taken +by a young man of large fortune from the north of England; that he came +down on Monday in a chaise and four to see the place, and was so much +delighted with it, that he agreed with Mr. Morris immediately; that he +is to take possession before Michaelmas, and some of his servants are to +be in the house by the end of next week." + +"What is his name?" + +"Bingley." + +"Is he married or single?" + +"Oh! Single, my dear, to be sure! A single man of large fortune; four or +five thousand a year. What a fine thing for our girls!" + +"How so? How can it affect them?" + +"My dear Mr. Bennet," replied his wife, "how can you be so tiresome! You +must know that I am thinking of his marrying one of them." + +"Is that his design in settling here?" + +"Design! Nonsense, how can you talk so! But it is very likely that he +_may_ fall in love with one of them, and therefore you must visit him as +soon as he comes." + +"I see no occasion for that. You and the girls may go, or you may send +them by themselves, which perhaps will be still better, for as you are +as handsome as any of them, Mr. Bingley may like you the best of the +party." + +"My dear, you flatter me. I certainly _have_ had my share of beauty, but +I do not pretend to be anything extraordinary now. When a woman has five +grown-up daughters, she ought to give over thinking of her own beauty." + +"In such cases, a woman has not often much beauty to think of." + +"But, my dear, you must indeed go and see Mr. Bingley when he comes into +the neighbourhood." + +"It is more than I engage for, I assure you." + +"But consider your daughters. Only think what an establishment it would +be for one of them. Sir William and Lady Lucas are determined to +go, merely on that account, for in general, you know, they visit no +newcomers. Indeed you must go, for it will be impossible for _us_ to +visit him if you do not." + +"You are over-scrupulous, surely. I dare say Mr. Bingley will be very +glad to see you; and I will send a few lines by you to assure him of my +hearty consent to his marrying whichever he chooses of the girls; though +I must throw in a good word for my little Lizzy." + +"I desire you will do no such thing. Lizzy is not a bit better than the +others; and I am sure she is not half so handsome as Jane, nor half so +good-humoured as Lydia. But you are always giving _her_ the preference." + +"They have none of them much to recommend them," replied he; "they are +all silly and ignorant like other girls; but Lizzy has something more of +quickness than her sisters." + +"Mr. Bennet, how _can_ you abuse your own children in such a way? You +take delight in vexing me. You have no compassion for my poor nerves." + +"You mistake me, my dear. I have a high respect for your nerves. They +are my old friends. I have heard you mention them with consideration +these last twenty years at least." + +"Ah, you do not know what I suffer." + +"But I hope you will get over it, and live to see many young men of four +thousand a year come into the neighbourhood." + +"It will be no use to us, if twenty such should come, since you will not +visit them." + +"Depend upon it, my dear, that when there are twenty, I will visit them +all." + +Mr. Bennet was so odd a mixture of quick parts, sarcastic humour, +reserve, and caprice, that the experience of three-and-twenty years had +been insufficient to make his wife understand his character. _Her_ mind +was less difficult to develop. She was a woman of mean understanding, +little information, and uncertain temper. When she was discontented, +she fancied herself nervous. The business of her life was to get her +daughters married; its solace was visiting and news. diff --git a/test/index.spec.js b/test/index.spec.js index 0fa0faa..d84e1ab 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -9,6 +9,7 @@ chai.use(dirtyChai) const loadFixture = require('aegir/fixtures') const ipfs = require('ipfs') const DaemonFactory = require('ipfsd-ctl') +const getStream = require('get-stream') const { getResponse } = require('../src') const makeWebResponseEnv = require('./utils/web-response-env') @@ -49,9 +50,12 @@ describe('resolve file', function () { const res = await getResponse(ipfs, `/ipfs/${file.cid}`) expect(res).to.exist() - expect(res).to.deep.include({ - status: 200 - }) + expect(res.status).to.equal(200) + + const contents = await getStream(res.body) + const expectedContents = loadFixture('test/fixtures/testfile.txt').toString() + + expect(contents).to.equal(expectedContents) }) }) @@ -100,7 +104,17 @@ describe('resolve directory', function () { it('should return the list of files of a directory', async () => { const res = await getResponse(ipfs, `/ipfs/${directory.cid}`, directory.cid) - expect(res).to.exist() + expect(res.status).to.equal(200) + expect(res.body).to.match(//) + }) + + it('should return the pp.txt file', async () => { + const res = await getResponse(ipfs, `/ipfs/${directory.cid}/pp.txt`, directory.cid) + + const contents = await getStream(res.body) + const expectedContents = loadFixture('test/fixtures/test-folder/pp.txt').toString() + + expect(contents).to.equal(expectedContents) }) }) @@ -151,7 +165,86 @@ describe('resolve web page', function () { it('should return the entry point of a web page when a trying to fetch a directory containing a web page', async () => { const res = await getResponse(ipfs, `/ipfs/${webpage.cid}`, webpage.cid) - expect(res).to.exist() - expect(res).to.equal(`/ipfs/${webpage.cid}/index.html`) + expect(res.status).to.equal(302) + expect(res.headers.get('Location')).to.equal(`/ipfs/${webpage.cid}/index.html`) + }) +}) + +describe('mime-types', () => { + let ipfs = null + let ipfsd = null + + const webpage = { + cid: 'QmWU1PAWCyd3MBAnALacXXbGU44RpgwdnGPrShSZoQj1H6', + files: { + 'cat.jpg': loadFixture('test/fixtures/test-mime-types/cat.jpg'), + 'hexagons-xml.svg': loadFixture('test/fixtures/test-mime-types/hexagons-xml.svg'), + 'hexagons.svg': loadFixture('test/fixtures/test-mime-types/hexagons.svg'), + 'pp.txt': loadFixture('test/fixtures/test-mime-types/pp.txt'), + 'index.html': loadFixture('test/fixtures/test-mime-types/index.html') + } + } + + before(function (done) { + this.timeout(20 * 1000) + Object.assign(global, makeWebResponseEnv()) + + df.spawn({ initOptions: { bits: 512 } }, (err, _ipfsd) => { + expect(err).to.not.exist() + ipfsd = _ipfsd + ipfs = ipfsd.api + + const content = (name) => ({ + path: `test-mime-types/${name}`, + content: webpage.files[name] + }) + + const dirs = [ + content('cat.jpg'), + content('hexagons-xml.svg'), + content('hexagons.svg'), + content('pp.txt'), + content('index.html') + ] + + ipfs.files.add(dirs, (err, res) => { + expect(err).to.not.exist() + const root = res[res.length - 1] + + expect(root.path).to.equal('test-mime-types') + expect(root.hash).to.equal(webpage.cid) + done() + }) + }) + }) + + it('should return the correct mime-type for pp.txt', async () => { + const res = await getResponse(ipfs, `/ipfs/${webpage.cid}/pp.txt`, webpage.cid) + + expect(res.headers.get('Content-Type')).to.equal('text/plain; charset=utf-8') + }) + + it('should return the correct mime-type for cat.jpg', async () => { + const res = await getResponse(ipfs, `/ipfs/${webpage.cid}/cat.jpg`, webpage.cid) + + expect(res.headers.get('Content-Type')).to.equal('image/jpeg') + }) + + it('should return the correct mime-type for index.html', async () => { + const res = await getResponse(ipfs, `/ipfs/${webpage.cid}/index.html`, webpage.cid) + + expect(res.headers.get('Content-Type')).to.equal('text/html; charset=utf-8') + }) + + it('should return the correct mime-type for hexagons.svg', async () => { + const res = await getResponse(ipfs, `/ipfs/${webpage.cid}/hexagons.svg`, webpage.cid) + + expect(res.headers.get('Content-Type')).to.equal('image/svg+xml') + }) + + it('should return the correct mime-type for hexagons.svg', async () => { + const res = await getResponse(ipfs, `/ipfs/${webpage.cid}/hexagons.svg`, webpage.cid) + + expect(res.headers.get('Content-Type')).to.equal('image/svg+xml') }) }) diff --git a/test/utils/web-response-env.js b/test/utils/web-response-env.js index 2a50ede..cde7bca 100644 --- a/test/utils/web-response-env.js +++ b/test/utils/web-response-env.js @@ -6,25 +6,20 @@ class Response { this.status = (init && typeof init.status === 'number') ? init.status : 200 this.ok = this.status >= 200 && this.status < 300 this.statusText = (init && init.statusText) || 'OK' - this.headers = (init && init.headers) + this.headers = new Map(init && init.headers ? Object.entries(init.headers) : []) this.type = this.status === 0 ? 'opaque' : 'basic' - this.redirected = false this.url = (init && init.url) || 'http://example.com/asset' } - - redirect (url) { - return { - status: 200, - url: url - } - } } class WebResponseGlobalScope { constructor () { this.Response = Response - this.Response.redirect = (url) => url + this.Response.redirect = (url) => new Response(null, { + status: 302, + headers: { Location: url } + }) } }