Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add files page #669

Merged
merged 28 commits into from
Jun 5, 2018
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6b5c046
starting files page ::D
hacdias May 23, 2018
81a7171
feat: delete and inspect
hacdias May 23, 2018
b71169e
hide title
hacdias May 23, 2018
338ef51
feat: add doFilesRename
hacdias May 23, 2018
9b81b00
add previews
hacdias May 23, 2018
5acc5d6
fix links, package json and remove route
hacdias May 23, 2018
c7b8854
fix breadcrumbs
hacdias May 23, 2018
12508ac
add message when folder is empty
hacdias May 23, 2018
af62ed4
add alternative message file preview
hacdias May 23, 2018
4b6fd6b
display titlte
hacdias May 24, 2018
72bcab7
make tests pass
hacdias May 24, 2018
88dbb95
readd unsupported message
hacdias May 24, 2018
9d74d90
feat: rename is working with ugly prompt
hacdias May 24, 2018
2e1ce94
better renaming
hacdias May 24, 2018
87331b3
add mkdir bundle
hacdias May 24, 2018
136deee
add bundle files write
hacdias May 24, 2018
3837533
Upload files working
hacdias May 24, 2018
7648cd2
add max width to bar
hacdias May 24, 2018
6dd84ce
fix small issues
hacdias May 24, 2018
b8eb6a1
simplify some fns
hacdias May 24, 2018
bfca078
fix link on test
hacdias May 27, 2018
3371e3c
fix: remove doGetGatewayUrl from App
hacdias May 31, 2018
5d9514a
fix: remove unused hooks
hacdias May 31, 2018
566605d
refactor: move readAsBuffer to files bundle
hacdias May 31, 2018
a2fe297
remove unnecessary console.log
hacdias May 31, 2018
6344b9d
call doGetGatewayUrl
hacdias May 31, 2018
28878bf
make extenstions to type a map
hacdias May 31, 2018
1251464
fix: avoid action bar jiggle
hacdias Jun 5, 2018
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
227 changes: 92 additions & 135 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"ipfs-css": "^0.5.0",
"ipld-dag-pb": "^0.14.3",
"ipfs-unixfs": "^0.1.14",
"is-binary": "^0.1.0",
"milliseconds": "^1.0.3",
"multibase": "^0.4.0",
"multicodec": "^0.2.6",
Expand Down
8 changes: 7 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,10 @@ export class App extends Component {
}
}

export default connect('selectRoute', 'doUpdateUrl', 'doInitIpfs', App)
export default connect(
'selectRoute',
'doUpdateUrl',
'doInitIpfs',
'doGetGatewayUrl',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doGetGatewayUrl can be removed here, it's not used.

App
)
2 changes: 1 addition & 1 deletion src/App.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ it('example test', async () => {
let titleText = await page.evaluate(el => el.innerHTML, title)
expect(titleText).toBe('Status')

await page.click('nav a[href="#/files"]')
await page.click('nav a[href="#/files/"]')

title = await page.$('[data-id=title]')
titleText = await page.evaluate(el => el.innerHTML, title)
Expand Down
121 changes: 121 additions & 0 deletions src/bundles/files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { createAsyncResourceBundle, createSelector } from 'redux-bundler'
import { join } from '../lib/path'

const bundle = createAsyncResourceBundle({
name: 'files',
actionBaseType: 'FILES',
getPromise: (args) => {
const {store, getIpfs} = args
let path = store.selectRouteParams().path

if (!path) {
store.doUpdateHash('/files/')
return Promise.resolve()
}

// FIX: dirs with actually encoded names
// will get decoded... it shouldn't happen
path = decodeURIComponent(path)

return getIpfs().files.stat(path)
.then(stats => {
if (stats.type === 'directory') {
return getIpfs().files.ls(path, {l: true}).then((res) => {
// FIX: open PR on js-ipfs-api
if (res) {
res = res.map(file => {
file.type = file.type === 0 ? 'file' : 'directory'
return file
})
}

return {
path: path,
type: 'directory',
files: res
}
})
} else {
stats.name = path

return {
path: path,
type: 'file',
stats: stats,
read: () => getIpfs().files.read(path)
}
}
})
},
staleAfter: 100,
checkIfOnline: false
})

bundle.reactFilesFetch = createSelector(
'selectFilesShouldUpdate',
'selectIpfsReady',
'selectRouteInfo',
'selectFiles',
(shouldUpdate, ipfsReady, {url, params}, files) => {
if (shouldUpdate && ipfsReady && url.startsWith('/files')) {
if (!files || files.path !== params.path) {
return { actionCreator: 'doFetchFiles' }
}
}
}
)

bundle.doFilesDelete = (files) => ({dispatch, getIpfs, store}) => {
dispatch({ type: 'FILES_DELETE_STARTED' })

const promises = files.map(file => getIpfs().files.rm(file, { recursive: true }))
Promise.all(promises)
.then(() => {
store.doFetchFiles()
dispatch({ type: 'FILES_DELETE_FINISHED' })
})
.catch((error) => {
dispatch({ type: 'FILES_DELETE_ERRORED', payload: error })
})
}

function runAndFetch ({ dispatch, getIpfs, store }, type, action, args) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice ✨

dispatch({ type: `${type}_STARTED` })

return getIpfs().files[action](...args)
.then(() => {
store.doFetchFiles()
dispatch({ type: `${type}_FINISHED` })
})
.catch((error) => {
dispatch({ type: `${type}_ERRORED`, payload: error })
})
}

bundle.doFilesRename = (from, to) => (args) => {
runAndFetch(args, 'FILES_RENAME', 'mv', [[from, to]])
}

bundle.doFilesCopy = (from, to) => (args) => {
runAndFetch(args, 'FILES_RENAME', 'cp', [[from, to]])
}

bundle.doFilesMakeDir = (path) => (args) => {
runAndFetch(args, 'FILES_MKDIR', 'mkdir', [path, { parents: true }])
}

bundle.doFilesWrite = (root, files) => ({dispatch, getIpfs, store}) => {
dispatch({ type: 'FILES_WRITE_STARTED' })

return Promise.all(files.map((file) => {
const target = join(root, file.name)
return getIpfs().files.write(target, file.content, { create: true })
})).then(() => {
store.doFetchFiles()
dispatch({ type: 'FILES_WRITE_FINISHED' })
}).catch((error) => {
dispatch({ type: 'FILES_WRITE_ERRORED', payload: error })
})
}

export default bundle
4 changes: 3 additions & 1 deletion src/bundles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import appIdle from './app-idle'
import peersBundle from './peers'
import routesBundle from './routes'
import redirectsBundle from './redirects'
import filesBundle from './files'

export default composeBundles(
appIdle({idleTimeout: 5000}),
ipfsBundle,
objectBundle,
peersBundle,
routesBundle,
redirectsBundle
redirectsBundle,
filesBundle
)
27 changes: 27 additions & 0 deletions src/bundles/ipfs.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export default {
if (action.type === 'IPFS_INIT_FINISHED') {
return { ...state, ipfsReady: true }
}

if (action.type === 'IPFS_GATEWAY_URL_FINISHED') {
return { ...state, gatewayUrl: action.payload }
}

if (!state.gatewayUrl) {
return { ...state, gatewayUrl: 'https://ipfs.io' }
}

return state
},

Expand All @@ -17,6 +26,8 @@ export default {

selectIpfsReady: state => state.ipfs.ipfsReady,

selectGatewayUrl: state => state.ipfs.gatewayUrl,

doInitIpfs: () => async ({ dispatch }) => {
dispatch({ type: 'IPFS_INIT_STARTED' })

Expand All @@ -27,5 +38,21 @@ export default {
}

dispatch({ type: 'IPFS_INIT_FINISHED' })
},

doGetGatewayUrl: () => async ({ dispatch }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doGetGatewayUrl isn't called? I think it's a good plan to have something like this. We might end up with both a "publicGatewayUrl" that points to https://ipfs.io most of the time, and an "localGatewayUrl" that usually points to localhost. I think we've got something like that in companion already.

What you have is good for now. We need to think about how we make this work against a js-ipfs instance, as we connect to window.ipfs, so we can't be sure that the user has http gateway. We could just use the public one in that case, but it would be neat if we could load the users files directly via window.ipfs rather than assuming an http gateway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uhh, forgot to call it. Now it's called after doInitIPFS. 😄 It doesn't work when using companion though because it doesn't allow .config.get.

Copy link
Member

@lidel lidel May 31, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should start thinking on adding window.ipfs in "privileged mode" for WebUI, which provides access to all APIs and has no sandboxing.

Initial idea was to add a mechanism for pre-emptively requesting permissions, as suggested in ipfs/ipfs-companion#454 (comment) and ipfs/ipfs-companion#454 (comment), it could be extended with sandbox flag:

window.ipfs.proxy.acl.whitelist(['config.get'], {sandboxing: false})

The problem is that other dapps could easily disable sandboxing this way.
Not sure if we are able to secure it without some sort of pubkey crypto, which requires us to maintain key for signing webui request for access to privileged APIs.

Is this a blocker or just an inconvenience?
Are you able to work with HTTP API until we sort this out?

dispatch({ type: 'IPFS_GATWAY_URL_STARTED' })

getIpfs().config.get('Addresses.Gateway', (err, res) => {
if (err) {
dispatch({ type: 'IPFS_GATWAY_URL_ERRORED', payload: err })
return
}

const split = res.split('/')
const gateway = '//' + split[2] + ':' + split[4]

dispatch({ type: 'IPFS_GATWAY_URL_FINISHED', payload: gateway })
})
}
}
1 change: 1 addition & 0 deletions src/bundles/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import SettingsPage from '../settings/SettingsPage'
export default createRouteBundle({
'/files': FilesPage,
'/explore*': IpldPage,
'/files*': FilesPage,
'/peers': PeersPage,
'/settings': SettingsPage,
'/': StatusPage,
Expand Down
114 changes: 113 additions & 1 deletion src/files/FilesPage.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,114 @@
import React from 'react'
export default () => <h1 data-id='title'>Files</h1>
import PropTypes from 'prop-types'
import { connect } from 'redux-bundler-react'
import Breadcrumbs from './breadcrumbs/Breadcrumbs'
import FilesList from './files-list/FilesList'
import FilePreview from './file-preview/FilePreview'
import FileInput from './file-input/FileInput'

const action = (name) => {
return (...args) => {
console.log(name, args)
}
}

const empty = (
<div>
<h2>It seems a bit lonely here :(</h2>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@akrych when you get time, could you create some ideas for a "you have no files yet, why not add some" page for the files view? It could use some of the space to introduce some IPFS or at least some of the issues around "they are only on your machine until a peer fetches them from you", and "this is p2p. this is not a cloud"

</div>
)

class FilesPage extends React.Component {
static propTypes = {
files: PropTypes.object
}

state = {
clipboard: [],
copy: false
}

onLinkClick = (link) => {
const {doUpdateHash} = this.props
doUpdateHash(`/files${link}`)
}

onInspect = (hash) => {
const {doUpdateHash} = this.props
doUpdateHash(`/explore/ipfs/${hash}`)
}

onRename = ([path]) => {
const oldName = path.split('/').pop()
const newName = window.prompt('Insert the new name:')

if (newName) {
this.props.doFilesRename(path, path.replace(oldName, newName))
}
}

onFilesUpload = (files) => {
this.props.doFilesWrite(this.props.files.path, files)
}

componentWillMount () {
console.log('WUOLL MOUNT')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WULLY

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't even remember why I added this listeners... 😆

}

componentWillUnmount () {
console.log('WILL UNMOUNT')
}

render () {
const {files} = this.props

if (!files) {
return <div>
<h1 data-id='title'>Files</h1>
</div>
}

let body
if (files.type === 'directory') {
if (files.files.length === 0) {
body = empty
} else {
body = <FilesList
maxWidth='calc(100% - 240px)'
root={files.path}
files={files.files}
onShare={action('Share')}
onInspect={this.onInspect}
onRename={this.onRename}
onDownload={action('Download')}
onDelete={this.props.doFilesDelete}
onNavigate={this.onLinkClick}
onCancelUpload={action('Cancel Upload')}
/>
}
} else {
body = <FilePreview {...files} gatewayUrl={this.props.gatewayUrl} />
}

return (
<div>
<div className='flex items-center justify-between mb4'>
<Breadcrumbs path={files.path} onClick={this.onLinkClick} />
<FileInput upload={this.onFilesUpload} />
</div>
{body}
<h1 data-id='title'>Files</h1>
</div>
)
}
}

export default connect(
'doUpdateHash',
'doFilesDelete',
'doFilesRename',
'doFilesWrite',
'selectFiles',
'selectGatewayUrl',
FilesPage
)
5 changes: 3 additions & 2 deletions src/files/breadcrumbs/Breadcrumbs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,16 @@ function makeBread (root) {
}
})

parts[0].name = 'Root'

for (let i = 1; i < parts.length; i++) {
parts[i] = {
name: parts[i].name,
path: parts[i - 1].path + '/' + parts[i].path
}
}

parts[0].name = 'Root'
parts[0].path = '/'

return parts
}

Expand Down
Loading