diff --git a/package.json b/package.json index 7abc9051e..78d475951 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "dependencies": { "auto-launch": "^5.0.5", "bl": "^2.0.1", - "debug": "^3.1.0", "electron-compile": "^6.4.3", "electron-menubar": "^1.0.1", "electron-squirrel-startup": "^1.0.0", + "electron-store": "^2.0.0", "file-extension": "^4.0.5", + "fs-extra": "^7.0.0", "go-ipfs-dep": "^0.4.17", + "ipfs": "^0.31.2", "ipfs-stats": "^1.2.4", "ipfsd-ctl": "^0.39.0", "is-ipfs": "^0.4.2", @@ -26,7 +28,8 @@ "react": "^16.4.1", "react-dnd": "^5.0.0", "react-dnd-html5-backend": "^5.0.1", - "react-dom": "^16.4.1" + "react-dom": "^16.4.1", + "winston": "^3.0.0" }, "devDependencies": { "babel-eslint": "^8.2.6", diff --git a/src/components/Heartbeat.js b/src/components/Heartbeat.js index 8a83ca432..dec62ad17 100644 --- a/src/components/Heartbeat.js +++ b/src/components/Heartbeat.js @@ -15,14 +15,14 @@ const blackLogo = resolve(join(__dirname, '../img/ipfs-logo-black.png')) * * @return {ReactElement} */ -export default function Heartbeat (props) { - if (props.dead) { +export default function Heartbeat ({dead, className, ...props}) { + if (dead) { return ( - + ) } else { return ( - + ) } } @@ -32,5 +32,6 @@ Heartbeat.propTypes = { } Heartbeat.defaultProps = { - dead: false + dead: false, + className: '' } diff --git a/src/config.js b/src/config.js deleted file mode 100644 index 978dc5eaa..000000000 --- a/src/config.js +++ /dev/null @@ -1,86 +0,0 @@ -import path from 'path' -import fs from 'fs' -import os from 'os' -import {app, dialog} from 'electron' -import dbgger from 'debug' -import {EventEmitter} from 'events' - -import KeyValueStore from './utils/key-value-store' - -const debug = dbgger('desktop') - -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`) -} - -function ensurePath (path) { - if (!fs.existsSync(path)) { - fs.mkdirSync(path) - } - - return path -} - -const ipfsAppData = ensurePath(path.join(app.getPath('appData'), 'ipfs-desktop')) -const logsPath = ensurePath(path.join(ipfsAppData, 'logs')) - -const settingsStore = new KeyValueStore(path.join(ipfsAppData, 'config.json'), { - dhtClient: true -}) - -if (!settingsStore.get('ipfsPath')) { - const p = path.join(process.env.IPFS_PATH || (process.env.HOME || process.env.USERPROFILE), '.ipfs') - settingsStore.set('ipfsPath', p) -} - -function fatal (error) { - debug(`Uncaught Exception: ${error.stack}`) - - dialog.showErrorBox( - 'Something wrong happened', - `Some unexpected error occurred and we couldn't handle it. Please check ${path.join(logsPath, 'error.log')}` + - ` for the latest logs and open an issue on https://github.com/ipfs-shipyard/ipfs-desktop/issues.` - ) - - process.exit(1) -} - -// Set up what to do on Uncaught Exceptions and Unhandled Rejections -process.on('uncaughtException', fatal) -process.on('unhandledRejection', fatal) - -export default { - events: new EventEmitter(), - debug: debug, - appData: ipfsAppData, - settingsStore: settingsStore, - logo: { - ice: logo('ice'), - black: logo('black') - }, - // Will be replaced by a Menubar instance. - 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: (settingsStore.get('lightTheme') ? '#FFFFFF' : '#000000'), - webPreferences: { - nodeIntegration: true, - webSecurity: false - } - } - } -} diff --git a/src/controls/main/auto-launch.js b/src/controls/main/auto-launch.js index 5fb1638c9..c4070fb08 100644 --- a/src/controls/main/auto-launch.js +++ b/src/controls/main/auto-launch.js @@ -1,27 +1,28 @@ -const AutoLaunch = require('auto-launch') +import AutoLaunch from 'auto-launch' +import { store, logger } from '../../utils' const settingsOption = 'autoLaunch' const autoLauncher = new AutoLaunch({ name: 'IPFS Desktop' }) -export default function (opts) { - let {debug, settingsStore} = opts - - let activate = (value, oldValue) => { +export default function () { + let activate = async (value, oldValue) => { if (value === oldValue) return - if (value === true) { - autoLauncher.enable() - .then(() => { debug('Launch on startup enabled') }) - .catch(e => { debug(e.stack) }) - } else { - autoLauncher.disable() - .then(() => { debug('Launch on startup disabled') }) - .catch(e => { debug(e.stack) }) + try { + if (value === true) { + await autoLauncher.enable() + logger.info('Launch on startup enabled') + } else { + await autoLauncher.disable() + logger.info('Launch on startup disabled') + } + } catch (e) { + logger.error(e.stack) } } - activate(settingsStore.get(settingsOption)) - settingsStore.on(settingsOption, activate) + activate(store.get(settingsOption)) + store.onDidChange(settingsOption, activate) } diff --git a/src/controls/main/download-hash.js b/src/controls/main/download-hash.js index 06983bb27..b4da9472e 100644 --- a/src/controls/main/download-hash.js +++ b/src/controls/main/download-hash.js @@ -1,15 +1,16 @@ import path from 'path' -import fs from 'fs' -import {clipboard, app, dialog, globalShortcut} from 'electron' -import {validateIPFS} from '../utils' +import fs from 'fs-extra' +import { clipboard, app, dialog, globalShortcut } from 'electron' +import { validateIPFS } from '../utils' +import { store, logger } from '../../utils' const settingsOption = 'downloadHashShortcut' const shortcut = 'CommandOrControl+Alt+D' function selectDirectory (opts) { - const {menubar} = opts + const { menubar } = opts - return new Promise((resolve, reject) => { + return new Promise(resolve => { dialog.showOpenDialog(menubar.window, { title: 'Select a directory', defaultPath: app.getPath('downloads'), @@ -27,28 +28,26 @@ function selectDirectory (opts) { }) } -function saveFile (opts, dir, file) { - const {debug} = opts +async function saveFile (dir, file) { const location = path.join(dir, file.path) - if (fs.existsSync(location)) { - // Ignore the hash itself. + if (await fs.pathExists(location)) { + // ignore the hash itself return } - fs.writeFile(location, file.content, (err) => { - if (err) { - debug(err.stack) - } else { - debug(`File '${file.path}' downloaded to ${location}.`) - } - }) + try { + await fs.writeFile(location, file.content) + logger.info(`File '${file.path}' downloaded to ${location}.`) + } catch (e) { + logger.error(e.stack) + } } function handler (opts) { - const {debug, ipfs} = opts + const { ipfs } = opts - return () => { + return async () => { const text = clipboard.readText().trim() if (!ipfs() || !text) { @@ -63,49 +62,45 @@ function handler (opts) { return } - ipfs().get(text) - .then((files) => { - debug(`Hash ${text} downloaded.`) - selectDirectory(opts) - .then((dir) => { - if (!dir) { - debug(`Dropping hash ${text}: user didn't choose a path.`) - return - } - - if (files.length > 1) { - fs.mkdirSync(path.join(dir, text)) - } - - files.forEach(file => { saveFile(opts, dir, file) }) - }) - .catch(e => debug(e.stack)) - }) - .catch(e => { - debug(e.stack) - dialog.showErrorBox( - 'Error while downloading', - 'Some error happened while getting the hash. Please check the logs.' - ) - }) + try { + const files = await ipfs().get(text) + logger.info(`Hash ${text} downloaded.`) + + const dir = await selectDirectory(opts) + + if (!dir) { + logger.info(`Dropping hash ${text}: user didn't choose a path.`) + return + } + + if (files.length > 1) { + fs.mkdirSync(path.join(dir, text)) + } + + files.forEach(file => { saveFile(dir, file) }) + } catch (e) { + logger.error(e.stack) + dialog.showErrorBox( + 'Error while downloading', + 'Some error happened while getting the hash. Please check the logs.' + ) + } } } export default function (opts) { - let {debug, settingsStore} = opts - let activate = (value, oldValue) => { if (value === oldValue) return if (value === true) { globalShortcut.register(shortcut, handler(opts)) - debug('Hash download shortcut enabled') + logger.info('Hash download shortcut enabled') } else { globalShortcut.unregister(shortcut) - debug('Hash download shortcut disabled') + logger.info('Hash download shortcut disabled') } } - activate(settingsStore.get(settingsOption)) - settingsStore.on(settingsOption, activate) + activate(store.get(settingsOption)) + store.onDidChange(settingsOption, activate) } diff --git a/src/controls/main/files.js b/src/controls/main/files.js index 333aad895..0728fcda4 100644 --- a/src/controls/main/files.js +++ b/src/controls/main/files.js @@ -1,5 +1,6 @@ import {ipcMain} from 'electron' import uploadFiles from '../utils/upload-files' +import { logger } from '../../utils' function basename (path) { const parts = path.split('/') @@ -17,62 +18,73 @@ function sort (a, b) { return a.name > b.name } -function listAndSend (opts, root) { - const {debug, ipfs, send} = opts - - ipfs().files.ls(root) - .then(files => { - Promise.all(files.map(file => { - return ipfs().files.stat([root, file.name].join('/')) - .then(stats => Object.assign({}, file, stats)) - })) - .then(res => res.sort(sort)) - .then(res => send('files', { - root: root, - contents: res - })) - .catch(e => { debug(e.stack) }) +async function listAndSend (opts, root) { + const {ipfs, send} = opts + + try { + const files = await ipfs().files.ls(root) + + const res = (await Promise.all(files.map(file => ( + ipfs().files.stat([root, file.name].join('/')) + .then(stats => Object.assign({}, file, stats)) + )))).sort(sort) + + send('files', { + root: root, + contents: res }) + } catch (e) { + logger.error(e.stack) + } } function list (opts) { - return (event, root) => { + return (_, root) => { listAndSend(opts, root) } } function createDirectory (opts) { - const {ipfs, debug} = opts + const { ipfs } = opts - return (event, path) => { - ipfs().files.mkdir(path, {parents: true}) - .then(() => { listAndSend(opts, basename(path)) }) - .catch(e => { debug(e.stack) }) + return async (_, path) => { + try { + await ipfs().files.mkdir(path, {parents: true}) + listAndSend(opts, basename(path)) + } catch (e) { + logger.error(e.stack) + } } } function remove (opts) { - const {ipfs, debug} = opts + const { ipfs } = opts - return (event, path) => { - ipfs().files.rm(path, {recursive: true}) - .then(() => { listAndSend(opts, basename(path)) }) - .catch(e => { debug(e.stack) }) + return async (_, path) => { + try { + await ipfs().files.rm(path, {recursive: true}) + listAndSend(opts, basename(path)) + } catch (e) { + logger.error(e.stack) + } } } function move (opts) { - const {ipfs, debug} = opts + const { ipfs } = opts - return (event, from, to) => { - ipfs().files.mv([from, to]) - .then(() => { listAndSend(opts, basename(to)) }) - .catch(e => { debug(e.stack) }) + return async (_, from, to) => { + try { + await ipfs().files.mv([from, to]) + listAndSend(opts, basename(to)) + } catch (e) { + logger.error(e.stack) + } } } export default function (opts) { - const {menubar} = opts + const { menubar } = opts ipcMain.on('request-files', list(opts)) ipcMain.on('create-directory', createDirectory(opts)) diff --git a/src/controls/main/gc.js b/src/controls/main/gc.js index e79c02655..99d621220 100644 --- a/src/controls/main/gc.js +++ b/src/controls/main/gc.js @@ -1,12 +1,17 @@ import {ipcMain} from 'electron' +import { logger } from '../../utils' function gc (opts) { - let {debug, ipfs} = opts + let { ipfs } = opts - return () => { - ipfs().repo.gc() - .then(() => { debug('Garbage collector run sucessfully') }) - .catch(e => { debug(e.stack) }) + return async () => { + try { + logger.info('Garbage collector starting') + await ipfs().repo.gc() + logger.info('Garbage collector run sucessfully') + } catch (e) { + logger.error(e.stack) + } } } diff --git a/src/controls/main/menu-shortcuts.js b/src/controls/main/menu-shortcuts.js index 51ac13482..21f7d7345 100644 --- a/src/controls/main/menu-shortcuts.js +++ b/src/controls/main/menu-shortcuts.js @@ -1,6 +1,6 @@ import {Menu} from 'electron' -export default function (opts) { +export default function () { Menu.setApplicationMenu(Menu.buildFromTemplate([ { // Edit menu entries so the shortcuts work on macOS diff --git a/src/controls/main/open-file-dialog.js b/src/controls/main/open-file-dialog.js index b561b129c..7f791a553 100644 --- a/src/controls/main/open-file-dialog.js +++ b/src/controls/main/open-file-dialog.js @@ -2,10 +2,8 @@ import {dialog, ipcMain} from 'electron' import uploadFiles from '../utils/upload-files' function openFileDialog (opts, dir = false) { - let window = opts.window - return (event, root) => { - dialog.showOpenDialog(window, { + dialog.showOpenDialog(opts.menubar.window, { properties: [dir ? 'openDirectory' : 'openFile', 'multiSelections'] }, (files) => { if (!files || files.length === 0) return diff --git a/src/controls/main/open-webui.js b/src/controls/main/open-webui.js index b05659632..c15fb4b1b 100644 --- a/src/controls/main/open-webui.js +++ b/src/controls/main/open-webui.js @@ -1,15 +1,17 @@ import {shell, ipcMain} from 'electron' import {apiAddrToUrl} from '../utils' +import { logger } from '../../utils' function open (opts) { - let {debug, ipfs} = opts + let { ipfs } = opts - return () => { - ipfs().config.get('Addresses.API') - .then((res) => { - shell.openExternal(apiAddrToUrl(res)) - }) - .catch(e => { debug(e.stack) }) + return async () => { + try { + const res = await ipfs().config.get('Addresses.API') + shell.openExternal(apiAddrToUrl(res)) + } catch (e) { + logger.error(e.stack) + } } } diff --git a/src/controls/main/pinned-files.js b/src/controls/main/pinned-files.js index fe77f9d43..2880143c2 100644 --- a/src/controls/main/pinned-files.js +++ b/src/controls/main/pinned-files.js @@ -1,5 +1,6 @@ import {dialog, ipcMain} from 'electron' import {validateIPFS} from '../utils' +import { logger } from '../../utils' import bl from 'bl' const PATH = '/.pinset' @@ -46,7 +47,7 @@ function writePinset (opts) { } function pinset (opts) { - const {ipfs, debug} = opts + const {ipfs} = opts return () => { pins = {} @@ -75,12 +76,12 @@ function pinset (opts) { return writePinset(opts) }) - .catch(error => debug(error.stack)) + .catch(error => logger.error(error.stack)) } } function pinHash (opts) { - const {ipfs, send, debug} = opts + const {ipfs, send} = opts let pinning = 0 @@ -98,44 +99,42 @@ function pinHash (opts) { } inc() - debug(`Pinning ${hash}`) + logger.info(`Pinning ${hash}`) ipfs().pin.add(hash) .then(() => { dec() - debug(`${hash} pinned`) + logger.info(`${hash} pinned`) pins[hash] = tag return writePinset(opts) }) .catch(e => { dec() - debug(e.stack) + logger.error(e.stack) }) } } function unpinHash (opts) { - const {ipfs, debug} = opts + const {ipfs} = opts - return (event, hash) => { - debug(`Unpinning ${hash}`) + return (_, hash) => { + logger.info(`Unpinning ${hash}`) ipfs().pin.rm(hash) .then(() => { - debug(`${hash} unpinned`) + logger.info(`${hash} unpinned`) delete pins[hash] return writePinset(opts) }) - .catch(e => { debug(e.stack) }) + .catch(e => { logger.error(e.stack) }) } } function tagHash (opts) { - const {debug} = opts - - return (event, hash, tag) => { + return (_, hash, tag) => { pins[hash] = tag - writePinset(opts).catch(e => { debug(e.stack) }) + writePinset(opts).catch(e => { logger.error(e.stack) }) } } diff --git a/src/controls/main/settings.js b/src/controls/main/settings.js index 81c0e1824..1d9ed93b4 100644 --- a/src/controls/main/settings.js +++ b/src/controls/main/settings.js @@ -1,32 +1,77 @@ -import {join} from 'path' -import {shell, ipcMain} from 'electron' +import { join } from 'path' +import { shell, ipcMain, app, dialog } from 'electron' +import { store } from '../../utils' -function openNodeConfig (opts) { - const {settingsStore} = opts +const openNodeConfig = () => { + const path = store.get('ipfs').path + shell.openItem(join(path, 'config')) +} - return () => { - const path = settingsStore.get('ipfsPath') - shell.openItem(join(path, 'config')) +const settingsToSend = { + direct: [ + 'autoLaunch', + 'screenshotShortcut', + 'downloadHashShortcut', + 'lightTheme' + ], + flags: { + dhtClient: '--routing=dhtclient' } } -function updateSettings (opts) { - const {settingsStore} = opts +const sendSettings = ({ send }) => () => { + const options = {} + + for (const opt of settingsToSend.direct) { + options[opt] = store.get(opt, false) + } + + const flags = store.get('ipfs.flags', []) - return (event, key, value) => { - settingsStore.set(key, value) + for (const flag of Object.keys(settingsToSend.flags)) { + options[flag] = flags.includes(settingsToSend.flags[flag]) } + + send('settings', options) } -export default function (opts) { - const {send, settingsStore} = opts +const updateSettings = (opts) => (_, key, value) => { + if (settingsToSend.direct.includes(key)) { + store.set(key, value) + return sendSettings(opts)() + } - const handler = () => { - send('settings', settingsStore.toObject()) + const flags = store.get('ipfs.flags') + const flag = settingsToSend.flags[key] + const index = flags.indexOf(flag) + + if (value) { + if (index === -1) { + flags.push(flag) + } + } else { + if (index !== -1) { + flags.splice(index, 1) + } } - ipcMain.on('request-settings', handler) - settingsStore.on('change', handler) + store.set('ipfs.flags', flags) + sendSettings(opts)() +} + +const cleanIpfsSettings = () => { + store.delete('ipfs') + dialog.showMessageBox({ + type: 'info', + message: 'The IPFS settings were cleared and IPFS Desktop will shutdown now. You need to start it again afterwards.' + }, () => { + app.quit() + }) +} + +export default function (opts) { + ipcMain.on('request-settings', sendSettings(opts)) ipcMain.on('update-setting', updateSettings(opts)) - ipcMain.on('open-node-settings', openNodeConfig(opts)) + ipcMain.on('open-node-settings', openNodeConfig) + ipcMain.on('clean-ipfs-settings', cleanIpfsSettings) } diff --git a/src/controls/main/stats.js b/src/controls/main/stats.js index 31e1e35ee..5b00a1013 100644 --- a/src/controls/main/stats.js +++ b/src/controls/main/stats.js @@ -1,11 +1,12 @@ import StatsPoller from 'ipfs-stats' import {ipcMain} from 'electron' import cloneDeep from 'lodash.clonedeep' +import { logger } from '../../utils' let poller let polling = [] -function requestStats (event, stats) { +function requestStats (_, stats) { if (!poller) { // Save what to poll next. polling = stats @@ -73,12 +74,12 @@ function onChange (opts) { } export default function (opts) { - const {debug, events, menubar, ipfs} = opts + const {events, menubar, ipfs} = opts ipcMain.on('request-stats', requestStats) events.on('node:started', () => { - debug('Configuring Stats Poller') + logger.info('Configuring Stats Poller') poller = new StatsPoller(ipfs(), { all: 3 * 1000, @@ -96,7 +97,7 @@ export default function (opts) { }) events.on('node:stopped', () => { - debug('Removing Stats Poller') + logger.info('Removing Stats Poller') if (poller) { poller.stop() diff --git a/src/controls/main/take-screenshot.js b/src/controls/main/take-screenshot.js index 2a097e98b..b08522863 100644 --- a/src/controls/main/take-screenshot.js +++ b/src/controls/main/take-screenshot.js @@ -1,70 +1,72 @@ -import {clipboard, ipcMain, globalShortcut} from 'electron' +import { clipboard, ipcMain, globalShortcut } from 'electron' +import { store, logger } from '../../utils' const settingsOption = 'screenshotShortcut' const shortcut = 'CommandOrControl+Alt+S' function makeScreenshotDir (opts) { - const {ipfs} = opts + const { ipfs } = opts - return new Promise((resolve, reject) => { - ipfs().files.stat('/screenshots') - .then(resolve) - .catch(() => { - ipfs().files.mkdir('/screenshots') - .then(resolve) - .catch(reject) - }) - }) + return async () => { + try { + await ipfs().files.stat('/screenshots') + } catch (e) { + await ipfs().files.mkdir('/screenshots') + } + } } function handleScreenshot (opts) { - let {debug, ipfs, send} = opts + let { ipfs, send } = opts - return (event, image) => { + return async (_, image) => { let base64Data = image.replace(/^data:image\/png;base64,/, '') - debug('Screenshot taken') + logger.info('Screenshot taken') if (!ipfs()) { - debug('Daemon not running. Aborting screenshot upload.') + logger.info('Daemon not running. Aborting screenshot upload.') return } const path = `/screenshots/${new Date().toISOString()}.png` const content = Buffer.from(base64Data, 'base64') - makeScreenshotDir(opts) - .then(() => ipfs().files.write(path, content, {create: true})) - .then(() => ipfs().files.stat(path)) - .then((res) => { - const url = `https://ipfs.io/ipfs/${res.hash}` - clipboard.writeText(url) - send('files-updated') - debug('Screenshot uploaded', {path: path}) - }) - .catch(e => { debug(e.stack) }) + try { + await makeScreenshotDir(opts) + await ipfs().files.write(path, content, {create: true}) + + const stats = await ipfs().files.stat(path) + const url = `https://ipfs.io/ipfs/${stats.hash}` + + clipboard.writeText(url) + send('files-updated') + logger.info('Screenshot uploaded', {path: path}) + } catch (e) { + logger.error(e.stack) + } } } export default function (opts) { - let {send, debug, settingsStore} = opts + let { send } = opts let activate = (value, oldValue) => { if (value === oldValue) return if (value === true) { globalShortcut.register(shortcut, () => { - debug('Taking Screenshot') + logger.info('Taking Screenshot') send('screenshot') }) - debug('Screenshot shortcut enabled') + logger.info('Screenshot shortcut enabled') } else { globalShortcut.unregister(shortcut) - debug('Screenshot shortcut disabled') + logger.info('Screenshot shortcut disabled') } } - activate(settingsStore.get(settingsOption)) - settingsStore.on(settingsOption, activate) + activate(store.get(settingsOption)) + store.onDidChange(settingsOption, activate) ipcMain.on('screenshot', handleScreenshot(opts)) } diff --git a/src/controls/main/toggle-sticky.js b/src/controls/main/toggle-sticky.js index 0e25c0670..124229b24 100644 --- a/src/controls/main/toggle-sticky.js +++ b/src/controls/main/toggle-sticky.js @@ -1,9 +1,9 @@ -import {ipcMain} from 'electron' +import { ipcMain } from 'electron' let sticky = false export default function (opts) { - let {menubar, send} = opts + let { menubar, send } = opts ipcMain.on('toggle-sticky', () => { sticky = !sticky diff --git a/src/controls/utils/upload-files.js b/src/controls/utils/upload-files.js index f7e03479e..c38a949dd 100644 --- a/src/controls/utils/upload-files.js +++ b/src/controls/utils/upload-files.js @@ -1,4 +1,5 @@ import path from 'path' +import { logger } from '../../utils' function join (...parts) { const replace = new RegExp('/{1,}', 'g') @@ -19,7 +20,7 @@ async function add (files, root, ipfs) { } export default function uploadFiles (opts) { - let {ipfs, debug, send} = opts + let {ipfs, send} = opts const sendAdding = () => { send('adding', adding > 0) } const inc = () => { adding++; sendAdding() } @@ -31,11 +32,11 @@ export default function uploadFiles (opts) { } return (event, files, root = '/') => { - debug('Uploading files', {files}) + logger.info('Uploading files', {files}) inc() add(files, root, ipfs) .then(anyway) - .catch((e) => { anyway(); debug(e.stack) }) + .catch((e) => { anyway(); logger.error(e.stack) }) } } diff --git a/src/errors.js b/src/errors.js index 818d60570..2e4e2afa2 100644 --- a/src/errors.js +++ b/src/errors.js @@ -1,5 +1,7 @@ import {dialog} from 'electron' +import { logger } from './utils' + const errors = [ { find: [ @@ -11,7 +13,7 @@ const errors = [ } ] -export default function (e) { +export function handleKnownErrors (e) { const msg = e.toString() const error = errors.find((error) => { @@ -28,6 +30,23 @@ export default function (e) { error.message ) - process.exit(1) + process.exit(0) } } + +function fatal (error) { + logger.error(error) + logger.end() + + dialog.showErrorBox( + 'Something wrong happened', + `Some unexpected error occurred and we couldn't handle it.` + + ` for the latest logs and open an issue on https://github.com/ipfs-shipyard/ipfs-desktop/issues.` + ) +} + +export default function setupErrorHandling () { + // Set up what to do on Uncaught Exceptions and Unhandled Rejections + process.on('uncaughtException', fatal) + process.on('unhandledRejection', fatal) +} diff --git a/src/index.js b/src/index.js index d39f258c6..88f0a16de 100644 --- a/src/index.js +++ b/src/index.js @@ -1,14 +1,7 @@ -import {Menubar} from 'electron-menubar' -import fs from 'fs' -import DaemonFactory from 'ipfsd-ctl' -import {join} from 'path' -import {dialog, ipcMain, app, BrowserWindow} from 'electron' - -import config from './config' -import registerControls from './controls/main' -import handleKnownErrors from './errors' - -const {debug} = config +import setup from './setup' +import start from './start' +import setupErrorHandling from './errors' +import { app, dialog } from 'electron' // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (require('electron-squirrel-startup')) { @@ -17,276 +10,21 @@ if (require('electron-squirrel-startup')) { // Ensure it's a single instance. app.makeSingleInstance(() => { - debug('Trying to start a second instance') dialog.showErrorBox( 'Multiple instances', 'Sorry, but there can be only one instance of IPFS Desktop running at the same time.' ) }) -// Local Variables - -let IPFS -let menubar -let state = 'stopped' - -function send (type, ...args) { - if (menubar && menubar.window && menubar.window.webContents) { - menubar.window.webContents.send(type, ...args) - } -} - -config.send = send -config.ipfs = () => IPFS - -function updateState (st) { - state = st - onRequestState() -} - -function onRequestState (node, event) { - send('node-status', state) -} - -// Moves files from appData/file-history.json to MFS so -// v0.4.0 is backwards compatible with v0.3.0. -function moveFilesOver () { - const path = join(config.appData, 'file-history.json') - - if (!fs.existsSync(path)) { - return - } - - let files - - try { - files = JSON.parse(fs.readFileSync(path)) - } catch (e) { - debug(e) - return - } - - Promise.all(files.map((file) => IPFS.files.cp([`/ipfs/${file.hash}`, `/${file.name}`]))) - .then(() => { - fs.unlinkSync(path) - }) - .catch((e) => { - fs.unlinkSync(path) - debug(e) - }) -} - -function onStartDaemon (node) { - debug('Starting daemon') - updateState('starting') - - // Tries to remove the repo.lock file if it already exists. - // This fixes a bug on Windows, where the daemon seems - // not to be exiting correctly, hence the file is not - // removed. - const lockPath = join(config.settingsStore.get('ipfsPath'), 'repo.lock') - const apiPath = join(config.settingsStore.get('ipfsPath'), 'api') - - if (fs.existsSync(lockPath)) { - try { - fs.unlinkSync(lockPath) - } catch (e) { - debug('Could not remove lock. Daemon might be running.') - } - } - - if (fs.existsSync(apiPath)) { - try { - fs.unlinkSync(apiPath) - } catch (e) { - debug('Could not remove API file. Daemon might be running.') - } - } - - const flags = [] - if (config.settingsStore.get('dhtClient')) { - flags.push('--routing=dhtclient') - } - - node.start(flags, (err, api) => { - if (err) { - handleKnownErrors(err) - return - } - - IPFS = api - debug('Daemon started') - config.events.emit('node:started') - - if (node.subprocess) { - // Stop the executation of the program if some error - // occurs on the node. - node.subprocess.on('error', (e) => { - updateState('stopped') - debug(e) - }) - } - - // Move files from V0.3.0 - moveFilesOver() - - menubar.tray.setImage(config.logo.ice) - updateState('running') - }) -} - -function onStopDaemon (node, done) { - debug('Stopping daemon') - updateState('stopping') - - config.events.emit('node:stopped') - - node.stop((err) => { - if (err) { - return debug(err.stack) - } +// Avoid quitting the app when all windows are closed +app.on('window-all-closed', e => e.preventDefault()) - debug('Stopped daemon') - menubar.tray.setImage(config.logo.black) +setupErrorHandling() - IPFS = null - updateState('stopped') - done() - }) +async function run () { + const ipfsd = await setup() + start(ipfsd) } -function onWillQuit (node, event) { - debug('Shutting down application') - - if (IPFS == null) { - return - } - - event.preventDefault() - onStopDaemon(node, () => { - app.quit() - }) -} - -// Initalize a new IPFS node -function initialize (path, node) { - debug('Initialzing new node') - - // Initialize the welcome window. - const window = new BrowserWindow({ - title: 'Welcome to IPFS', - icon: config.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() - }) - - // Send the default path as soon as the window is ready. - window.webContents.on('did-finish-load', () => { - window.webContents.send('setup-config-path', path) - }) - - // Close the application if the welcome dialog is canceled - window.once('close', () => { - if (!node.initialized) app.quit() - }) - - window.setMenu(null) - window.loadURL(`file://${__dirname}/views/welcome.html`) - - let userPath = path - - ipcMain.on('setup-browse-path', () => { - dialog.showOpenDialog(window, { - title: 'Select a directory', - defaultPath: path, - properties: [ - 'openDirectory', - 'createDirectory' - ] - }, (res) => { - if (!res) return - - userPath = res[0] - - if (!userPath.match(/.ipfs\/?$/)) { - userPath = join(userPath, '.ipfs') - } - - window.webContents.send('setup-config-path', userPath) - }) - }) - - // Wait for the user to hit 'Install IPFS' - ipcMain.on('initialize', (event, { keySize }) => { - debug(`Initializing new node with key size: ${keySize} in ${userPath}.`) - window.webContents.send('initializing') - - node.init({ - directory: userPath, - keySize: keySize - }, (err, res) => { - if (err) { - return send('initialization-error', String(err)) - } - - config.settingsStore.set('ipfsPath', userPath) - - send('initialization-complete') - updateState('stopped') - - onStartDaemon(node) - window.close() - }) - }) -} - -// main entry point -DaemonFactory.create().spawn({ - repoPath: config.settingsStore.get('ipfsPath'), - disposable: false, - init: false, - start: false, - defaultAddrs: true -}, (err, node) => { - if (err) { - // We can't start if we fail to aquire - // a ipfs node - debug(err.stack) - process.exit(1) - } - - let appReady = () => { - debug('Application is ready') - menubar.tray.setHighlightMode('always') - - ipcMain.on('request-state', onRequestState.bind(null, node)) - ipcMain.on('start-daemon', onStartDaemon.bind(null, node)) - ipcMain.on('stop-daemon', onStopDaemon.bind(null, node, () => {})) - ipcMain.on('quit-application', app.quit.bind(app)) - app.once('will-quit', onWillQuit.bind(null, node)) - - registerControls(config) - - let exists = fs.existsSync(node.repoPath) - - if (!exists) { - initialize(config.settingsStore.get('ipfsPath'), node) - } else { - onStartDaemon(node) - } - } - - menubar = new Menubar(config.menubar) - config.menubar = menubar - - if (menubar.isReady()) appReady() - else menubar.on('ready', appReady) -}) +if (app.isReady()) run() +else app.on('ready', run) diff --git a/src/panes/Files.js b/src/panes/Files.js index bbfb9ec72..37edb6705 100644 --- a/src/panes/Files.js +++ b/src/panes/Files.js @@ -102,9 +102,7 @@ class Files extends Component { visibility: (isOver && canDrop) ? 'visible' : 'hidden' } - let files = this.props.files.filter((file) => { - return !(file.name === '.pinset' && this.props.root === '/') - }).map((file) => { + let files = this.props.files.map((file) => { return ( { this.setState({ type }) } + + onEngineChange = engine => { this.setState({ engine }) } + + onKeySizeChange = keysize => { this.setState({ keysize }) } + + handleApiAddressInput = event => { this.setState({ apiAddress: event.target.value })} + onAdvancedClick = () => { - this.setState({ showAdvanced: true }) + this.setState({ advanced: true }) } onClick = (e) => { @@ -40,25 +46,27 @@ export default class Intro extends Component { ipcRenderer.send('setup-browse-path') } - render () { - let advanced = null - if (this.state.showAdvanced) { - advanced = ([ -
- - - {this.props.configPath} - -
, - - ]) + onInstall = () => { + const settings = {} + + if (this.state.type === 'Embedded') { + settings.type = this.state.engine.toLowerCase() + if (settings.type === 'javascript') { + settings.type = 'proc' + } + + settings.path = this.props.configPath + settings.flags = ['--routing=dhtclient'] + settings.keysize = this.state.keysize + } else { + settings.type = 'api' + settings.apiAddress = this.state.apiAddress } + this.props.onInstallClick(settings) + } + + render () { return (
@@ -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.type === 'Embedded' + ? ( + + ) : ( +
+

Please insert the API address of the node you want to connect to:

+ +
+ ) + } +
+ }
- {advanced} -
-
-