Skip to content

Commit

Permalink
Merge pull request ipfs#323 from tableflip/feat/dir-view
Browse files Browse the repository at this point in the history
Adds self served directory listings! 🚀
  • Loading branch information
lidel authored Dec 16, 2017
2 parents 16b7f31 + 1fed7b3 commit 686c648
Show file tree
Hide file tree
Showing 6 changed files with 301 additions and 308 deletions.
79 changes: 79 additions & 0 deletions add-on/src/lib/dir-view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'use strict'

const filesize = require('filesize')
const mainStyle = require('ipfs/src/http/gateway/dir-view/style')

function buildFilesList (path, links) {
const rows = links.map((link) => {
let row = [
`<div class="ipfs-icon ipfs-_blank">&nbsp;</div>`,
`<a href="${path}${path.endsWith('/') ? '' : '/'}${link.name}">${link.name}</a>`,
filesize(link.size)
]

row = row.map((cell) => `<td>${cell}</td>`).join('')

return `<tr>${row}</tr>`
})

return rows.join('')
}

function isRoot (path) {
// Remove leading ipfs// and trailing / and split by /
const parts = path.replace(/^ipfs:\/\//, '').replace(/\/$/, '').split('/')
// If there's only 1 part, then it's the hash, so we are at root
return parts.length === 1
}

function buildTable (path, links) {
return `
<table class="table table-striped">
<tbody>
${isRoot(path) ? '' : (`
<tr>
<td class="narrow">
<div class="ipfs-icon ipfs-_blank">&nbsp;</div>
</td>
<td class="padding">
<a href="${path.split('/').slice(0, -1).join('/')}">..</a>
</td>
<td></td>
</tr>
`)}
${buildFilesList(path, links)}
</tbody>
</table>
`
}

function render (path, links) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>${path}</title>
<style>${mainStyle}</style>
</head>
<body>
<div id="header" class="row">
<div class="col-xs-2">
<div id="logo" class="ipfs-logo"></div>
</div>
</div>
<br>
<div class="col-xs-12">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Index of ${path}</strong>
</div>
${buildTable(path, links)}
</div>
</div>
</body>
</html>
`
}

exports.render = render
40 changes: 21 additions & 19 deletions add-on/src/lib/ipfs-companion.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,26 +145,28 @@ module.exports = async function init () {
}

async function sendStatusUpdateToBrowserAction () {
if (browserActionPort) {
const info = {
ipfsNodeType: state.ipfsNodeType,
peerCount: state.peerCount,
repoStats: state.repoStats,
gwURLString: state.gwURLString,
pubGwURLString: state.pubGwURLString,
currentTab: await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0])
}
try {
let v = await ipfs.version()
if (v) {
info.gatewayVersion = v.commit ? v.version + '/' + v.commit : v.version
}
} catch (error) {
info.gatewayVersion = null
}
if (info.currentTab) {
info.ipfsPageActionsContext = ipfsPathValidator.isIpfsPageActionsContext(info.currentTab.url)
if (!browserActionPort) return
const info = {
ipfsNodeType: state.ipfsNodeType,
peerCount: state.peerCount,
repoStats: state.repoStats,
gwURLString: state.gwURLString,
pubGwURLString: state.pubGwURLString,
currentTab: await browser.tabs.query({active: true, currentWindow: true}).then(tabs => tabs[0])
}
try {
let v = await ipfs.version()
if (v) {
info.gatewayVersion = v.commit ? v.version + '/' + v.commit : v.version
}
} catch (error) {
info.gatewayVersion = null
}
if (info.currentTab) {
info.ipfsPageActionsContext = ipfsPathValidator.isIpfsPageActionsContext(info.currentTab.url)
}
// Still here?
if (browserActionPort) {
browserActionPort.postMessage({statusUpdate: info})
}
}
Expand Down
50 changes: 38 additions & 12 deletions add-on/src/lib/ipfs-protocol.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,57 @@
const { mimeSniff } = require('./mime-sniff')
const dirView = require('./dir-view')
const PathUtils = require('ipfs/src/http/gateway/utils/path')

exports.createIpfsUrlProtocolHandler = (getIpfs) => {
return async (request, reply) => {
console.time('[ipfs-companion] IpfsUrlProtocolHandler')
console.log(`[ipfs-companion] handling ${request.url}`)

const path = request.url.split('ipfs://')[1]
let path = request.url.replace('ipfs://', '/')
path = path.startsWith('/ipfs') ? path : `/ipfs${path}`

const ipfs = getIpfs()

try {
const {data, mimeType} = await getDataAndGuessMimeType(ipfs, path)
console.log(`[ipfs-companion] returning ${path} as ${mimeType}`)
reply({mimeType, data})
const {data, mimeType, charset} = await getDataAndGuessMimeType(ipfs, path)
console.log(`[ipfs-companion] returning ${path} as mime ${mimeType} and charset ${charset}`)
reply({mimeType, data, charset})
} catch (err) {
console.error('[ipfs-companion] failed to get data', err)
reply({mimeType: 'text/html', data: `Error ${err.message}`})
}

console.timeEnd('[ipfs-companion] IpfsUrlProtocolHandler')
}
}

function getDataAndGuessMimeType (ipfs, path) {
return new Promise((resolve, reject) => {
ipfs.files.cat(path, (err, res) => {
if (err) return reject(err)
const mimeType = mimeSniff(res, path)
resolve({mimeType, data: res.toString('utf8')})
})
})
async function getDataAndGuessMimeType (ipfs, path) {
let data

try {
data = await ipfs.files.cat(path)
} catch (err) {
if (err.message.toLowerCase() === 'this dag node is a directory') {
return getDirectoryListingOrIndexData(ipfs, path)
}
throw err
}

const mimeType = mimeSniff(data, path) || 'text/plain'
return {mimeType, data: data.toString('utf8'), charset: 'utf8'}
}

async function getDirectoryListingOrIndexData (ipfs, path) {
const listing = await ipfs.ls(path)
const index = listing.find((l) => ['index', 'index.html', 'index.htm'].includes(l.name))

if (index) {
return getDataAndGuessMimeType(ipfs, PathUtils.joinURLParts(path, index.name))
}

return {
mimeType: 'text/html',
data: dirView.render(path.replace(/^\/ipfs\//, 'ipfs://'), listing),
charset: 'utf8'
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
"lru_map": "0.3.3",
"mime-types": "2.1.17",
"prundupify": "1.0.0",
"tachyons": "^4.9.0",
"tachyons": "4.9.0",
"webextension-polyfill": "0.1.2"
}
}
51 changes: 51 additions & 0 deletions test/functional/lib/ipfs-protocol.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use strict'
const { describe, it } = require('mocha')
const { expect } = require('chai')
const { createIpfsUrlProtocolHandler } = require('../../../add-on/src/lib/ipfs-protocol')

describe('ipfs-protocol', () => {
it('should serve an IPFS file', async () => {
const url = 'ipfs://QmQxeMcbqW9npq5h5kyE2iPECR9jxJF4j5x4bSRQ2phLY4'
const content = 'TEST' + Date.now()
const ipfs = { files: { cat: () => Promise.resolve(content) } }
const handler = createIpfsUrlProtocolHandler(() => ipfs)
const request = { url }

const res = await new Promise(async (resolve, reject) => {
try {
await handler(request, resolve)
} catch (err) {
reject(err)
}
})

expect(res.data).to.equal(content)
})

it('should serve a directory listing', async () => {
const url = 'ipfs://QmQxeMcbqW9npq5h5kyE2iPECR9jxJF4j5x4bSRQ2phLY4'
const links = [
{ name: `one${Date.now()}`, size: Date.now() },
{ name: `two${Date.now()}`, size: Date.now() },
{ name: `three${Date.now()}`, size: Date.now() }
]
const ipfs = {
files: { cat: () => Promise.reject(new Error('this dag node is a directory')) },
ls: () => Promise.resolve(links)
}
const handler = createIpfsUrlProtocolHandler(() => ipfs)
const request = { url }

const res = await new Promise(async (resolve, reject) => {
try {
await handler(request, resolve)
} catch (err) {
reject(err)
}
})

expect(res.mimeType).to.equal('text/html')
expect(res.charset).to.equal('utf8')
links.forEach((link) => expect(res.data).to.contain(`${url}/${link.name}`))
})
})
Loading

0 comments on commit 686c648

Please sign in to comment.