Skip to content

Commit 8e0a31b

Browse files
hacdiasdaviddias
authored andcommitted
feat: pinning files (#571)
1 parent a62b636 commit 8e0a31b

27 files changed

+695
-39
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ Now, to incorporate your pane into Station iself so it is visible, you have to i
116116

117117
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`.
118118

119+
### [Stateless Components](./src/js/components/view)
120+
119121
+ [**Button**](./src/js/components/view/button.js) is a simple button with text.
120122
+ [**CheckboxBlock**](./src/js/components/view/checkbox-block.js) is like an `InfoBlock`, but with a checkbox attached to it.
121123
+ [**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.
@@ -126,7 +128,14 @@ The components are classes exported with CamelCase names. The corresponding file
126128
+ [**IconDropdownList**](./src/js/components/view/icon-dropdown-list.js) is a dropdown list with an icon.
127129
+ [**Icon**](./src/js/components/view/icon.js) shows an icon.
128130
+ [**InfoBlock**](./src/js/components/view/info-block.js) shows a block of information (used on node info pane).
131+
+ [**KeyCombo**](./src/js/components/view/key-combo.js) is a key combination.
132+
+ [**Key**](./src/js/components/view/key.js) is a key.
129133
+ [**MenuOption**](./src/js/components/view/menu-option.js) is a menu option to show within a menu bar.
134+
+ [**PinnedHash**](./src/js/components/view/pinned-hash.js) is a pinned hash.
135+
136+
### [Statefull Components](./src/js/components/logic)
137+
138+
+ [**NewPinnedHash**](./src/js/components/view/new-pinned-hash.js) is a new pinned hash form.
130139

131140
## Contribute
132141

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"file-extension": "^4.0.0",
1515
"ipfs-geoip": "^2.3.0",
1616
"ipfsd-ctl": "^0.26.0",
17+
"is-ipfs": "^0.3.2",
1718
"moment": "^2.19.3",
1819
"multiaddr": "^3.0.1",
1920
"normalize.css": "^7.0.0",

src/config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {app, dialog} from 'electron'
77

88
import FileHistory from './utils/file-history'
99
import KeyValueStore from './utils/key-value-store'
10+
import PinnedFiles from './utils/pinned-files'
1011

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

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

@@ -87,6 +89,7 @@ process.on('unhandledRejection', fatal)
8789
export default {
8890
logger: logger,
8991
fileHistory: fileHistory,
92+
pinnedFiles: pinnedFiles,
9093
settingsStore: settingsStore,
9194
logo: {
9295
ice: logo('ice'),

src/controls/main/download-hash.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import {clipboard, app, dialog, globalShortcut} from 'electron'
21
import path from 'path'
32
import fs from 'fs'
3+
import {clipboard, app, dialog, globalShortcut} from 'electron'
4+
import {validateIPFS} from '../utils'
45

56
const settingsOption = 'downloadHashShortcut'
67
const shortcut = 'CommandOrControl+Alt+D'
@@ -17,8 +18,11 @@ function selectDirectory (opts) {
1718
'createDirectory'
1819
]
1920
}, (res) => {
20-
if (!res) resolve()
21-
resolve(res[0])
21+
if (!res || res.length === 0) {
22+
resolve()
23+
} else {
24+
resolve(res[0])
25+
}
2226
})
2327
})
2428
}
@@ -51,6 +55,14 @@ function handler (opts) {
5155
return
5256
}
5357

58+
if (!validateIPFS(text)) {
59+
dialog.showErrorBox(
60+
'Invalid Hash',
61+
'The hash you provided is invalid.'
62+
)
63+
return
64+
}
65+
5466
ipfs().get(text)
5567
.then((files) => {
5668
logger.info(`Hash ${text} downloaded.`)
@@ -69,7 +81,13 @@ function handler (opts) {
6981
})
7082
.catch(e => logger.error(e.stack))
7183
})
72-
.catch(e => logger.warn(e.stack))
84+
.catch(e => {
85+
logger.error(e.stack)
86+
dialog.showErrorBox(
87+
'Error while downloading',
88+
'Some error happened while getting the hash. Please check the logs.'
89+
)
90+
})
7391
}
7492
}
7593

src/controls/main/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import downloadHash from './download-hash'
33
import fileHistory from './file-history'
44
import openFileDialog from './open-file-dialog'
55
import openWebUI from './open-webui'
6+
import pinnedFiles from './pinned-files'
67
import settings from './settings'
78
import takeScreenshot from './take-screenshot'
89
import toggleSticky from './toggle-sticky'
@@ -14,6 +15,7 @@ export default function (opts) {
1415
fileHistory(opts)
1516
openFileDialog(opts)
1617
openWebUI(opts)
18+
pinnedFiles(opts)
1719
settings(opts)
1820
takeScreenshot(opts)
1921
toggleSticky(opts)

src/controls/main/pinned-files.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {dialog, ipcMain} from 'electron'
2+
import {validateIPFS} from '../utils'
3+
4+
function pinHash (opts) {
5+
const {pinnedFiles, ipfs, send, logger} = opts
6+
7+
let pinning = 0
8+
9+
const sendPinning = () => { send('pinning', pinning > 0) }
10+
const inc = () => { pinning++; sendPinning() }
11+
const dec = () => { pinning--; sendPinning() }
12+
13+
return (event, hash, tag) => {
14+
if (!validateIPFS(hash)) {
15+
dialog.showErrorBox(
16+
'Invalid Hash',
17+
'The hash you provided is invalid.'
18+
)
19+
return
20+
}
21+
22+
inc()
23+
logger.info(`Pinning ${hash}`)
24+
25+
ipfs().pin.add(hash)
26+
.then(() => {
27+
dec()
28+
logger.info(`${hash} pinned`)
29+
pinnedFiles.add(hash, tag)
30+
})
31+
.catch(e => {
32+
dec()
33+
logger.error(e.stack)
34+
dialog.showErrorBox(
35+
'Error while pinning',
36+
'Some error happened while pinning the hash. Please check the logs.'
37+
)
38+
})
39+
}
40+
}
41+
42+
function unpinHash (opts) {
43+
const {pinnedFiles, ipfs, logger} = opts
44+
45+
return (event, hash) => {
46+
logger.info(`Unpinning ${hash}`)
47+
48+
ipfs().pin.rm(hash)
49+
.then(() => {
50+
logger.info(`${hash} unpinned`)
51+
pinnedFiles.remove(hash)
52+
})
53+
.catch(e => { logger.error(e.stack) })
54+
}
55+
}
56+
57+
export default function (opts) {
58+
const {pinnedFiles, send} = opts
59+
60+
const handler = () => {
61+
send('pinned', pinnedFiles.toObject())
62+
}
63+
64+
// Set event handlers.
65+
ipcMain.on('tag-hash', (event, hash, tag) => {
66+
pinnedFiles.add(hash, tag)
67+
})
68+
69+
ipcMain.on('request-pinned', handler)
70+
pinnedFiles.on('change', handler)
71+
ipcMain.on('pin-hash', pinHash(opts))
72+
ipcMain.on('unpin-hash', unpinHash(opts))
73+
}

src/controls/utils.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import multiaddr from 'multiaddr'
22
import {clipboard} from 'electron'
3+
import isIPFS from 'is-ipfs'
34

45
export function apiAddrToUrl (apiAddr) {
56
const parts = multiaddr(apiAddr).nodeAddress()
@@ -9,13 +10,21 @@ export function apiAddrToUrl (apiAddr) {
910
}
1011

1112
export function uploadFiles (opts) {
12-
let {ipfs, logger, fileHistory} = opts
13+
let {ipfs, logger, fileHistory, send} = opts
14+
let adding = 0
15+
16+
const sendAdding = () => { send('adding', adding > 0) }
17+
const inc = () => { adding++; sendAdding() }
18+
const dec = () => { adding--; sendAdding() }
1319

1420
return (event, files) => {
21+
logger.info('Uploading files', {files})
22+
inc()
23+
1524
ipfs()
1625
.add(files, {recursive: true, wrap: true})
1726
.then((res) => {
18-
logger.info('Uploading files', {files})
27+
dec()
1928

2029
res.forEach((file) => {
2130
const url = `https://ipfs.io/ipfs/${file.hash}`
@@ -24,6 +33,16 @@ export function uploadFiles (opts) {
2433
fileHistory.add(file.path, file.hash)
2534
})
2635
})
27-
.catch(e => { logger.error(e.stack) })
36+
.catch(e => {
37+
dec()
38+
logger.error(e.stack)
39+
})
2840
}
2941
}
42+
43+
export function validateIPFS (text) {
44+
return isIPFS.multihash(text) ||
45+
isIPFS.cid(text) ||
46+
isIPFS.ipfsPath(text) ||
47+
isIPFS.ipfsPath(`/ipfs/${text}`)
48+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, {Component} from 'react'
2+
import PropTypes from 'prop-types'
3+
4+
import IconButton from '../view/icon-button'
5+
6+
/**
7+
* Is a New Pinned Hash form.
8+
*
9+
* @prop {Function} onSubmit - the on submit handler.
10+
* @prop {Bool} hidden - should the box be hidden?
11+
*/
12+
export default class NewPinnedHash extends Component {
13+
static propTypes = {
14+
onSubmit: PropTypes.func.isRequired,
15+
hidden: PropTypes.bool.isRequired
16+
}
17+
18+
state = {
19+
tag: '',
20+
hash: ''
21+
}
22+
23+
/**
24+
* KeyUp event handler.
25+
* @param {Event} event
26+
* @returns {Void}
27+
*/
28+
keyUp = (event) => {
29+
if (event.keyCode === 13) {
30+
event.preventDefault()
31+
this.submit()
32+
}
33+
}
34+
35+
/**
36+
* Tag change event handler.
37+
* @param {Event} event
38+
* @returns {Void}
39+
*/
40+
tagChange = (event) => {
41+
this.setState({tag: event.target.value})
42+
}
43+
44+
/**
45+
* Hash change event handler.
46+
* @param {Event} event
47+
* @returns {Void}
48+
*/
49+
hashChange = (event) => {
50+
this.setState({hash: event.target.value})
51+
}
52+
53+
/**
54+
* Resets the hash and tag.
55+
* @returns {Void}
56+
*/
57+
reset = () => {
58+
this.setState({
59+
hash: '',
60+
tag: ''
61+
})
62+
}
63+
64+
/**
65+
* Submits the hash and tag.
66+
* @returns {Void}
67+
*/
68+
submit = () => {
69+
const {hash, tag} = this.state
70+
71+
if (hash) {
72+
this.props.onSubmit(hash, tag)
73+
this.reset()
74+
} else {
75+
this.hashInput.focus()
76+
}
77+
}
78+
79+
componentDidUpdate (prevProps) {
80+
if (!this.props.hidden && prevProps.hidden) {
81+
this.tagInput.focus()
82+
}
83+
84+
if (this.props.hidden && !prevProps.hidden) {
85+
this.reset()
86+
}
87+
}
88+
89+
/**
90+
* Render function.
91+
* @returns {ReactElement}
92+
*/
93+
render () {
94+
let className = 'info-block new-pinned'
95+
if (this.props.hidden) {
96+
className += ' hide'
97+
}
98+
99+
return (
100+
<div className={className}>
101+
<div className='wrapper'>
102+
<div>
103+
<input
104+
type='text'
105+
className='label'
106+
placeholder='Untagged'
107+
ref={(input) => { this.tagInput = input }}
108+
onChange={this.tagChange}
109+
onKeyUp={this.keyUp}
110+
value={this.state.tag} />
111+
<input
112+
type='text'
113+
className='info'
114+
ref={(input) => { this.hashInput = input }}
115+
onChange={this.hashChange}
116+
onKeyUp={this.keyUp}
117+
value={this.state.hash}
118+
placeholder='Hash' />
119+
</div>
120+
<div className='right'>
121+
<IconButton icon='check' onClick={this.submit} />
122+
</div>
123+
</div>
124+
</div>
125+
)
126+
}
127+
}

src/js/components/view/checkbox-block.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ export default function CheckboxBlock (props) {
2020

2121
return (
2222
<div onClick={_onClick} className='info-block checkbox'>
23-
<div>
23+
<div className='wrapper'>
2424
<div>
2525
<p className='label'>{props.title}</p>
2626
<p className='info'>{props.info}</p>
2727
</div>
28-
<div>
28+
<div className='right'>
2929
<input type='checkbox' onChange={_onClick} checked={props.value} />
3030
<span className='checkbox' />
3131
</div>

0 commit comments

Comments
 (0)