├── .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 |