@@ -66,12 +74,54 @@ export default class Intro extends Component {
Welcome to the Distributed Web
You are about to install IPFS, the InterPlanetary File System.
+ { this.state.advanced &&
+
-
- { !this.state.showAdvanced &&
+
+ { !this.state.advanced &&
Advanced Options
diff --git a/src/panes/Settings.js b/src/panes/Settings.js
index a41226e8a..a22c3285e 100644
--- a/src/panes/Settings.js
+++ b/src/panes/Settings.js
@@ -18,6 +18,10 @@ function garbageCollector () {
ipcRenderer.send('run-gc')
}
+function cleanConnSettings () {
+ ipcRenderer.send('clean-ipfs-settings')
+}
+
function quit () {
ipcRenderer.send('quit-application')
}
@@ -88,6 +92,12 @@ export default function Settings (props) {
button={false}
onClick={garbageCollector} />
+
+
{
this.setState({status: INTITIALZING})
}
- _onError = (event, error) => {
+ _onError = (_, error) => {
this.setState({
status: ERROR,
error
})
}
- _onConfigPath = (event, path) => {
+ _onConfigPath = (_, path) => {
this.setState({configPath: path})
}
- _selectAdvanced = () => {
- this.setState({status: ADVANCED})
- }
-
- _startInstallation = () => {
- ipcRenderer.send('initialize', {keySize: this.state.keySize})
- }
-
- _onKeySizeChange = (keySize) => {
- this.setState({keySize})
+ _startInstallation = (options) => {
+ ipcRenderer.send('install', options)
}
componentDidMount () {
@@ -65,7 +53,6 @@ export default class Welcome extends Component {
render () {
switch (this.state.status) {
case INTRO:
- case ADVANCED:
return (
@@ -74,10 +61,7 @@ export default class Welcome extends Component {
+ configPath={this.state.configPath} />
)
case ERROR:
diff --git a/src/setup.js b/src/setup.js
new file mode 100644
index 000000000..94a3583d8
--- /dev/null
+++ b/src/setup.js
@@ -0,0 +1,111 @@
+import { logo, getIpfs, logger, store } from './utils'
+import { join } from 'path'
+import fs from 'fs-extra'
+import { BrowserWindow, dialog, ipcMain, app } from 'electron'
+
+function welcome ({ path }) {
+ return new Promise(resolve => {
+ let ipfs = null
+
+ // Initialize the welcome window.
+ const window = new BrowserWindow({
+ title: 'Welcome to IPFS',
+ icon: logo('ice'),
+ show: false,
+ // resizable: false,
+ width: 850,
+ height: 450
+ })
+
+ // Only show the window when the contents have finished loading.
+ window.on('ready-to-show', () => {
+ window.show()
+ window.focus()
+ logger.info('Welcome window ready')
+ })
+
+ // window.setMenu(null)
+ window.loadURL(`file://${__dirname}/views/welcome.html`)
+
+ // Send the default path as soon as the window is ready.
+ window.webContents.on('did-finish-load', () => {
+ window.webContents.send('setup-config-path', path)
+ logger.info('Welcome window has path')
+ })
+
+ window.once('close', () => {
+ logger.info('Welcome screen was closed')
+ if (!ipfs) app.quit()
+ })
+
+ ipcMain.on('setup-browse-path', () => {
+ dialog.showOpenDialog(window, {
+ title: 'Select a directory',
+ defaultPath: path,
+ properties: [
+ 'openDirectory',
+ 'createDirectory'
+ ]
+ }, (res) => {
+ if (!res) return
+
+ let userPath = res[0]
+
+ if (!userPath.endsWith('ipfs')) {
+ userPath = join(userPath, '.ipfs')
+ }
+
+ logger.info('Got new path %s', userPath)
+ window.webContents.send('setup-config-path', userPath)
+ })
+ })
+
+ ipcMain.on('install', async (event, opts) => {
+ window.webContents.send('initializing')
+
+ opts = {
+ ...opts,
+ init: !(await fs.pathExists(opts.path)) || fs.readdirSync(opts.path).length === 0
+ }
+
+ logger.info('Trying connection with: %o', opts)
+
+ try {
+ ipfs = await getIpfs(opts)
+
+ if (opts.type === 'api') {
+ await ipfs.id()
+ } else {
+ await ipfs.api.id()
+ }
+
+ store.set('ipfs', opts)
+ window.close()
+ resolve(ipfs)
+ } catch (e) {
+ logger.info('Connection failed with error: %o', e)
+ window.webContents.send('errored')
+ }
+ })
+ })
+}
+
+export default async function () {
+ const opts = store.get('ipfs', {
+ path: join(process.env.IPFS_PATH || (process.env.HOME || process.env.USERPROFILE), '.ipfs')
+ })
+
+ let ipfs
+
+ if (Object.keys(opts).length === 1) {
+ ipfs = await welcome(opts)
+ } else {
+ try {
+ ipfs = await getIpfs(opts)
+ } catch (_) {
+ ipfs = await welcome(opts)
+ }
+ }
+
+ return ipfs
+}
diff --git a/src/start.js b/src/start.js
new file mode 100644
index 000000000..745d24827
--- /dev/null
+++ b/src/start.js
@@ -0,0 +1,132 @@
+import { Menubar } from 'electron-menubar'
+import { logo, store, logger } from './utils'
+import { ipcMain, app } from 'electron'
+import { EventEmitter } from 'events'
+import { handleKnownErrors } from './errors'
+import registerControls from './controls/main'
+import getIpfs from './utils/ipfs'
+
+export default async function (ipfsd) {
+ const menubar = new Menubar({
+ index: `file://${__dirname}/views/menubar.html`,
+ icon: logo('black'),
+ tooltip: 'Your IPFS instance',
+ preloadWindow: true,
+ window: {
+ resizable: false,
+ fullscreen: false,
+ skipTaskbar: true,
+ width: 600,
+ height: 400,
+ backgroundColor: (store.get('lightTheme', false) ? '#FFFFFF' : '#000000'),
+ webPreferences: {
+ nodeIntegration: true
+ }
+ }
+ })
+
+ let state = 'running'
+
+ const send = (type, ...args) => {
+ if (menubar && menubar.window && menubar.window.webContents) {
+ menubar.window.webContents.send(type, ...args)
+ }
+ }
+
+ const isApi = store.get('ipfs.type') === 'api'
+
+ const config = {
+ events: new EventEmitter(),
+ menubar: menubar,
+ send: send,
+ isApi: isApi,
+ ipfs: () => isApi ? ipfsd : ipfsd.api
+ }
+
+ const updateState = (st) => {
+ state = st
+ onRequestState()
+ }
+
+ const onRequestState = () => {
+ send('node-status', state)
+ }
+
+ const onStopDaemon = (done = () => {}) => {
+ logger.info('Stopping daemon')
+ updateState('stopping')
+
+ config.events.emit('node:stopped')
+
+ ipfsd.stop((err) => {
+ if (err) {
+ return logger.error(err.stack)
+ }
+
+ logger.info('Stopped daemon')
+ menubar.tray.setImage(logo('black'))
+
+ ipfsd = null
+ updateState('stopped')
+ done()
+ })
+ }
+
+ const onWillQuit = () => {
+ logger.info('Shutting down application')
+
+ if (ipfsd == null) {
+ return
+ }
+
+ onStopDaemon(() => {
+ app.quit()
+ })
+ }
+
+ const daemonStarted = () => {
+ logger.info('Daemon started')
+ config.events.emit('node:started')
+
+ if (ipfsd.subprocess) {
+ // Stop the executation of the program if some error
+ // occurs on the node.
+ ipfsd.subprocess.on('error', (e) => {
+ updateState('stopped')
+ logger.error(e)
+ })
+ }
+
+ menubar.tray.setImage(logo('ice'))
+ updateState('running')
+ }
+
+ const onStartDaemon = async () => {
+ logger.info('Starting daemon')
+ updateState('starting')
+
+ try {
+ ipfsd = await getIpfs(store.get('ipfs'))
+ daemonStarted()
+ } catch (e) {
+ handleKnownErrors(e)
+ }
+ }
+
+ const ready = () => {
+ logger.info('Menubar is ready')
+ menubar.tray.setHighlightMode('always')
+
+ ipcMain.on('request-state', onRequestState)
+ ipcMain.on('quit-application', app.quit.bind(app))
+ app.once('will-quit', onWillQuit)
+ ipcMain.on('stop-daemon', onStopDaemon.bind(null, () => {}))
+ ipcMain.on('start-daemon', onStartDaemon)
+
+ registerControls(config)
+ daemonStarted()
+ }
+
+ if (menubar.isReady()) ready()
+ else menubar.on('ready', ready)
+}
diff --git a/src/utils/file-store.js b/src/utils/file-store.js
deleted file mode 100644
index fe1f4c588..000000000
--- a/src/utils/file-store.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import fs from 'fs'
-import {EventEmitter} from 'events'
-
-/**
- * It's a File Store.
- * @extends EventEmitter
- */
-export default class FileStore extends EventEmitter {
- /**
- * FileStore constructor.
- * @param {String} path - File path
- * @param {Object} [initial] - Initial value of the file
- */
- constructor (path, initial = []) {
- super()
- let data = initial
-
- if (fs.existsSync(path)) {
- data = JSON.parse(fs.readFileSync(path))
- } else {
- fs.writeFileSync(path, JSON.stringify(data))
- }
-
- this.data = data
- this.path = path
- this.modified = false
-
- // Function that runs every 5 seconds to write the
- // file to the disk if there are any modifications.
- const timer = () => {
- this.writeToDisk()
- setTimeout(timer.bind(this), 5000)
- }
-
- timer()
-
- // If the process is exiting, we should save the data.
- process.on('exit', () => { this.writeToDisk() })
- }
-
- /**
- * Writes the data to the disk if it was modified.
- * @returns {Void}
- */
- writeToDisk () {
- if (this.modified) {
- fs.writeFileSync(this.path, JSON.stringify(this.data))
- this.modified = false
- }
- }
-
- /**
- * Tells the store that it was modified and emits
- * a change event with the data.
- * @returns {Void}
- */
- write () {
- this.modified = true
- this.emit('change', this.data)
- }
-
- /**
- * Gets the data into an array. If it isn't an array,
- * an error is thrown.
- * @returns {Array}
- */
- toArray () {
- if (Array.isArray(this.data)) {
- return this.data
- } else {
- throw new Error('object is not array')
- }
- }
-
- /**
- * Gets the data object.
- * @returns {Object}
- */
- toObject () {
- return this.data
- }
-}
diff --git a/src/utils/index.js b/src/utils/index.js
new file mode 100644
index 000000000..6bb89a39b
--- /dev/null
+++ b/src/utils/index.js
@@ -0,0 +1,17 @@
+import path from 'path'
+import os from 'os'
+import store from './store'
+import logger from './logger'
+import getIpfs from './ipfs'
+
+export function logo (color) {
+ const p = path.resolve(path.join(__dirname, '../img'))
+
+ if (os.platform() === 'darwin') {
+ return path.join(p, `icons/${color}.png`)
+ }
+
+ return path.join(p, `ipfs-logo-${color}.png`)
+}
+
+export {store, logger, getIpfs}
diff --git a/src/utils/ipfs.js b/src/utils/ipfs.js
new file mode 100644
index 000000000..5d50ed86b
--- /dev/null
+++ b/src/utils/ipfs.js
@@ -0,0 +1,79 @@
+import IPFSFactory from 'ipfsd-ctl'
+import IPFSApi from 'ipfs-api'
+import { join } from 'path'
+import fs from 'fs-extra'
+import logger from './logger'
+
+function cleanLocks (path) {
+ // This fixes a bug on Windows, where the daemon seems
+ // not to be exiting correctly, hence the file is not
+ // removed.
+ logger.info('Cleaning repo.lock and api files')
+ const lockPath = join(path, 'repo.lock')
+ const apiPath = join(path, 'api')
+
+ if (fs.existsSync(lockPath)) {
+ try {
+ fs.unlinkSync(lockPath)
+ } catch (_) {
+ logger.warn('Could not remove repo.lock. Daemon might be running')
+ }
+ }
+
+ if (fs.existsSync(apiPath)) {
+ try {
+ fs.unlinkSync(apiPath)
+ } catch (_) {
+ logger.warn('Could not remove api. Daemon might be running')
+ }
+ }
+}
+
+export default async function ({ type, apiAddress, path, flags, keysize, init }) {
+ let factOpts = { type: type }
+
+ if (type === 'proc') {
+ factOpts.exec = require('ipfs')
+ }
+
+ if (type === 'api') {
+ return IPFSApi(apiAddress)
+ }
+
+ cleanLocks(path)
+
+ const factory = IPFSFactory.create(factOpts)
+
+ return new Promise((resolve, reject) => {
+ const start = (ipfsd) => ipfsd.start(flags, (err, api) => {
+ if (err) return reject(err)
+ else resolve(ipfsd)
+ })
+
+ factory.spawn({
+ init: false,
+ start: false,
+ disposable: false,
+ defaultAddrs: true,
+ repoPath: path
+ }, (err, ipfsd) => {
+ if (err) return reject(err)
+
+ if (ipfsd.started) {
+ return resolve(ipfsd)
+ }
+
+ if (!ipfsd.initialized && init) {
+ return ipfsd.init({
+ directory: path,
+ keysize: keysize
+ }, err => {
+ if (err) return reject(err)
+ else start(ipfsd)
+ })
+ }
+
+ start(ipfsd)
+ })
+ })
+}
diff --git a/src/utils/key-value-store.js b/src/utils/key-value-store.js
deleted file mode 100644
index a81e374fe..000000000
--- a/src/utils/key-value-store.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import FileStore from './file-store'
-
-/**
- * It's a key value store on a file.
- * @extends FileStore
- */
-export default class KeyValueStore extends FileStore {
- constructor (location, d = {}) {
- super(location, d)
- }
-
- /**
- * Gets a value.
- * @param {String} key
- * @returns {Any}
- */
- get (key) {
- return this.data[key]
- }
-
- /**
- * Sets a key with a value.
- * @param {String} key
- * @param {String} value
- * @returns {Void}
- */
- set (key, value) {
- this.emit(key, value, this.data[key])
- this.data[key] = value
- this.write()
- }
-}
diff --git a/src/utils/logger.js b/src/utils/logger.js
new file mode 100644
index 000000000..d39680a0e
--- /dev/null
+++ b/src/utils/logger.js
@@ -0,0 +1,36 @@
+import { createLogger, format, transports } from 'winston'
+import { join } from 'path'
+import { app } from 'electron'
+
+const { combine, splat, timestamp, printf } = format
+const logsPath = app.getPath('userData')
+
+const errorFile = new transports.File({
+ level: 'error',
+ filename: join(logsPath, 'error.log')
+})
+
+errorFile.on('finish', () => {
+ process.exit(1)
+})
+
+const logger = createLogger({
+ format: combine(
+ timestamp(),
+ splat(),
+ printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
+ ),
+ transports: [
+ new transports.Console({
+ level: 'debug',
+ silent: process.env.NODE_ENV === 'production'
+ }),
+ errorFile,
+ new transports.File({
+ level: 'debug',
+ filename: join(logsPath, 'combined.log')
+ })
+ ]
+})
+
+export default logger
diff --git a/src/utils/store.js b/src/utils/store.js
new file mode 100644
index 000000000..070fb0607
--- /dev/null
+++ b/src/utils/store.js
@@ -0,0 +1,20 @@
+import Store from 'electron-store'
+
+const store = new Store()
+
+if (store.get('version', 1) === 1) {
+ // migrate data to v2
+ const path = store.get('ipfsPath', null)
+ const dhtClient = store.get('dhtClient', true)
+
+ store.delete('ipfsPath')
+ store.delete('dhtClient')
+
+ store.set('ipfs.path', path)
+ store.set('ipfs.flags', dhtClient ? ['--routing=dhtclient'] : [])
+
+ // set new config version
+ store.set('version', 2)
+}
+
+export default store