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: pinning #571

Merged
merged 1 commit into from
Dec 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ Now, to incorporate your pane into Station iself so it is visible, you have to i

The components are classes exported with CamelCase names. The corresponding files have the associated class name with hyphen-separated-words. So, e.g., `simple-stat.js` exports a class named `SimpleStat`.

### [Stateless Components](./src/js/components/view)

+ [**Button**](./src/js/components/view/button.js) is a simple button with text.
+ [**CheckboxBlock**](./src/js/components/view/checkbox-block.js) is like an `InfoBlock`, but with a checkbox attached to it.
+ [**FileBlock**](./src/js/components/view/file-block.js) is used within a file list to describe a file with a button to copy its link.
Expand All @@ -126,7 +128,14 @@ The components are classes exported with CamelCase names. The corresponding file
+ [**IconDropdownList**](./src/js/components/view/icon-dropdown-list.js) is a dropdown list with an icon.
+ [**Icon**](./src/js/components/view/icon.js) shows an icon.
+ [**InfoBlock**](./src/js/components/view/info-block.js) shows a block of information (used on node info pane).
+ [**KeyCombo**](./src/js/components/view/key-combo.js) is a key combination.
+ [**Key**](./src/js/components/view/key.js) is a key.
+ [**MenuOption**](./src/js/components/view/menu-option.js) is a menu option to show within a menu bar.
+ [**PinnedHash**](./src/js/components/view/pinned-hash.js) is a pinned hash.

### [Statefull Components](./src/js/components/logic)

+ [**NewPinnedHash**](./src/js/components/view/new-pinned-hash.js) is a new pinned hash form.

## Contribute

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"file-extension": "^4.0.0",
"ipfs-geoip": "^2.3.0",
"ipfsd-ctl": "^0.26.0",
"is-ipfs": "^0.3.2",
"moment": "^2.19.3",
"multiaddr": "^3.0.1",
"normalize.css": "^7.0.0",
Expand Down
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {app, dialog} from 'electron'

import FileHistory from './utils/file-history'
import KeyValueStore from './utils/key-value-store'
import PinnedFiles from './utils/pinned-files'

// Set up crash reporter or electron debug
if (isDev) {
Expand Down Expand Up @@ -34,6 +35,7 @@ function ensurePath (path) {
const ipfsAppData = ensurePath(path.join(app.getPath('appData'), 'ipfs-station'))
const logsPath = ensurePath(path.join(ipfsAppData, 'logs'))

const pinnedFiles = new PinnedFiles(path.join(ipfsAppData, 'pinned-files.json'))
const fileHistory = new FileHistory(path.join(ipfsAppData, 'file-history.json'))
const settingsStore = new KeyValueStore(path.join(ipfsAppData, 'config.json'))

Expand Down Expand Up @@ -87,6 +89,7 @@ process.on('unhandledRejection', fatal)
export default {
logger: logger,
fileHistory: fileHistory,
pinnedFiles: pinnedFiles,
settingsStore: settingsStore,
logo: {
ice: logo('ice'),
Expand Down
26 changes: 22 additions & 4 deletions src/controls/main/download-hash.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {clipboard, app, dialog, globalShortcut} from 'electron'
import path from 'path'
import fs from 'fs'
import {clipboard, app, dialog, globalShortcut} from 'electron'
import {validateIPFS} from '../utils'

const settingsOption = 'downloadHashShortcut'
const shortcut = 'CommandOrControl+Alt+D'
Expand All @@ -17,8 +18,11 @@ function selectDirectory (opts) {
'createDirectory'
]
}, (res) => {
if (!res) resolve()
resolve(res[0])
if (!res || res.length === 0) {
resolve()
} else {
resolve(res[0])
}
})
})
}
Expand Down Expand Up @@ -51,6 +55,14 @@ function handler (opts) {
return
}

if (!validateIPFS(text)) {
dialog.showErrorBox(
'Invalid Hash',
'The hash you provided is invalid.'
)
return
}

ipfs().get(text)
.then((files) => {
logger.info(`Hash ${text} downloaded.`)
Expand All @@ -69,7 +81,13 @@ function handler (opts) {
})
.catch(e => logger.error(e.stack))
})
.catch(e => logger.warn(e.stack))
.catch(e => {
logger.error(e.stack)
dialog.showErrorBox(
'Error while downloading',
'Some error happened while getting the hash. Please check the logs.'
)
})
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/controls/main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import downloadHash from './download-hash'
import fileHistory from './file-history'
import openFileDialog from './open-file-dialog'
import openWebUI from './open-webui'
import pinnedFiles from './pinned-files'
import settings from './settings'
import takeScreenshot from './take-screenshot'
import toggleSticky from './toggle-sticky'
Expand All @@ -14,6 +15,7 @@ export default function (opts) {
fileHistory(opts)
openFileDialog(opts)
openWebUI(opts)
pinnedFiles(opts)
settings(opts)
takeScreenshot(opts)
toggleSticky(opts)
Expand Down
73 changes: 73 additions & 0 deletions src/controls/main/pinned-files.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {dialog, ipcMain} from 'electron'
import {validateIPFS} from '../utils'

function pinHash (opts) {
const {pinnedFiles, ipfs, send, logger} = opts

let pinning = 0

const sendPinning = () => { send('pinning', pinning > 0) }
const inc = () => { pinning++; sendPinning() }
const dec = () => { pinning--; sendPinning() }

return (event, hash, tag) => {
if (!validateIPFS(hash)) {
dialog.showErrorBox(
'Invalid Hash',
'The hash you provided is invalid.'
)
return
}

inc()
logger.info(`Pinning ${hash}`)

ipfs().pin.add(hash)
.then(() => {
dec()
logger.info(`${hash} pinned`)
pinnedFiles.add(hash, tag)
})
.catch(e => {
dec()
logger.error(e.stack)
dialog.showErrorBox(
'Error while pinning',
'Some error happened while pinning the hash. Please check the logs.'
)
})
}
}

function unpinHash (opts) {
const {pinnedFiles, ipfs, logger} = opts

return (event, hash) => {
logger.info(`Unpinning ${hash}`)

ipfs().pin.rm(hash)
.then(() => {
logger.info(`${hash} unpinned`)
pinnedFiles.remove(hash)
})
.catch(e => { logger.error(e.stack) })
}
}

export default function (opts) {
const {pinnedFiles, send} = opts

const handler = () => {
send('pinned', pinnedFiles.toObject())
}

// Set event handlers.
ipcMain.on('tag-hash', (event, hash, tag) => {
pinnedFiles.add(hash, tag)
})

ipcMain.on('request-pinned', handler)
pinnedFiles.on('change', handler)
ipcMain.on('pin-hash', pinHash(opts))
ipcMain.on('unpin-hash', unpinHash(opts))
}
25 changes: 22 additions & 3 deletions src/controls/utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import multiaddr from 'multiaddr'
import {clipboard} from 'electron'
import isIPFS from 'is-ipfs'

export function apiAddrToUrl (apiAddr) {
const parts = multiaddr(apiAddr).nodeAddress()
Expand All @@ -9,13 +10,21 @@ export function apiAddrToUrl (apiAddr) {
}

export function uploadFiles (opts) {
let {ipfs, logger, fileHistory} = opts
let {ipfs, logger, fileHistory, send} = opts
let adding = 0

const sendAdding = () => { send('adding', adding > 0) }
const inc = () => { adding++; sendAdding() }
const dec = () => { adding--; sendAdding() }

return (event, files) => {
logger.info('Uploading files', {files})
inc()

ipfs()
.add(files, {recursive: true, wrap: true})
.then((res) => {
logger.info('Uploading files', {files})
dec()

res.forEach((file) => {
const url = `https://ipfs.io/ipfs/${file.hash}`
Expand All @@ -24,6 +33,16 @@ export function uploadFiles (opts) {
fileHistory.add(file.path, file.hash)
})
})
.catch(e => { logger.error(e.stack) })
.catch(e => {
dec()
logger.error(e.stack)
})
}
}

export function validateIPFS (text) {
return isIPFS.multihash(text) ||
isIPFS.cid(text) ||
isIPFS.ipfsPath(text) ||
isIPFS.ipfsPath(`/ipfs/${text}`)
}
127 changes: 127 additions & 0 deletions src/js/components/logic/new-pinned-hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'

import IconButton from '../view/icon-button'

/**
* Is a New Pinned Hash form.
*
* @prop {Function} onSubmit - the on submit handler.
* @prop {Bool} hidden - should the box be hidden?
*/
export default class NewPinnedHash extends Component {
static propTypes = {
onSubmit: PropTypes.func.isRequired,
hidden: PropTypes.bool.isRequired
}

state = {
tag: '',
hash: ''
}

/**
* KeyUp event handler.
* @param {Event} event
* @returns {Void}
*/
keyUp = (event) => {
if (event.keyCode === 13) {
event.preventDefault()
this.submit()
}
}

/**
* Tag change event handler.
* @param {Event} event
* @returns {Void}
*/
tagChange = (event) => {
this.setState({tag: event.target.value})
}

/**
* Hash change event handler.
* @param {Event} event
* @returns {Void}
*/
hashChange = (event) => {
this.setState({hash: event.target.value})
}

/**
* Resets the hash and tag.
* @returns {Void}
*/
reset = () => {
this.setState({
hash: '',
tag: ''
})
}

/**
* Submits the hash and tag.
* @returns {Void}
*/
submit = () => {
const {hash, tag} = this.state

if (hash) {
this.props.onSubmit(hash, tag)
this.reset()
} else {
this.hashInput.focus()
}
}

componentDidUpdate (prevProps) {
if (!this.props.hidden && prevProps.hidden) {
this.tagInput.focus()
}

if (this.props.hidden && !prevProps.hidden) {
this.reset()
}
}

/**
* Render function.
* @returns {ReactElement}
*/
render () {
let className = 'info-block new-pinned'
if (this.props.hidden) {
className += ' hide'
}

return (
<div className={className}>
<div className='wrapper'>
<div>
<input
type='text'
className='label'
placeholder='Untagged'
ref={(input) => { this.tagInput = input }}
onChange={this.tagChange}
onKeyUp={this.keyUp}
value={this.state.tag} />
<input
type='text'
className='info'
ref={(input) => { this.hashInput = input }}
onChange={this.hashChange}
onKeyUp={this.keyUp}
value={this.state.hash}
placeholder='Hash' />
</div>
<div className='right'>
<IconButton icon='check' onClick={this.submit} />
</div>
</div>
</div>
)
}
}
4 changes: 2 additions & 2 deletions src/js/components/view/checkbox-block.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ export default function CheckboxBlock (props) {

return (
<div onClick={_onClick} className='info-block checkbox'>
<div>
<div className='wrapper'>
<div>
<p className='label'>{props.title}</p>
<p className='info'>{props.info}</p>
</div>
<div>
<div className='right'>
<input type='checkbox' onChange={_onClick} checked={props.value} />
<span className='checkbox' />
</div>
Expand Down
Loading