mirror of
https://github.com/balena-io/etcher.git
synced 2025-08-31 05:50:24 +00:00
Compare commits
116 Commits
v1.5.23
...
react-driv
Author | SHA1 | Date | |
---|---|---|---|
![]() |
868a35337c | ||
![]() |
1398ca2931 | ||
![]() |
96c865f14a | ||
![]() |
6dbd425e89 | ||
![]() |
5b2769d0e9 | ||
![]() |
5f38cca60c | ||
![]() |
78cebdb7a4 | ||
![]() |
a6aedab0a0 | ||
![]() |
4aeccbe963 | ||
![]() |
126b3fbb40 | ||
![]() |
9ea8a6134e | ||
![]() |
3706770322 | ||
![]() |
7be07bfe8c | ||
![]() |
791c047fa1 | ||
![]() |
35ad0340b9 | ||
![]() |
0a0be3a13d | ||
![]() |
86238af380 | ||
![]() |
d10073a052 | ||
![]() |
b99b0d4bf8 | ||
![]() |
27b5b1bf10 | ||
![]() |
bab9069dee | ||
![]() |
52a3258814 | ||
![]() |
da548f59d1 | ||
![]() |
ecc500907c | ||
![]() |
724dade1f6 | ||
![]() |
c5dc869c03 | ||
![]() |
273f7e4535 | ||
![]() |
a58e060138 | ||
![]() |
ef4d2fcc72 | ||
![]() |
330c06d926 | ||
![]() |
be9c36828a | ||
![]() |
17f83135c5 | ||
![]() |
543ba51d3c | ||
![]() |
33df23fc8c | ||
![]() |
3236d6b934 | ||
![]() |
e0e7775367 | ||
![]() |
198679583c | ||
![]() |
6dae2a604f | ||
![]() |
68905c6ae4 | ||
![]() |
26630c4d64 | ||
![]() |
d382f030f0 | ||
![]() |
08fca87b2f | ||
![]() |
33441a1c5c | ||
![]() |
6d8346b13a | ||
![]() |
d9b340ca45 | ||
![]() |
ebbc52ee1f | ||
![]() |
de5bee29ef | ||
![]() |
25f843ec0b | ||
![]() |
df600a9e14 | ||
![]() |
156c25cea1 | ||
![]() |
f7dd04e3de | ||
![]() |
3036d86cfa | ||
![]() |
3fccd52884 | ||
![]() |
00640274fc | ||
![]() |
a7e8fb98b3 | ||
![]() |
bed6643437 | ||
![]() |
f815e8511f | ||
![]() |
6360fd42e7 | ||
![]() |
62a9656888 | ||
![]() |
ffb89c7e5b | ||
![]() |
aa52735006 | ||
![]() |
b65526d8ee | ||
![]() |
ea8e2999ae | ||
![]() |
0b5017f992 | ||
![]() |
8bf1bdaa04 | ||
![]() |
01eb3b1c94 | ||
![]() |
3402c9f601 | ||
![]() |
13c3518c5e | ||
![]() |
821fad27dc | ||
![]() |
50a34e2f4c | ||
![]() |
2a19b2afbe | ||
![]() |
dc92d010fb | ||
![]() |
9cb27a616a | ||
![]() |
518a0ca45b | ||
![]() |
3526a0e3c5 | ||
![]() |
6386f85258 | ||
![]() |
e80106d8f8 | ||
![]() |
1145cbc75c | ||
![]() |
e669b81072 | ||
![]() |
9d78da941b | ||
![]() |
63d0f5e2c6 | ||
![]() |
dae047eff1 | ||
![]() |
8a2db8bced | ||
![]() |
792fab20e6 | ||
![]() |
f40c0f6bd3 | ||
![]() |
ccf11b9861 | ||
![]() |
1fcde5a17c | ||
![]() |
88f543dd25 | ||
![]() |
8b6f3f6022 | ||
![]() |
294ef8045a | ||
![]() |
1f7e4c886b | ||
![]() |
63c047009f | ||
![]() |
2c5f5004cc | ||
![]() |
2fa5426cf5 | ||
![]() |
428c777402 | ||
![]() |
7e2c62c520 | ||
![]() |
3d3b4f4a46 | ||
![]() |
a543dcf166 | ||
![]() |
5de54bb6bf | ||
![]() |
d95401e614 | ||
![]() |
2c835437e9 | ||
![]() |
498e70ed2b | ||
![]() |
fce5b500bf | ||
![]() |
11def54adb | ||
![]() |
9da9e73f7a | ||
![]() |
f90cd49a6d | ||
![]() |
6e72c07190 | ||
![]() |
1997e1faeb | ||
![]() |
b33b34bd71 | ||
![]() |
6a9b739541 | ||
![]() |
6cb0bdd1a4 | ||
![]() |
b73ebb6f92 | ||
![]() |
24a83260ca | ||
![]() |
fc1c1b402b | ||
![]() |
af462b3486 | ||
![]() |
3e236996c8 |
@@ -14,3 +14,6 @@ trim_trailing_whitespace = false
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.ts]
|
||||
indent_style = tab
|
||||
|
@@ -318,8 +318,6 @@ rules:
|
||||
- always
|
||||
prefer-const:
|
||||
- error
|
||||
prefer-reflect:
|
||||
- error
|
||||
prefer-spread:
|
||||
- error
|
||||
prefer-numeric-literals:
|
||||
|
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -1,4 +1,5 @@
|
||||
# Javascript files must retain LF line-endings (to keep eslint happy)
|
||||
*.ts text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
|
||||
@@ -25,6 +26,7 @@ Makefile text
|
||||
*.yml text
|
||||
*.patch text
|
||||
*.txt text
|
||||
CODEOWNERS text
|
||||
|
||||
# Binary files (no line-ending conversions)
|
||||
*.bz2 binary diff=hex
|
||||
@@ -47,4 +49,10 @@ Makefile text
|
||||
*.rpi-sdcard binary diff=hex
|
||||
*.wic binary diff=hex
|
||||
*.foo binary diff=hex
|
||||
*.eot binary diff=hex
|
||||
*.otf binary diff=hex
|
||||
*.woff binary diff=hex
|
||||
*.woff2 binary diff=hex
|
||||
*.ttf binary diff=hex
|
||||
xz-without-extension binary diff=hex
|
||||
wmic-output.txt binary diff=hex
|
||||
|
@@ -16,11 +16,67 @@
|
||||
"appId": "io.balena.etcher",
|
||||
"copyright": "Copyright 2016-2019 Balena Ltd",
|
||||
"productName": "balenaEtcher",
|
||||
"nodeGypRebuild": false,
|
||||
"nodeGypRebuild": true,
|
||||
"files": [
|
||||
"!node_modules/**/*.js.map",
|
||||
"!node_modules/**/*.h",
|
||||
"!node_modules/**/*.hpp",
|
||||
"!node_modules/**/*.cpp",
|
||||
"!node_modules/**/*.md",
|
||||
"!node_modules/**/*.ts",
|
||||
"!node_modules/**/*.coffee",
|
||||
"!node_modules/**/*.scss",
|
||||
"!node_modules/**/*.less",
|
||||
"!node_modules/**/*.hbs",
|
||||
"!node_modules/**/*.mkd",
|
||||
"!node_modules/**/LICENSE",
|
||||
"!node_modules/**/LICENCE",
|
||||
"!node_modules/**/license",
|
||||
"!node_modules/**/License",
|
||||
"!node_modules/**/LICENSE.txt",
|
||||
"!node_modules/**/Makefile",
|
||||
"!node_modules/**/.editorconfig",
|
||||
"!node_modules/**/.babelrc",
|
||||
"!node_modules/**/.prettierrc",
|
||||
"!node_modules/**/.prettierrc-*",
|
||||
"!node_modules/**/.eslintrc.yml",
|
||||
"!node_modules/**/.eslintignore",
|
||||
"!node_modules/**/.publishrc",
|
||||
"!lib/gui/app",
|
||||
"lib/gui/app/index.html",
|
||||
"generated"
|
||||
"generated",
|
||||
"!node_modules/chart.js/dist/docs",
|
||||
"!node_modules/ext2fs/config",
|
||||
"!node_modules/ext2fs/deps",
|
||||
"!node_modules/ext2fs/LICENSE",
|
||||
"!node_modules/ext2fs/src",
|
||||
"!node_modules/winusb-driver-generator/src",
|
||||
"!node_modules/winusb-driver-generator/deps",
|
||||
"!node_modules/winusb-driver-generator/ci",
|
||||
"!node_modules/rendition/__screenshots__",
|
||||
"!node_modules/polished/docs",
|
||||
"!node_modules/mermaid/src",
|
||||
"!node_modules/mermaid/dist",
|
||||
"node_modules/mermaid/dist/mermaid.core.js",
|
||||
"!node_modules/raven-js/src",
|
||||
"!node_modules/raven-js/dist",
|
||||
"node_modules/raven-js/dist/raven.js",
|
||||
"!node_modules/raven-js/plugins",
|
||||
"!node_modules/react-jsonschema-form/dist",
|
||||
"!node_modules/xxhash/deps",
|
||||
"!node_modules/xxhash/src",
|
||||
"!node_modules/unzip-stream/testData*",
|
||||
"!node_modules/usb",
|
||||
"node_modules/usb/usb.js",
|
||||
"node_modules/usb/package.json",
|
||||
"node_modules/usb/build",
|
||||
"node_modules/usb/src/binding",
|
||||
"!node_modules/roboto-fontface/fonts",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff",
|
||||
"node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff"
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.developer-tools"
|
||||
|
146
CHANGELOG.md
146
CHANGELOG.md
@@ -3,6 +3,152 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# v1.5.51
|
||||
## (2019-06-28)
|
||||
|
||||
* Update sudo-prompt to ^9.0.0 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.50
|
||||
## (2019-06-13)
|
||||
|
||||
* Option for trimming ext partitions on raw images [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.49
|
||||
## (2019-06-13)
|
||||
|
||||
* Make window size configurable [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.48
|
||||
## (2019-06-13)
|
||||
|
||||
* Don't use sudo-prompt when already elevated [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.47
|
||||
## (2019-06-10)
|
||||
|
||||
* Rework drive-selector with react + rendition [Lorenzo Alberto Maria Ambrosi]
|
||||
* Use rendition theme property for step buttons [Lorenzo Alberto Maria Ambrosi]
|
||||
* Upgrade styled-system to v4.1.0 [Lorenzo Alberto Maria Ambrosi]
|
||||
* Upgrade rendition to v8.7.2 [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.46
|
||||
## (2019-06-09)
|
||||
|
||||
* Update ext2fs to 1.0.29 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.45
|
||||
## (2019-06-04)
|
||||
|
||||
* Empty commit to trigger build [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.44
|
||||
## (2019-06-03)
|
||||
|
||||
* Fix elevation on windows when the path contains "&" or "'" [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.43
|
||||
## (2019-05-28)
|
||||
|
||||
* Revert "Include sass in webpack configs" [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.42
|
||||
## (2019-05-28)
|
||||
|
||||
* Include sass in webpack configs [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.41
|
||||
## (2019-05-27)
|
||||
|
||||
* waffle.io removal and adding a link to the license [Mateusz Hajder]
|
||||
|
||||
# v1.5.40
|
||||
## (2019-05-24)
|
||||
|
||||
* windows installer and portable version support both ia32 and x64 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.39
|
||||
## (2019-05-14)
|
||||
|
||||
* Add clean-shrinkwrap script to postshrinkwrap step [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.38
|
||||
## (2019-05-13)
|
||||
|
||||
* Add mention to usbboot compatibility [Carlo Maria Curinga]
|
||||
|
||||
# v1.5.37
|
||||
## (2019-05-13)
|
||||
|
||||
* Bump react dependency to v16.8.5 [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.36
|
||||
## (2019-05-13)
|
||||
|
||||
* Update etcher-sdk to ^2.0.9 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.35
|
||||
## (2019-05-10)
|
||||
|
||||
* Downgrade electron 4.1.5 -> 3.1.9 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.34
|
||||
## (2019-05-09)
|
||||
|
||||
* Use https url for fetching config, avoid redirection [Alexis Svinartchouk]
|
||||
* win32: fix running diskpart when the tmp file path contains spaces [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.33
|
||||
## (2019-04-30)
|
||||
|
||||
* Fix gzipped files verification percentage and dmg verification. [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.32
|
||||
## (2019-04-30)
|
||||
|
||||
* Export NPM_VERSION variable in Makefile [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.31
|
||||
## (2019-04-29)
|
||||
|
||||
* Update etcher-sdk to ^2.0.3 [Alexis Svinartchouk]
|
||||
* Update electron to 4.1.5 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.30
|
||||
## (2019-04-24)
|
||||
|
||||
* Don't show a dialog when the write fails. [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.29
|
||||
## (2019-04-19)
|
||||
|
||||
* Add support for auto-updating feature [Giovanni Garufi]
|
||||
|
||||
# v1.5.28
|
||||
## (2019-04-18)
|
||||
|
||||
* Update electron-builder to ^20.40.2 [Alexis Svinartchouk]
|
||||
* Update etcher-sdk to ^2.0.1 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.27
|
||||
## (2019-04-16)
|
||||
|
||||
* (Windows): Fix reading images from network drives when the tmp dir has spaces [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.26
|
||||
## (2019-04-12)
|
||||
|
||||
* (Windows): Fix reading images from network drives containing non ascii characters [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.25
|
||||
## (2019-04-09)
|
||||
|
||||
* New parameter in webview for opt-out analytics [Lorenzo Alberto Maria Ambrosi]
|
||||
|
||||
# v1.5.24
|
||||
## (2019-04-05)
|
||||
|
||||
* Update resin-corvus to ^2.0.3 [Alexis Svinartchouk]
|
||||
|
||||
# v1.5.23
|
||||
## (2019-04-03)
|
||||
|
||||
|
2
CODEOWNERS
Normal file
2
CODEOWNERS
Normal file
@@ -0,0 +1,2 @@
|
||||
* @thundron @zvin @jviotti
|
||||
/scripts @nazrhom
|
9
Makefile
9
Makefile
@@ -3,7 +3,7 @@
|
||||
# ---------------------------------------------------------------------
|
||||
|
||||
RESIN_SCRIPTS ?= ./scripts/resin
|
||||
NPM_VERSION ?= 6.7.0
|
||||
export NPM_VERSION ?= 6.7.0
|
||||
S3_BUCKET = artifacts.ci.balena-cloud.com
|
||||
|
||||
# This directory will be completely deleted by the `clean` rule
|
||||
@@ -150,6 +150,9 @@ sass:
|
||||
npm rebuild node-sass
|
||||
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
|
||||
|
||||
lint-ts:
|
||||
resin-lint --typescript lib
|
||||
|
||||
lint-js:
|
||||
eslint --ignore-pattern scripts/resin/**/*.js lib tests scripts bin webpack.config.js
|
||||
|
||||
@@ -169,9 +172,9 @@ lint-spell:
|
||||
--skip *.svg *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,*.wic,.DS_Store,*.dtb,*.dtbo,*.dat,*.elf,*.bin,*.foo,xz-without-extension \
|
||||
lib tests docs scripts Makefile *.md LICENSE
|
||||
|
||||
lint: lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
lint: lint-ts lint-js lint-sass lint-cpp lint-html lint-spell
|
||||
|
||||
MOCHA_OPTIONS=--recursive --reporter spec
|
||||
MOCHA_OPTIONS=--recursive --reporter spec --require ts-node/register
|
||||
|
||||
# See https://github.com/electron/spectron/issues/127
|
||||
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
|
||||
|
@@ -5,13 +5,12 @@
|
||||
Etcher is a powerful OS image flasher built with web technologies to ensure
|
||||
flashing an SDCard or USB drive is a pleasant and safe experience. It protects
|
||||
you from accidentally writing to your hard-drives, ensures every byte of data
|
||||
was written correctly and much more.
|
||||
was written correctly and much more. It can also flash directly Raspberry Pi devices that support the usbboot protocol
|
||||
|
||||
[](https://balena.io/etcher)
|
||||

|
||||
[](https://github.com/balena-io/etcher/blob/master/LICENSE)
|
||||
[](https://david-dm.org/balena-io/etcher)
|
||||
[](https://forums.balena.io/c/etcher)
|
||||
[](https://waffle.io/balena-io/etcher)
|
||||
|
||||
***
|
||||
|
||||
|
4
dev-app-update.yml
Normal file
4
dev-app-update.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
owner: balena-io
|
||||
repo: etcher
|
||||
provider: github
|
||||
updaterCacheDirName: balena-etcher-updater
|
@@ -62,7 +62,7 @@ since fresh eyes could help unveil things that we take for granted, but should
|
||||
be documented instead!
|
||||
|
||||
[lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328
|
||||
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js
|
||||
[exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/gui/app/modules/exit-codes.js
|
||||
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
|
||||
[electron]: http://electron.atom.io
|
||||
[nodejs]: https://nodejs.org
|
||||
|
@@ -1,8 +1,8 @@
|
||||
appId: io.balena.etcher
|
||||
copyright: Copyright 2016-2019 Balena Ltd
|
||||
productName: balenaEtcher
|
||||
npmRebuild: false
|
||||
nodeGypRebuild: false
|
||||
npmRebuild: true
|
||||
nodeGypRebuild: true
|
||||
publish: null
|
||||
files:
|
||||
- lib
|
||||
@@ -37,9 +37,9 @@ nsis:
|
||||
uninstallerIcon: assets/icon.ico
|
||||
deleteAppDataOnUninstall: true
|
||||
license: LICENSE
|
||||
artifactName: "${productName}-Setup-${version}-${env.TARGET_ARCH}.${ext}"
|
||||
artifactName: "${productName}-Setup-${version}.${ext}"
|
||||
portable:
|
||||
artifactName: "${productName}-Portable-${version}-${env.TARGET_ARCH}.${ext}"
|
||||
artifactName: "${productName}-Portable-${version}.${ext}"
|
||||
requestExecutionLevel: user
|
||||
linux:
|
||||
category: Utility
|
||||
|
@@ -27,30 +27,27 @@ var angular = require('angular')
|
||||
/* eslint-enable no-var */
|
||||
|
||||
const electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const sdk = require('etcher-sdk')
|
||||
const _ = require('lodash')
|
||||
const semver = require('semver')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
|
||||
const EXIT_CODES = require('../../shared/exit-codes')
|
||||
const messages = require('../../shared/messages')
|
||||
const s3Packages = require('../../shared/s3-packages')
|
||||
const release = require('../../shared/release')
|
||||
const EXIT_CODES = require('../../gui/app/modules/exit-codes')
|
||||
const messages = require('../../gui/app/modules/messages')
|
||||
const store = require('./models/store')
|
||||
const errors = require('../../shared/errors')
|
||||
const packageJSON = require('../../../package.json')
|
||||
const flashState = require('./models/flash-state')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('./models/settings')
|
||||
const windowProgress = require('./os/window-progress')
|
||||
const analytics = require('./modules/analytics')
|
||||
const updateNotifier = require('./components/update-notifier')
|
||||
const availableDrives = require('./models/available-drives')
|
||||
const selectionState = require('./models/selection-state')
|
||||
const driveScanner = require('./modules/drive-scanner')
|
||||
const osDialog = require('./os/dialog')
|
||||
const exceptionReporter = require('./modules/exception-reporter')
|
||||
const updateLock = require('./modules/update-lock')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const screensaver = require('./modules/screensaver')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method,lodash/prefer-get */
|
||||
|
||||
@@ -134,84 +131,6 @@ app.run(() => {
|
||||
version: currentVersion,
|
||||
applicationSessionUuid
|
||||
})
|
||||
|
||||
const shouldCheckForUpdates = updateNotifier.shouldCheckForUpdates({
|
||||
currentVersion,
|
||||
lastSleptUpdateNotifier: settings.get('lastSleptUpdateNotifier'),
|
||||
lastSleptUpdateNotifierVersion: settings.get('lastSleptUpdateNotifierVersion')
|
||||
})
|
||||
|
||||
const isStableRelease = release.isStableRelease(currentVersion)
|
||||
const updatesEnabled = settings.get('updatesEnabled')
|
||||
|
||||
if (!shouldCheckForUpdates || !updatesEnabled) {
|
||||
analytics.logEvent('Not checking for updates', {
|
||||
shouldCheckForUpdates,
|
||||
updatesEnabled,
|
||||
stable: isStableRelease,
|
||||
applicationSessionUuid
|
||||
})
|
||||
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
const updateSemverRange = packageJSON.updates.semverRange
|
||||
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel')
|
||||
|
||||
analytics.logEvent('Checking for updates', {
|
||||
currentVersion,
|
||||
stable: isStableRelease,
|
||||
updateSemverRange,
|
||||
includeUnstableChannel,
|
||||
applicationSessionUuid
|
||||
})
|
||||
|
||||
return s3Packages.getLatestVersion(release.getReleaseType(currentVersion), {
|
||||
range: updateSemverRange,
|
||||
includeUnstableChannel
|
||||
}).then((latestVersion) => {
|
||||
if (semver.gte(currentVersion, latestVersion || '0.0.0')) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Latest version',
|
||||
applicationSessionUuid
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
// In case the internet connection is not good and checking the
|
||||
// latest published version takes too long, only show notify
|
||||
// the user about the new version if he didn't start the flash
|
||||
// process (e.g: selected an image), otherwise such interruption
|
||||
// might be annoying.
|
||||
if (selectionState.hasImage()) {
|
||||
analytics.logEvent('Update notification skipped', {
|
||||
reason: 'Image selected',
|
||||
applicationSessionUuid
|
||||
})
|
||||
return Bluebird.resolve()
|
||||
}
|
||||
|
||||
analytics.logEvent('Notifying update', {
|
||||
latestVersion,
|
||||
applicationSessionUuid
|
||||
})
|
||||
|
||||
return updateNotifier.notify(latestVersion, {
|
||||
allowSleepUpdateCheck: isStableRelease
|
||||
})
|
||||
|
||||
// If the error is an update user error, then we don't want
|
||||
// to bother users each time they open the app.
|
||||
// See: https://github.com/resin-io/etcher/issues/1525
|
||||
}).catch((error) => {
|
||||
return errors.isUserError(error) && error.code === 'UPDATE_USER_ERROR'
|
||||
}, (error) => {
|
||||
analytics.logEvent('Update check user error', {
|
||||
title: errors.getTitle(error),
|
||||
description: errors.getDescription(error),
|
||||
applicationSessionUuid
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
||||
|
||||
app.run(() => {
|
||||
@@ -537,6 +456,32 @@ app.controller('HeaderController', function (OSOpenExternalService) {
|
||||
this.shouldShowHelp = () => {
|
||||
return !settings.get('disableExternalLinks')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Whether to show the sleep button
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* HeaderController.shouldShowSleep()
|
||||
*/
|
||||
this.shouldShowSleep = () => {
|
||||
return settings.get('showScreensaverDelay')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Enables the screensaver
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @example
|
||||
* HeaderController.sleep()
|
||||
*/
|
||||
this.sleep = () => {
|
||||
screensaver.off()
|
||||
}
|
||||
})
|
||||
|
||||
app.controller('StateController', function ($rootScope, $scope) {
|
||||
@@ -590,3 +535,5 @@ angular.element(document).ready(() => {
|
||||
angular.bootstrap(document, [ 'Etcher' ])
|
||||
}).catch(exceptionReporter.report)
|
||||
})
|
||||
|
||||
screensaver.init()
|
||||
|
@@ -19,12 +19,13 @@
|
||||
const angular = require('angular')
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const constraints = require('../../../../../shared/drive-constraints')
|
||||
const constraints = require('../../../../../gui/app/modules/drive-constraints')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
|
||||
module.exports = function (
|
||||
$q,
|
||||
|
32
lib/gui/app/components/drive-selector/index.ts
Normal file
32
lib/gui/app/components/drive-selector/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module Etcher.Components.TargetSelector
|
||||
*/
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.TargetSelector';
|
||||
const SelectTargetButton = angular.module(MODULE_NAME, []);
|
||||
|
||||
SelectTargetButton.component(
|
||||
'targetSelector',
|
||||
react2angular(require('./target-selector.jsx')),
|
||||
);
|
||||
|
||||
export = MODULE_NAME;
|
166
lib/gui/app/components/drive-selector/target-selector.jsx
Normal file
166
lib/gui/app/components/drive-selector/target-selector.jsx
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { default: styled } = require('styled-components')
|
||||
const {
|
||||
ChangeButton,
|
||||
DetailsText,
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
const { Txt } = require('rendition')
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
const { bytesToClosestUnit } = require('./../../../../gui/app/modules/units')
|
||||
|
||||
const TargetDetail = styled((props) => (
|
||||
<Txt.span {...props}>
|
||||
</Txt.span>
|
||||
)) `
|
||||
float: ${({ float }) => float}
|
||||
`
|
||||
|
||||
const TargetDisplayText = ({
|
||||
description,
|
||||
size,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Txt.span {...props}>
|
||||
<TargetDetail
|
||||
float='left'>
|
||||
{description}
|
||||
</TargetDetail>
|
||||
<TargetDetail
|
||||
float='right'
|
||||
>
|
||||
{size}
|
||||
</TargetDetail>
|
||||
</Txt.span>
|
||||
)
|
||||
}
|
||||
|
||||
const TargetSelector = (props) => {
|
||||
const targets = props.selection.getSelectedDrives()
|
||||
|
||||
if (targets.length === 1) {
|
||||
const target = targets[0]
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(target.description, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.reselectDrive}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
{ props.constraints.hasListDriveImageCompatibilityStatus(targets, props.image) &&
|
||||
<Txt.span className='glyphicon glyphicon-exclamation-sign'
|
||||
ml={2}
|
||||
tooltip={
|
||||
props.constraints.getListDriveImageCompatibilityStatuses(targets, props.image)[0].message
|
||||
}
|
||||
/>
|
||||
}
|
||||
{ bytesToClosestUnit(target.size) }
|
||||
</DetailsText>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
if (targets.length > 1) {
|
||||
const targetsTemplate = []
|
||||
for (const target of targets) {
|
||||
targetsTemplate.push((
|
||||
<DetailsText
|
||||
key={target.device}
|
||||
tooltip={
|
||||
`${target.description} ${target.displayName} ${bytesToClosestUnit(target.size)}`
|
||||
}
|
||||
px={21}
|
||||
>
|
||||
<TargetDisplayText
|
||||
description={middleEllipsis(target.description, 14)}
|
||||
size={bytesToClosestUnit(target.size)}
|
||||
>
|
||||
</TargetDisplayText>
|
||||
</DetailsText>
|
||||
))
|
||||
}
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plain
|
||||
tooltip={props.tooltip}
|
||||
>
|
||||
{targets.length} Targets
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
onClick={props.reselectDrive}
|
||||
mb={14}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
{targetsTemplate}
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<StepButton
|
||||
tabindex={(targets.length > 0) ? -1 : 2 }
|
||||
disabled={props.disabled}
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
Select target
|
||||
</StepButton>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
TargetSelector.propTypes = {
|
||||
disabled: propTypes.bool,
|
||||
openDriveSelector: propTypes.func,
|
||||
selection: propTypes.object,
|
||||
reselectDrive: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
constraints: propTypes.object,
|
||||
show: propTypes.bool,
|
||||
tooltip: propTypes.string
|
||||
}
|
||||
|
||||
module.exports = TargetSelector
|
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button class="button button-primary button-block"
|
||||
<button class="button button-primary"
|
||||
tabindex="{{ 15 + modal.getDrives().length }}"
|
||||
ng-class="{
|
||||
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())
|
||||
|
264
lib/gui/app/components/drive-selector2/drive-selector.tsx
Normal file
264
lib/gui/app/components/drive-selector2/drive-selector.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Meter } from 'grommet';
|
||||
import { sortBy } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Badge, Modal, Table } from 'rendition';
|
||||
|
||||
import { getDrives } from '../../models/available-drives';
|
||||
import { COMPATIBILITY_STATUS_TYPES } from '../../modules/drive-constraints';
|
||||
import { subscribe } from '../../models/store';
|
||||
import { ThemedProvider } from '../../styled-components';
|
||||
import { bytesToClosestUnit } from '../../modules/units';
|
||||
|
||||
interface Drive {
|
||||
description: string;
|
||||
device: string;
|
||||
isSystem: boolean;
|
||||
isReadOnly: boolean;
|
||||
progress?: number;
|
||||
size?: number;
|
||||
link?: string;
|
||||
linkCTA?: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
interface CompatibilityStatus {
|
||||
type: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface DriveSelectorProps {
|
||||
title: string;
|
||||
close: () => void;
|
||||
setSelectedDrives: (drives: Drive[]) => void;
|
||||
isDriveSelected: (drive: Drive) => boolean;
|
||||
isDriveValid: (drive: Drive) => boolean;
|
||||
getDriveBadges: (drive: Drive) => CompatibilityStatus[];
|
||||
}
|
||||
|
||||
interface DriveSelectorState {
|
||||
drives: Drive[];
|
||||
selected: Drive[];
|
||||
disabledDrives: string[];
|
||||
}
|
||||
|
||||
const modalStyle = {
|
||||
width: '800px',
|
||||
height: '600px',
|
||||
paddingTop: '20px',
|
||||
paddingLeft: '30px',
|
||||
paddingRight: '30px',
|
||||
paddingBottom: '11px',
|
||||
};
|
||||
|
||||
const titleStyle = {
|
||||
color: '#2a506f',
|
||||
};
|
||||
|
||||
const subtitleStyle = {
|
||||
marginLeft: '10px',
|
||||
fontSize: '11px',
|
||||
color: '#5b82a7',
|
||||
};
|
||||
|
||||
const wrapperStyle = {
|
||||
height: '250px',
|
||||
overflowX: 'hidden' as 'hidden',
|
||||
overflowY: 'auto' as 'auto',
|
||||
};
|
||||
|
||||
export class DriveSelector2 extends React.Component<
|
||||
DriveSelectorProps,
|
||||
DriveSelectorState
|
||||
> {
|
||||
private table: Table<Drive> | null = null;
|
||||
private columns: {
|
||||
field: keyof Drive;
|
||||
label: string;
|
||||
render?: (value: any, row: Drive) => string | number | JSX.Element | null;
|
||||
}[];
|
||||
private unsubscribe?: () => void;
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
super(props);
|
||||
this.columns = [
|
||||
{
|
||||
field: 'description',
|
||||
label: 'Name',
|
||||
} as const,
|
||||
{
|
||||
field: 'size',
|
||||
label: 'Size',
|
||||
render: this.renderSize.bind(this),
|
||||
} as const,
|
||||
{
|
||||
field: 'displayName',
|
||||
label: 'Location',
|
||||
render: this.renderLocation.bind(this),
|
||||
} as const,
|
||||
{
|
||||
field: 'isReadOnly', // We don't use this, but a valid field that is not used in another column is required
|
||||
label: ' ',
|
||||
render: this.renderBadges.bind(this),
|
||||
} as const,
|
||||
];
|
||||
this.state = this.getNewState();
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.update();
|
||||
if (this.unsubscribe === undefined) {
|
||||
this.unsubscribe = subscribe(this.update.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
if (this.unsubscribe !== undefined) {
|
||||
this.unsubscribe();
|
||||
this.unsubscribe = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private getNewState() {
|
||||
let drives: Drive[] = getDrives();
|
||||
for (let i = 0; i < drives.length; i++) {
|
||||
drives[i] = { ...drives[i] };
|
||||
}
|
||||
drives = sortBy(drives, 'device');
|
||||
const selected = drives.filter(d => this.props.isDriveSelected(d));
|
||||
const disabledDrives = drives
|
||||
.filter(d => !this.props.isDriveValid(d))
|
||||
.map(d => d.device);
|
||||
return { drives, disabledDrives, selected };
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.setState(this.getNewState());
|
||||
this.updateTableSelection();
|
||||
}
|
||||
|
||||
private updateTableSelection() {
|
||||
if (this.table !== null) {
|
||||
this.table.setRowSelection(this.state.selected);
|
||||
}
|
||||
}
|
||||
|
||||
private renderSize(size: number) {
|
||||
if (size) {
|
||||
return bytesToClosestUnit(size);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private renderLocation(displayName: string, drive: Drive) {
|
||||
const result: Array<string | JSX.Element> = [displayName];
|
||||
if (drive.link && drive.linkCTA) {
|
||||
result.push(<a href={drive.link}>{drive.linkCTA}</a>);
|
||||
}
|
||||
return <React.Fragment>{result}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderBadges(_value: any, row: Drive) {
|
||||
const result = [];
|
||||
if (row.progress !== undefined) {
|
||||
result.push(
|
||||
<Meter
|
||||
size="small"
|
||||
thickness="xxsmall"
|
||||
values={[
|
||||
{
|
||||
value: row.progress,
|
||||
label: row.progress + '%',
|
||||
color: '#2297de',
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
result.push(
|
||||
...this.props.getDriveBadges(row).map((status: CompatibilityStatus) => {
|
||||
const props: {
|
||||
key: string;
|
||||
xsmall: true;
|
||||
danger?: boolean;
|
||||
warning?: boolean;
|
||||
} = { xsmall: true, key: status.message };
|
||||
if (status.type === COMPATIBILITY_STATUS_TYPES.ERROR) {
|
||||
props.danger = true;
|
||||
} else if (status.type === COMPATIBILITY_STATUS_TYPES.WARNING) {
|
||||
props.warning = true;
|
||||
}
|
||||
return <Badge {...props}>{status.message}</Badge>;
|
||||
}),
|
||||
);
|
||||
return <React.Fragment>{result}</React.Fragment>;
|
||||
}
|
||||
|
||||
private renderTbodyPrefix() {
|
||||
if (this.state.drives.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={this.columns.length} style={{ textAlign: 'center' }}>
|
||||
<b>Connect a drive</b>
|
||||
<div>No removable drive detected.</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<ThemedProvider>
|
||||
<Modal
|
||||
titleElement={
|
||||
<div style={titleStyle}>
|
||||
{this.props.title}
|
||||
<span style={subtitleStyle}>
|
||||
{this.state.drives.length} found
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
action={`Select (${this.state.selected.length})`}
|
||||
style={modalStyle}
|
||||
done={this.props.close}
|
||||
>
|
||||
<div style={wrapperStyle}>
|
||||
<Table<Drive>
|
||||
ref={t => {
|
||||
this.table = t;
|
||||
this.updateTableSelection();
|
||||
}}
|
||||
rowKey="device"
|
||||
onCheck={this.onCheck.bind(this)}
|
||||
columns={this.columns}
|
||||
data={this.state.drives}
|
||||
disabledRows={this.state.disabledDrives}
|
||||
tbodyPrefix={this.renderTbodyPrefix()}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</ThemedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
private onCheck(checkedDrives: Drive[]): void {
|
||||
this.props.setSelectedDrives(checkedDrives);
|
||||
}
|
||||
}
|
38
lib/gui/app/components/drive-selector2/index.ts
Normal file
38
lib/gui/app/components/drive-selector2/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as angular from 'angular';
|
||||
import { react2angular } from 'react2angular';
|
||||
|
||||
import { DriveSelector2 } from './drive-selector.tsx';
|
||||
|
||||
const MODULE_NAME = 'Etcher.Components.DriveSelector2';
|
||||
|
||||
angular
|
||||
.module(MODULE_NAME, [])
|
||||
.component(
|
||||
'driveSelector2',
|
||||
react2angular(DriveSelector2, [
|
||||
'close',
|
||||
'getDriveBadges',
|
||||
'isDriveSelected',
|
||||
'isDriveValid',
|
||||
'setSelectedDrives',
|
||||
'title',
|
||||
]),
|
||||
);
|
||||
|
||||
export = MODULE_NAME;
|
@@ -18,8 +18,10 @@
|
||||
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
const angular = require('angular')
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method */
|
||||
|
@@ -25,7 +25,7 @@ const colors = require('./colors')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const files = require('../../../models/files')
|
||||
const middleEllipsis = require('../../../utils/middle-ellipsis')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
|
||||
@@ -87,7 +87,7 @@ class UnstyledFileListWrap extends React.PureComponent {
|
||||
render () {
|
||||
return (
|
||||
<Flex className={ this.props.className }
|
||||
innerRef={ ::this.setScrollElem }
|
||||
ref={ ::this.setScrollElem }
|
||||
wrap="wrap">
|
||||
{ this.props.children }
|
||||
</Flex>
|
||||
|
@@ -35,9 +35,9 @@ const selectionState = require('../../../models/selection-state')
|
||||
const store = require('../../../models/store')
|
||||
const osDialog = require('../../../os/dialog')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const errors = require('../../../../../shared/errors')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
const errors = require('../../../../../gui/app/modules/errors')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
|
||||
const debug = require('debug')('etcher:gui:file-selector')
|
||||
@@ -58,7 +58,7 @@ const Flex = styled.div`
|
||||
overflow: ${ props => props.overflow };
|
||||
`
|
||||
|
||||
const Header = Flex.extend`
|
||||
const Header = styled(Flex) `
|
||||
padding: 10px 15px 0;
|
||||
border-bottom: 1px solid ${ colors.primary.faded };
|
||||
|
||||
@@ -67,9 +67,9 @@ const Header = Flex.extend`
|
||||
}
|
||||
`
|
||||
|
||||
const Main = Flex.extend``
|
||||
const Main = styled(Flex) ``
|
||||
|
||||
const Footer = Flex.extend`
|
||||
const Footer = styled(Flex) `
|
||||
padding: 10px;
|
||||
flex: 0 0 auto;
|
||||
border-top: 1px solid ${ colors.primary.faded };
|
||||
|
@@ -72,7 +72,7 @@ class Crumb extends React.PureComponent {
|
||||
return (
|
||||
<rendition.Button
|
||||
onClick={ ::this.navigate }
|
||||
plaintext={ true }>
|
||||
plain={ true }>
|
||||
<rendition.Txt bold={ this.props.bold }>
|
||||
{ middleEllipsis(this.props.dir.name, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Txt>
|
||||
|
@@ -49,7 +49,7 @@ class RecentFileLink extends React.PureComponent {
|
||||
return (
|
||||
<rendition.Button
|
||||
onClick={ ::this.select }
|
||||
plaintext={ true }>
|
||||
plain={ true }>
|
||||
{ middleEllipsis(file.name, FILENAME_CHAR_LIMIT_SHORT) }
|
||||
</rendition.Button>
|
||||
)
|
||||
|
@@ -16,10 +16,12 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const React = require('react')
|
||||
const PropTypes = require('prop-types')
|
||||
const styled = require('styled-components').default
|
||||
const { position, right } = require('styled-system')
|
||||
const { BaseButton, ThemedProvider } = require('../../styled-components')
|
||||
|
||||
const Div = styled.div `
|
||||
${position}
|
||||
@@ -28,11 +30,15 @@ const Div = styled.div `
|
||||
|
||||
const FlashAnother = (props) => {
|
||||
return (
|
||||
<Div position='absolute' right='152px'>
|
||||
<button className="button button-primary button-brick" onClick={props.onClick.bind(null, { preserveImage: true })}>
|
||||
<b>Flash Another</b>
|
||||
</button>
|
||||
</Div>
|
||||
<ThemedProvider>
|
||||
<Div position='absolute' right='152px'>
|
||||
<BaseButton
|
||||
primary
|
||||
onClick={props.onClick.bind(null, { preserveImage: true })}>
|
||||
Flash Another
|
||||
</BaseButton>
|
||||
</Div>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -19,12 +19,12 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const { Badge, DropDownButton, Select } = require('rendition')
|
||||
const { default: styled } = require('styled-components')
|
||||
|
||||
const middleEllipsis = require('./../../utils/middle-ellipsis')
|
||||
|
||||
const { Provider } = require('rendition')
|
||||
|
||||
const shared = require('./../../../../shared/units')
|
||||
const shared = require('./../../../../gui/app/modules/units')
|
||||
const {
|
||||
StepButton,
|
||||
StepNameButton,
|
||||
@@ -32,44 +32,71 @@ const {
|
||||
Footer,
|
||||
Underline,
|
||||
DetailsText,
|
||||
ChangeButton
|
||||
ChangeButton,
|
||||
ThemedProvider
|
||||
} = require('./../../styled-components')
|
||||
|
||||
const DropdownItem = styled.p`
|
||||
padding-top: 10px;
|
||||
text-align: left;
|
||||
width: 150px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const DropdownItemIcon = styled.i`
|
||||
padding-right: 10px;
|
||||
`
|
||||
|
||||
const SelectImageButton = (props) => {
|
||||
if (props.hasImage) {
|
||||
return (
|
||||
<Provider>
|
||||
<ThemedProvider>
|
||||
<StepNameButton
|
||||
plaintext
|
||||
plain
|
||||
onClick={props.showSelectedImageDetails}
|
||||
tooltip={props.imageBasename}
|
||||
>
|
||||
{/* eslint-disable no-magic-numbers */}
|
||||
{ middleEllipsis(props.imageName || props.imageBasename, 20) }
|
||||
</StepNameButton>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={props.deselectImage}
|
||||
>
|
||||
Remove
|
||||
</ChangeButton>
|
||||
}
|
||||
<DetailsText>
|
||||
{shared.bytesToClosestUnit(props.imageSize)}
|
||||
</DetailsText>
|
||||
{ !props.flashing &&
|
||||
<ChangeButton
|
||||
plaintext
|
||||
onClick={props.reselectImage}
|
||||
>
|
||||
Change
|
||||
</ChangeButton>
|
||||
}
|
||||
</Provider>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Provider>
|
||||
<ThemedProvider>
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
<DropDownButton
|
||||
primary
|
||||
onClick={props.openImageSelector}
|
||||
label={
|
||||
<div onClick={props.openImageSelector}>Select image</div>
|
||||
}
|
||||
style={{height: '48px'}}
|
||||
>
|
||||
Select image
|
||||
</StepButton>
|
||||
<DropdownItem
|
||||
onClick={props.openImageSelector}
|
||||
>
|
||||
<DropdownItemIcon className="far fa-file"/>
|
||||
Select image file
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onClick={props.openDriveSelector}
|
||||
>
|
||||
<DropdownItemIcon className="far fa-copy"/>
|
||||
Duplicate drive
|
||||
</DropdownItem>
|
||||
</DropDownButton>
|
||||
<Footer>
|
||||
{ props.mainSupportedExtensions.join(', ') }, and{' '}
|
||||
<Underline
|
||||
@@ -79,21 +106,23 @@ const SelectImageButton = (props) => {
|
||||
</Underline>
|
||||
</Footer>
|
||||
</StepSelection>
|
||||
</Provider>
|
||||
</ThemedProvider>
|
||||
)
|
||||
}
|
||||
|
||||
SelectImageButton.propTypes = {
|
||||
openImageSelector: propTypes.func,
|
||||
openDriveSelector: propTypes.func,
|
||||
mainSupportedExtensions: propTypes.array,
|
||||
extraSupportedExtensions: propTypes.array,
|
||||
hasImage: propTypes.bool,
|
||||
showSelectedImageDetails: propTypes.func,
|
||||
imageName: propTypes.string,
|
||||
imageBasename: propTypes.string,
|
||||
reselectImage: propTypes.func,
|
||||
deselectImage: propTypes.func,
|
||||
flashing: propTypes.bool,
|
||||
imageSize: propTypes.number
|
||||
imageSize: propTypes.number,
|
||||
sourceType: propTypes.string
|
||||
}
|
||||
|
||||
module.exports = SelectImageButton
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@@ -83,6 +83,7 @@
|
||||
.modal-footer {
|
||||
flex-grow: 0;
|
||||
border: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal {
|
||||
|
@@ -20,7 +20,11 @@ const React = require('react')
|
||||
const propTypes = require('prop-types')
|
||||
const Color = require('color')
|
||||
|
||||
const { default: styled, keyframes } = require('styled-components')
|
||||
const {
|
||||
default: styled,
|
||||
css,
|
||||
keyframes
|
||||
} = require('styled-components')
|
||||
|
||||
const { ProgressBar, Provider } = require('rendition')
|
||||
|
||||
@@ -49,6 +53,10 @@ const ProgressButtonStripes = keyframes `
|
||||
}
|
||||
`
|
||||
|
||||
const ProgressButtonStripesRule = css `
|
||||
${ProgressButtonStripes} 1s linear infinite;
|
||||
`
|
||||
|
||||
const FlashProgressBar = styled(ProgressBar) `
|
||||
> div {
|
||||
width: 200px;
|
||||
@@ -83,7 +91,7 @@ const FlashProgressBarValidating = styled(FlashProgressBar) `
|
||||
|
||||
background-color: white;
|
||||
|
||||
animation: ${ProgressButtonStripes} 1s linear infinite;
|
||||
animation: ${ProgressButtonStripesRule};
|
||||
overflow: hidden;
|
||||
|
||||
background-size: 20px 20px;
|
||||
@@ -130,7 +138,6 @@ class ProgressButton extends React.Component {
|
||||
<Provider>
|
||||
<StepSelection>
|
||||
<StepButton
|
||||
primary
|
||||
onClick= { this.props.callback }
|
||||
disabled= { this.props.disabled }
|
||||
>
|
||||
|
@@ -51,6 +51,14 @@ const ETCHER_VERSION_PARAM = 'etcher-version'
|
||||
*/
|
||||
const API_VERSION_PARAM = 'api-version'
|
||||
|
||||
/**
|
||||
* @summary Opt-out analytics search-parameter key
|
||||
* @constant
|
||||
* @private
|
||||
* @type {String}
|
||||
*/
|
||||
const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics'
|
||||
|
||||
/**
|
||||
* @summary Webview API version
|
||||
* @constant
|
||||
@@ -91,6 +99,7 @@ class SafeWebview extends react.PureComponent {
|
||||
// We set the version GET parameters here.
|
||||
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version)
|
||||
url.searchParams.set(API_VERSION_PARAM, API_VERSION)
|
||||
url.searchParams.set(OPT_OUT_ANALYTICS_PARAM, !settings.get('errorReporting'))
|
||||
|
||||
this.entryHref = url.href
|
||||
|
||||
|
@@ -28,5 +28,5 @@ const react2angular = require('react2angular').react2angular
|
||||
const MODULE_NAME = 'Etcher.Components.SVGIcon'
|
||||
const angularSVGIcon = angular.module(MODULE_NAME, [])
|
||||
|
||||
angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon/svg-icon.jsx')))
|
||||
angularSVGIcon.component('svgIcon', react2angular(require('./svg-icon.jsx')))
|
||||
module.exports = MODULE_NAME
|
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const _ = require('lodash')
|
||||
const store = require('../models/store')
|
||||
const settings = require('../models/settings')
|
||||
const analytics = require('../modules/analytics')
|
||||
const units = require('../../../shared/units')
|
||||
const release = require('../../../shared/release')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
|
||||
/**
|
||||
* @summary The number of days the update notifier can be put to sleep
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Number}
|
||||
*/
|
||||
exports.UPDATE_NOTIFIER_SLEEP_DAYS = packageJSON.updates.sleepDays
|
||||
|
||||
/**
|
||||
* @summary The current Electron browser window
|
||||
* @constant
|
||||
* @private
|
||||
* @type {Object}
|
||||
*/
|
||||
const currentWindow = electron.remote.getCurrentWindow()
|
||||
|
||||
/**
|
||||
* @summary Determine if it's time to check for updates
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {Number} [options.lastSleptUpdateNotifier] - last slept update notifier time
|
||||
* @param {String} [options.lastSleptUpdateNotifierVersion] - last slept update notifier version
|
||||
* @param {String} options.currentVersion - current version
|
||||
* @returns {Boolean} should check for updates
|
||||
*
|
||||
* @example
|
||||
* if (updateNotifier.shouldCheckForUpdates({
|
||||
* lastSleptUpdateNotifier: Date.now(),
|
||||
* lastSleptUpdateNotifierVersion: '1.0.0',
|
||||
* currentVersion: '1.0.0'
|
||||
* })) {
|
||||
* console.log('We should check for updates!');
|
||||
* }
|
||||
*/
|
||||
exports.shouldCheckForUpdates = (options) => {
|
||||
if (settings.get('resinUpdateLock')) {
|
||||
return false
|
||||
}
|
||||
|
||||
_.defaults(options, {
|
||||
lastSleptUpdateNotifierVersion: options.currentVersion
|
||||
})
|
||||
|
||||
if (_.some([
|
||||
!options.lastSleptUpdateNotifier,
|
||||
!release.isStableRelease(options.currentVersion),
|
||||
options.currentVersion !== options.lastSleptUpdateNotifierVersion
|
||||
])) {
|
||||
return true
|
||||
}
|
||||
|
||||
return Date.now() - options.lastSleptUpdateNotifier > units.daysToMilliseconds(exports.UPDATE_NOTIFIER_SLEEP_DAYS)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Open the update notifier widget
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} version - version
|
||||
* @param {Object} [options] - options
|
||||
* @param {Boolean} [options.allowSleepUpdateCheck=true] - allow sleeping the update check
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* updateNotifier.notify('1.0.0-beta.16', {
|
||||
* allowSleepUpdateCheck: true
|
||||
* });
|
||||
*/
|
||||
exports.notify = (version, options = {}) => {
|
||||
const BUTTONS = [
|
||||
'Download',
|
||||
'Skip'
|
||||
]
|
||||
|
||||
const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, _.first(BUTTONS))
|
||||
const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, _.last(BUTTONS))
|
||||
|
||||
const dialogOptions = {
|
||||
type: 'info',
|
||||
buttons: BUTTONS,
|
||||
defaultId: BUTTON_CONFIRMATION_INDEX,
|
||||
cancelId: BUTTON_REJECTION_INDEX,
|
||||
title: 'New Update Available!',
|
||||
message: `Etcher ${version} is available for download`
|
||||
}
|
||||
|
||||
if (_.get(options, [ 'allowSleepUpdateCheck' ], true)) {
|
||||
_.merge(dialogOptions, {
|
||||
checkboxLabel: `Remind me again in ${exports.UPDATE_NOTIFIER_SLEEP_DAYS} days`,
|
||||
checkboxChecked: false
|
||||
})
|
||||
}
|
||||
|
||||
return new Bluebird((resolve) => {
|
||||
electron.remote.dialog.showMessageBox(currentWindow, dialogOptions, (response, checkboxChecked) => {
|
||||
return resolve({
|
||||
agreed: response === BUTTON_CONFIRMATION_INDEX,
|
||||
sleepUpdateCheck: checkboxChecked || false
|
||||
})
|
||||
})
|
||||
}).tap((results) => {
|
||||
// Only update the last slept update timestamp if the
|
||||
// user ticked the "Remind me again in ..." checkbox,
|
||||
// but didn't agree.
|
||||
if (results.sleepUpdateCheck && !results.agreed) {
|
||||
return Bluebird.all([
|
||||
settings.set('lastSleptUpdateNotifier', Date.now()),
|
||||
settings.set('lastSleptUpdateNotifierVersion', packageJSON.version)
|
||||
])
|
||||
}
|
||||
|
||||
return Bluebird.resolve()
|
||||
}).then((results) => {
|
||||
analytics.logEvent('Close update modal', {
|
||||
sleepUpdateCheck: results.sleepUpdateCheck,
|
||||
notifyVersion: version,
|
||||
currentVersion: packageJSON.version,
|
||||
agreed: results.agreed,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
|
||||
if (results.agreed) {
|
||||
electron.shell.openExternal('https://etcher.io?ref=etcher_update')
|
||||
}
|
||||
})
|
||||
}
|
@@ -11,22 +11,36 @@
|
||||
</head>
|
||||
<body>
|
||||
<header class="section-header" ng-controller="HeaderController as header">
|
||||
<button
|
||||
class="button button-link sleep-button"
|
||||
tabindex="4"
|
||||
ng-if="header.shouldShowSleep()"
|
||||
ng-click="header.sleep()"
|
||||
>
|
||||
<svg-icon paths="[ '../../assets/moon.svg' ]"
|
||||
width="'14px'"
|
||||
height="'14px'"></svg-icon>
|
||||
<span >
|
||||
Sleep
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
ng-if="header.shouldShowHelp()"
|
||||
ng-click="header.openHelpPage()"
|
||||
tabindex="4">
|
||||
tabindex="5">
|
||||
<span class="glyphicon glyphicon-question-sign"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
ui-sref="settings"
|
||||
hide-if-state="settings"
|
||||
tabindex="5">
|
||||
tabindex="6">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</button>
|
||||
|
||||
<button class="button button-link"
|
||||
tabindex="5"
|
||||
tabindex="7"
|
||||
ui-sref="main"
|
||||
show-if-state="settings">
|
||||
<span class="glyphicon glyphicon-chevron-left"></span> Back
|
||||
|
@@ -18,7 +18,7 @@
|
||||
|
||||
const _ = require('lodash')
|
||||
const store = require('./store')
|
||||
const units = require('../../../shared/units')
|
||||
const units = require('../../../gui/app/modules/units')
|
||||
|
||||
/**
|
||||
* @summary Reset flash state
|
||||
|
70
lib/gui/app/models/leds.ts
Normal file
70
lib/gui/app/models/leds.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { delay } from 'bluebird';
|
||||
import { Gpio } from 'pigpio';
|
||||
|
||||
class Led {
|
||||
private gpio: Gpio;
|
||||
|
||||
constructor(gpioNumber: number) {
|
||||
this.gpio = new Gpio(gpioNumber, { mode: Gpio.OUTPUT });
|
||||
}
|
||||
|
||||
public set intensity(value: number) {
|
||||
// TODO: check that 0 <= value <= 1
|
||||
this.gpio.pwmWrite(Math.round(value * 255));
|
||||
}
|
||||
}
|
||||
|
||||
export type Color = [number, number, number];
|
||||
export type AnimationFunction = (t: number) => Color;
|
||||
|
||||
export class RGBLed {
|
||||
private leds: [Led, Led, Led];
|
||||
private currentAnimation?: AnimationFunction;
|
||||
private static animations: Map<string, AnimationFunction> = new Map();
|
||||
|
||||
constructor(gpioNumbers: [number, number, number], public frequency = 60) {
|
||||
this.leds = gpioNumbers.map(n => new Led(n)) as [Led, Led, Led];
|
||||
}
|
||||
|
||||
private async loop() {
|
||||
while (this.currentAnimation !== undefined) {
|
||||
this.$setColor(...this.currentAnimation(new Date().getTime()));
|
||||
await delay(1000 / this.frequency);
|
||||
}
|
||||
}
|
||||
|
||||
private $setColor(red: number, green: number, blue: number) {
|
||||
this.leds[0].intensity = red;
|
||||
this.leds[1].intensity = green;
|
||||
this.leds[2].intensity = blue;
|
||||
}
|
||||
|
||||
public setColor(red: number, green: number, blue: number) {
|
||||
// stop any running animation
|
||||
this.setAnimation();
|
||||
this.$setColor(red, green, blue);
|
||||
}
|
||||
|
||||
public static registerAnimation(name: string, animation: AnimationFunction) {
|
||||
RGBLed.animations.set(name, animation);
|
||||
}
|
||||
|
||||
public setAnimation(name?: string) {
|
||||
const hadAnimation = this.currentAnimation !== undefined;
|
||||
this.currentAnimation = name ? RGBLed.animations.get(name) : undefined;
|
||||
// Don't launch the loop a second time
|
||||
if (!hadAnimation) {
|
||||
this.loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RGBLed.registerAnimation('breathe-white', (t: number) => {
|
||||
const intensity = Math.sin(t / 1000);
|
||||
return [intensity, intensity, intensity];
|
||||
});
|
||||
|
||||
RGBLed.registerAnimation('blink-white', (t: number) => {
|
||||
const intensity = Math.floor(t / 1000) % 2;
|
||||
return [intensity, intensity, intensity];
|
||||
});
|
@@ -14,18 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
import { app, remote } from 'electron';
|
||||
import { readFile, unlink, writeFile } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { inspect, promisify } from 'util';
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const readFileAsync = promisify(readFile);
|
||||
const writeFileAsync = promisify(writeFile);
|
||||
const unlinkAsync = promisify(unlink);
|
||||
|
||||
/**
|
||||
* @summary Number of spaces to indent JSON output with
|
||||
* @type {Number}
|
||||
* @constant
|
||||
*/
|
||||
const JSON_INDENT = 2
|
||||
const JSON_INDENT = 2;
|
||||
|
||||
/**
|
||||
* @summary Userdata directory path
|
||||
@@ -38,21 +41,16 @@ const JSON_INDENT = 2
|
||||
* @constant
|
||||
* @type {String}
|
||||
*/
|
||||
const USER_DATA_DIR = (() => {
|
||||
// NOTE: The ternary is due to this module being loaded both,
|
||||
// Electron's main process and renderer process
|
||||
const electron = require('electron')
|
||||
return electron.app
|
||||
? electron.app.getPath('userData')
|
||||
: electron.remote.app.getPath('userData')
|
||||
})()
|
||||
// NOTE: The ternary is due to this module being loaded both,
|
||||
// Electron's main process and renderer process
|
||||
const USER_DATA_DIR = (app || remote.app).getPath('userData');
|
||||
|
||||
/**
|
||||
* @summary Configuration file path
|
||||
* @type {String}
|
||||
* @constant
|
||||
*/
|
||||
const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json')
|
||||
const CONFIG_PATH = join(USER_DATA_DIR, 'config.json');
|
||||
|
||||
/**
|
||||
* @summary Read a local config.json file
|
||||
@@ -68,26 +66,15 @@ const CONFIG_PATH = path.join(USER_DATA_DIR, 'config.json')
|
||||
* console.log(settings)
|
||||
* })
|
||||
*/
|
||||
const readConfigFile = (filename) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
fs.readFile(filename, { encoding: 'utf8' }, (error, contents) => {
|
||||
let data = {}
|
||||
if (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(error)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
data = JSON.parse(contents)
|
||||
} catch (parseError) {
|
||||
console.error(parseError)
|
||||
}
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
async function readConfigFile(filename: string): Promise<any> {
|
||||
try {
|
||||
return JSON.parse(await readFileAsync(filename, { encoding: 'utf8' }));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to load settings from ${filename}: ${inspect(error)}`,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,17 +93,10 @@ const readConfigFile = (filename) => {
|
||||
* console.log('data written')
|
||||
* })
|
||||
*/
|
||||
const writeConfigFile = (filename, data) => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
const contents = JSON.stringify(data, null, JSON_INDENT)
|
||||
fs.writeFile(filename, contents, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve(data)
|
||||
}
|
||||
})
|
||||
})
|
||||
async function writeConfigFile(filename: string, data: any) {
|
||||
const contents = JSON.stringify(data, null, JSON_INDENT);
|
||||
await writeFileAsync(filename, contents);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,8 +112,8 @@ const writeConfigFile = (filename, data) => {
|
||||
* console.log(settings);
|
||||
* });
|
||||
*/
|
||||
exports.readAll = () => {
|
||||
return readConfigFile(CONFIG_PATH)
|
||||
export async function readAll(): Promise<any> {
|
||||
return await readConfigFile(CONFIG_PATH);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,8 +132,8 @@ exports.readAll = () => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.writeAll = (settings) => {
|
||||
return writeConfigFile(CONFIG_PATH, settings)
|
||||
export async function writeAll(settings: any) {
|
||||
return await writeConfigFile(CONFIG_PATH, settings);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,14 +151,6 @@ exports.writeAll = (settings) => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.clear = () => {
|
||||
return new Bluebird((resolve, reject) => {
|
||||
fs.unlink(CONFIG_PATH, (error) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
export async function clear(): Promise<void> {
|
||||
await unlinkAsync(CONFIG_PATH);
|
||||
}
|
@@ -1,233 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* @module Etcher.Models.Settings
|
||||
*/
|
||||
|
||||
const _ = require('lodash')
|
||||
const Bluebird = require('bluebird')
|
||||
const localSettings = require('./local-settings')
|
||||
const errors = require('../../../shared/errors')
|
||||
const release = require('../../../shared/release')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
const debug = require('debug')('etcher:models:settings')
|
||||
|
||||
/**
|
||||
* @summary Default settings
|
||||
* @constant
|
||||
* @type {Object}
|
||||
*/
|
||||
const DEFAULT_SETTINGS = {
|
||||
unsafeMode: false,
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
updatesEnabled: packageJSON.updates.enabled && !_.includes([ 'rpm', 'deb' ], packageJSON.packageType),
|
||||
includeUnstableUpdateChannel: !release.isStableRelease(packageJSON.version),
|
||||
lastSleptUpdateNotifier: null,
|
||||
lastSleptUpdateNotifierVersion: null,
|
||||
desktopNotifications: true
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Settings state
|
||||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
let settings = _.cloneDeep(DEFAULT_SETTINGS)
|
||||
|
||||
/**
|
||||
* @summary Reset settings to their default values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.reset().then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.reset = () => {
|
||||
debug('reset')
|
||||
|
||||
// TODO: Remove default settings from config file (?)
|
||||
settings = _.cloneDeep(DEFAULT_SETTINGS)
|
||||
return localSettings.writeAll(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the current settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} value - value
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.assign({
|
||||
* foo: 'bar'
|
||||
* }).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.assign = (value) => {
|
||||
debug('assign', value)
|
||||
if (_.isNil(value)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Missing settings'
|
||||
}))
|
||||
}
|
||||
|
||||
if (!_.isPlainObject(value)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Settings must be an object'
|
||||
}))
|
||||
}
|
||||
|
||||
const newSettings = _.assign({}, settings, value)
|
||||
|
||||
return localSettings.writeAll(newSettings)
|
||||
.then((updatedSettings) => {
|
||||
// NOTE: Only update in memory settings when successfully written
|
||||
settings = updatedSettings
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Extend the application state with the local settings
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.load().then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.load = () => {
|
||||
debug('load')
|
||||
return localSettings.readAll().then((loadedSettings) => {
|
||||
return _.assign(settings, loadedSettings)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Set a setting value
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @param {*} value - setting value
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* settings.set('unmountOnSuccess', true).then(() => {
|
||||
* console.log('Done!');
|
||||
* });
|
||||
*/
|
||||
exports.set = (key, value) => {
|
||||
debug('set', key, value)
|
||||
if (_.isNil(key)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: 'Missing setting key'
|
||||
}))
|
||||
}
|
||||
|
||||
if (!_.isString(key)) {
|
||||
return Bluebird.reject(errors.createError({
|
||||
title: `Invalid setting key: ${key}`
|
||||
}))
|
||||
}
|
||||
|
||||
const previousValue = settings[key]
|
||||
|
||||
settings[key] = value
|
||||
|
||||
return localSettings.writeAll(settings)
|
||||
.catch((error) => {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a setting value
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @returns {*} setting value
|
||||
*
|
||||
* @example
|
||||
* const value = settings.get('unmountOnSuccess');
|
||||
*/
|
||||
exports.get = (key) => {
|
||||
return _.cloneDeep(_.get(settings, [ key ]))
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if setting value exists
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String} key - setting key
|
||||
* @returns {Boolean} exists
|
||||
*
|
||||
* @example
|
||||
* const hasValue = settings.has('unmountOnSuccess');
|
||||
*/
|
||||
exports.has = (key) => {
|
||||
/* eslint-disable no-eq-null */
|
||||
return settings[key] != null
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get all setting values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Object} all setting values
|
||||
*
|
||||
* @example
|
||||
* const allSettings = settings.getAll();
|
||||
* console.log(allSettings.unmountOnSuccess);
|
||||
*/
|
||||
exports.getAll = () => {
|
||||
debug('getAll')
|
||||
return _.cloneDeep(settings)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the default setting values
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Object} all setting values
|
||||
*
|
||||
* @example
|
||||
* const defaults = settings.getDefaults();
|
||||
* console.log(defaults.unmountOnSuccess);
|
||||
*/
|
||||
exports.getDefaults = () => {
|
||||
debug('getDefaults')
|
||||
return _.cloneDeep(DEFAULT_SETTINGS)
|
||||
}
|
103
lib/gui/app/models/settings.ts
Normal file
103
lib/gui/app/models/settings.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as debug_ from 'debug';
|
||||
import { EventEmitter } from 'events';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
import { createError } from '../modules/errors';
|
||||
import { Dict } from '../modules/utils';
|
||||
import { readAll, writeAll } from './local-settings';
|
||||
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
|
||||
const debug = debug_('etcher:models:settings');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
unsafeMode: false,
|
||||
errorReporting: true,
|
||||
unmountOnSuccess: true,
|
||||
validateWriteOnSuccess: true,
|
||||
trim: false,
|
||||
updatesEnabled:
|
||||
packageJSON.updates.enabled &&
|
||||
!['rpm', 'deb'].includes(packageJSON.packageType),
|
||||
lastSleptUpdateNotifier: null,
|
||||
lastSleptUpdateNotifierVersion: null,
|
||||
desktopNotifications: true,
|
||||
};
|
||||
|
||||
let settings: Dict<any> = cloneDeep(DEFAULT_SETTINGS);
|
||||
|
||||
export const events = new EventEmitter();
|
||||
|
||||
// Exported for tests only, don't use that
|
||||
export async function reset(): Promise<void> {
|
||||
debug('reset');
|
||||
settings = cloneDeep(DEFAULT_SETTINGS);
|
||||
await writeAll(settings);
|
||||
}
|
||||
|
||||
export async function load(): Promise<any> {
|
||||
debug('load');
|
||||
const loadedSettings = await readAll();
|
||||
const oldSettings = cloneDeep(settings);
|
||||
settings = { ...settings, ...loadedSettings };
|
||||
for (const key of Object.keys(settings)) {
|
||||
const value = settings[key];
|
||||
if (!oldSettings.hasOwnProperty(key) || value !== oldSettings[key]) {
|
||||
events.emit(key, value);
|
||||
}
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function set(key: string, value: any): Promise<void> {
|
||||
debug('set', key, value);
|
||||
if (typeof key !== 'string') {
|
||||
throw createError({ title: `Invalid setting key: ${key}` });
|
||||
}
|
||||
const previousValue = settings[key];
|
||||
settings[key] = value;
|
||||
try {
|
||||
await writeAll(settings);
|
||||
} catch (error) {
|
||||
// Revert to previous value if persisting settings failed
|
||||
settings[key] = previousValue;
|
||||
throw error;
|
||||
}
|
||||
if (value !== previousValue) {
|
||||
events.emit(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
export function get(key: string): any {
|
||||
return cloneDeep(settings[key]);
|
||||
}
|
||||
|
||||
export function has(key: string): boolean {
|
||||
return settings[key] !== undefined;
|
||||
}
|
||||
|
||||
export function getAll(): any {
|
||||
debug('getAll');
|
||||
return cloneDeep(settings);
|
||||
}
|
||||
|
||||
export function getDefaults(): any {
|
||||
debug('getDefaults');
|
||||
return cloneDeep(DEFAULT_SETTINGS);
|
||||
}
|
@@ -20,11 +20,14 @@ const Immutable = require('immutable')
|
||||
const _ = require('lodash')
|
||||
const redux = require('redux')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
const constraints = require('../../../shared/drive-constraints')
|
||||
const supportedFormats = require('../../../shared/supported-formats')
|
||||
const errors = require('../../../shared/errors')
|
||||
const fileExtensions = require('../../../shared/file-extensions')
|
||||
const utils = require('../../../shared/utils')
|
||||
const constraints = require('../modules/drive-constraints')
|
||||
const supportedFormats = require('../modules/supported-formats')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../modules/errors')
|
||||
const fileExtensions = require('../modules/file-extensions')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../modules/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('./settings')
|
||||
|
||||
/**
|
||||
@@ -65,10 +68,7 @@ const flashStateNoNilFields = [
|
||||
* @constant
|
||||
* @private
|
||||
*/
|
||||
const selectImageNoNilFields = [
|
||||
'path',
|
||||
'extension'
|
||||
]
|
||||
const selectImageNoNilFields = [ 'path' ]
|
||||
|
||||
/**
|
||||
* @summary Application default state
|
||||
@@ -382,42 +382,44 @@ const storeReducer = (state = DEFAULT_STATE, action) => {
|
||||
})
|
||||
}
|
||||
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
const extension = _.toLower(action.data.extension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
|
||||
lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
|
||||
|
||||
if (lastImageExtension !== extension) {
|
||||
if (!_.isString(action.data.archiveExtension)) {
|
||||
if (!action.data.isDrive) { // We don't care about extensions if the source is a drive
|
||||
if (!_.isString(action.data.extension)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing image archive extension'
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
const archiveExtension = _.toLower(action.data.archiveExtension)
|
||||
const extension = _.toLower(action.data.extension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), extension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image archive extension: ${action.data.archiveExtension}`
|
||||
title: `Invalid image extension: ${action.data.extension}`
|
||||
})
|
||||
}
|
||||
|
||||
if (lastImageExtension !== archiveExtension) {
|
||||
throw errors.createError({
|
||||
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
|
||||
})
|
||||
let lastImageExtension = fileExtensions.getLastFileExtension(action.data.path)
|
||||
lastImageExtension = _.isString(lastImageExtension) ? _.toLower(lastImageExtension) : lastImageExtension
|
||||
|
||||
if (lastImageExtension !== extension) {
|
||||
if (!_.isString(action.data.archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: 'Missing image archive extension'
|
||||
})
|
||||
}
|
||||
|
||||
const archiveExtension = _.toLower(action.data.archiveExtension)
|
||||
|
||||
if (!_.includes(supportedFormats.getAllExtensions(), archiveExtension)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid image archive extension: ${action.data.archiveExtension}`
|
||||
})
|
||||
}
|
||||
|
||||
if (lastImageExtension !== archiveExtension) {
|
||||
throw errors.createError({
|
||||
title: `Image archive extension mismatch: ${action.data.archiveExtension} and ${lastImageExtension}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -19,19 +19,19 @@
|
||||
const _ = require('lodash')
|
||||
const resinCorvus = require('resin-corvus/browser')
|
||||
const packageJSON = require('../../../../package.json')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
const { hasProps } = require('../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { getConfig, hasProps } = require('../../../gui/app/modules/utils')
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const request = Bluebird.promisifyAll(require('request'))
|
||||
const sentryToken = settings.get('analyticsSentryToken') ||
|
||||
_.get(packageJSON, [ 'analytics', 'sentry', 'token' ])
|
||||
const mixpanelToken = settings.get('analyticsMixpanelToken') ||
|
||||
_.get(packageJSON, [ 'analytics', 'mixpanel', 'token' ])
|
||||
|
||||
const configUrl = settings.get('configUrl') || 'http://balena.io/etcher/static/config.json'
|
||||
const configUrl = settings.get('configUrl') || 'https://balena.io/etcher/static/config.json'
|
||||
|
||||
const DEFAULT_PROBABILITY = 1
|
||||
const DEFAULT_PROBABILITY = 0.1
|
||||
|
||||
const services = {
|
||||
sentry: sentryToken,
|
||||
@@ -48,36 +48,38 @@ resinCorvus.install({
|
||||
}
|
||||
})
|
||||
|
||||
getConfig(configUrl)
|
||||
.then((config) => {
|
||||
resinCorvus.setConfigs({
|
||||
mixpanel: getMixpanelConfig(config)
|
||||
})
|
||||
})
|
||||
let mixpanelSample = DEFAULT_PROBABILITY
|
||||
|
||||
/**
|
||||
* @summary Get etcher configs stored online
|
||||
* @param {String} - url where config.json is stored
|
||||
* @summary Init analytics configurations
|
||||
* @example initConfig()
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
function getConfig(url) {
|
||||
return request.getAsync(url, { json: true })
|
||||
.get('body')
|
||||
const initConfig = async () => {
|
||||
let validatedConfig = null
|
||||
try {
|
||||
const config = await getConfig(configUrl)
|
||||
const mixpanel = _.get(config, [ 'analytics', 'mixpanel' ], {})
|
||||
mixpanelSample = mixpanel.probability || DEFAULT_PROBABILITY
|
||||
if (isClientEligible(mixpanelSample)) {
|
||||
validatedConfig = validateMixpanelConfig(mixpanel)
|
||||
}
|
||||
} catch (err) {
|
||||
resinCorvus.logException(err)
|
||||
}
|
||||
resinCorvus.setConfigs({
|
||||
mixpanel: validatedConfig
|
||||
})
|
||||
}
|
||||
|
||||
initConfig()
|
||||
|
||||
/**
|
||||
* @summary Check that the client is eligible for analytics
|
||||
* @param {Object} - config
|
||||
*/
|
||||
// eslint-disable-next-line
|
||||
function getMixpanelConfig(config) {
|
||||
const analytics = config.analytics || {}
|
||||
const mixpanel = analytics.mixpanel || {}
|
||||
const probability = mixpanel.probability || DEFAULT_PROBABILITY
|
||||
if (Math.random() > probability) {
|
||||
return null
|
||||
}
|
||||
return validateMixpanelConfig(mixpanel)
|
||||
function isClientEligible(probability) {
|
||||
return Math.random() < probability
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +130,9 @@ exports.logDebug = resinCorvus.logDebug
|
||||
* image: '/dev/disk2'
|
||||
* });
|
||||
*/
|
||||
exports.logEvent = resinCorvus.logEvent
|
||||
exports.logEvent = (message, data) => {
|
||||
resinCorvus.logEvent(message, { ...data, sample: mixpanelSample })
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Log an exception
|
||||
|
@@ -20,8 +20,9 @@ const Bluebird = require('bluebird')
|
||||
const _ = require('lodash')
|
||||
const ipc = require('node-ipc')
|
||||
const sdk = require('etcher-sdk')
|
||||
const EXIT_CODES = require('../../shared/exit-codes')
|
||||
const errors = require('../../shared/errors')
|
||||
const EXIT_CODES = require('./exit-codes')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('./errors')
|
||||
|
||||
ipc.config.id = process.env.IPC_CLIENT_ID
|
||||
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT
|
||||
@@ -82,12 +83,10 @@ const terminate = (code) => {
|
||||
* @example
|
||||
* handleError(new Error('Something bad happened!'))
|
||||
*/
|
||||
const handleError = (error) => {
|
||||
const handleError = async (error) => {
|
||||
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
|
||||
Bluebird.delay(DISCONNECT_DELAY)
|
||||
.then(() => {
|
||||
terminate(EXIT_CODES.GENERAL_ERROR)
|
||||
})
|
||||
await Bluebird.delay(DISCONNECT_DELAY)
|
||||
terminate(EXIT_CODES.GENERAL_ERROR)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,42 +94,45 @@ const handleError = (error) => {
|
||||
* @param {SourceDestination} source - source
|
||||
* @param {SourceDestination[]} destinations - destinations
|
||||
* @param {Boolean} verify - whether to validate the writes or not
|
||||
* @param {Boolean} trim - whether to trim ext partitions before writing
|
||||
* @param {Function} onProgress - function to call on progress
|
||||
* @param {Function} onFail - function to call on fail
|
||||
* @param {Function} onFinish - function to call on finish
|
||||
* @param {Function} onError - function to call on error
|
||||
* @returns {Promise<void>}
|
||||
* @returns {Promise<{ bytesWritten, devices, errors} >}
|
||||
*
|
||||
* @example
|
||||
* writeAndValidate(source, destinations, verify, onProgress, onFail, onFinish, onError)
|
||||
*/
|
||||
const writeAndValidate = (source, destinations, verify, onProgress, onFail, onFinish, onError) => {
|
||||
return source.getInnerSource()
|
||||
.then((innerSource) => {
|
||||
return sdk.multiWrite.pipeSourceToDestinations(
|
||||
innerSource,
|
||||
destinations,
|
||||
onFail,
|
||||
onProgress,
|
||||
verify
|
||||
)
|
||||
})
|
||||
.then(({ failures, bytesWritten }) => {
|
||||
const result = {
|
||||
bytesWritten,
|
||||
devices: {
|
||||
failed: failures.size,
|
||||
successful: destinations.length - failures.size
|
||||
},
|
||||
errors: []
|
||||
}
|
||||
for (const [ destination, error ] of failures) {
|
||||
error.device = destination.drive.device
|
||||
result.errors.push(error)
|
||||
}
|
||||
onFinish(result)
|
||||
})
|
||||
.catch(onError)
|
||||
const writeAndValidate = async (source, destinations, verify, trim, onProgress, onFail) => {
|
||||
let innerSource = await source.getInnerSource()
|
||||
if (trim && (await innerSource.canRead())) {
|
||||
innerSource = new sdk.sourceDestination.ConfiguredSource(
|
||||
innerSource,
|
||||
trim,
|
||||
|
||||
// Create stream from file-disk (not source stream)
|
||||
true
|
||||
)
|
||||
}
|
||||
const { failures, bytesWritten } = await sdk.multiWrite.pipeSourceToDestinations(
|
||||
innerSource,
|
||||
destinations,
|
||||
onFail,
|
||||
onProgress,
|
||||
verify
|
||||
)
|
||||
const result = {
|
||||
bytesWritten,
|
||||
devices: {
|
||||
failed: failures.size,
|
||||
successful: destinations.length - failures.size
|
||||
},
|
||||
errors: []
|
||||
}
|
||||
for (const [ destination, error ] of failures) {
|
||||
error.device = destination.drive.device
|
||||
result.errors.push(error)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
@@ -159,7 +161,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
terminate(EXIT_CODES.SUCCESS)
|
||||
})
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('write', (options) => {
|
||||
ipc.of[IPC_SERVER_ID].on('write', async (options) => {
|
||||
/**
|
||||
* @summary Progress handler
|
||||
* @param {Object} state - progress state
|
||||
@@ -172,52 +174,20 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
|
||||
let exitCode = EXIT_CODES.SUCCESS
|
||||
|
||||
/**
|
||||
* @summary Finish handler
|
||||
* @param {Object} results - Flash results
|
||||
* @example
|
||||
* writer.on('finish', onFinish)
|
||||
*/
|
||||
const onFinish = (results) => {
|
||||
log(`Finish: ${results.bytesWritten}`)
|
||||
results.errors = _.map(results.errors, (error) => {
|
||||
return errors.toJSON(error)
|
||||
})
|
||||
ipc.of[IPC_SERVER_ID].emit('done', { results })
|
||||
Bluebird.delay(DISCONNECT_DELAY)
|
||||
.then(() => {
|
||||
terminate(exitCode)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Abort handler
|
||||
* @example
|
||||
* writer.on('abort', onAbort)
|
||||
*/
|
||||
const onAbort = () => {
|
||||
const onAbort = async () => {
|
||||
log('Abort')
|
||||
ipc.of[IPC_SERVER_ID].emit('abort')
|
||||
Bluebird.delay(DISCONNECT_DELAY)
|
||||
.then(() => {
|
||||
terminate(exitCode)
|
||||
})
|
||||
await Bluebird.delay(DISCONNECT_DELAY)
|
||||
terminate(exitCode)
|
||||
}
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('cancel', onAbort)
|
||||
|
||||
/**
|
||||
* @summary Error handler
|
||||
* @param {Error} error - error
|
||||
* @example
|
||||
* writer.on('error', onError)
|
||||
*/
|
||||
const onError = (error) => {
|
||||
log(`Error: ${error.message}`)
|
||||
exitCode = EXIT_CODES.GENERAL_ERROR
|
||||
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Failure handler (non-fatal errors)
|
||||
* @param {SourceDestination} destination - destination
|
||||
@@ -234,24 +204,36 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
}
|
||||
|
||||
const destinations = _.map(options.destinations, 'device')
|
||||
const dests = _.map(options.destinations, (destination) => {
|
||||
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
|
||||
})
|
||||
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
||||
writeAndValidate(
|
||||
source,
|
||||
dests,
|
||||
options.validateWriteOnSuccess,
|
||||
onProgress,
|
||||
onFail,
|
||||
onFinish,
|
||||
onError
|
||||
)
|
||||
|
||||
log(`Image: ${options.imagePath}`)
|
||||
log(`Devices: ${destinations.join(', ')}`)
|
||||
log(`Umount on success: ${options.unmountOnSuccess}`)
|
||||
log(`Validate on success: ${options.validateWriteOnSuccess}`)
|
||||
log(`Trim: ${options.trim}`)
|
||||
const dests = _.map(options.destinations, (destination) => {
|
||||
return new sdk.sourceDestination.BlockDevice(destination, options.unmountOnSuccess)
|
||||
})
|
||||
const source = new sdk.sourceDestination.File(options.imagePath, sdk.sourceDestination.File.OpenFlags.Read)
|
||||
try {
|
||||
const results = await writeAndValidate(
|
||||
source,
|
||||
dests,
|
||||
options.validateWriteOnSuccess,
|
||||
options.trim,
|
||||
onProgress,
|
||||
onFail
|
||||
)
|
||||
log(`Finish: ${results.bytesWritten}`)
|
||||
results.errors = _.map(results.errors, (error) => {
|
||||
return errors.toJSON(error)
|
||||
})
|
||||
ipc.of[IPC_SERVER_ID].emit('done', { results })
|
||||
await Bluebird.delay(DISCONNECT_DELAY)
|
||||
terminate(exitCode)
|
||||
} catch (error) {
|
||||
log(`Error: ${error.message}`)
|
||||
exitCode = EXIT_CODES.GENERAL_ERROR
|
||||
ipc.of[IPC_SERVER_ID].emit('error', errors.toJSON(error))
|
||||
}
|
||||
})
|
||||
|
||||
ipc.of[IPC_SERVER_ID].on('connect', () => {
|
@@ -19,6 +19,7 @@
|
||||
const sdk = require('etcher-sdk')
|
||||
const process = require('process')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
|
||||
/**
|
||||
|
347
lib/gui/app/modules/errors.ts
Normal file
347
lib/gui/app/modules/errors.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
assign,
|
||||
flow,
|
||||
invoke,
|
||||
isEmpty,
|
||||
isError,
|
||||
isNil,
|
||||
isPlainObject,
|
||||
isString,
|
||||
toString,
|
||||
trim,
|
||||
} from 'lodash';
|
||||
|
||||
import { Dict } from './utils';
|
||||
|
||||
const INDENTATION_SPACES = 2;
|
||||
|
||||
/**
|
||||
* @summary Human-friendly error messages
|
||||
*/
|
||||
export const HUMAN_FRIENDLY: Dict<{
|
||||
title: (error?: { path?: string }) => string;
|
||||
description: (error?: any) => string;
|
||||
}> = {
|
||||
ENOENT: {
|
||||
title: (error: { path: string }) =>
|
||||
`No such file or directory: ${error.path}`,
|
||||
description: () => "The file you're trying to access doesn't exist",
|
||||
},
|
||||
EPERM: {
|
||||
title: () => "You're not authorized to perform this operation",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions for this task',
|
||||
},
|
||||
EACCES: {
|
||||
title: () => "You don't have access to this resource",
|
||||
description: () =>
|
||||
'Please ensure you have necessary permissions to access this resource',
|
||||
},
|
||||
ENOMEM: {
|
||||
title: () => 'Your system ran out of memory',
|
||||
description: () =>
|
||||
'Please make sure your system has enough available memory for this task',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Get user friendly property from an error
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @param {String} property - HUMAN_FRIENDLY property
|
||||
* @returns {(String|Undefined)} user friendly message
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('My error');
|
||||
* error.code = 'ENOMEM';
|
||||
*
|
||||
* const friendlyDescription = getUserFriendlyMessageProperty(error, 'description');
|
||||
*
|
||||
* if (friendlyDescription) {
|
||||
* console.log(friendlyDescription);
|
||||
* }
|
||||
*/
|
||||
function getUserFriendlyMessageProperty(
|
||||
error: { code?: string; path?: string },
|
||||
property: 'title' | 'description',
|
||||
): string | null {
|
||||
const code = error.code;
|
||||
if (!isString(code)) {
|
||||
return null;
|
||||
}
|
||||
return invoke(HUMAN_FRIENDLY, [code, property], error);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if a string is blank
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @param {String} string - string
|
||||
* @returns {Boolean} whether the string is blank
|
||||
*
|
||||
* @example
|
||||
* if (isBlank(' ')) {
|
||||
* console.log('The string is blank');
|
||||
* }
|
||||
*/
|
||||
const isBlank = flow([trim, isEmpty]);
|
||||
|
||||
/**
|
||||
* @summary Get the title of an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* Try to get as much information as possible about the error
|
||||
* rather than falling back to generic messages right away.
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {String} error title
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('Foo bar');
|
||||
* const title = errors.getTitle(error);
|
||||
* console.log(title);
|
||||
*/
|
||||
export function getTitle(error: Error | Dict<any>): string {
|
||||
if (!isError(error) && !isPlainObject(error) && !isNil(error)) {
|
||||
return toString(error);
|
||||
}
|
||||
|
||||
const codeTitle = getUserFriendlyMessageProperty(error, 'title');
|
||||
if (!isNil(codeTitle)) {
|
||||
return codeTitle;
|
||||
}
|
||||
|
||||
const message = error.message;
|
||||
if (!isBlank(message)) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const code = error.code;
|
||||
if (!isNil(code) && !isBlank(code)) {
|
||||
return `Error code: ${code}`;
|
||||
}
|
||||
|
||||
return 'An error ocurred';
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the description of an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {String} error description
|
||||
*
|
||||
* @example
|
||||
* const error = new Error('Foo bar');
|
||||
* const description = errors.getDescription(error);
|
||||
* console.log(description);
|
||||
*/
|
||||
export function getDescription(error: {
|
||||
code?: string;
|
||||
description?: string;
|
||||
stack?: string;
|
||||
}): string {
|
||||
if (!isError(error) && !isPlainObject(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!isBlank(error.description)) {
|
||||
return error.description as string;
|
||||
}
|
||||
|
||||
const codeDescription = getUserFriendlyMessageProperty(error, 'description');
|
||||
if (!isNil(codeDescription)) {
|
||||
return codeDescription;
|
||||
}
|
||||
|
||||
if (error.stack) {
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
if (isEmpty(error)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return JSON.stringify(error, null, INDENTATION_SPACES);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create an error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.title - error title
|
||||
* @param {String} [options.description] - error description
|
||||
* @param {Boolean} [options.report] - report error
|
||||
* @returns {Error} error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createError({
|
||||
* title: 'Foo'
|
||||
* description: 'Bar'
|
||||
* });
|
||||
*
|
||||
* throw error;
|
||||
*/
|
||||
export function createError(options: {
|
||||
title: string;
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
}): Error & { description?: string; report?: boolean; code?: string } {
|
||||
if (isBlank(options.title)) {
|
||||
throw new Error(`Invalid error title: ${options.title}`);
|
||||
}
|
||||
|
||||
const error: Error & {
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
} = new Error(options.title);
|
||||
error.description = options.description;
|
||||
|
||||
if (!isNil(options.report) && !options.report) {
|
||||
error.report = false;
|
||||
}
|
||||
|
||||
if (!isNil(options.code)) {
|
||||
error.code = options.code;
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Create a user error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* User errors represent invalid states that the user
|
||||
* caused, that are not errors on the application itself.
|
||||
* Therefore, user errors don't get reported to analytics
|
||||
* and error reporting services.
|
||||
*
|
||||
* @returns {Error} user error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createUserError({
|
||||
* title: 'Foo',
|
||||
* description: 'Bar'
|
||||
* });
|
||||
*
|
||||
* throw error;
|
||||
*/
|
||||
export function createUserError(options: {
|
||||
title: string;
|
||||
description: string;
|
||||
code?: string;
|
||||
}): Error {
|
||||
return createError({
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
report: false,
|
||||
code: options.code,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if an error is an user error
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error
|
||||
* @returns {Boolean} whether the error is a user error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.createUserError('Foo', 'Bar');
|
||||
*
|
||||
* if (errors.isUserError(error)) {
|
||||
* console.log('This error is a user error');
|
||||
* }
|
||||
*/
|
||||
export function isUserError(error: { report?: boolean }): boolean {
|
||||
return isNil(error.report) ? false : !error.report;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert an Error object to a JSON object
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} error - error object
|
||||
* @returns {Object} json error
|
||||
*
|
||||
* @example
|
||||
* const error = errors.toJSON(new Error('foo'))
|
||||
*
|
||||
* console.log(error.message);
|
||||
* > 'foo'
|
||||
*/
|
||||
export function toJSON(
|
||||
error: Error & {
|
||||
description?: string;
|
||||
report?: boolean;
|
||||
code?: string;
|
||||
syscall?: string;
|
||||
errno?: string | number;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
device?: any;
|
||||
},
|
||||
) {
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
description: error.description,
|
||||
stack: error.stack,
|
||||
report: error.report,
|
||||
code: error.code,
|
||||
syscall: error.syscall,
|
||||
errno: error.errno,
|
||||
stdout: error.stdout,
|
||||
stderr: error.stderr,
|
||||
device: error.device,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert a JSON object to an Error object
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Error} json - json object
|
||||
* @returns {Object} error object
|
||||
*
|
||||
* @example
|
||||
* const error = errors.fromJSON(errors.toJSON(new Error('foo')));
|
||||
*
|
||||
* console.log(error.message);
|
||||
* > 'foo'
|
||||
*/
|
||||
export function fromJSON(json: Dict<any>): Error {
|
||||
return assign(new Error(json.message), json);
|
||||
}
|
@@ -24,10 +24,12 @@ const ipc = require('node-ipc')
|
||||
const isRunningInAsar = require('electron-is-running-in-asar')
|
||||
const electron = require('electron')
|
||||
const store = require('../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
const flashState = require('../models/flash-state')
|
||||
const errors = require('../../../shared/errors')
|
||||
const permissions = require('../../../shared/permissions')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../gui/app/modules/errors')
|
||||
const permissions = require('../../../gui/app/modules/permissions')
|
||||
const windowProgress = require('../os/window-progress')
|
||||
const analytics = require('../modules/analytics')
|
||||
const updateLock = require('./update-lock')
|
||||
@@ -172,7 +174,8 @@ exports.performWrite = (image, drives, onProgress) => {
|
||||
uuid: flashState.getFlashUuid(),
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess')
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
trim: settings.get('trim')
|
||||
}
|
||||
|
||||
ipc.server.on('fail', ({ device, error }) => {
|
||||
@@ -200,8 +203,8 @@ exports.performWrite = (image, drives, onProgress) => {
|
||||
imagePath: image,
|
||||
destinations: drives,
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
checksumAlgorithms: [ 'xxhash' ]
|
||||
trim: settings.get('trim'),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -259,14 +262,14 @@ exports.performWrite = (image, drives, onProgress) => {
|
||||
})
|
||||
}
|
||||
|
||||
return resolve(flashResults)
|
||||
resolve(flashResults)
|
||||
}).catch((error) => {
|
||||
// This happens when the child is killed using SIGKILL
|
||||
const SIGKILL_EXIT_CODE = 137
|
||||
if (error.code === SIGKILL_EXIT_CODE) {
|
||||
error.code = 'ECHILDDIED'
|
||||
}
|
||||
return reject(error)
|
||||
reject(error)
|
||||
}).finally(() => {
|
||||
console.log('Terminating IPC server')
|
||||
terminateServer()
|
||||
@@ -313,6 +316,7 @@ exports.flash = (image, drives) => {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
trim: settings.get('trim'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
}
|
||||
@@ -376,6 +380,7 @@ exports.cancel = () => {
|
||||
flashInstanceUuid: flashState.getFlashUuid(),
|
||||
unmountOnSuccess: settings.get('unmountOnSuccess'),
|
||||
validateWriteOnSuccess: settings.get('validateWriteOnSuccess'),
|
||||
trim: settings.get('trim'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid,
|
||||
status: 'cancel'
|
||||
|
@@ -171,7 +171,7 @@ module.exports = {
|
||||
},
|
||||
|
||||
genericFlashError: () => {
|
||||
return 'Oops, seems something went wrong.'
|
||||
return 'Something went wrong. If it is a compressed image, please check that the archive is not corrupted.'
|
||||
},
|
||||
|
||||
validation: () => {
|
224
lib/gui/app/modules/permissions.js
Executable file
224
lib/gui/app/modules/permissions.js
Executable file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable lodash/prefer-lodash-method,quotes,no-magic-numbers,require-jsdoc */
|
||||
|
||||
'use strict'
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const childProcess = Bluebird.promisifyAll(require('child_process'))
|
||||
const fs = require('fs')
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
const sudoPrompt = Bluebird.promisifyAll(require('sudo-prompt'))
|
||||
const { promisify } = require('util')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('./errors')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { tmpFileDisposer } = require('./utils')
|
||||
|
||||
const writeFileAsync = promisify(fs.writeFile)
|
||||
|
||||
/**
|
||||
* @summary The user id of the UNIX "superuser"
|
||||
* @constant
|
||||
* @type {Number}
|
||||
*/
|
||||
const UNIX_SUPERUSER_USER_ID = 0
|
||||
|
||||
/**
|
||||
* @summary Check if the current process is running with elevated permissions
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
* This function has been adapted from https://github.com/sindresorhus/is-elevated,
|
||||
* which was originally licensed under MIT.
|
||||
*
|
||||
* We're not using such module directly given that it
|
||||
* contains dependencies with dynamic undeclared dependencies,
|
||||
* causing a mess when trying to concatenate the code.
|
||||
*
|
||||
* @fulfil {Boolean} - whether the current process has elevated permissions
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* permissions.isElevated().then((isElevated) => {
|
||||
* if (isElevated) {
|
||||
* console.log('This process has elevated permissions');
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
exports.isElevated = () => {
|
||||
if (os.platform() === 'win32') {
|
||||
// `fltmc` is available on WinPE, XP, Vista, 7, 8, and 10
|
||||
// Works even when the "Server" service is disabled
|
||||
// See http://stackoverflow.com/a/28268802
|
||||
return childProcess.execAsync('fltmc')
|
||||
.then(_.constant(true))
|
||||
.catch({
|
||||
code: os.constants.errno.EPERM
|
||||
}, _.constant(false))
|
||||
}
|
||||
|
||||
return Bluebird.resolve(process.geteuid() === UNIX_SUPERUSER_USER_ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if the current process is running with elevated permissions
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @description
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* permissions.isElevatedUnixSync()
|
||||
* if (isElevated) {
|
||||
* console.log('This process has elevated permissions');
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
exports.isElevatedUnixSync = () => {
|
||||
return (process.geteuid() === UNIX_SUPERUSER_USER_ID)
|
||||
}
|
||||
|
||||
const escapeSh = (value) => {
|
||||
// Make sure it's a string
|
||||
// Replace ' -> '\'' (closing quote, escaped quote, opening quote)
|
||||
// Surround with quotes
|
||||
return `'${String(value).replace(/'/g, "'\\''")}'`
|
||||
}
|
||||
|
||||
const escapeParamCmd = (value) => {
|
||||
// Make sure it's a string
|
||||
// Escape " -> \"
|
||||
// Surround with double quotes
|
||||
return `"${String(value).replace(/"/g, '\\"')}"`
|
||||
}
|
||||
|
||||
const setEnvVarSh = (value, name) => {
|
||||
return `export ${name}=${escapeSh(value)}`
|
||||
}
|
||||
|
||||
const setEnvVarCmd = (value, name) => {
|
||||
return `set "${name}=${String(value)}"`
|
||||
}
|
||||
|
||||
// Exported for tests
|
||||
exports.createLaunchScript = (command, argv, environment) => {
|
||||
const isWindows = os.platform() === 'win32'
|
||||
const lines = []
|
||||
if (isWindows) {
|
||||
// Switch to utf8
|
||||
lines.push('chcp 65001')
|
||||
}
|
||||
const [ setEnvVarFn, escapeFn ] = isWindows ? [ setEnvVarCmd, escapeParamCmd ] : [ setEnvVarSh, escapeSh ]
|
||||
lines.push(..._.map(environment, setEnvVarFn))
|
||||
lines.push([ command, ...argv ].map(escapeFn).join(' '))
|
||||
return lines.join(os.EOL)
|
||||
}
|
||||
|
||||
const elevateScriptWindows = async (path) => {
|
||||
// './nativeModule' imported here as it only exists on windows
|
||||
// TODO: replace this with sudo-prompt once https://github.com/jorangreef/sudo-prompt/issues/96 is fixed
|
||||
const nativeModule = require('./native-module')
|
||||
const elevateAsync = promisify(nativeModule.load('elevator').elevate)
|
||||
|
||||
// '&' needs to be escaped here (but not when written to a .cmd file)
|
||||
const cmd = [ 'cmd', '/c', escapeParamCmd(path).replace(/&/g, '^&') ]
|
||||
const { cancelled } = await elevateAsync(cmd)
|
||||
return { cancelled }
|
||||
}
|
||||
|
||||
const elevateScriptUnix = async (path, name) => {
|
||||
const cmd = [ 'sh', escapeSh(path) ].join(' ')
|
||||
const [ , stderr ] = await sudoPrompt.execAsync(cmd, { name })
|
||||
if (!_.isEmpty(stderr)) {
|
||||
throw errors.createError({ title: stderr })
|
||||
}
|
||||
return { cancelled: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Elevate a command
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {String[]} command - command arguments
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.applicationName - application name
|
||||
* @param {Object} options.environment - environment variables
|
||||
* @fulfil {Object} - elevation results
|
||||
* @returns {Promise}
|
||||
*
|
||||
* @example
|
||||
* permissions.elevateCommand([ 'foo', 'bar' ], {
|
||||
* applicationName: 'My App',
|
||||
* environment: {
|
||||
* FOO: 'bar'
|
||||
* }
|
||||
* }).then((results) => {
|
||||
* if (results.cancelled) {
|
||||
* console.log('Elevation has been cancelled');
|
||||
* }
|
||||
* });
|
||||
*/
|
||||
exports.elevateCommand = async (command, options) => {
|
||||
if (await exports.isElevated()) {
|
||||
await childProcess.execFileAsync(command[0], command.slice(1), { env: options.environment })
|
||||
return { cancelled: false }
|
||||
}
|
||||
const isWindows = os.platform() === 'win32'
|
||||
const launchScript = exports.createLaunchScript(command[0], command.slice(1), options.environment)
|
||||
return Bluebird.using(tmpFileDisposer({ postfix: '.cmd' }), async ({ path }) => {
|
||||
await writeFileAsync(path, launchScript)
|
||||
if (isWindows) {
|
||||
return elevateScriptWindows(path)
|
||||
}
|
||||
try {
|
||||
return await elevateScriptUnix(path, options.applicationName)
|
||||
} catch (error) {
|
||||
// We're hardcoding internal error messages declared by `sudo-prompt`.
|
||||
// There doesn't seem to be a better way to handle these errors, so
|
||||
// for now, we should make sure we double check if the error messages
|
||||
// have changed every time we upgrade `sudo-prompt`.
|
||||
console.log('error', error)
|
||||
if (_.includes(error.message, 'is not in the sudoers file')) {
|
||||
throw errors.createUserError({
|
||||
title: "Your user doesn't have enough privileges to proceed",
|
||||
description: 'This application requires sudo privileges to be able to write to drives'
|
||||
})
|
||||
} else if (_.startsWith(error.message, 'Command failed:')) {
|
||||
throw errors.createUserError({
|
||||
title: 'The elevated process died unexpectedly',
|
||||
description: `The process error code was ${error.code}`
|
||||
})
|
||||
} else if (error.message === 'User did not grant permission.') {
|
||||
return { cancelled: true }
|
||||
} else if (error.message === 'No polkit authentication agent found.') {
|
||||
throw errors.createUserError({
|
||||
title: 'No polkit authentication agent found',
|
||||
description: 'Please install a polkit authentication agent for your desktop environment of choice to continue'
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
})
|
||||
}
|
@@ -16,9 +16,11 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
const utils = require('../../../shared/utils')
|
||||
const units = require('../../../shared/units')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../gui/app/modules/utils')
|
||||
const units = require('../../../gui/app/modules/units')
|
||||
|
||||
/**
|
||||
* @summary Make the progress status subtitle string
|
||||
|
100
lib/gui/app/modules/screensaver.ts
Normal file
100
lib/gui/app/modules/screensaver.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2019 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as settings from '../models/settings';
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const EVENT_TYPES = [
|
||||
'focus',
|
||||
'keydown',
|
||||
'keyup',
|
||||
'pointerdown',
|
||||
'pointermove',
|
||||
'pointerup',
|
||||
] as const;
|
||||
|
||||
function exec(
|
||||
command: string,
|
||||
...args: string[]
|
||||
): Promise<{ stdout: string; stderr: string }> {
|
||||
return execFileAsync(command, args);
|
||||
}
|
||||
|
||||
async function screenOff(): Promise<void> {
|
||||
await exec('xset', 'dpms', 'force', 'suspend');
|
||||
}
|
||||
|
||||
async function ledsOn(): Promise<void> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
async function ledsOff(): Promise<void> {
|
||||
// TODO
|
||||
}
|
||||
|
||||
export async function off() {
|
||||
await Promise.all([ledsOff(), screenOff()]);
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
let delay: number | null = null;
|
||||
|
||||
async function listener() {
|
||||
if (timeout !== undefined) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
if (delay !== null) {
|
||||
timeout = setTimeout(off, delay);
|
||||
}
|
||||
await ledsOn();
|
||||
}
|
||||
|
||||
async function setDelay($delay: number | null) {
|
||||
const listenersSetUp = delay === null;
|
||||
delay = $delay;
|
||||
if (timeout !== undefined) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
if (delay === null) {
|
||||
for (const eventType of EVENT_TYPES) {
|
||||
removeEventListener(eventType, listener);
|
||||
}
|
||||
} else {
|
||||
timeout = setTimeout(screenOff, delay);
|
||||
if (!listenersSetUp) {
|
||||
for (const eventType of EVENT_TYPES) {
|
||||
addEventListener(eventType, listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function delayValue(d?: string): number | null {
|
||||
if (d === undefined || d === 'never') {
|
||||
return null;
|
||||
}
|
||||
return parseInt(d, 10) * 60 * 1000;
|
||||
}
|
||||
|
||||
export async function init(): Promise<void> {
|
||||
setDelay(delayValue(await settings.get('screensaverDelay')));
|
||||
settings.events.on('screensaverDelay', d => {
|
||||
setDelay(delayValue(d));
|
||||
});
|
||||
}
|
@@ -21,6 +21,7 @@ const EventEmitter = require('events')
|
||||
const createInactivityTimer = require('inactivity-timer')
|
||||
const debug = require('debug')('etcher:update-lock')
|
||||
const analytics = require('./analytics')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
|
||||
/* eslint-disable no-magic-numbers, callback-return */
|
||||
|
225
lib/gui/app/modules/utils.ts
Executable file
225
lib/gui/app/modules/utils.ts
Executable file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2017 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as Bluebird from 'bluebird';
|
||||
import * as _ from 'lodash';
|
||||
import * as request from 'request';
|
||||
import * as tmp from 'tmp';
|
||||
import { promisify } from 'util';
|
||||
|
||||
import * as errors from './errors';
|
||||
|
||||
const getAsync = promisify(request.get);
|
||||
|
||||
/**
|
||||
* @summary Minimum percentage value
|
||||
* @constant
|
||||
* @public
|
||||
* @type {Number}
|
||||
*/
|
||||
export const PERCENTAGE_MINIMUM = 0;
|
||||
|
||||
/**
|
||||
* @summary Maximum percentage value
|
||||
* @constant
|
||||
* @public
|
||||
* @type {Number}
|
||||
*/
|
||||
export const PERCENTAGE_MAXIMUM = 100;
|
||||
|
||||
/**
|
||||
* @summary Check if a percentage is valid
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Number} percentage - percentage
|
||||
* @returns {Boolean} whether the percentage is valid
|
||||
*
|
||||
* @example
|
||||
* if (utils.isValidPercentage(85)) {
|
||||
* console.log('The percentage is valid');
|
||||
* }
|
||||
*/
|
||||
export function isValidPercentage(percentage: number) {
|
||||
return _.every([
|
||||
_.isNumber(percentage),
|
||||
percentage >= exports.PERCENTAGE_MINIMUM,
|
||||
percentage <= exports.PERCENTAGE_MAXIMUM,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Convert a percentage to a float
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Number} percentage - percentage
|
||||
* @returns {Number} float percentage
|
||||
*
|
||||
* @example
|
||||
* const value = utils.percentageToFloat(50);
|
||||
* console.log(value);
|
||||
* > 0.5
|
||||
*/
|
||||
export function percentageToFloat(percentage: number) {
|
||||
if (!isValidPercentage(percentage)) {
|
||||
throw errors.createError({
|
||||
title: `Invalid percentage: ${percentage}`,
|
||||
});
|
||||
}
|
||||
|
||||
return percentage / PERCENTAGE_MAXIMUM;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Memoize a function
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* This workaround is needed to avoid AngularJS from getting
|
||||
* caught in an infinite digest loop when using `ngRepeat`
|
||||
* over a function that returns a mutable version of an
|
||||
* ImmutableJS object.
|
||||
*
|
||||
* The problem is that every time you call `myImmutableObject.toJS()`
|
||||
* you will get a new object, whose reference is different from
|
||||
* the one you previously got, even if the data is exactly the same.
|
||||
*
|
||||
* @param {Function} func - function that returns an ImmutableJS list
|
||||
* @param {Function} comparer - function to compare old and new args and state
|
||||
* @returns {Function} memoized function
|
||||
*
|
||||
* @example
|
||||
* const getList = () => {
|
||||
* return Store.getState().toJS().myList;
|
||||
* };
|
||||
*
|
||||
* const memoizedFunction = memoize(getList, angular.equals);
|
||||
*/
|
||||
export function memoize(
|
||||
func: (...args: any[]) => any,
|
||||
comparer: (a: any, b: any) => boolean,
|
||||
) {
|
||||
let previousTuples: any[] = [];
|
||||
|
||||
return (...restArgs: any[]) => {
|
||||
let areArgsInTuple = false;
|
||||
let state = Reflect.apply(func, this, restArgs);
|
||||
|
||||
previousTuples = _.map(previousTuples, ([oldArgs, oldState]) => {
|
||||
if (comparer(oldArgs, restArgs)) {
|
||||
areArgsInTuple = true;
|
||||
|
||||
if (comparer(state, oldState)) {
|
||||
// Use the previously memoized state for this argument
|
||||
state = oldState;
|
||||
}
|
||||
|
||||
// Update the tuple state
|
||||
return [oldArgs, state];
|
||||
}
|
||||
|
||||
// Return the tuple unchanged
|
||||
return [oldArgs, oldState];
|
||||
});
|
||||
|
||||
// Add the state associated with these args to be memoized
|
||||
if (!areArgsInTuple) {
|
||||
previousTuples.push([restArgs, state]);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Check if obj has one or many specific props
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @param {Object} obj - object
|
||||
* @param {Array<String>} props - properties
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
* @example
|
||||
* const doesIt = hasProps({ foo: 'bar' }, [ 'foo' ]);
|
||||
*/
|
||||
export function hasProps(obj: any, props: string[]) {
|
||||
return _.every(props, prop => {
|
||||
return _.has(obj, prop);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get etcher configs stored online
|
||||
* @param {String} - url where config.json is stored
|
||||
*/
|
||||
export async function getConfig(configUrl: string) {
|
||||
// @ts-ignore
|
||||
return (await getAsync(configUrl, { json: true })).body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary returns { path: String, cleanup: Function }
|
||||
* @function
|
||||
*
|
||||
* @param {Object} options - options
|
||||
*
|
||||
* @returns {Promise<{ path: String, cleanup: Function }>}
|
||||
*
|
||||
* @example
|
||||
* tmpFileAsync()
|
||||
* .then({ path, cleanup } => {
|
||||
* console.log(path)
|
||||
* cleanup()
|
||||
* });
|
||||
*/
|
||||
function tmpFileAsync(options: tmp.FileOptions) {
|
||||
return new Promise((resolve, reject) => {
|
||||
tmp.file(options, (error, path, _fd, cleanup) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ path, cleanup });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Disposer for tmpFileAsync, calls cleanup()
|
||||
* @function
|
||||
*
|
||||
* @param {Object} options - options
|
||||
*
|
||||
* @returns {Disposer<{ path: String, cleanup: Function }>}
|
||||
*
|
||||
* @example
|
||||
* await Bluebird.using(tmpFileDisposer(), ({ path }) => {
|
||||
* console.log(path);
|
||||
* })
|
||||
*/
|
||||
export function tmpFileDisposer(options: tmp.FileOptions) {
|
||||
return Bluebird.resolve(tmpFileAsync(options)).disposer(({ cleanup }) => {
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
export interface Dict<T> {
|
||||
[key: string]: T;
|
||||
}
|
@@ -19,8 +19,9 @@
|
||||
const _ = require('lodash')
|
||||
const electron = require('electron')
|
||||
const Bluebird = require('bluebird')
|
||||
const errors = require('../../../shared/errors')
|
||||
const supportedFormats = require('../../../shared/supported-formats')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../gui/app/modules/errors')
|
||||
const supportedFormats = require('../../../gui/app/modules/supported-formats')
|
||||
|
||||
/**
|
||||
* @summary Current renderer BrowserWindow instance
|
||||
|
@@ -17,6 +17,7 @@
|
||||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../models/settings')
|
||||
|
||||
/**
|
||||
|
@@ -19,6 +19,7 @@
|
||||
const electron = require('electron')
|
||||
const store = require('../../../models/store')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
|
||||
module.exports = function () {
|
||||
@@ -34,7 +35,7 @@ module.exports = function () {
|
||||
*/
|
||||
this.open = (url) => {
|
||||
// Don't open links if they're disabled by the env var
|
||||
if (settings.get('disableExternalLinks')) {
|
||||
if (settings.get('disableExternalLinks') || !url) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,8 @@
|
||||
'use strict'
|
||||
|
||||
const electron = require('electron')
|
||||
const utils = require('../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../gui/app/modules/utils')
|
||||
const progressStatus = require('../modules/progress-status')
|
||||
|
||||
/**
|
||||
|
78
lib/gui/app/os/windows-network-drives.js
Normal file → Executable file
78
lib/gui/app/os/windows-network-drives.js
Normal file → Executable file
@@ -16,40 +16,62 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const Bluebird = require('bluebird')
|
||||
const cp = require('child_process')
|
||||
const fs = require('fs')
|
||||
const _ = require('lodash')
|
||||
const os = require('os')
|
||||
const path = require('path')
|
||||
const Path = require('path')
|
||||
const process = require('process')
|
||||
const { promisify } = require('util')
|
||||
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const { tmpFileDisposer } = require('../../../gui/app/modules/utils')
|
||||
|
||||
const readFileAsync = promisify(fs.readFile)
|
||||
|
||||
const execAsync = promisify(cp.exec)
|
||||
|
||||
/**
|
||||
* @summary Promisified child_process.execFile
|
||||
* @summary Returns wmic's output for network drives
|
||||
* @function
|
||||
*
|
||||
* @param {String} file - command
|
||||
* @param {String[]} args - arguments
|
||||
* @param {Object} options - child_process.execFile options
|
||||
*
|
||||
* @returns {Promise<Object>} - { stdout, stderr }
|
||||
* @returns {Promise<String>}
|
||||
*
|
||||
* @example
|
||||
* execFileAsync('ls', [ '.' ])
|
||||
* .then(console.log);
|
||||
* const output = await getWmicNetworkDrivesOutput()
|
||||
*/
|
||||
const execFileAsync = async (file, args, options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
cp.execFile(
|
||||
file,
|
||||
args,
|
||||
options,
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
} else {
|
||||
resolve({ stdout, stderr })
|
||||
}
|
||||
}
|
||||
)
|
||||
exports.getWmicNetworkDrivesOutput = async () => {
|
||||
// Exported for tests.
|
||||
// When trying to read wmic's stdout directly from node, it is encoded with the current
|
||||
// console codepage (depending on the computer).
|
||||
// Decoding this would require getting this codepage somehow and using iconv as node
|
||||
// doesn't know how to read cp850 directly for example.
|
||||
// We could also use wmic's "/output:" switch but it doesn't work when the filename
|
||||
// contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example).
|
||||
// So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded.
|
||||
const options = {
|
||||
|
||||
// Close the file once it's created
|
||||
discardDescriptor: true,
|
||||
|
||||
// Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-")
|
||||
prefix: 'tmp'
|
||||
}
|
||||
return Bluebird.using(tmpFileDisposer(options), async ({ path }) => {
|
||||
const command = [
|
||||
Path.join(process.env.SystemRoot, 'System32', 'Wbem', 'wmic'),
|
||||
'path',
|
||||
'Win32_LogicalDisk',
|
||||
'Where',
|
||||
'DriveType="4"',
|
||||
'get',
|
||||
'DeviceID,ProviderName',
|
||||
'>',
|
||||
`"${path}"`
|
||||
]
|
||||
await execAsync(command.join(' '), { windowsHide: true })
|
||||
return readFileAsync(path, 'ucs2')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -64,12 +86,8 @@ const execFileAsync = async (file, args, options) => {
|
||||
* .then(console.log);
|
||||
*/
|
||||
const getWindowsNetworkDrives = async () => {
|
||||
const result = await execFileAsync(
|
||||
path.join(process.env.SystemRoot, 'System32', 'Wbem', 'wmic'),
|
||||
[ 'path', 'Win32_LogicalDisk', 'Where', 'DriveType="4"', 'get', 'DeviceID,ProviderName' ],
|
||||
{ windowsHide: true, windowsVerbatimArguments: true }
|
||||
)
|
||||
const couples = _.chain(result.stdout)
|
||||
const result = await exports.getWmicNetworkDrivesOutput()
|
||||
const couples = _.chain(result)
|
||||
.split('\n')
|
||||
|
||||
// Remove header line
|
||||
@@ -85,7 +103,7 @@ const getWindowsNetworkDrives = async () => {
|
||||
const colonPosition = str.indexOf(':')
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
if (colonPosition === -1) {
|
||||
throw new Error(`Can't parse wmic output: ${result.stdout}`)
|
||||
throw new Error(`Can't parse wmic output: ${result}`)
|
||||
}
|
||||
// eslint-disable-next-line no-magic-numbers
|
||||
return [ str.slice(0, colonPosition + 1), _.trim(str.slice(colonPosition + 1)) ]
|
||||
|
@@ -19,12 +19,13 @@
|
||||
const _ = require('lodash')
|
||||
const uuidV4 = require('uuid/v4')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const updateLock = require('../../../modules/update-lock')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
|
||||
module.exports = function ($state) {
|
||||
/**
|
||||
|
@@ -49,7 +49,6 @@
|
||||
|
||||
> b {
|
||||
color: $palette-theme-dark-soft-foreground;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -20,13 +20,19 @@ const _ = require('lodash')
|
||||
const angular = require('angular')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const driveConstraints = require('../../../modules/drive-constraints')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const utils = require('../../../../../shared/utils')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const utils = require('../../../../../gui/app/modules/utils')
|
||||
|
||||
module.exports = function ($timeout, DriveSelectorService) {
|
||||
this.driveSelectorModalOpen = false;
|
||||
|
||||
module.exports = function (DriveSelectorService) {
|
||||
/**
|
||||
* @summary Get drive title based on device quantity
|
||||
* @function
|
||||
@@ -101,20 +107,23 @@ module.exports = function (DriveSelectorService) {
|
||||
* DriveSelectionController.openDriveSelector();
|
||||
*/
|
||||
this.openDriveSelector = () => {
|
||||
DriveSelectorService.open().then((drive) => {
|
||||
if (!drive) {
|
||||
return
|
||||
}
|
||||
this.driveSelectorModalOpen = true;
|
||||
// Trigger re-render
|
||||
$timeout()
|
||||
//DriveSelectorService.open().then((drive) => {
|
||||
// if (!drive) {
|
||||
// return
|
||||
// }
|
||||
|
||||
selectionState.selectDrive(drive.device)
|
||||
// selectionState.selectDrive(drive.device)
|
||||
|
||||
analytics.logEvent('Select drive', {
|
||||
device: drive.device,
|
||||
unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
})
|
||||
}).catch(exceptionReporter.report)
|
||||
// analytics.logEvent('Select drive', {
|
||||
// device: drive.device,
|
||||
// unsafeMode: settings.get('unsafeMode') && !settings.get('disableUnsafeMode'),
|
||||
// applicationSessionUuid: store.getState().toJS().applicationSessionUuid,
|
||||
// flashingWorkflowUuid: store.getState().toJS().flashingWorkflowUuid
|
||||
// })
|
||||
//}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,4 +165,34 @@ module.exports = function (DriveSelectorService) {
|
||||
this.shouldShowDrivesButton = () => {
|
||||
return !settings.get('disableExplicitDriveSelection')
|
||||
}
|
||||
|
||||
this.closeDriveSelectorModal = () => {
|
||||
this.driveSelectorModalOpen = false
|
||||
// Trigger re-render
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.setSelectedDrives = (drives) => {
|
||||
const devices = drives.map(d => d.device);
|
||||
for (const drive of availableDrives.getDrives()) {
|
||||
if (devices.indexOf(drive.device) !== -1) {
|
||||
selectionState.selectDrive(drive.device)
|
||||
} else {
|
||||
selectionState.deselectDrive(drive.device)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isDriveSelected = (drive) => {
|
||||
return selectionState.isDriveSelected(drive.device)
|
||||
}
|
||||
|
||||
this.isDriveValid = (drive) => {
|
||||
return driveConstraints.isDriveValid(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
this.getDriveBadges = (drive) => {
|
||||
return driveConstraints.getDriveImageCompatibilityStatuses(drive, selectionState.getImage());
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,16 +17,16 @@
|
||||
'use strict'
|
||||
|
||||
const _ = require('lodash')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const driveScanner = require('../../../modules/drive-scanner')
|
||||
const progressStatus = require('../../../modules/progress-status')
|
||||
const notification = require('../../../os/notification')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const imageWriter = require('../../../modules/image-writer')
|
||||
const path = require('path')
|
||||
const store = require('../../../models/store')
|
||||
const constraints = require('../../../../../shared/drive-constraints')
|
||||
const constraints = require('../../../../../gui/app/modules/drive-constraints')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selection = require('../../../models/selection-state')
|
||||
|
||||
@@ -145,12 +145,13 @@ module.exports = function (
|
||||
driveScanner.stop()
|
||||
|
||||
const iconPath = '../../../assets/icon.png'
|
||||
const basename = path.basename(image.path)
|
||||
try {
|
||||
await imageWriter.flash(image.path, drives)
|
||||
if (!flashState.wasLastFlashCancelled()) {
|
||||
const flashResults = flashState.getFlashResults()
|
||||
notification.send('Flash complete!', {
|
||||
body: messages.info.flashComplete(path.basename(image.path), drives, flashResults.results.devices),
|
||||
body: messages.info.flashComplete(basename, drives, flashResults.results.devices),
|
||||
icon: iconPath
|
||||
})
|
||||
$state.go('success')
|
||||
@@ -181,7 +182,8 @@ module.exports = function (
|
||||
FlashErrorModalService.show(messages.error.childWriterDied())
|
||||
} else {
|
||||
FlashErrorModalService.show(messages.error.genericFlashError())
|
||||
exceptionReporter.report(error)
|
||||
error.image = basename
|
||||
analytics.logException(error)
|
||||
}
|
||||
} finally {
|
||||
availableDrives.setDrives([])
|
||||
|
@@ -22,10 +22,12 @@ const path = require('path')
|
||||
const sdk = require('etcher-sdk')
|
||||
|
||||
const store = require('../../../models/store')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const errors = require('../../../../../shared/errors')
|
||||
const supportedFormats = require('../../../../../shared/supported-formats')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../../../gui/app/modules/errors')
|
||||
const supportedFormats = require('../../../../../gui/app/modules/supported-formats')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const osDialog = require('../../../os/dialog')
|
||||
@@ -245,6 +247,11 @@ module.exports = function (
|
||||
this.openImageSelector()
|
||||
}
|
||||
|
||||
this.deselectImage = () => {
|
||||
selectionState.deselectImage()
|
||||
$timeout()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the basename of the selected image
|
||||
* @function
|
||||
@@ -262,4 +269,54 @@ module.exports = function (
|
||||
|
||||
return path.basename(selectionState.getImagePath())
|
||||
}
|
||||
|
||||
this.driveSelectorModalOpen = false
|
||||
|
||||
this.openDriveSelector = () => {
|
||||
this.driveSelectorModalOpen = true;
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.closeDriveSelectorModal = () => {
|
||||
this.driveSelectorModalOpen = false;
|
||||
$timeout()
|
||||
}
|
||||
|
||||
this.setSelectedDrives = (drives) => {
|
||||
const currentlySelected = this.getSelectedDrive()
|
||||
if (currentlySelected) {
|
||||
drives = drives.filter(d => d.device !== currentlySelected.path)
|
||||
}
|
||||
if (drives.length === 0) {
|
||||
this.deselectImage()
|
||||
} else {
|
||||
selectionState.selectImage({
|
||||
path: drives[0].device,
|
||||
size: drives[0].size,
|
||||
isDrive: true
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
this.getSelectedDrive = () => {
|
||||
const image = selectionState.getImage()
|
||||
if (image && image.isDrive) {
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
this.isDriveSelected = (drive) => {
|
||||
const selectedDrive = this.getSelectedDrive()
|
||||
return selectedDrive && selectedDrive.path === drive.device
|
||||
}
|
||||
|
||||
this.isDriveValid = (drive) => {
|
||||
return true // TODO: not valid if already a destination drive
|
||||
}
|
||||
|
||||
this.getDriveBadges = (drive) => {
|
||||
return [] // TODO: selected as destination (same as above)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -18,14 +18,15 @@
|
||||
|
||||
const path = require('path')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const flashState = require('../../../models/flash-state')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
const availableDrives = require('../../../models/available-drives')
|
||||
const selectionState = require('../../../models/selection-state')
|
||||
const driveConstraints = require('../../../../../shared/drive-constraints')
|
||||
const messages = require('../../../../../shared/messages')
|
||||
const driveConstraints = require('../../../../../gui/app/modules/drive-constraints')
|
||||
const messages = require('../../../../../gui/app/modules/messages')
|
||||
const prettyBytes = require('pretty-bytes')
|
||||
|
||||
module.exports = function (
|
||||
|
@@ -34,6 +34,7 @@ const MainPage = angular.module(MODULE_NAME, [
|
||||
require('angular-seconds-to-date'),
|
||||
|
||||
require('../../components/drive-selector/drive-selector'),
|
||||
require('../../components/drive-selector2'),
|
||||
require('../../components/tooltip-modal/tooltip-modal'),
|
||||
require('../../components/flash-error-modal/flash-error-modal'),
|
||||
require('../../components/progress-button'),
|
||||
@@ -44,6 +45,7 @@ const MainPage = angular.module(MODULE_NAME, [
|
||||
require('../../components/reduced-flashing-infos'),
|
||||
require('../../components/flash-another'),
|
||||
require('../../components/flash-results'),
|
||||
require('../../components/drive-selector'),
|
||||
|
||||
require('../../os/open-external/open-external'),
|
||||
require('../../os/dropzone/dropzone'),
|
||||
|
@@ -46,12 +46,6 @@ svg-icon > img[disabled] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-main .button-brick {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.page-main .button-abort-write {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -67,6 +61,7 @@ svg-icon > img[disabled] {
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
%step-border {
|
||||
@@ -125,10 +120,13 @@ svg-icon > img[disabled] {
|
||||
}
|
||||
|
||||
.page-main .button.step-footer {
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
color: $palette-theme-primary-background;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
font-weight: 300;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.page-main .step-drive.glyphicon {
|
||||
@@ -167,7 +165,11 @@ svg-icon > img[disabled] {
|
||||
|
||||
.page-main .step-size {
|
||||
color: $palette-theme-dark-disabled-foreground;
|
||||
margin-top: 10px;
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
height: 21px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-main .step-list {
|
||||
|
@@ -1,5 +1,15 @@
|
||||
<div class="page-main row around-xs">
|
||||
<div class="col-xs" ng-controller="ImageSelectionController as image">
|
||||
<drive-selector-2
|
||||
ng-if="image.driveSelectorModalOpen"
|
||||
title="'Select a source drive'"
|
||||
close="image.closeDriveSelectorModal"
|
||||
set-selected-drives="image.setSelectedDrives"
|
||||
is-drive-selected="image.isDriveSelected"
|
||||
is-drive-valid="image.isDriveValid"
|
||||
get-drive-badges="image.getDriveBadges"
|
||||
>
|
||||
</drive-selector-2>
|
||||
<div class="box text-center relative" os-dropzone="image.selectImageByPath($file)">
|
||||
|
||||
<div class="center-block">
|
||||
@@ -10,22 +20,35 @@
|
||||
<image-selector
|
||||
has-image="main.selection.hasImage()"
|
||||
open-image-selector="image.openImageSelector"
|
||||
open-drive-selector="image.openDriveSelector"
|
||||
main-supported-extensions="image.mainSupportedExtensions"
|
||||
extra-supported-extensions="image.extraSupportedExtensions"
|
||||
show-selected-image-details="main.showSelectedImageDetails"
|
||||
image-name="main.selection.getImageName()"
|
||||
image-basename="image.getImageBasename()"
|
||||
reselect-image="image.reselectImage"
|
||||
deselect-image="image.deselectImage"
|
||||
flashing="main.state.isFlashing()"
|
||||
image-size="main.selection.getImageSize()"
|
||||
>
|
||||
</image-selector>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-xs" ng-controller="DriveSelectionController as drive">
|
||||
<drive-selector-2
|
||||
ng-if="drive.driveSelectorModalOpen"
|
||||
title="'Available targets'"
|
||||
close="drive.closeDriveSelectorModal"
|
||||
set-selected-drives="drive.setSelectedDrives"
|
||||
is-drive-selected="drive.isDriveSelected"
|
||||
is-drive-valid="drive.isDriveValid"
|
||||
get-drive-badges="drive.getDriveBadges"
|
||||
>
|
||||
</drive-selector-2>
|
||||
<div class="box text-center relative">
|
||||
|
||||
<div class="step-border-left" ng-disabled="main.shouldDriveStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
|
||||
<div class="step-border-right" ng-disabled="main.shouldFlashStepBeDisabled()" ng-hide="main.state.isFlashing() && main.isWebviewShowing"></div>
|
||||
|
||||
@@ -35,58 +58,20 @@
|
||||
</div>
|
||||
|
||||
<div class="space-vertical-large">
|
||||
<div ng-if="!main.selection.hasDrive() && drive.shouldShowDrivesButton()">
|
||||
|
||||
<div>
|
||||
<button class="button button-primary button-brick"
|
||||
tabindex="{{ main.selection.hasDrive() ? -1 : 2 }}"
|
||||
ng-disabled="main.shouldDriveStepBeDisabled()"
|
||||
ng-click="drive.openDriveSelector()">Select drive</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div ng-if="main.selection.hasDrive() || !drive.shouldShowDrivesButton()">
|
||||
|
||||
<div class="step-selection-text"
|
||||
ng-class="{
|
||||
'text-disabled': main.shouldDriveStepBeDisabled()
|
||||
}">
|
||||
<span class="step-drive step-name"
|
||||
ng-class="{
|
||||
'text-warning': !main.selection.getSelectedDevices().length
|
||||
}"
|
||||
uib-tooltip="{{ drive.getDriveListLabel() }}">
|
||||
<!-- middleEllipsis errors on undefined, therefore fallback to empty string -->
|
||||
{{ drive.getDrivesTitle() | middleEllipsis:20 }}
|
||||
</span>
|
||||
<span class="step-drive step-warning glyphicon glyphicon-exclamation-sign"
|
||||
uib-tooltip="{{ main.constraints.getListDriveImageCompatibilityStatuses(main.selection.getSelectedDrives(), main.selection.getImage())[0].message }}"
|
||||
ng-show="main.constraints.hasListDriveImageCompatibilityStatus(main.selection.getSelectedDrives(), main.selection.getImage())"></span>
|
||||
<button class="button button-link step-footer"
|
||||
tabindex="{{ main.selection.hasDrive() ? 2 : -1 }}"
|
||||
ng-hide="main.state.isFlashing() || !drive.shouldShowDrivesButton()"
|
||||
ng-click="drive.reselectDrive()">Change</button>
|
||||
<span
|
||||
ng-if="main.selection.getSelectedDevices().length <= 1"
|
||||
ng-class="{
|
||||
'step-fill': !drive.shouldShowDrivesButton()
|
||||
}"
|
||||
class="step-drive step-size">
|
||||
{{ drive.getDrivesSubtitle() }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="main.selection.getSelectedDevices().length > 1"
|
||||
ng-class="{
|
||||
'step-fill': !drive.shouldShowDrivesButton()
|
||||
}"
|
||||
class="step-drive step-list">
|
||||
<div ng-repeat="driveObj in drive.getMemoizedSelectedDrives()"
|
||||
uib-tooltip="{{ driveObj.description }} ({{ driveObj.displayName }})">
|
||||
{{ driveObj.description | middleEllipsis:14 }}
|
||||
</div>
|
||||
</div>
|
||||
<target-selector
|
||||
disabled="main.shouldDriveStepBeDisabled()"
|
||||
show="!main.selection.hasDrive() && drive.shouldShowDrivesButton()"
|
||||
tooltip="drive.getDriveListLabel()"
|
||||
selection="main.selection"
|
||||
open-drive-selector="drive.openDriveSelector"
|
||||
reselect-drive="drive.reselectDrive"
|
||||
flashing="main.state.isFlashing()"
|
||||
constraints="main.constraints"
|
||||
targets="drive.getMemoizedSelectedDrives()"
|
||||
>
|
||||
</target-selector>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@@ -19,6 +19,7 @@
|
||||
const os = require('os')
|
||||
const _ = require('lodash')
|
||||
const store = require('../../../models/store')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const settings = require('../../../models/settings')
|
||||
const analytics = require('../../../modules/analytics')
|
||||
const exceptionReporter = require('../../../modules/exception-reporter')
|
||||
@@ -107,6 +108,16 @@ module.exports = function (WarningModalService) {
|
||||
}).catch(exceptionReporter.report)
|
||||
}
|
||||
|
||||
this.set = (setting, value) => {
|
||||
analytics.logEvent('Set setting', {
|
||||
setting,
|
||||
value,
|
||||
applicationSessionUuid: store.getState().toJS().applicationSessionUuid
|
||||
})
|
||||
this.model.set(setting, value)
|
||||
this.refreshSettings()
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Show unsafe mode based on an env var
|
||||
* @function
|
||||
@@ -120,4 +131,19 @@ module.exports = function (WarningModalService) {
|
||||
this.shouldShowUnsafeMode = () => {
|
||||
return !settings.get('disableUnsafeMode')
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Show the screensaverDelay setting
|
||||
* @function
|
||||
* @public
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* SettingsController.shouldShowScreensaverDelay()
|
||||
*/
|
||||
this.shouldShowScreensaverDelay = () => {
|
||||
return settings.get('showScreensaverDelay')
|
||||
}
|
||||
}
|
||||
|
@@ -43,14 +43,25 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox" ng-show="settings.model.get('updatesEnabled')">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
tabindex="8"
|
||||
ng-model="settings.currentData.trim"
|
||||
ng-change="settings.toggle('trim')">
|
||||
|
||||
<span>Trim ext{2,3,4} partitions before writing (raw images only)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
tabindex="9"
|
||||
ng-model="settings.currentData.includeUnstableUpdateChannel"
|
||||
ng-change="settings.toggle('includeUnstableUpdateChannel')">
|
||||
ng-model="settings.currentData.updatesEnabled"
|
||||
ng-change="settings.toggle('updatesEnabled')">
|
||||
|
||||
<span>Include unstable update channel</span>
|
||||
<span>Auto-updates enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -66,4 +77,41 @@
|
||||
<span>Unsafe mode <span class="label label-danger">Dangerous</span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="settings.shouldShowScreensaverDelay()">
|
||||
<h4>
|
||||
Screensaver
|
||||
</h4>
|
||||
<label>
|
||||
<input
|
||||
ng-model="settings.currentData.screensaverDelay"
|
||||
ng-change="settings.set('screensaverDelay', '5')"
|
||||
type="radio"
|
||||
value="5"
|
||||
tabindex="11"
|
||||
>
|
||||
5 min
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
ng-model="settings.currentData.screensaverDelay"
|
||||
ng-change="settings.set('screensaverDelay', '10')"
|
||||
type="radio"
|
||||
value="10"
|
||||
tabindex="12"
|
||||
>
|
||||
10 min
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
ng-model="settings.currentData.screensaverDelay"
|
||||
ng-change="settings.set('screensaverDelay', 'never')"
|
||||
type="radio"
|
||||
value="never"
|
||||
tabindex="13"
|
||||
>
|
||||
never
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -20,7 +20,7 @@
|
||||
padding: 10px;
|
||||
padding-top: 11px;
|
||||
|
||||
border-radius: 2px;
|
||||
border-radius: 24px;
|
||||
border: 0;
|
||||
|
||||
letter-spacing: .5px;
|
||||
@@ -33,6 +33,11 @@
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
&.button-primary{
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
@extend .button-no-hover;
|
||||
background-color: $palette-theme-dark-disabled-background;
|
||||
|
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
$icon-font-path: "../../../node_modules/bootstrap-sass/assets/fonts/bootstrap/";
|
||||
$font-size-base: 13px;
|
||||
$font-size-base: 16px;
|
||||
$cursor-disabled: initial;
|
||||
$link-hover-decoration: none;
|
||||
$btn-min-width: 170px;
|
||||
@@ -45,46 +45,93 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free-webfonts/web
|
||||
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fontawesome";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-solid";
|
||||
@import "../../../../node_modules/@fortawesome/fontawesome-free-webfonts/scss/fa-regular";
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Thin.woff');
|
||||
font-weight: 100;
|
||||
font-family: 'Nunito';
|
||||
src: url('Nunito-Regular.eot');
|
||||
src: url('./fonts/Nunito-Regular.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/Nunito-Regular.woff2') format('woff2'),
|
||||
url('./fonts/Nunito-Regular.woff') format('woff'),
|
||||
url('./fonts/Nunito-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Light.woff');
|
||||
font-family: 'Nunito';
|
||||
src: url('Nunito-Bold.eot');
|
||||
src: url('./fonts/Nunito-Bold.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/Nunito-Bold.woff2') format('woff2'),
|
||||
url('./fonts/Nunito-Bold.woff') format('woff'),
|
||||
url('./fonts/Nunito-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Nunito';
|
||||
src: url('Nunito-Light.eot');
|
||||
src: url('./fonts/Nunito-Light.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/Nunito-Light.woff2') format('woff2'),
|
||||
url('./fonts/Nunito-Light.woff') format('woff'),
|
||||
url('./fonts/Nunito-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Regular.woff');
|
||||
font-weight: 400;
|
||||
font-family: 'CircularStd';
|
||||
src: url('./fonts/CircularStd-Bold.eot');
|
||||
src: url('./fonts/CircularStd-Bold.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/CircularStd-Bold.woff2') format('woff2'),
|
||||
url('./fonts/CircularStd-Bold.woff') format('woff'),
|
||||
url('./fonts/CircularStd-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Medium.woff');
|
||||
font-family: 'CircularStd';
|
||||
src: url('./fonts/CircularStd-Book.eot');
|
||||
src: url('./fonts/CircularStd-Book.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/CircularStd-Book.woff2') format('woff2'),
|
||||
url('./fonts/CircularStd-Book.woff') format('woff'),
|
||||
url('./fonts/CircularStd-Book.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: Roboto;
|
||||
src: url('../../../node_modules/roboto-fontface/fonts/roboto/Roboto-Bold.woff');
|
||||
font-weight: 700;
|
||||
font-family: 'CircularStd';
|
||||
src: url('./fonts/CircularStd-Medium.eot');
|
||||
src: url('./fonts/CircularStd-Medium.eot?#iefix') format('embedded-opentype'),
|
||||
url('./fonts/CircularStd-Medium.woff2') format('woff2'),
|
||||
url('./fonts/CircularStd-Medium.woff') format('woff'),
|
||||
url('./fonts/CircularStd-Medium.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
.circular {
|
||||
font-family: 'CircularStd';
|
||||
font-weight: 500;
|
||||
}
|
||||
.nunito {
|
||||
font-family: 'Nunito';
|
||||
}
|
||||
|
||||
body {
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: 'CircularStd';
|
||||
|
||||
> header {
|
||||
flex: 0 0 auto;
|
||||
@@ -100,12 +147,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
body,
|
||||
.tooltip,
|
||||
.popover {
|
||||
font-family: Roboto;
|
||||
}
|
||||
|
||||
.section-footer-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -173,7 +214,6 @@ body,
|
||||
.section-header {
|
||||
text-align: right;
|
||||
padding: 5px 8px;
|
||||
font-size: 15px;
|
||||
|
||||
> .button {
|
||||
padding-left: 3px;
|
||||
@@ -199,3 +239,19 @@ featured-project {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.sleep-button {
|
||||
float: left;
|
||||
background-color: #3c3e42;
|
||||
width: 76px;
|
||||
height: 24px;
|
||||
border-radius: 24px;
|
||||
padding: 0px;
|
||||
margin-top: 10px;
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: #414347;
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,44 +16,79 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const styled = require('styled-components').default
|
||||
const { colors } = require('./theme')
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const React = require('react')
|
||||
const { default: styled } = require('styled-components')
|
||||
const {
|
||||
Button, Txt, Flex
|
||||
Button,
|
||||
Txt,
|
||||
Flex,
|
||||
Provider
|
||||
} = require('rendition')
|
||||
const {
|
||||
space
|
||||
} = require('styled-system')
|
||||
const { colors } = require('./theme')
|
||||
|
||||
exports.StepButton = styled(Button) `
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
const theme = {
|
||||
button: {
|
||||
border: {
|
||||
width: '0',
|
||||
radius: '24px'
|
||||
},
|
||||
disabled: {
|
||||
opacity: 1
|
||||
},
|
||||
extend: () => `
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
|
||||
&:disabled {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
&:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
&:disabled {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
opacity: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: ${colors.dark.disabled.background};
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
exports.ThemedProvider = (props) => (
|
||||
<Provider theme={theme} {...props}>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
const BaseButton = styled(Button) `
|
||||
height: 48px;
|
||||
`
|
||||
|
||||
exports.ChangeButton = styled(Button) `
|
||||
exports.BaseButton = BaseButton
|
||||
|
||||
exports.StepButton = (props) => (
|
||||
<BaseButton primary {...props}>
|
||||
</BaseButton>
|
||||
)
|
||||
|
||||
exports.ChangeButton = styled(BaseButton) `
|
||||
color: ${colors.primary.background};
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
${space}
|
||||
|
||||
&:hover, &:focus, &:active {
|
||||
color: ${colors.primary.background};
|
||||
}
|
||||
`
|
||||
exports.StepNameButton = styled(Button) `
|
||||
exports.StepNameButton = styled(BaseButton) `
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 39px;
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
color: ${colors.dark.foreground};
|
||||
@@ -77,5 +112,5 @@ exports.Underline = styled(Txt.span) `
|
||||
`
|
||||
exports.DetailsText = styled(Txt.p) `
|
||||
color: ${colors.dark.disabled.foreground};
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016 resin.io
|
||||
* Copyright 2018 resin.io
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"),
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@@ -16,7 +16,7 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const units = require('../../../../shared/units')
|
||||
const units = require('../../../../gui/app/modules/units')
|
||||
|
||||
module.exports = () => {
|
||||
/**
|
||||
|
@@ -16,7 +16,8 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
const errors = require('../../../../../shared/errors')
|
||||
// eslint-disable-next-line node/no-missing-require
|
||||
const errors = require('../../../../../gui/app/modules/errors')
|
||||
|
||||
/**
|
||||
* @summary ManifestBind directive
|
||||
|
3
lib/gui/assets/moon.svg
Normal file
3
lib/gui/assets/moon.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#D3D6DB" fill-rule="evenodd" stroke="#D3D6DB" stroke-linecap="round" d="M5.313 1.002a.173.173 0 0 0-.038.01C2.559 2.02 1 4.642 1 7.686a7.12 7.12 0 0 0 7.116 7.116c3.044 0 5.667-1.565 6.673-4.28a.173.173 0 0 0-.226-.221c-.77.309-1.607.468-2.49.468-3.686 0-7.046-3.354-7.046-7.04 0-.883.16-1.72.469-2.49a.173.173 0 0 0-.183-.237zm-.248.49a7.033 7.033 0 0 0-.383 2.237c0 3.894 3.497 7.386 7.39 7.386.786 0 1.53-.147 2.237-.383-1.04 2.362-3.399 3.725-6.193 3.725-3.741 0-6.771-3.03-6.771-6.77 0-2.794 1.36-5.154 3.72-6.195z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 630 B |
BIN
lib/gui/css/fonts/CircularStd-Bold.eot
Normal file
BIN
lib/gui/css/fonts/CircularStd-Bold.eot
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Bold.otf
Normal file
BIN
lib/gui/css/fonts/CircularStd-Bold.otf
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Bold.ttf
Normal file
BIN
lib/gui/css/fonts/CircularStd-Bold.ttf
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Bold.woff
Normal file
BIN
lib/gui/css/fonts/CircularStd-Bold.woff
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Bold.woff2
Normal file
BIN
lib/gui/css/fonts/CircularStd-Bold.woff2
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Book.eot
Normal file
BIN
lib/gui/css/fonts/CircularStd-Book.eot
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Book.otf
Executable file
BIN
lib/gui/css/fonts/CircularStd-Book.otf
Executable file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Book.ttf
Normal file
BIN
lib/gui/css/fonts/CircularStd-Book.ttf
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Book.woff
Normal file
BIN
lib/gui/css/fonts/CircularStd-Book.woff
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Book.woff2
Normal file
BIN
lib/gui/css/fonts/CircularStd-Book.woff2
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Medium.eot
Normal file
BIN
lib/gui/css/fonts/CircularStd-Medium.eot
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Medium.otf
Normal file
BIN
lib/gui/css/fonts/CircularStd-Medium.otf
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Medium.ttf
Normal file
BIN
lib/gui/css/fonts/CircularStd-Medium.ttf
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Medium.woff
Normal file
BIN
lib/gui/css/fonts/CircularStd-Medium.woff
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/CircularStd-Medium.woff2
Normal file
BIN
lib/gui/css/fonts/CircularStd-Medium.woff2
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/Nunito-Bold.eot
Normal file
BIN
lib/gui/css/fonts/Nunito-Bold.eot
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/Nunito-Bold.ttf
Normal file
BIN
lib/gui/css/fonts/Nunito-Bold.ttf
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/Nunito-Bold.woff
Normal file
BIN
lib/gui/css/fonts/Nunito-Bold.woff
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/Nunito-Bold.woff2
Normal file
BIN
lib/gui/css/fonts/Nunito-Bold.woff2
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/Nunito-Light.eot
Normal file
BIN
lib/gui/css/fonts/Nunito-Light.eot
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/Nunito-Light.ttf
Normal file
BIN
lib/gui/css/fonts/Nunito-Light.ttf
Normal file
Binary file not shown.
BIN
lib/gui/css/fonts/Nunito-Light.woff
Normal file
BIN
lib/gui/css/fonts/Nunito-Light.woff
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user