diff --git a/src/http/gateway/dir-view/index.js b/src/http/gateway/dir-view/index.js index 83ca95f1a8..9b8c6cb423 100644 --- a/src/http/gateway/dir-view/index.js +++ b/src/http/gateway/dir-view/index.js @@ -6,7 +6,7 @@ const mainStyle = require('./style') const pathUtil = require('../utils/path') function getParentDirectoryURL (originalParts) { - const parts = originalParts.splice() + const parts = originalParts.slice() if (parts.length > 1) { parts.pop() diff --git a/src/http/gateway/resolver.js b/src/http/gateway/resolver.js index 11a0db4db5..45847182d8 100644 --- a/src/http/gateway/resolver.js +++ b/src/http/gateway/resolver.js @@ -2,7 +2,7 @@ const mh = require('multihashes') const promisify = require('promisify-es6') -const eachOfSeries = require('async/eachOfSeries') +const reduce = require('async/reduce') const CID = require('cids') const Unixfs = require('ipfs-unixfs') const debug = require('debug') @@ -22,11 +22,7 @@ function getIndexFiles (links) { return links.filter((link) => INDEX_HTML_FILES.indexOf(link.name) !== -1) } -function noop () {} - const resolveDirectory = promisify((ipfs, path, multihash, callback) => { - callback = callback || noop - mh.validate(mh.fromB58String(multihash)) ipfs.object.get(multihash, { enc: 'base58' }, (err, dagNode) => { @@ -43,43 +39,28 @@ const resolveDirectory = promisify((ipfs, path, multihash, callback) => { }) const resolveMultihash = promisify((ipfs, path, callback) => { - callback = callback || noop - const parts = pathUtil.splitPath(path) - const partsLength = parts.length - - let currentMultihash = parts[0] + let firstMultihash = parts.shift() let currentCid - eachOfSeries(parts, (multihash, currentIndex, next) => { - // throws error when invalid CID is passed + + reduce(parts, firstMultihash, (memo, item, next) => { try { - currentCid = new CID(mh.fromB58String(currentMultihash)) + currentCid = new CID(mh.fromB58String(memo)) } catch (err) { return next(err) } - log('currentMultihash: ', currentMultihash) - log('currentIndex: ', currentIndex, '/', partsLength) + log('memo: ', memo) + log('item: ', item) ipfs.dag.get(currentCid, (err, result) => { if (err) { return next(err) } - let dagNode = result.value - - if (currentIndex === partsLength - 1) { - let dagDataObj = Unixfs.unmarshal(dagNode.data) - if (dagDataObj.type === 'directory') { - let isDirErr = new Error('This dag node is a directory') - // add currentMultihash as a fileName so it can be used by resolveDirectory - isDirErr.fileName = currentMultihash - return next(isDirErr) - } - - return next() - } + let dagNode = result.value // find multihash of requested named-file in current dagNode's links let multihashOfNextFile - const nextFileName = parts[currentIndex + 1] + let nextFileName = item + const links = dagNode.links for (let link of links) { @@ -92,17 +73,34 @@ const resolveMultihash = promisify((ipfs, path, callback) => { } if (!multihashOfNextFile) { - log.error(`no link named "${nextFileName}" under ${currentMultihash}`) - return next(new Error(`no link named "${nextFileName}" under ${currentMultihash}`)) + return next(new Error(`no link named "${nextFileName}" under ${memo}`)) } - currentMultihash = multihashOfNextFile - next() + next(null, multihashOfNextFile) }) - }, (err) => { + }, (err, result) => { if (err) { return callback(err) } - callback(null, { multihash: currentMultihash }) + let cid + try { + cid = new CID(mh.fromB58String(result)) + } catch (err) { + return callback(err) + } + + ipfs.dag.get(cid, (err, dagResult) => { + if (err) return callback(err) + + let dagDataObj = Unixfs.unmarshal(dagResult.value.data) + if (dagDataObj.type === 'directory') { + let isDirErr = new Error('This dag node is a directory') + // add memo (last multihash) as a fileName so it can be used by resolveDirectory + isDirErr.fileName = result + return callback(isDirErr) + } + + callback(null, { multihash: result }) + }) }) }) diff --git a/src/http/gateway/resources/gateway.js b/src/http/gateway/resources/gateway.js index d2b8f8777c..cb93897bc8 100644 --- a/src/http/gateway/resources/gateway.js +++ b/src/http/gateway/resources/gateway.js @@ -9,7 +9,7 @@ const fileType = require('file-type') const mime = require('mime-types') const Stream = require('readable-stream') -const GatewayResolver = require('../resolver') +const gatewayResolver = require('../resolver') const PathUtils = require('../utils/path') module.exports = { @@ -29,113 +29,116 @@ module.exports = { const ref = request.pre.args.ref const ipfs = request.server.app.ipfs - return GatewayResolver - .resolveMultihash(ipfs, ref) - .then((data) => { - ipfs - .files - .cat(data.multihash) - .then((stream) => { - if (ref.endsWith('/')) { - // remove trailing slash for files - return reply - .redirect(PathUtils.removeTrailingSlash(ref)) - .permanent(true) - } else { - if (!stream._read) { - stream._read = () => {} - stream._readableState = {} - } - // response.continue() - let filetypeChecked = false - let stream2 = new Stream.PassThrough({highWaterMark: 1}) - let response = reply(stream2).hold() + function handleGatewayResolverError (err) { + if (err) { + log.error('err: ', err.toString(), ' fileName: ', err.fileName) - pull( - toPull.source(stream), - pull.drain((chunk) => { - // Check file type. do this once. - if (chunk.length > 0 && !filetypeChecked) { - log('got first chunk') - let fileSignature = fileType(chunk) - log('file type: ', fileSignature) + const errorToString = err.toString() + // switch case with true feels so wrong. + switch (true) { + case (errorToString === 'Error: This dag node is a directory'): + gatewayResolver.resolveDirectory(ipfs, ref, err.fileName, (err, data) => { + if (err) { + log.error(err) + return reply(err.toString()).code(500) + } + if (typeof data === 'string') { + // no index file found + if (!ref.endsWith('/')) { + // for a directory, if URL doesn't end with a / + // append / and redirect permanent to that URL + return reply.redirect(`${ref}/`).permanent(true) + } else { + // send directory listing + return reply(data) + } + } else { + // found index file + // redirect to URL/ + return reply.redirect(PathUtils.joinURLParts(ref, data[0].name)) + } + }) + break + case (errorToString.startsWith('Error: no link named')): + return reply(errorToString).code(404) + case (errorToString.startsWith('Error: multihash length inconsistent')): + case (errorToString.startsWith('Error: Non-base58 character')): + return reply({Message: errorToString, code: 0}).code(400) + default: + log.error(err) + return reply({Message: errorToString, code: 0}).code(500) + } + } + } + + return gatewayResolver.resolveMultihash(ipfs, ref, (err, data) => { + if (err) { + return handleGatewayResolverError(err) + } + ipfs.files.cat(data.multihash, (err, stream) => { + if (err) { + log.error(err) + return reply(err.toString()).code(500) + } - filetypeChecked = true - const mimeType = mime.lookup((fileSignature) ? fileSignature.ext : null) - log('ref ', ref) - log('mime-type ', mimeType) + if (ref.endsWith('/')) { + // remove trailing slash for files + return reply + .redirect(PathUtils.removeTrailingSlash(ref)) + .permanent(true) + } else { + if (!stream._read) { + stream._read = () => {} + stream._readableState = {} + } + // response.continue() + let filetypeChecked = false + let stream2 = new Stream.PassThrough({highWaterMark: 1}) + let response = reply(stream2).hold() - if (mimeType) { - log('writing mimeType') + pull( + toPull.source(stream), + pull.through((chunk) => { + // Check file type. do this once. + if (chunk.length > 0 && !filetypeChecked) { + log('got first chunk') + let fileSignature = fileType(chunk) + log('file type: ', fileSignature) - response - .header('Content-Type', mime.contentType(mimeType)) - .header('Access-Control-Allow-Headers', 'X-Stream-Output, X-Chunked-Ouput') - .header('Access-Control-Allow-Methods', 'GET') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Expose-Headers', 'X-Stream-Output, X-Chunked-Ouput') - .send() - } else { - response - .header('Access-Control-Allow-Headers', 'X-Stream-Output, X-Chunked-Ouput') - .header('Access-Control-Allow-Methods', 'GET') - .header('Access-Control-Allow-Origin', '*') - .header('Access-Control-Expose-Headers', 'X-Stream-Output, X-Chunked-Ouput') - .send() - } - } + filetypeChecked = true + const mimeType = mime.lookup((fileSignature) ? fileSignature.ext : null) + log('ref ', ref) + log('mime-type ', mimeType) - stream2.write(chunk) - }, (err) => { - if (err) throw err - log('stream ended.') - stream2.end() - }) - ) - } - }) - .catch((err) => { - if (err) { - log.error(err) - return reply(err.toString()).code(500) - } - }) - }).catch((err) => { - log('err: ', err.toString(), ' fileName: ', err.fileName) + if (mimeType) { + log('writing mimeType') - const errorToString = err.toString() - if (errorToString === 'Error: This dag node is a directory') { - return GatewayResolver - .resolveDirectory(ipfs, ref, err.fileName) - .then((data) => { - if (typeof data === 'string') { - // no index file found - if (!ref.endsWith('/')) { - // for a directory, if URL doesn't end with a / - // append / and redirect permanent to that URL - return reply.redirect(`${ref}/`).permanent(true) - } else { - // send directory listing - return reply(data) - } - } else { - // found index file - // redirect to URL/ - return reply.redirect(PathUtils.joinURLParts(ref, data[0].name)) - } - }).catch((err) => { - log.error(err) - return reply(err.toString()).code(500) - }) - } else if (errorToString.startsWith('Error: no link named')) { - return reply(errorToString).code(404) - } else if (errorToString.startsWith('Error: multihash length inconsistent') || - errorToString.startsWith('Error: Non-base58 character')) { - return reply({Message: errorToString, code: 0}).code(400) - } else { - log.error(err) - return reply({Message: errorToString, code: 0}).code(500) - } - }) + response + .header('Content-Type', mime.contentType(mimeType)) + .header('Access-Control-Allow-Headers', 'X-Stream-Output, X-Chunked-Ouput') + .header('Access-Control-Allow-Methods', 'GET') + .header('Access-Control-Allow-Origin', '*') + .header('Access-Control-Expose-Headers', 'X-Stream-Output, X-Chunked-Ouput') + .send() + } else { + response + .header('Access-Control-Allow-Headers', 'X-Stream-Output, X-Chunked-Ouput') + .header('Access-Control-Allow-Methods', 'GET') + .header('Access-Control-Allow-Origin', '*') + .header('Access-Control-Expose-Headers', 'X-Stream-Output, X-Chunked-Ouput') + .send() + } + } + + stream2.write(chunk) + }), + pull.onEnd(() => { + log('stream ended.') + stream2.end() + }) + ) + } + }) + }) } } diff --git a/test/gateway/index.js b/test/gateway/index.js index 7e5330ecb2..d4341342ec 100644 --- a/test/gateway/index.js +++ b/test/gateway/index.js @@ -38,9 +38,6 @@ describe('HTTP Gateway', () => { (cb) => http.api.start(true, cb), (cb) => { gateway = http.api.server.select('Gateway') - cb() - }, - (cb) => { const content = (name) => ({ path: `test-folder/${name}`, content: directoryContent[name]