diff --git a/src/bundles/files-provs.js b/src/bundles/files-provs.js new file mode 100644 index 000000000..2585305b8 --- /dev/null +++ b/src/bundles/files-provs.js @@ -0,0 +1,148 @@ +import { createSelector } from 'redux-bundler' + +export default function (opts) { + opts = opts || {} + opts.concurrency = opts.concurrency || 5 + + const defaultState = { + provs: {}, + queue: [], + resolving: [] + } + + return { + name: 'filesProvs', + + reducer (state = defaultState, action) { + if (action.type === 'FILES_PROVS_QUEUED') { + const hashes = action.payload + const provs = {} + + hashes.forEach(hash => { + provs[hash] = { + state: 'queued' + } + }) + + return { + ...state, + queue: state.queue.concat(hashes), + provs: { + ...state.provs, + ...provs + } + } + } + + if (action.type === 'FILES_PROVS_RESOLVE_STARTED') { + const { hash } = action.payload + + return { + ...state, + queue: state.queue.filter(h => hash !== h), + resolving: state.resolving.concat(hash), + provs: { + ...state.provs, + [hash]: { + state: 'resolving' + } + } + } + } + + if (action.type === 'FILES_PROVS_RESOLVE_FINISHED') { + const { hash, count } = action.payload + + return { + ...state, + resolving: state.resolving.filter(h => h !== hash), + provs: { + ...state.provs, + [hash]: { + state: 'resolved', + count: count + } + } + } + } + + if (action.type === 'FILES_PROVS_RESOLVE_FAILED') { + const { hash, error } = action.payload + + return { + ...state, + resolving: state.resolving.filter(h => h !== hash), + provs: { + ...state.provs, + [hash]: { + state: 'failed', + error: error + } + } + } + } + + return state + }, + + selectFilesProvs: state => state.filesProvs.provs, + selectFilesProvsQueuing: state => state.filesProvs.queue, + selectFilesProvsResolving: state => state.filesProvs.resolving, + + doFindProvs: hash => async ({ dispatch, getIpfs }) => { + dispatch({ type: 'FILES_PROVS_RESOLVE_STARTED', payload: { hash } }) + + const ipfs = getIpfs() + let count + + try { + const res = await ipfs.dht.findprovs(hash, { timeout: '30s', 'num-providers': 5 }) + count = res.filter(t => t.Type === 4).length + } catch (err) { + return dispatch({ + type: 'FILES_PROVS_RESOLVE_FAILED', + payload: { hash, error: err } + }) + } + + dispatch({ + type: 'FILES_PROVS_RESOLVE_FINISHED', + payload: { hash, count } + }) + }, + + reactFindProvs: createSelector( + 'selectIpfsReady', + 'selectFilesProvsQueuing', + 'selectFilesProvsResolving', + (ipfsReady, queuing, resolving) => { + if (ipfsReady && queuing.length && resolving.length < opts.concurrency) { + return { + actionCreator: 'doFindProvs', + args: [ queuing[0] ] + } + } + } + ), + + reactFindProvsQueue: createSelector( + 'selectFiles', + 'selectFilesProvs', + (files, filesProvs) => { + if (!files || files.type !== 'directory') { + return + } + + const payload = files.content.reduce((acc, { hash }) => { + if (!filesProvs[hash]) { + return [...acc, hash] + } else { + return acc + } + }, []) + + return { type: 'FILES_PROVS_QUEUED', payload } + } + ) + } +} diff --git a/src/bundles/index.js b/src/bundles/index.js index 8fcc31016..58711d263 100644 --- a/src/bundles/index.js +++ b/src/bundles/index.js @@ -11,6 +11,7 @@ import peerLocationsBundle from './peer-locations' import routesBundle from './routes' import redirectsBundle from './redirects' import filesBundle from './files' +import filesProvsBundle from './files-provs' import configBundle from './config' import configSaveBundle from './config-save' import navbarBundle from './navbar' @@ -27,6 +28,7 @@ export default composeBundles( routesBundle, redirectsBundle, filesBundle(), + filesProvsBundle(), configBundle, configSaveBundle, navbarBundle diff --git a/src/files/FilesPage.js b/src/files/FilesPage.js index 17ff3132a..1c9967c93 100644 --- a/src/files/FilesPage.js +++ b/src/files/FilesPage.js @@ -22,6 +22,7 @@ class FilesPage extends React.Component { filesErrors: PropTypes.array.isRequired, filesPathFromHash: PropTypes.string, writeFilesProgress: PropTypes.number, + filesProvs: PropTypes.object, gatewayUrl: PropTypes.string.isRequired, navbarWidth: PropTypes.number.isRequired, doUpdateHash: PropTypes.func.isRequired, @@ -95,6 +96,7 @@ class FilesPage extends React.Component { render () { const { files, + filesProvs, writeFilesProgress, navbarWidth, doFilesDismissErrors, @@ -104,6 +106,18 @@ class FilesPage extends React.Component { filesErrors: errors } = this.props + if (files && files.content) { + files.content = files.content.map(file => { + if (filesProvs[file.hash] && filesProvs[file.hash].count) { + file.peers = filesProvs[file.hash].count + } else { + file.peers = 0 + } + + return file + }) + } + return (