Skip to content
This repository was archived by the owner on Apr 3, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions examples/browser-gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# ipfs-browser-gateway

Given an IPFS multihash, print the content and try to render web page if the content is HTML.

## Development

This is a "create-react-app" app, so just:

`yarn && yarn start`

## Drawback

1. The first visit is way slower than traditional HTTP page. Though ```http://ipfs.io/ipfs``` is slow too.
1. Can't promise long-term cache, compared to a gateway running on a server, who can pin a file for a longer time.
1. Large folders like [QmRoYXgYMfcP7YQR4sCuSeJy9afgA5XDJ78JzWntpRhmcu](http://ipfs.io/ipfs/QmRoYXgYMfcP7YQR4sCuSeJy9afgA5XDJ78JzWntpRhmcu) may destroy service worker (maybe due to my using ```files.get```), so it's more suitable to just load HTML pages in this way.
1. Don't work with AJAX likes fetch API.
1. Loading dependencies by importScripts synchronously are slow (10s).

## Road Map

- Bundle all dependencies.
- There will be a pull request to [ipfs-service-worker](https://github.com/ipfs/ipfs-service-worker) ones this project is done.

## Reference

- [Discussion](https://github.com/ipfs/ipfs-service-worker/issues/11)
- [JS-IPFS-Gateway](https://github.com/ipfs/js-ipfs/tree/master/src/http/gateway)
- [readable-stream in SW](https://developers.google.com/web/updates/2016/06/sw-readablestreams)
26 changes: 26 additions & 0 deletions examples/browser-gateway/config-overrides.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { injectBabelPlugin } = require('react-app-rewired');

module.exports = function override(config, env) {
config = injectBabelPlugin(
[
'flow-runtime',
{
assert: true,
annotate: true,
},
],
config,
);

if (env === 'production') {
console.log('⚡ Production build with optimization ⚡');
config = injectBabelPlugin('closure-elimination', config);
config = injectBabelPlugin('transform-react-inline-elements', config);
config = injectBabelPlugin('transform-react-constant-elements', config);
}

// remove eslint in eslint, we only need it on VSCode
config.module.rules.splice(1, 1);

return config;
};
40 changes: 40 additions & 0 deletions examples/browser-gateway/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"name": "ipfs-browser-gateway",
"version": "0.1.0",
"private": true,
"dependencies": {
"history": "^4.7.2",
"ipfs": "^0.28.2",
"react": "^16.3.2",
"react-dom": "^16.3.2",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-scripts": "1.1.4",
"styled-components": "^3.2.6",
"styled-flex-component": "^2.2.2"
},
"devDependencies": {
"babel-eslint": "^8.2.3",
"babel-plugin-closure-elimination": "^1.3.0",
"babel-plugin-flow-runtime": "^0.17.0",
"babel-plugin-transform-react-constant-elements": "^6.23.0",
"babel-plugin-transform-react-inline-elements": "^6.22.0",
"eslint": "^4.19.1",
"eslint-config-airbnb": "^16.1.0",
"eslint-config-prettier": "^2.9.0",
"eslint-plugin-flowtype": "^2.46.3",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-jest": "^21.15.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-prettier": "^2.6.0",
"eslint-plugin-react": "^7.7.0",
"flow-typed": "^2.4.0",
"react-app-rewired": "^1.5.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
Binary file added examples/browser-gateway/public/favicon.ico
Binary file not shown.
40 changes: 40 additions & 0 deletions examples/browser-gateway/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
16 changes: 16 additions & 0 deletions examples/browser-gateway/public/mainStyle.js

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions examples/browser-gateway/public/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
38 changes: 38 additions & 0 deletions examples/browser-gateway/public/pathUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/* eslint-disable no-unused-vars */
function splitPath(path) {
if (path[path.length - 1] === '/') {
path = path.substring(0, path.length - 1);
}

return path.split('/');
}

function removeLeadingSlash(url) {
if (url[0] === '/') {
url = url.substring(1);
}

return url;
}

function removeTrailingSlash(url) {
if (url.endsWith('/')) {
url = url.substring(0, url.length - 1);
}

return url;
}

function removeSlashFromBothEnds(url) {
url = removeLeadingSlash(url);
url = removeTrailingSlash(url);

return url;
}

function joinURLParts(...urls) {
urls = urls.filter(url => url.length > 0);
urls = [''].concat(urls.map(url => removeSlashFromBothEnds(url)));

return urls.join('/');
}
82 changes: 82 additions & 0 deletions examples/browser-gateway/public/renderFolder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* eslint-disable no-unused-vars, no-restricted-globals */
/* global importScripts, mainStyle, joinURLParts, filesize, splitPath */

importScripts('./mainStyle.js');
const filesize = require('https://unpkg.com/[email protected]/lib/filesize.js');

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="${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 = 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 renderFolder(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>
`;
}
8 changes: 8 additions & 0 deletions examples/browser-gateway/public/require.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* eslint-disable no-unused-vars, no-restricted-globals */
/* global self, importScripts */
function require(moduleName) {
self.module = { exports: null };
self.global = {};
importScripts(moduleName);
return Object.keys(self.global).length > 0 ? self.global[Object.keys(self.global)[0]] : self.module.exports;
}
103 changes: 103 additions & 0 deletions examples/browser-gateway/public/resolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* eslint-disable no-unused-vars */
/* global importScripts, Cids, Multihashes, IpfsUnixfs, promisify, splitPath, async, renderFolder */
importScripts('https://unpkg.com/[email protected]/dist/index.min.js');
importScripts('https://unpkg.com/[email protected]/dist/index.min.js');
importScripts('https://npmcdn.com/[email protected]/dist/index.min.js');
importScripts('https://unpkg.com/[email protected]/index.min.js');
importScripts('https://unpkg.com/[email protected]/dist/async.js');
importScripts('./renderFolder.js');

const INDEX_HTML_FILES = ['index.html', 'index.htm', 'index.shtml'];
function getIndexHtml(links) {
return links.filter(link => INDEX_HTML_FILES.indexOf(link.name) !== -1);
}

const resolveDirectory = promisify((ipfs, path, multihash, callback) => {
Multihashes.validate(Multihashes.fromB58String(multihash));

ipfs.object.get(multihash, { enc: 'base58' }, (err, dagNode) => {
if (err) {
return callback(err);
}

// if it is a web site, return index.html
const indexFiles = getIndexHtml(dagNode.links);
if (indexFiles.length > 0) {
// TODO: add *.css and *.ico to cache
return callback(null, indexFiles);
}

return callback(null, renderFolder(path, dagNode.links));
});
});

const resolveMultihash = promisify((ipfs, path, callback) => {
const parts = splitPath(path);
const firstMultihash = parts.shift();
let currentCid;

return async.reduce(
parts,
firstMultihash,
(memo, item, next) => {
try {
currentCid = new Cids(Multihashes.fromB58String(memo));
} catch (err) {
return next(err);
}

ipfs.dag.get(currentCid, (err, result) => {
if (err) {
return next(err);
}

const dagNode = result.value;
// find multihash of requested named-file in current dagNode's links
let multihashOfNextFile;
const nextFileName = item;

const { links } = dagNode;

for (const link of links) {
if (link.name === nextFileName) {
// found multihash of requested named-file
multihashOfNextFile = Multihashes.toB58String(link.multihash);
break;
}
}

if (!multihashOfNextFile) {
return next(new Error(`no link named "${nextFileName}" under ${memo}`));
}

next(null, multihashOfNextFile);
});
},
(err, result) => {
if (err) {
return callback(err);
}

let cid;
try {
cid = new Cids(Multihashes.fromB58String(result));
} catch (error) {
return callback(error);
}

ipfs.dag.get(cid, (error, dagResult) => {
if (error) return callback(err);

const dagDataObj = IpfsUnixfs.unmarshal(dagResult.value.data);
if (dagDataObj.type === 'directory') {
const 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 });
});
},
);
});
Loading