├── .babelrc
├── .gitignore
├── README.md
├── app
├── actionCreators
│ └── data-actions.js
├── components
│ ├── about-view.jsx
│ ├── add-comment.jsx
│ ├── add-file-view.jsx
│ ├── browse-view.jsx
│ ├── file-view.jsx
│ ├── index-view.jsx
│ ├── info-row.jsx
│ ├── modal-container.jsx
│ ├── redirect-modal.jsx
│ └── setup-modal.jsx
├── dao
│ └── set-db-dao.js
├── index.jsx
├── manager
│ └── file-manager.js
├── reducers
│ ├── data-reducer.js
│ └── modal-reducer.js
└── settings.json
├── index.js
├── package-lock.json
├── package.json
├── public
├── css
│ └── stylesheet.css
├── images
│ └── favicon.png
├── index.html
├── js
│ ├── bundle.js
│ └── bundle.js.map
└── stats.json
├── scripts
├── monitor-peers.js
└── publish-to-ipfs.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets" : ["es2015", "react"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | public/dump.json
3 | npm-debug.log
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Index
2 |
3 | The Index is an IPFS-hosted app which acts as a directory for files added to IPFS.
4 |
5 | Users can browse already-added files and add information about their own uploaded files. This app is completely decentralized, using PubSub to allow different IPFS nodes to communicate with each other and share database state.
6 |
7 | To configure your IPFS daemon to allow access to this service, run the following commands
8 | ```
9 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]"
10 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials "[\"true\"]"
11 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "[\"PUT\", \"POST\", \"GET\"]"
12 | ```
13 | These will allow the app to control your daemon, something essential for the decentralized operation of the app.
14 |
15 | Then start up your daemon with the following command
16 | ```
17 | ipfs daemon --enable-pubsub-experiment
18 | ```
19 | Now you can navigate to [The Index](http://localhost:8080/ipfs/QmXny7UjYEiFXskWr5Un6p5DMZPU87yzdmC3VEQcCx9xBC)
20 |
--------------------------------------------------------------------------------
/app/actionCreators/data-actions.js:
--------------------------------------------------------------------------------
1 | import {
2 | searchFileNameAndDescription,
3 | searchFileNameAndDescriptionAndCategory,
4 | loadFile,
5 | addFile,
6 | addComment
7 | } from '../manager/file-manager';
8 | import {connect} from '../dao/set-db-dao';
9 | import {ipnsURL} from '../settings';
10 |
11 | export function initData() {
12 | return function (dispatch) {
13 | connect
14 | .then(() => {
15 | if (!window.location.pathname.startsWith(ipnsURL)) {
16 | fetch(`${ipnsURL}stats.json`)
17 | .then(response => response.json())
18 | .then(ipnsJson => {
19 | if (ipnsJson && ipnsJson.hash) {
20 | return fetch('stats.json')
21 | .then(response => response.json())
22 | .then(localJson => {
23 | if (localJson && localJson.hash !== ipnsJson.hash) {
24 | // Compares the hashes and asks for redirect if different
25 | dispatch({
26 | type: 'ADD_MODAL',
27 | data: 'REDIRECT'
28 | });
29 | }
30 | });
31 | }
32 | })
33 | .catch(() => {
34 | console.log('Failed to get response about stats, won\'t ask for upgrade');
35 | });
36 | }
37 | })
38 | .catch(err => {
39 | console.log(`Error during connect: ${err}`);
40 | dispatch({
41 | type: 'ADD_MODAL',
42 | data: 'SETUP'
43 | });
44 | });
45 | };
46 | }
47 |
48 | export function postFile(file, router) {
49 | return function () {
50 | router.replace('/');
51 | addFile(file);
52 | };
53 | }
54 |
55 | export function postComment(comment) {
56 | return function (dispatch) {
57 | addComment(comment);
58 | dispatch({
59 | type: 'ADD_COMMENT',
60 | data: comment
61 | });
62 | };
63 | }
64 |
65 | export function searchFiles(string, category) {
66 | return function (dispatch) {
67 | var search;
68 | if (category) {
69 | search = searchFileNameAndDescriptionAndCategory(string, category);
70 | } else {
71 | search = searchFileNameAndDescription(string);
72 | }
73 | search
74 | .then(results => {
75 | dispatch({
76 | type: 'LOAD_FILES',
77 | data: results
78 | });
79 | });
80 | };
81 | }
82 |
83 | export function loadFileById(id) {
84 | return function (dispatch) {
85 | loadFile(id)
86 | .then(file => {
87 | dispatch({
88 | type: 'LOAD_FILE',
89 | data: file
90 | });
91 | });
92 | };
93 | }
94 |
--------------------------------------------------------------------------------
/app/components/about-view.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default React.createClass({
4 | render: function () {
5 | return (
6 |
7 |
About
8 |
9 | The Index is a metadata association service where you can associate an IPFS file hash with metadata describing it.
10 |
11 |
How to use
12 |
13 | You need to set up your IPFS daemon in a special way in order to use this service. First, the following configurations need to be made.
14 |
{`ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\\"*\\"]"
15 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials "[\\"true\\"]"
16 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "[\\"PUT\\", \\"POST\\", \\"GET\\"]"`}
17 |
18 | Then you need to start up the daemon using the following command
19 |
{`ipfs daemon --enable-pubsub-experiment`}
20 |
21 |
22 | );
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/app/components/add-comment.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {postComment} from '../actionCreators/data-actions';
4 | import {COMMENT} from '../dao/set-db-dao';
5 |
6 | var AddComment = React.createClass({
7 | propTypes: {
8 | postComment: React.PropTypes.func,
9 | fileId: React.PropTypes.string
10 | },
11 | getInitialState: function () {
12 | return {
13 | comment: ''
14 | };
15 | },
16 | render: function () {
17 | return (
18 |
21 |
27 | Post
31 |
32 | );
33 | },
34 | handleCommentChange: function (e) {
35 | this.setState({
36 | comment: e.target.value
37 | });
38 | },
39 | handleCommentKeyPress: function (e) {
40 | if (e.key === 'Enter') {
41 | this.handlePostComment();
42 | }
43 | },
44 | handlePostComment: function () {
45 | this.props.postComment({
46 | text: this.state.comment,
47 | fileId: this.props.fileId,
48 | type: COMMENT
49 | });
50 | this.setState({
51 | comment: ''
52 | });
53 | }
54 | });
55 |
56 | var mapDispatchToProps = dispatch => {
57 | return {
58 | postComment: function (comment) {
59 | dispatch(postComment(comment));
60 | }
61 | };
62 | };
63 |
64 | export default connect(
65 | null,
66 | mapDispatchToProps
67 | )(AddComment);
68 |
--------------------------------------------------------------------------------
/app/components/add-file-view.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {withRouter} from 'react-router';
4 | import {multihash} from 'is-ipfs';
5 | import {postFile} from '../actionCreators/data-actions';
6 | import {searchNameOrHash} from '../manager/file-manager';
7 | import {FILE} from '../dao/set-db-dao';
8 | import {categories} from '../settings';
9 | import InfoRow from './info-row.jsx';
10 |
11 | var AddFileView = withRouter(React.createClass({
12 | propTypes: {
13 | postFile: React.PropTypes.func,
14 | router: React.PropTypes.object
15 | },
16 | getInitialState: function () {
17 | return {
18 | message: '',
19 | name: '',
20 | description: '',
21 | category: categories[0],
22 | hash: ''
23 | };
24 | },
25 | render: function () {
26 | var categoryOptions = categories.map((elem, i) => {
27 | return (
28 |
31 | );
32 | });
33 | var messageContainer = this.state.message ?
34 | {this.state.message} :
35 | null;
36 | var createStateInput = id => {
37 | var changeFunction = e => {
38 | var newState = {};
39 | newState[id] = e.target.value;
40 | this.setState(newState);
41 | };
42 | return (
43 |
47 | );
48 | };
49 | var createStateTextArea = id => {
50 | var changeFunction = e => {
51 | var newState = {};
52 | newState[id] = e.target.value;
53 | this.setState(newState);
54 | };
55 | return (
56 |
60 | );
61 | };
62 | var createStateSelect = (id, options) => {
63 | var changeFunction = e => {
64 | var newState = {};
65 | newState[id] = e.target.value;
66 | this.setState(newState);
67 | };
68 | return (
69 |
73 | );
74 | };
75 | return (
76 |
79 |
93 |
103 |
113 |
117 | Add
118 | {messageContainer}
119 |
120 | );
121 | },
122 | handleAddClick: function () {
123 | // First check in db to see if name and hash are unique
124 | var file = {};
125 | file.name = this.state.name;
126 | file.description = this.state.description;
127 | file._id = this.state.hash;
128 | file.type = FILE;
129 | file.category = this.state.category;
130 | console.log(file);
131 | if (!file.name || !file.description) {
132 | this.setState({
133 | message: 'Files must have a name and a description'
134 | });
135 | return;
136 | }
137 | if (!multihash(file._id)) {
138 | this.setState({
139 | message: 'Not a valid multihash'
140 | });
141 | return;
142 | }
143 | searchNameOrHash(file.name, file._id)
144 | .then(result => {
145 | console.log('search result', result.length > 0);
146 | if (result.length === 0) {
147 | this.props.postFile(file, this.props.router);
148 | } else {
149 | this.setState({
150 | message: 'A file with this name or hash already exists on the server'
151 | });
152 | }
153 | });
154 | }
155 | }));
156 |
157 | var mapDispatchToProps = dispatch => {
158 | return {
159 | postFile: function (file, router) {
160 | dispatch(postFile(file, router));
161 | }
162 | };
163 | };
164 |
165 | export default connect(
166 | null,
167 | mapDispatchToProps
168 | )(AddFileView);
169 |
--------------------------------------------------------------------------------
/app/components/browse-view.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {Link} from 'react-router';
4 | import {categories} from '../settings';
5 | import {searchFiles} from '../actionCreators/data-actions';
6 | import {connect as setConnect} from '../dao/set-db-dao';
7 |
8 | var BrowseView = React.createClass({
9 | propTypes: {
10 | files: React.PropTypes.array,
11 | searchFiles: React.PropTypes.func
12 | },
13 | getInitialState: function () {
14 | return {
15 | search: '',
16 | placeholder: 'Search Here',
17 | category: ''
18 | };
19 | },
20 | componentDidMount: function () {
21 | this.searchInput.focus();
22 | },
23 | render: function () {
24 | var files = this.props.files.map((elem, i) => {
25 | var className = i % 2 === 0 ?
26 | 'row' :
27 | 'row alt-row';
28 | return (
29 |
30 |
31 | {elem.name}
32 |
33 |
34 | );
35 | });
36 | var fileContainer = files.length ?
37 | (
40 | {files}
41 |
) :
42 | (
45 | No Search Results
46 |
);
47 | var allOption = [];
48 | var categoryOptions = allOption.concat(categories.map((elem, i) => {
49 | return (
50 |
53 | );
54 | }));
55 | return (
56 |
57 |
58 | Add File
59 | About
60 |
61 |
64 |
71 |
77 | Search
81 |
82 |
83 | {fileContainer}
84 |
85 |
86 | );
87 | },
88 | addSearchReference: function (input) {
89 | this.searchInput = input;
90 | },
91 | handleSearchKey: function (e) {
92 | if (e.key === 'Enter') {
93 | this.search();
94 | }
95 | },
96 | handleSearchClick: function () {
97 | this.search();
98 | },
99 | search: function () {
100 | this.props.searchFiles(this.state.search, this.state.category);
101 | this.setState({
102 | search: '',
103 | placeholder: this.state.search
104 | });
105 | },
106 | handleSearchChange: function (e) {
107 | this.setState({
108 | search: e.target.value
109 | });
110 | },
111 | handleCategoryChange: function (e) {
112 | this.setState({
113 | category: e.target.value
114 | });
115 | }
116 | });
117 |
118 | var mapStateToProps = state => {
119 | return {
120 | files: state.data.files
121 | };
122 | };
123 |
124 | var mapDispatchToProps = dispatch => {
125 | return {
126 | searchFiles: function (string, category) {
127 | dispatch(searchFiles(string, category));
128 | }
129 | };
130 | };
131 |
132 | export default connect(
133 | mapStateToProps,
134 | mapDispatchToProps
135 | )(BrowseView);
136 |
--------------------------------------------------------------------------------
/app/components/file-view.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {loadFileById} from '../actionCreators/data-actions';
4 | import InfoRow from './info-row.jsx';
5 | import AddComment from './add-comment.jsx';
6 |
7 | var FileView = React.createClass({
8 | propTypes: {
9 | loadFile: React.PropTypes.func,
10 | file: React.PropTypes.object,
11 | params: React.PropTypes.object
12 | },
13 | componentDidMount: function () {
14 | this.props.loadFile(this.props.params.id);
15 | },
16 | render: function () {
17 | if (this.props.file === null) {
18 | return null;
19 | }
20 | var comments = this.props.file.comments.map((elem, i) => {
21 | return (
22 |
26 | {elem.text}
27 |
28 | );
29 | });
30 | return (
31 |
32 |
35 |
49 |
59 | {this.props.file.description}
65 | }
66 | ]
67 | }
68 | />
69 |
81 |
82 |
85 |
Comments
86 | {comments}
87 |
90 |
91 |
92 | );
93 | }
94 | });
95 |
96 | var mapStateToProps = state => {
97 | return {
98 | file: state.data.file
99 | };
100 | };
101 |
102 | var mapDispatchToProps = dispatch => {
103 | return {
104 | loadFile: function (id) {
105 | dispatch(loadFileById(id));
106 | }
107 | };
108 | };
109 |
110 | export default connect(
111 | mapStateToProps,
112 | mapDispatchToProps
113 | )(FileView);
114 |
--------------------------------------------------------------------------------
/app/components/index-view.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {IndexLink} from 'react-router';
4 | import ModalContainer from '../components/modal-container.jsx';
5 | import RedirectModal from '../components/redirect-modal.jsx';
6 | import SetupModal from '../components/setup-modal.jsx';
7 | import {initData} from '../actionCreators/data-actions';
8 |
9 | var mapStateToProps = state => {
10 | return {
11 | modal: state.modal.modal
12 | };
13 | };
14 |
15 | var mapDispatchToProps = dispatch => {
16 | return {
17 | getData: function () {
18 | dispatch(initData());
19 | }
20 | };
21 | };
22 |
23 | var IndexView = React.createClass({
24 | propTypes: {
25 | getData: React.PropTypes.func,
26 | children: React.PropTypes.object,
27 | modal: React.PropTypes.node
28 | },
29 | componentDidMount: function () {
30 | this.props.getData();
31 | },
32 | render: function () {
33 | var modal;
34 | switch (this.props.modal) {
35 | case 'REDIRECT':
36 | modal = ;
37 | break;
38 | case 'SETUP':
39 | modal = ;
40 | break;
41 | default:
42 | modal = null;
43 | }
44 | return (
45 |
48 |
51 |
52 |
55 | The Index
56 |
57 |
58 | {this.props.children}
59 |
60 |
61 | );
62 | }
63 | });
64 |
65 | export default connect(
66 | mapStateToProps,
67 | mapDispatchToProps
68 | )(IndexView);
69 |
--------------------------------------------------------------------------------
/app/components/info-row.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default React.createClass({
4 | propTypes: {
5 | infoPairs: React.PropTypes.arrayOf(
6 | React.PropTypes.shape({
7 | label: React.PropTypes.node,
8 | info: React.PropTypes.node
9 | })
10 | )
11 | },
12 | render: function () {
13 | var labels = this.props.infoPairs.map((elem, i) => {
14 | var className;
15 | if (i === 0) {
16 | className = 'tl';
17 | } else if (i === this.props.infoPairs.length - 1) {
18 | className = 'tr';
19 | }
20 | return (
21 |
22 | {elem.label}
23 | |
24 | );
25 | });
26 | var infos = this.props.infoPairs.map((elem, i) => {
27 | var className;
28 | if (i === 0) {
29 | className = 'tl';
30 | } else if (i === this.props.infoPairs.length - 1) {
31 | className = 'tr';
32 | }
33 | return (
34 |
35 | {elem.info}
36 | |
37 | );
38 | });
39 | return (
40 |
41 |
42 |
43 | {labels}
44 |
45 |
46 | {infos}
47 |
48 |
49 |
50 | );
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/app/components/modal-container.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import React from 'react';
3 | import {connect} from 'react-redux';
4 | /* eslint-enable import/no-extraneous-dependencies */
5 |
6 | const ModalContainer = React.createClass({
7 | propTypes: {
8 | onLeave: React.PropTypes.func,
9 | modal: React.PropTypes.node,
10 | children: React.PropTypes.array,
11 | removeModal: React.PropTypes.func
12 | },
13 | render: function () {
14 | var className = this.props.modal ?
15 | 'blur' :
16 | '';
17 | var modal = this.props.modal ?
18 | (
22 |
25 | {this.props.modal}
26 |
27 |
) :
28 | null;
29 | return (
30 |
33 |
36 | {this.props.children}
37 |
38 | {modal}
39 |
40 | );
41 | },
42 | handleOuterClick: function () {
43 | if (this.props.onLeave) {
44 | this.props.onLeave();
45 | } else {
46 | this.props.removeModal();
47 | }
48 | },
49 | handleInnerClick: function (e) {
50 | e.stopPropagation();
51 | }
52 | });
53 |
54 | var mapDispatchToProps = dispatch => {
55 | return {
56 | removeModal: () => {
57 | dispatch({
58 | type: 'REMOVE_MODAL'
59 | });
60 | }
61 | };
62 | };
63 |
64 | export default connect(
65 | null,
66 | mapDispatchToProps
67 | )(ModalContainer);
68 |
--------------------------------------------------------------------------------
/app/components/redirect-modal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 | import {ipnsURL} from '../settings';
4 |
5 | var RedirectModal = React.createClass({
6 | propTypes: {
7 | removeModal: React.PropTypes.func
8 | },
9 | render: function () {
10 | return (
11 |
14 |
15 | There is an updated version of this site.
16 |
17 |
18 | Do you wish to navigate there?
19 |
20 |
26 |
30 | Yes
31 |
32 |
36 | No
37 |
38 |
39 |
40 | );
41 | },
42 | handleClickYes: function () {
43 | window.location.pathname = ipnsURL;
44 | },
45 | handleClickNo: function () {
46 | this.props.removeModal();
47 | }
48 | });
49 |
50 | var mapDispatchToProps = dispatch => {
51 | return {
52 | removeModal: function () {
53 | dispatch({
54 | type: 'REMOVE_MODAL'
55 | });
56 | }
57 | };
58 | };
59 |
60 | export default connect(
61 | null,
62 | mapDispatchToProps
63 | )(RedirectModal);
64 |
--------------------------------------------------------------------------------
/app/components/setup-modal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {connect} from 'react-redux';
3 |
4 | var SetupModal = React.createClass({
5 | propTypes: {
6 | removeModal: React.PropTypes.func
7 | },
8 | render: function () {
9 | return (
10 |
13 |
14 | Your ipfs daemon is not set up to run this site. Run the following commands and start your daemon using the last command
15 |
16 |
{`ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\\"*\\"]"
17 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Credentials "[\\"true\\"]"
18 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "[\\"PUT\\", \\"POST\\", \\"GET\\"]"
19 | ipfs daemon --enable-pubsub-experiment`}
20 |
21 | );
22 | }
23 | });
24 |
25 | export default connect(
26 | null,
27 | null
28 | )(SetupModal);
29 |
--------------------------------------------------------------------------------
/app/dao/set-db-dao.js:
--------------------------------------------------------------------------------
1 | import SetDB from 'set-db';
2 | import {multihash} from 'is-ipfs';
3 | import IPFS from 'ipfs';
4 |
5 | const dbHashKey = 'the.index.db';
6 | const dbTopic = process.env.NODE_ENV === 'development' ?
7 | 'the.index.development' : 'the.index.production';
8 |
9 | export const FILE = 'FILE';
10 | export const COMMENT = 'COMMENT';
11 |
12 | var dbHash = localStorage.getItem(dbHashKey);
13 |
14 | const node = new IPFS({
15 | EXPERIMENTAL: {
16 | pubsub: true
17 | },
18 | config: {
19 | Addresses: {
20 | Swarm: [
21 | '/dns4/ws-star.discovery.libp2p.io/tcp/443/wss/p2p-websocket-star'
22 | ]
23 | }
24 | }
25 | });
26 |
27 | export const connect = new Promise((resolve, reject) => {
28 | node.on('ready', () => {
29 | node.id().then(info => console.log('node id is', info.id));
30 | const db = new SetDB(dbTopic, {
31 | dbHash: dbHash,
32 | validator: elem => {
33 | if (elem.type === 'FILE') {
34 | return multihash(elem._id) && elem.name && elem.description && elem.category;
35 | } else if (elem.type === 'COMMENT') {
36 | return multihash(elem.fileId) && elem.text;
37 | }
38 | return false;
39 | },
40 | ipfs: node
41 | });
42 |
43 | let prevSync = setTimeout(() => db.ask(), 60000);
44 |
45 | db.on('sync', () => {
46 | localStorage.setItem(dbHashKey, db.dbHash);
47 | clearTimeout(prevSync);
48 | prevSync = setTimeout(() => db.ask(), 60000);
49 | });
50 |
51 | db.on('ready', () => {
52 | resolve(db)
53 | });
54 | db.on('error', err => reject(err));
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/app/index.jsx:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';// eslint-disable-line import/no-unassigned-import
2 | import React from 'react';
3 | import {Router, Route, IndexRoute, hashHistory} from 'react-router';
4 | import {render} from 'react-dom';
5 | import {Provider} from 'react-redux';
6 | import {createStore, combineReducers, applyMiddleware} from 'redux';
7 | import thunk from 'redux-thunk';
8 | import IndexView from './components/index-view.jsx';
9 | import BrowseView from './components/browse-view.jsx';
10 | import AddFileView from './components/add-file-view.jsx';
11 | import AboutView from './components/about-view.jsx';
12 | import FileView from './components/file-view.jsx';
13 | import dataReducer from './reducers/data-reducer';
14 | import modalReducer from './reducers/modal-reducer';
15 |
16 | var reducer = combineReducers({
17 | data: dataReducer,
18 | modal: modalReducer
19 | });
20 |
21 | var store = createStore(
22 | reducer,
23 | applyMiddleware(thunk)
24 | );
25 |
26 | var router = (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 |
37 | render(
38 | {router},
39 | document.getElementById('app')
40 | );
41 |
--------------------------------------------------------------------------------
/app/manager/file-manager.js:
--------------------------------------------------------------------------------
1 | import {connect, FILE, COMMENT} from '../dao/set-db-dao';
2 |
3 | export function listFileNames() {
4 | return connect
5 | .then(db => {
6 | return db.query(elem => elem.type === FILE)
7 | .map(elem => elem.name);
8 | });
9 | }
10 |
11 | export function searchFileNameAndDescription(string) {
12 | string = string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
13 | var regex = new RegExp('.*' + string + '.*', 'i');
14 | return connect
15 | .then(db => {
16 | return db.query(elem => elem.type === FILE && (regex.test(elem.name) || regex.test(elem.description)));
17 | });
18 | }
19 |
20 | export function searchFileNameAndDescriptionAndCategory(string, category) {
21 | string = string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
22 | var regex = new RegExp('.*' + string + '.*', 'i');
23 | return connect
24 | .then(db => {
25 | return db.query(elem => elem.type === FILE && elem.category === category && (regex.test(elem.name) || regex.test(elem.description)));
26 | });
27 | }
28 |
29 | export function searchNameOrHash(name, hash) {
30 | return connect
31 | .then(db => {
32 | return db.query(elem => elem.type === FILE && (elem.name === name || elem._id === hash));
33 | });
34 | }
35 |
36 | export function loadFile(id) {
37 | return connect
38 | .then(db => {
39 | var file = db.get(id);
40 | if (file) {
41 | file.comments = db.query(elem => elem.type === COMMENT && elem.fileId === id);
42 | }
43 | return file;
44 | });
45 | }
46 |
47 | export function addFile(file) {
48 | return connect
49 | .then(db => {
50 | db.put(file);
51 | });
52 | }
53 |
54 | export function addComment(comment) {
55 | return connect
56 | .then(db => {
57 | comment._id = generateRandomId();
58 | db.put(comment);
59 | });
60 | }
61 |
62 | function generateRandomId() {
63 | return Math.floor(Math.random() * 9999999999999999999999).toString(36);
64 | }
65 |
--------------------------------------------------------------------------------
/app/reducers/data-reducer.js:
--------------------------------------------------------------------------------
1 | var defaultState = {
2 | files: [],
3 | file: null
4 | };
5 |
6 | export default function (state = defaultState, action) {
7 | switch (action.type) {
8 | case 'LOAD_FILES':
9 | return Object.assign({}, state, {
10 | files: action.data
11 | });
12 | case 'LOAD_FILE':
13 | return Object.assign({}, state, {
14 | file: action.data
15 | });
16 | case 'ADD_COMMENT':
17 | var newFile = Object.assign({}, state.file);
18 | newFile.comments.push(action.data);
19 | return Object.assign({}, state, {
20 | file: newFile
21 | });
22 | default:
23 | return state;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/app/reducers/modal-reducer.js:
--------------------------------------------------------------------------------
1 | var defaultState = {
2 | modal: null
3 | };
4 |
5 | export default function (state = defaultState, action) {
6 | switch (action.type) {
7 | case 'REMOVE_MODAL':
8 | return Object.assign({}, state, {
9 | modal: null
10 | });
11 | case 'ADD_MODAL':
12 | return Object.assign({}, state, {
13 | modal: action.data
14 | });
15 | default:
16 | return state;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "categories": [
3 | "Movie",
4 | "Image",
5 | "Book",
6 | "Game",
7 | "Other"
8 | ],
9 | "ipnsURL": "/ipns/QmPZHE9MgGVvoshoFoHeigkZeHPMKdbDJsCQKtEJtjNqoZ/"
10 | }
11 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path');
4 | const httpLib = require('http');
5 | const express = require('express');
6 | const bodyParser = require('body-parser');
7 |
8 | const app = express();
9 | const http = httpLib.Server(app);// eslint-disable-line new-cap
10 |
11 | // parse application/json
12 | app.use(bodyParser.json());
13 | app.use(bodyParser.urlencoded({extended: false}));
14 |
15 | // Deliver the public folder statically
16 | app.use(express.static('public'));
17 |
18 | // This tells the server to listen
19 | var port = 3000;
20 | http.listen(port, function () {
21 | console.log('Example app listening on port ' + port + '!');
22 | });
23 |
24 | /*
25 | * This tells the server to always serve index.html no matter what,
26 | * excluding the previously defined api routes. This is so we can use
27 | * react-router's browserHistory feature.
28 | */
29 | app.get('*', function (req, res) {
30 | res.sendFile(path.join(__dirname, '/public/index.html'));
31 | });
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "index.js",
3 | "scripts": {
4 | "start": "node index.js",
5 | "build": "cross-env NODE_ENV=production webpack",
6 | "watch": "cross-env NODE_ENV=development webpack -d --watch",
7 | "ipfs": "cross-env NODE_ENV=production npm run build && cross-env NODE_ENV=production node scripts/publish-to-ipfs.js",
8 | "ipfs-publish-only": "cross-env NODE_ENV=production node scripts/publish-to-ipfs.js",
9 | "ipfs-dev": "cross-env NODE_ENV=development webpack -d && cross-env NODE_ENV=development node scripts/publish-to-ipfs.js"
10 | },
11 | "dependencies": {
12 | "babel-core": "^6.17.0",
13 | "babel-loader": "^6.2.5",
14 | "babel-polyfill": "^6.13.0",
15 | "babel-preset-es2015": "^6.13.2",
16 | "babel-preset-react": "^6.11.1",
17 | "body-parser": "^1.15.2",
18 | "cross-env": "^3.1.4",
19 | "express": "^4.14.0",
20 | "inquirer": "^3.0.1",
21 | "ipfs": "^0.28.2",
22 | "ipfs-api": "^12.1.7",
23 | "is-ipfs": "^0.2.1",
24 | "json-loader": "^0.5.4",
25 | "node-fetch": "^1.6.3",
26 | "react": "^15.3.1",
27 | "react-dom": "^15.3.1",
28 | "react-redux": "^4.4.5",
29 | "react-router": "^2.7.0",
30 | "redux": "^3.6.0",
31 | "redux-thunk": "^2.1.0",
32 | "set-db": "^1.0.0",
33 | "webpack": "^3.5.5"
34 | },
35 | "xo": {
36 | "rules": {
37 | "linebreak-style": [
38 | "off"
39 | ]
40 | },
41 | "extends": "xo-react",
42 | "envs": [
43 | "browser",
44 | "node"
45 | ]
46 | },
47 | "devDependencies": {
48 | "eslint-config-xo-react": "^0.10.0",
49 | "eslint-plugin-react": "^6.9.0",
50 | "request": "^2.81.0",
51 | "xo": "^0.17.1"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/public/css/stylesheet.css:
--------------------------------------------------------------------------------
1 | #app {
2 | max-width: 960px;
3 | margin: 0 auto;
4 | }
5 | .container {
6 | width: 100%
7 | display: flex;
8 | }
9 | h1 {
10 | font-family: 'Audiowide', cursive;
11 | }
12 | h1, h2, h3 {
13 | text-align: center;
14 | }
15 | body {
16 | font-family: 'Roboto', sans-serif;
17 | }
18 | input, textarea, select {
19 | border: 2px black solid;
20 | border-radius: 3px;
21 | margin: 3px;
22 | padding: 6px;
23 | font-family: 'Domine', serif;
24 | font-size: 16px;
25 | }
26 | .input-container input {
27 | flex: 1 0 auto;
28 | }
29 | .btn {
30 | border: 2px black solid;
31 | border-radius: 3px;
32 | margin: 5px;
33 | padding: 5px;
34 | background-color: #ffffff;
35 | display: inline-block;
36 | transition: all 0.1s;
37 | cursor: pointer;
38 | }
39 | .btn:hover {
40 | background-color: #cfcfcf;
41 | }
42 | .btn:active {
43 | background-color: #afafaf;
44 | }
45 | .bordered{
46 | border: 2px black solid;
47 | border-radius: 3px;
48 | margin: 3px;
49 | padding: 3px;
50 | background-color: white;
51 | }
52 | .link, h1 a, .nav a {
53 | text-decoration: none;
54 | color: black;
55 | padding: 5px;
56 | display: inline-block;
57 | }
58 | .link.active {
59 | font-weight: bold;
60 | }
61 | .input-container {
62 | display: flex;
63 | }
64 | .row {
65 | padding: 3px;
66 | }
67 | .row:hover {
68 | background-color: #efefef;
69 | }
70 | .alt-row {
71 | background-color: #cfcfcf;
72 | }
73 | .alt-row:hover {
74 | background-color: #bfbfbf;
75 | }
76 | .browse-results {
77 | padding: 0px;
78 | }
79 | .browse-results a {
80 | text-decoration: none;
81 | color: black;
82 | }
83 | .nav {
84 | display: flex;
85 | justify-content: center;
86 | }
87 | .nav a {
88 | font-weight: bold;
89 | }
90 | table {
91 | width: 100%;
92 | }
93 | .info-row {
94 | margin-bottom: 10px;
95 | }
96 | .strong {
97 | font-weight: bold;
98 | }
99 | .tr {
100 | text-align: right;
101 | }
102 | .tl {
103 | text-align: left;
104 | }
105 | .description {
106 | padding: 3px;
107 | background-color: #cfcfcf;
108 | margin-top: 2px;
109 | font-family: monospace;
110 | border-radius: 3px;
111 | white-space: pre-wrap;
112 | }
113 | .modal-container {
114 | position: relative;
115 | min-height: 500px;
116 | }
117 | .modal {
118 | display: flex;
119 | position: absolute;
120 | top: 0px;
121 | left: 0px;
122 | width: 100%;
123 | height: 100%;
124 | align-items: center;
125 | justify-content: center;
126 | }
127 | .fleft {
128 | float: left;
129 | }
130 | .fright {
131 | float: right;
132 | }
133 | .comment {
134 | margin: 10px 3px;
135 | }
136 | .download {
137 | margin: 3px;
138 | }
139 |
--------------------------------------------------------------------------------
/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cakenggt/ipfs-foundation-frontend/ab635fa5b2675fab879a3da31824d18edd0209d1/public/images/favicon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | The Index
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/public/stats.json:
--------------------------------------------------------------------------------
1 | {"hash":"60cbcab68c853e4870b5"}
--------------------------------------------------------------------------------
/scripts/monitor-peers.js:
--------------------------------------------------------------------------------
1 | var request = require('request');
2 |
3 | var lastPeers = new Set();
4 |
5 | setInterval(() => {
6 | var req = request.get('http://localhost:5001/api/v0/pubsub/peers?arg=the.index.production');
7 | req.on('data', message => {
8 | const json = JSON.parse(message.toString());
9 | const peers = json.Strings;
10 | var added = 0;
11 | var lost = 0;
12 | var newPeers = new Set(peers);
13 | newPeers.forEach(elem => {
14 | if (!lastPeers.has(elem)) {
15 | added++;
16 | }
17 | });
18 | lastPeers.forEach(elem => {
19 | if (!newPeers.has(elem)) {
20 | lost++;
21 | }
22 | });
23 | lastPeers = newPeers;
24 | if (added > 0 || lost > 0) {
25 | console.log(`Added: ${added}, Lost: ${lost}`);
26 | }
27 | req.abort();
28 | });
29 | }, 5000);
30 |
--------------------------------------------------------------------------------
/scripts/publish-to-ipfs.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var ipfsAPI = require('ipfs-api');
3 |
4 | // Default settings
5 | var ipfs = ipfsAPI();
6 |
7 | // node id
8 | var id;
9 |
10 | ipfs.id()
11 | .then(response => {
12 | id = response.id;
13 | console.log('id', id);
14 | return uploadToIPFS();
15 | })
16 | .catch(function (err) {
17 | console.log('error during main', err);
18 | });
19 |
20 | function uploadToIPFS() {
21 | var dirpath = path.resolve(__dirname, '..', 'public');
22 | console.log('dirpath', dirpath);
23 | return ipfs.util.addFromFs(dirpath, {recursive: true})
24 | .then(result => {
25 | console.log('length of result', result.length);
26 | /*
27 | * To combat the following bug, we are searching for
28 | * the path which starts at public and publishing
29 | * that one, instead of the erronious almost-root
30 | * path.
31 | * https://github.com/ipfs/js-ipfs-api/issues/408
32 | */
33 | var lastHash;
34 | for (var i = 0; i < result.length; i++) {
35 | var file = result[i];
36 | if (/public$/.test(file.path)) {
37 | lastHash = file.hash;
38 | break;
39 | }
40 | }
41 | console.log('lashHash', lastHash);
42 | return ipfs.name.publish(lastHash);
43 | })
44 | .then(() => {
45 | console.log(`published at http://localhost:8080/ipns/${id} and
46 | https://ipfs.io/ipns/${id}`);
47 | })
48 | .catch(err => {
49 | console.log('error caught', err);
50 | });
51 | }
52 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | var webpack = require('webpack');
2 | var path = require('path');
3 | var fs = require('fs');
4 |
5 | var BUILD_DIR = path.resolve(__dirname, 'public/js');
6 | var APP_DIR = path.resolve(__dirname, 'app');
7 |
8 | var config = {
9 | entry: APP_DIR + '/index.jsx',
10 | output: {
11 | path: BUILD_DIR,
12 | filename: 'bundle.js'
13 | },
14 | module : {
15 | loaders : [
16 | {
17 | test : /\.jsx?/,
18 | include : APP_DIR,
19 | loader : 'babel-loader'
20 | },
21 | { test: /\.json$/, loader: 'json-loader' }
22 | ]
23 | },
24 | resolve: {
25 | extensions: ['.json', '.jsx', '.js']
26 | },
27 | plugins: [
28 | new webpack.DefinePlugin({
29 | 'process.env': {
30 | 'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
31 | }
32 | }),
33 | function () {
34 | this.plugin('done', function (stats) {
35 | fs.writeFileSync(
36 | path.resolve(__dirname, 'public', 'stats.json'),
37 | JSON.stringify({
38 | hash: stats.hash
39 | })
40 | );
41 | })
42 | }
43 | ],
44 | node: {
45 | console: true,
46 | fs: 'empty',
47 | net: 'empty',
48 | tls: 'empty'
49 | }
50 | };
51 |
52 | module.exports = config;
53 |
--------------------------------------------------------------------------------