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 (
@@ -163,6 +177,7 @@ export default connect( 'doFilesNavigateTo', 'selectFiles', 'selectFilesErrors', + 'selectFilesProvs', 'selectGatewayUrl', 'selectWriteFilesProgress', 'selectNavbarWidth', diff --git a/src/files/file/File.js b/src/files/file/File.js index eb95337bd..0160e0d6f 100644 --- a/src/files/file/File.js +++ b/src/files/file/File.js @@ -4,6 +4,9 @@ import filesize from 'filesize' import Checkbox from '../../components/checkbox/Checkbox' import FileIcon from '../file-icon/FileIcon' import Tooltip from '../../components/tooltip/Tooltip' +import PeersSmallIcon from '../../icons/GlyphPeersSmall' +import PeersMediumIcon from '../../icons/GlyphPeersMedium' +import PeersLargeIcon from '../../icons/GlyphPeersLarge' import { DropTarget, DragSource } from 'react-dnd' import { NativeTypes } from 'react-dnd-html5-backend' import { join, basename } from 'path' @@ -16,6 +19,7 @@ function File ({ hash, name, path, + peers, type, size, onSelect, @@ -65,6 +69,17 @@ function File ({
)}
{size}
+
+ { peers <= 1 && + + } + { peers > 1 && peers < 5 && + + } + { peers >= 5 && + + } +
)) } @@ -75,6 +90,7 @@ File.propTypes = { path: PropTypes.string.isRequired, size: PropTypes.number.isRequired, hash: PropTypes.string.isRequired, + peers: PropTypes.number.isRequired, selected: PropTypes.bool.isRequired, onSelect: PropTypes.func.isRequired, onNavigate: PropTypes.func.isRequired, diff --git a/src/files/files-list/FilesList.js b/src/files/files-list/FilesList.js index 495d9651d..4dbc90404 100644 --- a/src/files/files-list/FilesList.js +++ b/src/files/files-list/FilesList.js @@ -289,6 +289,7 @@ class FileList extends React.Component { Size {this.sortByIcon(ORDER_BY_SIZE)} +
Health
{this.files} {this.selectedMenu}