Skip to content
This repository has been archived by the owner on Sep 28, 2021. It is now read-only.

Commit

Permalink
feat: initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
vasco-santos committed May 24, 2018
1 parent f236f1b commit d9d0c08
Show file tree
Hide file tree
Showing 20 changed files with 27,221 additions and 1 deletion.
64 changes: 64 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
dist/

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Typescript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# while testing npm5
package-lock.json
yarn.lock
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
# ipfs-http-response
# js-ipfs-http-response

> Creates an HTTP response from an IPFS Hash
### Installation

> TODO
## Usage

This project consists on creating a HTTP response from an IPFS Hash. This response can be a file, a directory list view or the entry point of a web page.

```js
const ipfsHttpResponse = require('ipfs-http-response')

ipfsHttpResponse(ipfsNode, ipfsPath)
.then((response) => {
...
})
```

![ipfs-http-response usage](docs/ipfs-http-response.png "ipfs-http-response usage")
2 changes: 2 additions & 0 deletions ci/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Warning: This file is automatically synced from https://github.com/ipfs/ci-sync so if you want to change it, please change it there and ask someone to sync all repositories.
javascript()
Binary file added docs/ipfs-http-response.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "js-ipfs-http-response",
"version": "0.1.0",
"description": "Creates an HTTP response from an IPFS Hash",
"main": "src/index.js",
"scripts": {
"lint": "aegir lint",
"release": "aegir release",
"build": "aegir build",
"test": "aegir test -t node"
},
"pre-push": [
"lint",
"test"
],
"repository": {
"type": "git",
"url": "git+https://github.com/ipfs/js-ipfs-http-response.git"
},
"keywords": [
"ipfs",
"http",
"response"
],
"author": "Vasco Santos <vasco.santos@moxy.studio>",
"license": "MIT",
"bugs": {
"url": "https://github.com/ipfs/js-ipfs-http-response/issues"
},
"homepage": "https://github.com/ipfs/js-ipfs-http-response#readme",
"dependencies": {
"async": "^2.6.0",
"cids": "^0.5.3",
"debug": "^3.1.0",
"file-type": "^8.0.0",
"filesize": "^3.6.1",
"ipfs-unixfs": "^0.1.14",
"mime-types": "^2.1.18",
"multihashes": "^0.4.13",
"promisify-es6": "^1.0.3",
"readable-stream-node-to-web": "^1.0.1"
},
"devDependencies": {
"aegir": "^13.1.0",
"chai": "^4.1.2",
"dirty-chai": "^2.0.1",
"ipfs": "^0.28.2",
"ipfsd-ctl": "^0.36.0"
}
}
86 changes: 86 additions & 0 deletions src/dir-view/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'use strict'

const filesize = require('filesize')

const mainStyle = require('./style')
const pathUtil = require('../utils/path')

function getParentDirectoryURL (originalParts) {
const parts = originalParts.slice()

if (parts.length > 1) {
parts.pop()
}

return [ '', 'ipfs' ].concat(parts).join('/')
}

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

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

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

return rows.join('')
}

function buildTable (path, links) {
const parts = pathUtil.splitPath(path)
const parentDirectoryURL = getParentDirectoryURL(parts)

return `
<table class="table table-striped">
<tbody>
<tr>
<td class="narrow">
<div class="ipfs-icon ipfs-_blank">&nbsp;</div>
</td>
<td class="padding">
<a href="${parentDirectoryURL}">..</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 = module.exports
exports.render = render
16 changes: 16 additions & 0 deletions src/dir-view/style.js

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/* global Response */

'use strict'

const fileType = require('file-type')
const mimeTypes = require('mime-types')
const stream = require('stream')
const nodeToWebStream = require('readable-stream-node-to-web')

const resolver = require('./resolver')
const pathUtils = require('./utils/path')

const header = (status = 200, statusText = 'OK', headers = {}) => ({
status,
statusText,
headers
})

module.exports = (ipfsNode, ipfsPath) => {
// handle hash resolve error (simple hash, test for directory now)
const handleResolveError = (node, path, error) => {
if (error) {
const errorString = error.toString()

return new Promise((resolve, reject) => {
// switch case with true feels so wrong.
switch (true) {
case (errorString === 'Error: This dag node is a directory'):
resolver.directory(node, path, error.fileName)
.then((content) => {
// dir render
if (typeof content === 'string') {
resolve(new Response(content, header(200, 'OK', { 'Content-Type': 'text/html' })))
}

// redirect to dir entry point (index)
resolve(Response.redirect(pathUtils.joinURLParts(path, content[0].name)))
})
.catch((error) => {
resolve(new Response(errorString, header(500, error.toString())))
})
break
case errorString.startsWith('Error: no link named'):
resolve(new Response(errorString, header(404, errorString)))
break
case errorString.startsWith('Error: multihash length inconsistent'):
case errorString.startsWith('Error: Non-base58 character'):
resolve(new Response(errorString, header(400, errorString)))
break
default:
resolve(new Response(errorString, header(500, errorString)))
}
})
}
}

return new Promise((resolve, reject) => {
// remove trailing slash for files if needed
if (ipfsPath.endsWith('/')) {
resolve(Response.redirect(pathUtils.removeTrailingSlash(ipfsPath)))
}

resolver.multihash(ipfsNode, ipfsPath)
.then((resolvedData) => {
const readableStream = ipfsNode.files.catReadableStream(resolvedData.multihash)
const responseStream = new stream.PassThrough({ highWaterMark: 1 })
readableStream.pipe(responseStream)

readableStream.once('error', (error) => {
if (error) {
resolve(new Response(error.toString(), header(500, 'Service Worker Error')))
}
})

// return only after first chunk being checked
let filetypeChecked = false
readableStream.on('data', (chunk) => {
// check mime on first chunk
if (filetypeChecked) {
return
}
filetypeChecked = true
// return Response with mime type
const fileSignature = fileType(chunk)
const mimeType = mimeTypes.lookup(fileSignature ? fileSignature.ext : null)

if (mimeType) {
resolve(
new Response(typeof ReadableStream === 'function' ? nodeToWebStream(responseStream) : responseStream,
header(200, 'OK', { 'Content-Type': mimeTypes.contentType(mimeType) }))
)
} else {
resolve(new Response(typeof ReadableStream === 'function' ? nodeToWebStream(responseStream) : responseStream,
header()))
}
})
})
.catch((error) => {
resolve(handleResolveError(ipfsNode, ipfsPath, error))
})
})
}
Loading

0 comments on commit d9d0c08

Please sign in to comment.