Compare commits

..

No commits in common. "master" and "v1.4.4" have entirely different histories.

438 changed files with 48084 additions and 61338 deletions

View File

@ -7,15 +7,10 @@ indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
[*.ts]
indent_style = tab
[*.tsx]
indent_style = tab

View File

@ -1,10 +0,0 @@
module.exports = {
extends: ["./node_modules/@balena/lint/config/.eslintrc.js"],
root: true,
ignorePatterns: ["node_modules/"],
rules: {
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/ban-ts-comment": "off",
},
};

446
.eslintrc.yml Normal file
View File

@ -0,0 +1,446 @@
env:
browser: true
commonjs: true
es6: true
node: true
mocha: true
plugins:
- lodash
- jsdoc
- node
extends: 'standard'
parserOptions:
sourceType: 'script'
settings:
jsdoc:
additionalTagNames:
customTags:
- fulfil
rules:
# Possible Errors
no-console:
- off
no-empty:
- error
no-extra-semi:
- error
no-negated-in-lhs:
- error
no-prototype-builtins:
- error
valid-jsdoc:
- error
- requireReturn: false
requireReturnDescription: false
requireReturnType: true
requireParamDescription: true
preferType:
boolean: "Boolean"
number: "Number"
object: "Object"
string: "String"
array: "Array"
prefer:
arg: "param"
return: "returns"
# Best Practices
array-callback-return:
- error
block-scoped-var:
- error
class-methods-use-this:
- error
complexity:
- off
consistent-return:
- error
curly:
- error
default-case:
- error
dot-notation:
- error
guard-for-in:
- error
no-alert:
- error
no-case-declarations:
- error
no-div-regex:
- error
no-else-return:
- error
no-empty-function:
- error
no-eq-null:
- error
no-extra-label:
- error
no-implicit-coercion:
- error
no-implicit-globals:
- error
no-loop-func:
- error
no-magic-numbers:
- error
no-native-reassign:
- error
no-param-reassign:
- error
no-restricted-properties:
- error
- property: __proto__
no-return-await:
- error
no-script-url:
- error
no-unused-expressions:
- error
no-unused-labels:
- error
no-useless-concat:
- error
no-void:
- error
no-warning-comments:
- off
radix:
- error
vars-on-top:
- off
# Strict mode
strict:
- error
- global
# Variables
init-declarations:
- error
- always
no-catch-shadow:
- error
no-restricted-globals:
- error
- event
no-shadow:
- error
no-undefined:
- error
no-unused-vars:
- error
no-use-before-define:
- error
# NodeJS and CommonJS
callback-return:
- error
global-require:
- off
no-mixed-requires:
- error
no-process-env:
- off
no-process-exit:
- off
no-sync:
- off
# Stylistic Issues
array-bracket-spacing:
- error
- always
capitalized-comments:
- error
- always
- ignoreConsecutiveComments: true
comma-spacing:
- error
- before: false
after: true
computed-property-spacing:
- error
- never
consistent-this:
- error
- self
func-name-matching:
- error
- always
func-names:
- error
- never
func-style:
- error
- expression
id-blacklist:
- error
id-length:
- error
- min: 2
exceptions:
- "_"
id-match:
- error
- "^[_0-9A-Za-z\\$]+$"
line-comment-position:
- error
- position: above
linebreak-style:
- error
- unix
lines-around-comment:
- error
- beforeBlockComment: true
afterBlockComment: false
beforeLineComment: true
afterLineComment: false
allowBlockStart: true
allowBlockEnd: false
allowObjectStart: true
allowObjectEnd: false
allowArrayStart: true
allowArrayEnd: false
lines-around-directive:
- error
- always
max-len:
- error
- code: 130
comments: 150
ignoreComments: false
ignoreTrailingComments: false
ignoreUrls: true
max-params:
- off
max-statements-per-line:
- error
- max: 1
multiline-ternary:
- off
newline-per-chained-call:
- off
no-bitwise:
- error
no-continue:
- error
no-inline-comments:
- error
no-lonely-if:
- error
no-mixed-operators:
- error
no-multi-assign:
- error
no-negated-condition:
- error
no-nested-ternary:
- error
no-plusplus:
- error
no-restricted-syntax:
- error
- WithStatement
- ForInStatement
no-spaced-func:
- error
no-underscore-dangle:
- error
- allowAfterThis: false
object-curly-newline:
- error
- minProperties: 3
consistent: true
object-curly-spacing:
- error
- always
one-var-declaration-per-line:
- error
- always
operator-assignment:
- error
- always
quotes:
- error
- single
quote-props:
- error
- as-needed
require-jsdoc:
- error
- require:
FunctionDeclaration: true
ClassDeclaration: true
MethodDefinition: true
ArrowFunctionExpression: true
space-before-function-paren:
- error
- anonymous: always
named: always
asyncArrow: never
template-tag-spacing:
- error
- always
unicode-bom:
- error
# ECMAScript 6
arrow-body-style:
- error
- always
arrow-parens:
- error
- always
arrow-spacing:
- error
- before: true
after: true
generator-star-spacing:
- error
- before: true
after: false
no-confusing-arrow:
- error
no-var:
- error
object-shorthand:
- error
- always
prefer-const:
- error
prefer-reflect:
- error
prefer-spread:
- error
prefer-numeric-literals:
- error
prefer-rest-params:
- error
prefer-template:
- error
prefer-arrow-callback:
- error
- allowNamedFunctions: false
require-yield:
- error
symbol-description:
- error
# Lodash
lodash/chain-style:
- error
- explicit
lodash/identity-shorthand:
- error
- always
lodash/import-scope:
- error
- full
lodash/matches-prop-shorthand:
- error
- always
lodash/matches-shorthand:
- error
- always
lodash/no-commit:
- error
lodash/path-style:
- error
- array
lodash/prefer-compact:
- error
lodash/prefer-filter:
- error
- 5
lodash/prefer-flat-map:
- error
lodash/prefer-invoke-map:
- error
lodash/prefer-map:
- error
lodash/prefer-reject:
- error
lodash/prefer-thru:
- error
lodash/prefer-wrapper-method:
- error
lodash/prop-shorthand:
- error
- always
lodash/prefer-constant:
- error
- true
- true
lodash/prefer-get:
- error
- 2
lodash/prefer-includes:
- error
- includeNative: true
lodash/prefer-is-nil:
- error
lodash/prefer-lodash-chain:
- error
lodash/prefer-lodash-method:
- error
lodash/prefer-lodash-typecheck:
- error
lodash/prefer-matches:
- error
- 3
lodash/prefer-noop:
- error
lodash/prefer-over-quantifier:
- error
lodash/prefer-startswith:
- error
lodash/prefer-times:
- error
# JSDoc
jsdoc/check-param-names:
- error
jsdoc/check-tag-names:
- error
jsdoc/newline-after-description:
- error
jsdoc/require-example:
- error
jsdoc/require-hyphen-before-param-description:
- error
jsdoc/require-param:
- error
jsdoc/require-param-description:
- error
jsdoc/require-param-type:
- error
jsdoc/require-returns-type:
- error
# Node
node/no-deprecated-api:
- error
node/no-missing-import:
- error
node/no-missing-require:
- error
node/process-exit-as-throw:
- error
node/no-extraneous-require:
- error
node/no-extraneous-import:
- error

22
.gitattributes vendored
View File

@ -1,11 +1,5 @@
# default
* text
# Javascript files must retain LF line-endings (to keep eslint happy)
*.js text eol=lf
*.jsx text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
# CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy)
*.css text eol=lf
*.scss text eol=lf
@ -17,7 +11,7 @@ Dockerfile* text
etcher text
.git* text
*.html text
*.json text eol=lf
*.json text
*.cpp text
*.h text
*.gyp text
@ -30,9 +24,6 @@ Makefile text
*.yml text
*.patch text
*.txt text
*.tpl text
CODEOWNERS text
*.plist text
# Binary files (no line-ending conversions)
*.bz2 binary diff=hex
@ -53,16 +44,5 @@ CODEOWNERS text
*.bin binary diff=hex
*.dmg binary diff=hex
*.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
# gitsecret
*.secret binary
.gitsecret/** binary

View File

@ -1,11 +1,6 @@
- **Etcher version:**
- **Operating system and architecture:**
- **Image flashed:**
- **What do you think should have happened:** <!-- or a step by step reproduction process -->
- **What happened:**
- **Do you see any meaningful error information in the DevTools?**
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Opt+I` if you're on macOS. -->
<!-- issues with missing information will be labeled as not-enough-info and closed shortly -->
<!-- please try to include as many influencing elements as possible are you root, does any other process block the device, etc. -->
<!-- if you find a solution in the meantime thank you for sharing the fix and not just closing / abandoning your issue -->
<!-- You can open DevTools by pressing `Ctrl+Shift+I` (`Ctrl+Alt+I` for Etcher before v1.3.x), or `Cmd+Alt+I` if you're on Mac OS. -->

View File

@ -1,205 +0,0 @@
---
name: package and publish GitHub (draft) release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: "JSON stringified object containing all the inputs from the calling workflow"
required: true
secrets:
description: "JSON stringified object containing all the secrets from the calling workflow"
required: true
# --- custom environment
NODE_VERSION:
type: string
# Beware that native modules will be built for this version,
# which might not be compatible with the one used by pkg (see forge.sidecar.ts)
# https://github.com/vercel/pkg-fetch/releases
default: "20.x"
VERBOSE:
type: string
default: "true"
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: "composite"
steps:
- name: Download custom source artifact
uses: actions/download-artifact@v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}
- name: Extract custom source artifact
if: runner.os != 'Windows'
shell: bash
working-directory: .
run: tar -xf ${{ runner.temp }}/custom.tgz
- name: Extract custom source artifact
if: runner.os == 'Windows'
shell: pwsh
working-directory: .
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -xf ${{ runner.temp }}\custom.tgz
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install host dependencies
if: runner.os == 'Linux'
shell: bash
run: sudo apt-get install -y --no-install-recommends fakeroot dpkg rpm
# rpmbuild will strip binaries by default, which breaks the sidecar.
# Use a macro to override the "strip" to bypass stripping.
- name: Configure rpmbuild to not strip executables
if: runner.os == 'Linux'
shell: bash
run: echo '%__strip /usr/bin/true' > ~/.rpmmacros
- name: Install host dependencies
if: runner.os == 'macOS'
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
# This is a temporary workaround to make the job use Python 3.11 until
# we update to npm 10+.
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
with:
python-version: '3.11'
# https://www.electron.build/code-signing.html
# https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof
- name: Import Apple code signing certificate
if: runner.os == 'macOS'
shell: bash
run: |
KEY_CHAIN=build.keychain
CERTIFICATE_P12=certificate.p12
# Recreate the certificate from the secure environment variable
echo $CERTIFICATE_P12_B64 | base64 --decode > $CERTIFICATE_P12
# Create a keychain
security create-keychain -p actions $KEY_CHAIN
# Make the keychain the default so identities are found
security default-keychain -s $KEY_CHAIN
# Unlock the keychain
security unlock-keychain -p actions $KEY_CHAIN
security import $CERTIFICATE_P12 -k $KEY_CHAIN -P $CERTIFICATE_PASSWORD -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k actions $KEY_CHAIN
# remove certs
rm -fr *.p12
env:
CERTIFICATE_P12_B64: ${{ fromJSON(inputs.secrets).APPLE_SIGNING }}
CERTIFICATE_PASSWORD: ${{ fromJSON(inputs.secrets).APPLE_SIGNING_PASSWORD }}
- name: Import Windows code signing certificate
if: runner.os == 'Windows'
id: import_win_signing_cert
shell: powershell
run: |
Set-Content -Path ${{ runner.temp }}/certificate.base64 -Value $env:SM_CLIENT_CERT_FILE_B64
certutil -decode ${{ runner.temp }}/certificate.base64 ${{ runner.temp }}/Certificate_pkcs12.p12
Remove-Item -path ${{ runner.temp }} -include certificate.base64
echo "certFilePath=${{ runner.temp }}/Certificate_pkcs12.p12" >> $GITHUB_OUTPUT
env:
SM_CLIENT_CERT_FILE_B64: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_FILE_B64 }}
- name: Package release
shell: bash
# IMPORTANT: before making changes to this step please consult @engineering in balena's chat.
run: |
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
# export DEBUG='electron-forge:*,sidecar'
# fi
APPLICATION_VERSION="$(jq -r '.version' package.json)"
HOST_ARCH="$(echo "${RUNNER_ARCH}" | tr '[:upper:]' '[:lower:]')"
if [[ "${RUNNER_OS}" == Linux ]]; then
PLATFORM=Linux
SHA256SUM_BIN=sha256sum
elif [[ "${RUNNER_OS}" == macOS ]]; then
PLATFORM=Darwin
SHA256SUM_BIN='shasum -a 256'
elif [[ "${RUNNER_OS}" == Windows ]]; then
PLATFORM=Windows
SHA256SUM_BIN=sha256sum
# Install DigiCert Signing Manager Tools
curl --silent --retry 3 --fail https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download \
-H "x-api-key:$SM_API_KEY" \
-o smtools-windows-x64.msi
msiexec -i smtools-windows-x64.msi -qn
PATH="/c/Program Files/DigiCert/DigiCert One Signing Manager Tools:${PATH}"
smksp_registrar.exe list
smctl.exe keypair ls
/c/Windows/System32/certutil.exe -csp "DigiCert Signing Manager KSP" -key -user
smksp_cert_sync.exe
# (signtool.exe) https://github.com/actions/runner-images/blob/main/images/win/Windows2019-Readme.md#installed-windows-sdks
PATH="/c/Program Files (x86)/Windows Kits/10/bin/${runner_arch}:${PATH}"
else
echo "ERROR: unexpected runner OS: ${RUNNER_OS}"
exit 1
fi
# Currently, we can only build for the host architecture.
npx electron-forge make
echo "version=${APPLICATION_VERSION}" >> $GITHUB_OUTPUT
# collect all artifacts from subdirectories under a common top-level directory
mkdir -p dist
find ./out/make -type f \( \
-iname "*.zip" -o \
-iname "*.dmg" -o \
-iname "*.rpm" -o \
-iname "*.deb" -o \
-iname "*.AppImage" -o \
-iname "*Setup.exe" \
\) -ls -exec cp '{}' dist/ \;
if [[ -n "${SHA256SUM_BIN}" ]]; then
# Compute and save digests.
cd dist/
${SHA256SUM_BIN} *.* >"SHA256SUMS.${PLATFORM}.${HOST_ARCH}.txt"
fi
env:
# ensure we sign the artifacts
NODE_ENV: production
# analytics tokens
SENTRY_TOKEN: https://739bbcfc0ba4481481138d3fc831136d@o95242.ingest.sentry.io/4504451487301632
AMPLITUDE_TOKEN: 'balena-etcher'
# Apple notarization
XCODE_APP_LOADER_EMAIL: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_EMAIL }}
XCODE_APP_LOADER_PASSWORD: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_PASSWORD }}
XCODE_APP_LOADER_TEAM_ID: ${{ fromJSON(inputs.secrets).XCODE_APP_LOADER_TEAM_ID }}
# Windows signing
SM_CLIENT_CERT_PASSWORD: ${{ fromJSON(inputs.secrets).SM_CLIENT_CERT_PASSWORD }}
SM_CLIENT_CERT_FILE: '${{ runner.temp }}\Certificate_pkcs12.p12'
SM_HOST: ${{ fromJSON(inputs.secrets).SM_HOST }}
SM_API_KEY: ${{ fromJSON(inputs.secrets).SM_API_KEY }}
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ fromJSON(inputs.secrets).SM_CODE_SIGNING_CERT_SHA1_HASH }}
TIMESTAMP_SERVER: http://timestamp.digicert.com
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: gh-release-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: dist
retention-days: 1
if-no-files-found: error

View File

@ -1,87 +0,0 @@
---
name: test release
# https://github.com/product-os/flowzone/tree/master/.github/actions
inputs:
json:
description: 'JSON stringified object containing all the inputs from the calling workflow'
required: true
secrets:
description: 'JSON stringified object containing all the secrets from the calling workflow'
required: true
# --- custom environment
NODE_VERSION:
type: string
default: '20.10'
VERBOSE:
type: string
default: 'true'
runs:
# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
using: 'composite'
steps:
# https://github.com/actions/setup-node#caching-global-packages-data
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.NODE_VERSION }}
cache: npm
- name: Install host dependencies
if: runner.os == 'Linux'
shell: bash
run: |
sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb libudev-dev
cat < package.json | jq -r '.hostDependencies[][]' - | \
xargs -L1 echo | sed 's/|//g' | xargs -L1 \
sudo apt-get --ignore-missing install || true
- name: Install host dependencies
if: runner.os == 'macOS'
# FIXME: Python 3.12 dropped distutils that node-gyp depends upon.
# This is a temporary workaround to make the job use Python 3.11 until
# we update to npm 10+.
uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4
with:
python-version: '3.11'
- name: Test release
shell: bash
run: |
## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled
# if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then
# export DEBUG='electron-forge:*,sidecar'
# fi
npm ci
# as the shrinkwrap might have been done on mac/linux, this is ensure the package is there for windows
if [[ "$RUNNER_OS" == "Windows" ]]; then
npm i -D winusb-driver-generator
fi
npm run lint
npm run package
npm run wdio # test stage, note that it requires the package to be done first
env:
# https://www.electronjs.org/docs/latest/api/environment-variables
ELECTRON_NO_ATTACH_CONSOLE: 'true'
- name: Compress custom source
if: runner.os != 'Windows'
shell: bash
run: tar -acf ${{ runner.temp }}/custom.tgz .
- name: Compress custom source
if: runner.os == 'Windows'
shell: pwsh
run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz .
- name: Upload custom artifact
uses: actions/upload-artifact@v4
with:
name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }}
path: ${{ runner.temp }}/custom.tgz
retention-days: 1

View File

@ -1,41 +0,0 @@
name: Flowzone
on:
pull_request:
types: [opened, synchronize, closed]
branches: [main, master]
# allow external contributions to use secrets within trusted code
pull_request_target:
types: [opened, synchronize, closed]
branches: [main, master]
jobs:
flowzone:
name: Flowzone
uses: product-os/flowzone/.github/workflows/flowzone.yml@master
# prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for
# internal or external contributions respectively
if: |
(github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') ||
(github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target')
secrets: inherit
with:
custom_test_matrix: >
{
"os": [
["ubuntu-22.04"],
["windows-2019"],
["macos-13"],
["macos-latest-xlarge"]
]
}
custom_publish_matrix: >
{
"os": [
["ubuntu-22.04"],
["windows-2019"],
["macos-13"],
["macos-latest-xlarge"]
]
}
restrict_custom_actions: false
github_prerelease: true
cloudflare_website: "etcher"

View File

@ -1,14 +0,0 @@
name: Publish to WinGet
on:
release:
types: [released]
jobs:
publish:
runs-on: windows-latest # action can only be run on windows
steps:
- uses: vedantmgoyal2009/winget-releaser@v2
with:
identifier: Balena.Etcher
# matches something like "balenaEtcher-1.19.0.Setup.exe"
installers-regex: 'balenaEtcher-[\d.-]+\.Setup.exe$'
token: ${{ secrets.WINGET_PAT }}

112
.gitignore vendored
View File

@ -1,103 +1,40 @@
# -- ADD NEW ENTRIES AT THE END OF THE FILE ---
# Logs
logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
/lib-cov
# Image stream output directory
/tests/image-stream/output
# Coverage directory used by tools like istanbul
coverage
*.lcov
/coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Compiled binary addons (http://nodejs.org/api/addons.html)
/build
# Dependency directories
node_modules/
jspm_packages/
# Generated files
/generated
# TypeScript v1 declaration files
typings/
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Vite
.vite/
# Electron-Forge
out/
# ---- Do not modify entries above this line ----
# Build artifacts
dist/
# Compiled Etcher releases
/dist
# Certificates
*.spc
@ -106,18 +43,3 @@ dist/
*.cer
*.crt
*.pem
# Secrets
.gitsecret/keys/random_seed
!*.secret
secrets/APPLE_SIGNING_PASSWORD.txt
secrets/WINDOWS_SIGNING_PASSWORD.txt
secrets/XCODE_APP_LOADER_PASSWORD.txt
secrets/WINDOWS_SIGNING.pfx
# Image stream output directory
/tests/image-stream/output
#local development
.yalc
yalc.lock

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,5 +0,0 @@
secrets/APPLE_SIGNING_PASSWORD.txt:5c9cfeb1ea5142b547bc842cc6e0b4a932641ae9811ee47abe2c3953f2a4de5d
secrets/WINDOWS_SIGNING_PASSWORD.txt:852e431628494f2559793c39cf09c34e9406dd79bb15b90c9f88194020470568
secrets/XCODE_APP_LOADER_PASSWORD.txt:005eb9a3c7035c77232973c9355468fc396b94e62783fb8e6dce16bce95b94a1
secrets/WINDOWS_SIGNING.pfx:929f401db38733ffc41572539de7c0d938023af51ed06c205a72a71c1f815714
secrets/APPLE_SIGNING.p12:61abf7b4ff2eec76ce889d71bcdd568b99a6a719b4947ac20f03966265b0946a

1
.nvmrc
View File

@ -1 +0,0 @@
18

View File

@ -1,6 +0,0 @@
const fs = require("fs");
const path = require("path");
module.exports = JSON.parse(
fs.readFileSync(path.join(__dirname, "node_modules", "@balena", "lint", "config", ".prettierrc"), "utf8"),
);

65
.resinci.json Normal file
View File

@ -0,0 +1,65 @@
{
"node-cli": {
"node": "6.1.0",
"main": "lib/cli/etcher.js",
"dependencies": {
"linux": [
"libudev-dev",
"libusb-1.0-0-dev"
]
}
},
"electron": {
"dependencies": {
"linux": [
"libudev-dev",
"libusb-1.0-0-dev",
"libyaml-dev"
]
},
"builder": {
"appId": "io.resin.etcher",
"copyright": "Copyright 2016-2018 Resinio Ltd",
"productName": "Etcher",
"nodeGypRebuild": true,
"files": [
"!lib/gui/app",
"lib/gui/app/index.html",
"generated"
],
"mac": {
"category": "public.app-category.developer-tools"
},
"dmg": {
"iconSize": 110,
"contents": [
{
"x": 140,
"y": 225
},
{
"x": 415,
"y": 225,
"type": "link",
"path": "/Applications"
}
],
"window": {
"width": 540,
"height": 405
}
},
"linux": {
"category": "Utility",
"packageCategory": "utils",
"synopsis": "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."
},
"deb": {
"priority": "optional",
"depends": [
"polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1"
]
}
}
}
}

17
.sass-lint.yml Normal file
View File

@ -0,0 +1,17 @@
# sass-lint config generated by make-sass-lint-config v0.1.2
files:
include: lib/gui/scss/**/*.scss
options:
formatter: stylish
merge-default-rules: false
rules:
no-css-comments: 0
no-important: 0
no-qualifying-elements: 0
placeholder-in-extend: 0
property-sort-order: 0
quotes:
- 1
- style: double

69
.travis.yml Normal file
View File

@ -0,0 +1,69 @@
language: node_js
sudo: false
node_js:
- "6.10.3"
# Remove wine from cache
before_cache:
- rm -rf $HOME/.cache/electron-builder/wine
cache:
ccache: true
directories:
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
- $HOME/Library/Caches/electron
- $HOME/Library/Caches/electron-builder
- $HOME/.pkg-cache
- node_modules
services:
- docker
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- libstdc++-6-dev
env:
global:
- CCACHE_TEMPDIR=/tmp/.ccache-temp
- CCACHE_COMPRESS=1
- CC="clang"
- CXX="clang++"
- HOMEBREW_NO_AUTO_UPDATE=1
matrix:
- TARGET_ARCH=x64
- TARGET_ARCH=x86
matrix:
fast_finish: true
exclude:
- os: osx
env: TARGET_ARCH=x86
os:
- linux
before_install:
- export HOST_OS="$TRAVIS_OS_NAME";
install:
- ./scripts/ci/install.sh -o $HOST_OS -r $TARGET_ARCH
script:
- ./scripts/ci/test.sh -o $HOST_OS -r $TARGET_ARCH
- ./scripts/ci/build-installers.sh -o $HOST_OS -r $TARGET_ARCH
deploy:
provider: script
skip_cleanup: true
script: scripts/ci/deploy.sh -o $HOST_OS -r $TARGET_ARCH
on:
branch: master
notifications:
email: false

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

607
Makefile Normal file
View File

@ -0,0 +1,607 @@
# ---------------------------------------------------------------------
# Build configuration
# ---------------------------------------------------------------------
# A non-existing target to force rules to rebuild
# See https://stackoverflow.com/a/816416
.FORCE:
# This directory will be completely deleted by the `clean` rule
BUILD_DIRECTORY ?= dist
# See http://stackoverflow.com/a/20763842/1641422
BUILD_DIRECTORY_PARENT = $(dir $(BUILD_DIRECTORY))
ifeq ($(wildcard $(BUILD_DIRECTORY_PARENT).),)
$(error $(BUILD_DIRECTORY_PARENT) does not exist)
endif
BUILD_TEMPORARY_DIRECTORY = $(BUILD_DIRECTORY)/.tmp
# See https://github.com/electron/spectron/issues/127
ETCHER_SPECTRON_ENTRYPOINT ?= $(shell node -e 'console.log(require("electron"))')
# See https://stackoverflow.com/a/13468229/1641422
SHELL := /bin/bash
PATH := $(shell pwd)/node_modules/.bin:$(PATH)
# ---------------------------------------------------------------------
# Operating system and architecture detection
# ---------------------------------------------------------------------
# http://stackoverflow.com/a/12099167
ifeq ($(OS),Windows_NT)
PLATFORM = win32
ifeq ($(PROCESSOR_ARCHITEW6432),AMD64)
HOST_ARCH = x64
else
ifeq ($(PROCESSOR_ARCHITECTURE),AMD64)
HOST_ARCH = x64
endif
ifeq ($(PROCESSOR_ARCHITECTURE),x86)
HOST_ARCH = x86
endif
endif
else
ifeq ($(shell uname -s),Linux)
PLATFORM = linux
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
ifneq ($(filter %86,$(shell uname -m)),)
HOST_ARCH = x86
endif
ifeq ($(shell uname -m),armv7l)
HOST_ARCH = armv7hf
endif
ifeq ($(shell uname -m),aarch64)
HOST_ARCH = aarch64
endif
ifeq ($(shell uname -m),armv8)
HOST_ARCH = aarch64
endif
ifeq ($(shell uname -m),arm64)
HOST_ARCH = aarch64
endif
endif
ifeq ($(shell uname -s),Darwin)
PLATFORM = darwin
ifeq ($(shell uname -m),x86_64)
HOST_ARCH = x64
endif
endif
endif
ifndef PLATFORM
$(error We couldn't detect your host platform)
endif
ifndef HOST_ARCH
$(error We couldn't detect your host architecture)
endif
# Default to host architecture. You can override by doing:
#
# make <target> TARGET_ARCH=<arch>
#
TARGET_ARCH ?= $(HOST_ARCH)
# Support x86 builds from x64 in GNU/Linux
# See https://github.com/addaleax/lzma-native/issues/27
ifeq ($(PLATFORM),linux)
ifneq ($(HOST_ARCH),$(TARGET_ARCH))
ifeq ($(TARGET_ARCH),x86)
export CFLAGS += -m32
else
$(error Can't build $(TARGET_ARCH) binaries on a $(HOST_ARCH) host)
endif
endif
endif
# ---------------------------------------------------------------------
# Application configuration
# ---------------------------------------------------------------------
ELECTRON_VERSION = $(shell jq -r '.devDependencies["electron"]' package.json)
NODE_VERSION = 6.1.0
COMPANY_NAME = Resinio Ltd
APPLICATION_NAME = $(shell jq -r '.displayName' package.json)
APPLICATION_DESCRIPTION = $(shell jq -r '.description' package.json)
APPLICATION_COPYRIGHT = $(shell cat electron-builder.yml | shyaml get-value copyright)
BINTRAY_ORGANIZATION = resin-io
BINTRAY_REPOSITORY_DEBIAN = debian
BINTRAY_REPOSITORY_REDHAT = redhat
# ---------------------------------------------------------------------
# Extra variables
# ---------------------------------------------------------------------
TARGET_ARCH_DEBIAN = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t debian)
TARGET_ARCH_REDHAT = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t redhat)
TARGET_ARCH_APPIMAGE = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t appimage)
TARGET_ARCH_ELECTRON_BUILDER = $(shell ./scripts/build/architecture-convert.sh -r $(TARGET_ARCH) -t electron-builder)
PLATFORM_PKG = $(shell ./scripts/build/platform-convert.sh -r $(PLATFORM) -t pkg)
ENTRY_POINT_CLI = lib/cli/etcher.js
ETCHER_CLI_BINARY = $(APPLICATION_NAME_LOWERCASE)
ifeq ($(PLATFORM),win32)
ETCHER_CLI_BINARY = $(APPLICATION_NAME_LOWERCASE).exe
endif
APPLICATION_NAME_LOWERCASE = $(shell echo $(APPLICATION_NAME) | tr A-Z a-z)
APPLICATION_VERSION_DEBIAN = $(shell echo $(APPLICATION_VERSION) | tr "-" "~")
APPLICATION_VERSION_REDHAT = $(shell echo $(APPLICATION_VERSION) | tr "-" "~")
# ---------------------------------------------------------------------
# Release type
# ---------------------------------------------------------------------
# Add the current commit to the version if release type is "snapshot"
RELEASE_TYPE ?= snapshot
PACKAGE_JSON_VERSION = $(shell jq -r '.version' package.json)
ifeq ($(RELEASE_TYPE),production)
APPLICATION_VERSION = $(PACKAGE_JSON_VERSION)
S3_BUCKET = resin-production-downloads
BINTRAY_COMPONENT = $(APPLICATION_NAME_LOWERCASE)
endif
ifeq ($(RELEASE_TYPE),snapshot)
CURRENT_COMMIT_HASH = $(shell git log -1 --format="%h")
APPLICATION_VERSION = $(PACKAGE_JSON_VERSION)+$(CURRENT_COMMIT_HASH)
S3_BUCKET = resin-nightly-downloads
BINTRAY_COMPONENT = $(APPLICATION_NAME_LOWERCASE)-devel
endif
ifndef APPLICATION_VERSION
$(error Invalid release type: $(RELEASE_TYPE))
endif
# ---------------------------------------------------------------------
# Code signing
# ---------------------------------------------------------------------
ifeq ($(PLATFORM),darwin)
ifndef CSC_NAME
$(warning No code-sign identity found (CSC_NAME is not set))
endif
endif
ifeq ($(PLATFORM),win32)
ifndef CSC_LINK
$(warning No code-sign certificate found (CSC_LINK is not set))
ifndef CSC_KEY_PASSWORD
$(warning No code-sign certificate password found (CSC_KEY_PASSWORD is not set))
endif
endif
endif
# ---------------------------------------------------------------------
# Electron Builder
# ---------------------------------------------------------------------
ELECTRON_BUILDER_OPTIONS = --$(TARGET_ARCH_ELECTRON_BUILDER)
# ---------------------------------------------------------------------
# Analytics
# ---------------------------------------------------------------------
ifndef ANALYTICS_SENTRY_TOKEN
$(warning No Sentry token found (ANALYTICS_SENTRY_TOKEN is not set))
else
ELECTRON_BUILDER_OPTIONS += --extraMetadata.analytics.sentry.token=$(ANALYTICS_SENTRY_TOKEN)
endif
ifndef ANALYTICS_MIXPANEL_TOKEN
$(warning No Mixpanel token found (ANALYTICS_MIXPANEL_TOKEN is not set))
else
ELECTRON_BUILDER_OPTIONS += --extraMetadata.analytics.mixpanel.token=$(ANALYTICS_MIXPANEL_TOKEN)
endif
# ---------------------------------------------------------------------
# Rules
# ---------------------------------------------------------------------
# See http://stackoverflow.com/a/12528721
# Note that the blank line before 'endef' is actually important - don't delete it
define execute-command
$(1)
endef
$(BUILD_DIRECTORY):
mkdir $@
$(BUILD_TEMPORARY_DIRECTORY): | $(BUILD_DIRECTORY)
mkdir $@
# ---------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)-app: \
package.json npm-shrinkwrap.json \
| $(BUILD_DIRECTORY)
mkdir -p $@
./scripts/build/dependencies-npm.sh -p \
-r "$(TARGET_ARCH)" \
-v "$(NODE_VERSION)" \
-x $@ \
-t node \
-s "$(PLATFORM)"
patch --directory=$@ --force --strip=1 --ignore-whitespace < patches/lzma-native-index-static-addon-require.patch
cp -r lib $@
cp package.json $@
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH): \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)-app \
| $(BUILD_DIRECTORY)
mkdir $@
cd $< && pkg --output ../../$@/$(ETCHER_CLI_BINARY) -t node6-$(PLATFORM_PKG)-$(TARGET_ARCH) $(ENTRY_POINT_CLI)
./scripts/build/dependencies-npm-extract-addons.sh \
-d $</node_modules \
-o $@/node_modules
# pkg currently has a bug where darwin executables
# can't be code-signed
# See https://github.com/zeit/pkg/issues/128
# ifeq ($(PLATFORM),darwin)
# ifdef CSC_NAME
# ./scripts/build/electron-sign-file-darwin.sh -f $@/$(ETCHER_CLI_BINARY) -i "$(CSC_NAME)"
# endif
# endif
# pkg currently has a bug where Windows executables
# can't be branded
# See https://github.com/zeit/pkg/issues/149
# ifeq ($(PLATFORM),win32)
# ./scripts/build/electron-brand-exe.sh \
# -f $@/$(ETCHER_CLI_BINARY) \
# -n $(APPLICATION_NAME) \
# -d "$(APPLICATION_DESCRIPTION)" \
# -v "$(APPLICATION_VERSION)" \
# -c "$(APPLICATION_COPYRIGHT)" \
# -m "$(COMPANY_NAME)" \
# -i assets/icon.ico \
# -w $(BUILD_TEMPORARY_DIRECTORY)
# endif
ifeq ($(PLATFORM),win32)
ifdef CSC_LINK
ifdef CSC_KEY_PASSWORD
./scripts/build/electron-sign-exe-win32.sh -f $@/$(ETCHER_CLI_BINARY) \
-d "$(APPLICATION_NAME) - $(APPLICATION_VERSION)" \
-c $(CSC_LINK) \
-p $(CSC_KEY_PASSWORD)
endif
endif
endif
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip: \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
./scripts/build/zip-file.sh -f $< -s $(PLATFORM) -o $@
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz: \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
./scripts/build/tar-gz-file.sh -f $< -o $@
# ---------------------------------------------------------------------
# GUI
# ---------------------------------------------------------------------
assets/dmg/background.tiff: assets/dmg/background.png assets/dmg/background@2x.png
tiffutil -cathidpicheck $^ -out $@
build/js/gui.js: .FORCE
webpack
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg: assets/dmg/background.tiff build/js/gui.js \
| $(BUILD_DIRECTORY)
TARGET_ARCH=$(TARGET_ARCH) build --mac dmg $(ELECTRON_BUILDER_OPTIONS) \
--extraMetadata.version=$(APPLICATION_VERSION) \
--extraMetadata.packageType=dmg
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip: assets/dmg/background.tiff build/js/gui.js \
| $(BUILD_DIRECTORY)
TARGET_ARCH=$(TARGET_ARCH) build --mac zip $(ELECTRON_BUILDER_OPTIONS) \
--extraMetadata.version=$(APPLICATION_VERSION) \
--extraMetadata.packageType=zip
APPLICATION_NAME_ELECTRON = $(APPLICATION_NAME_LOWERCASE)-electron
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm: build/js/gui.js \
| $(BUILD_DIRECTORY)
build --linux rpm $(ELECTRON_BUILDER_OPTIONS) \
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
--extraMetadata.version=$(APPLICATION_VERSION_REDHAT) \
--extraMetadata.packageType=rpm
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb: build/js/gui.js \
| $(BUILD_DIRECTORY)
build --linux deb $(ELECTRON_BUILDER_OPTIONS) \
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
--extraMetadata.version=$(APPLICATION_VERSION_DEBIAN) \
--extraMetadata.packageType=deb
ifeq ($(TARGET_ARCH),x64)
ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-unpacked
else
ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY = linux-$(TARGET_ARCH_ELECTRON_BUILDER)-unpacked
endif
$(BUILD_DIRECTORY)/$(ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY)/$(APPLICATION_NAME_ELECTRON): build/js/gui.js | $(BUILD_DIRECTORY)
build --dir --linux $(ELECTRON_BUILDER_OPTIONS) \
--extraMetadata.name=$(APPLICATION_NAME_ELECTRON) \
--extraMetadata.version=$(APPLICATION_VERSION) \
--extraMetadata.packageType=AppImage
touch $@
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM).AppDir: \
$(BUILD_DIRECTORY)/$(ELECTRON_BUILDER_LINUX_UNPACKED_DIRECTORY)/$(APPLICATION_NAME_ELECTRON) \
| $(BUILD_DIRECTORY)
./scripts/build/electron-create-appdir.sh \
-n $(APPLICATION_NAME) \
-d "$(APPLICATION_DESCRIPTION)" \
-p $(dir $<) \
-r $(TARGET_ARCH) \
-b $(APPLICATION_NAME_ELECTRON) \
-i assets/icon.png \
-o $@
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage: \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM).AppDir \
| $(BUILD_DIRECTORY) $(BUILD_TEMPORARY_DIRECTORY)
./scripts/build/electron-create-appimage-linux.sh \
-d $< \
-r $(TARGET_ARCH) \
-w $(BUILD_TEMPORARY_DIRECTORY) \
-o $@
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip: \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(TARGET_ARCH_APPIMAGE).AppImage \
| $(BUILD_DIRECTORY)
./scripts/build/zip-file.sh -f $< -s $(PLATFORM) -o $@
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: build/js/gui.js \
| $(BUILD_DIRECTORY)
TARGET_ARCH=$(TARGET_ARCH) build --win portable $(ELECTRON_BUILDER_OPTIONS) \
--extraMetadata.version=$(APPLICATION_VERSION) \
--extraMetadata.packageType=portable
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe: build/js/gui.js \
| $(BUILD_DIRECTORY)
TARGET_ARCH=$(TARGET_ARCH) build --win nsis $(ELECTRON_BUILDER_OPTIONS) \
--extraMetadata.version=$(APPLICATION_VERSION) \
--extraMetadata.packageType=nsis
# ---------------------------------------------------------------------
# Phony targets
# ---------------------------------------------------------------------
TARGETS = \
help \
info \
lint \
lint-js \
lint-sass \
lint-cpp \
lint-html \
lint-spell \
test-spectron \
test-gui \
test-sdk \
test-cli \
test \
sanity-checks \
clean \
distclean \
changelog \
webpack \
package-electron \
package-cli \
cli-develop \
installers-all \
publish-all \
electron-develop
changelog:
versionist
webpack: build/js/gui.js
package-electron:
TARGET_ARCH=$(TARGET_ARCH) build --dir $(ELECTRON_BUILDER_OPTIONS)
package-cli: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH)
ifeq ($(PLATFORM),darwin)
electron-installer-app-zip: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip
electron-installer-dmg: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg
cli-installer-tar-gz: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
TARGETS += \
electron-installer-dmg \
electron-installer-app-zip \
cli-installer-tar-gz
PUBLISH_AWS_S3 += \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION)-mac.zip \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-$(APPLICATION_VERSION).dmg \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
endif
ifeq ($(PLATFORM),linux)
electron-installer-appimage: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip
electron-installer-debian: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
electron-installer-redhat: $(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm
cli-installer-tar-gz: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
TARGETS += \
electron-installer-appimage \
electron-installer-debian \
electron-installer-redhat \
cli-installer-tar-gz
PUBLISH_AWS_S3 += \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_LOWERCASE)-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH_APPIMAGE).zip \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).tar.gz
PUBLISH_BINTRAY_DEBIAN += \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)_$(APPLICATION_VERSION_DEBIAN)_$(TARGET_ARCH_DEBIAN).deb
PUBLISH_BINTRAY_REDHAT += \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME_ELECTRON)-$(APPLICATION_VERSION_REDHAT).$(TARGET_ARCH_REDHAT).rpm
endif
ifeq ($(PLATFORM),win32)
electron-installer-portable: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe
electron-installer-nsis: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe
cli-installer-zip: $(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip
TARGETS += \
electron-installer-portable \
electron-installer-nsis \
cli-installer-zip
PUBLISH_AWS_S3 += \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Portable-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-Setup-$(APPLICATION_VERSION)-$(TARGET_ARCH).exe \
$(BUILD_DIRECTORY)/$(APPLICATION_NAME)-cli-$(APPLICATION_VERSION)-$(PLATFORM)-$(TARGET_ARCH).zip
endif
installers-all: $(PUBLISH_AWS_S3) $(PUBLISH_BINTRAY_DEBIAN) $(PUBLISH_BINTRAY_REDHAT)
ifdef PUBLISH_AWS_S3
publish-aws-s3: $(PUBLISH_AWS_S3)
ifeq ($(RELEASE_TYPE),production)
$(foreach publishable,$^,$(call execute-command,./scripts/publish/aws-s3.sh \
-f $(publishable) \
-b $(S3_BUCKET) \
-v $(APPLICATION_VERSION) \
-p $(APPLICATION_NAME_LOWERCASE)))
endif
ifeq ($(RELEASE_TYPE),snapshot)
$(foreach publishable,$^,$(call execute-command,./scripts/publish/aws-s3.sh \
-f $(publishable) \
-b $(S3_BUCKET) \
-v $(APPLICATION_VERSION) \
-p $(APPLICATION_NAME_LOWERCASE) \
-k $(shell date +"%Y-%m-%d")))
endif
PUBLISHABLES += publish-aws-s3
TARGETS += publish-aws-s3
endif
ifeq ($(RELEASE_TYPE),production)
ifdef PUBLISH_BINTRAY_DEBIAN
publish-bintray-debian: $(PUBLISH_BINTRAY_DEBIAN)
$(foreach publishable,$^,$(call execute-command,./scripts/publish/bintray.sh \
-f $(publishable) \
-v $(APPLICATION_VERSION_DEBIAN) \
-r $(TARGET_ARCH) \
-t $(RELEASE_TYPE) \
-o $(BINTRAY_ORGANIZATION) \
-p $(BINTRAY_REPOSITORY_DEBIAN) \
-c $(BINTRAY_COMPONENT) \
-y debian))
PUBLISHABLES += publish-bintray-debian
TARGETS += publish-bintray-debian
endif
ifdef PUBLISH_BINTRAY_REDHAT
publish-bintray-redhat: $(PUBLISH_BINTRAY_REDHAT)
$(foreach publishable,$^,$(call execute-command,./scripts/publish/bintray.sh \
-f $(publishable) \
-v $(APPLICATION_VERSION_REDHAT) \
-r $(TARGET_ARCH) \
-t $(RELEASE_TYPE) \
-o $(BINTRAY_ORGANIZATION) \
-p $(BINTRAY_REPOSITORY_REDHAT) \
-c $(BINTRAY_COMPONENT) \
-y redhat))
PUBLISHABLES += publish-bintray-redhat
TARGETS += publish-bintray-redhat
endif
endif
publish-all: $(PUBLISHABLES)
.PHONY: $(TARGETS)
cli-develop:
./scripts/build/dependencies-npm.sh \
-r "$(TARGET_ARCH)" \
-v "$(NODE_VERSION)" \
-t node \
-s "$(PLATFORM)"
electron-develop:
./scripts/build/dependencies-npm.sh \
-r "$(TARGET_ARCH)" \
-v "$(ELECTRON_VERSION)" \
-t electron \
-s "$(PLATFORM)"
sass:
node-sass lib/gui/app/scss/main.scss > lib/gui/css/main.css
lint-js:
eslint lib tests scripts bin versionist.conf.js webpack.config.js
lint-sass:
sass-lint lib/gui/scss
lint-cpp:
cpplint --recursive src
lint-html:
node scripts/html-lint.js
lint-spell:
codespell \
--dictionary - \
--dictionary dictionary.txt \
--skip *.gz,*.bz2,*.xz,*.zip,*.img,*.dmg,*.iso,*.rpi-sdcard,.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
MOCHA_OPTIONS=--recursive --reporter spec
test-spectron:
ETCHER_SPECTRON_ENTRYPOINT="$(ETCHER_SPECTRON_ENTRYPOINT)" mocha $(MOCHA_OPTIONS) tests/spectron
test-gui:
electron-mocha $(MOCHA_OPTIONS) --renderer tests/gui
test-sdk:
electron-mocha $(MOCHA_OPTIONS) \
tests/shared \
tests/image-stream
test-cli:
mocha $(MOCHA_OPTIONS) \
tests/shared \
tests/image-stream
test: test-gui test-sdk test-spectron
help:
@echo "Available targets: $(TARGETS)"
info:
@echo "Application version : $(APPLICATION_VERSION)"
@echo "Release type : $(RELEASE_TYPE)"
@echo "Platform : $(PLATFORM)"
@echo "Host arch : $(HOST_ARCH)"
@echo "Target arch : $(TARGET_ARCH)"
sanity-checks:
./scripts/ci/ensure-staged-sass.sh
./scripts/ci/ensure-staged-shrinkwrap.sh
./scripts/ci/ensure-npm-dependencies-compatibility.sh
./scripts/ci/ensure-npm-valid-dependencies.sh
./scripts/ci/ensure-npm-shrinkwrap-versions.sh
./scripts/ci/ensure-all-file-extensions-in-gitattributes.sh
clean:
rm -rf $(BUILD_DIRECTORY)
distclean: clean
rm -rf node_modules
rm -rf build
rm -rf generated
.DEFAULT_GOAL = help

172
README.md
View File

@ -1,90 +1,123 @@
# Etcher
Etcher
======
> Flash OS images to SD cards & USB drives, safely and easily.
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. It can also directly flash Raspberry Pi devices that support [USB device boot mode](https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#usb-device-boot-mode).
was written correctly and much more.
[![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher)
[![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE)
[![Balena.io Forums](https://img.shields.io/discourse/https/forums.balena.io/topics.svg?style=flat-square&label=balena.io%20forums)](https://forums.balena.io/c/etcher)
[![Current Release](https://img.shields.io/github/release/resin-io/etcher.svg?style=flat-square)](https://etcher.io)
![License](https://img.shields.io/github/license/resin-io/etcher.svg?style=flat-square)
[![Travis CI status](https://img.shields.io/travis/resin-io/etcher/master.svg?style=flat-square&label=linux)](https://travis-ci.org/resin-io/etcher/branches)
[![AppVeyor status](https://img.shields.io/appveyor/ci/resin-io/etcher/master.svg?style=flat-square&label=windows)](https://ci.appveyor.com/project/resin-io/etcher/branch/master)
[![Dependency status](https://img.shields.io/david/resin-io/etcher.svg?style=flat-square)](https://david-dm.org/resin-io/etcher)
[![Resin.io Forums](https://img.shields.io/discourse/https/forums.resin.io/topics.svg?style=flat-square&label=resin.io%20forums)](https://forums.resin.io/c/etcher)
[![Stories in Progress](https://img.shields.io/waffle/label/resin-io/etcher/in%20progress.svg?style=flat-square)](https://waffle.io/resin-io/etcher)
---
***
[**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones]
[**Download**][etcher] | [**Support**][SUPPORT] | [**Documentation**][USER-DOCUMENTATION] | [**Contributing**][CONTRIBUTING] | [**Roadmap**][milestones] | [**CLI**][CLI]
## Supported Operating Systems
![Etcher](https://raw.githubusercontent.com/resin-io/etcher/master/screenshot.png)
- Linux; most distros; Intel 64-bit.
- Windows 10 and later; Intel 64-bit.
- macOS 10.13 (High Sierra) and later; both Intel and Apple Silicon.
Supported Operating Systems
---------------------------
## Installers
- Linux (most distros)
- macOS 10.9 and later
- Microsoft Windows 7 and later
Note that Etcher will run on any platform officially supported by
[Electron][electron]. Read more in their
[documentation][electron-supported-platforms].
Installers
----------
Refer to the [downloads page][etcher] for the latest pre-made
installers for all supported operating systems.
## Packages
#### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64)
Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
1. Add Etcher debian repository:
##### Install .deb file using apt
```
echo "deb https://dl.bintray.com/resin-io/debian stable etcher" | sudo tee /etc/apt/sources.list.d/etcher.list
```
```sh
sudo apt install ./balena-etcher_******_amd64.deb
```
2. Trust Bintray.com's GPG key:
```sh
sudo apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 379CE192D401AB61
```
3. Update and install:
```sh
sudo apt-get update
sudo apt-get install etcher-electron
```
##### Uninstall
```sh
sudo apt remove balena-etcher
```
#### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64)
##### Yum
Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/)
1. Install using yum
```sh
sudo yum localinstall balena-etcher-***.x86_64.rpm
sudo apt-get remove etcher-electron
sudo rm /etc/apt/sources.list.d/etcher.list
sudo apt-get update
```
#### Redhat (RHEL) and Fedora based Package Repository (GNU/Linux x86/x64)
1. Add Etcher rpm repository:
```sh
sudo wget https://bintray.com/resin-io/redhat/rpm -O /etc/yum.repos.d/bintray-resin-io-redhat.repo
```
2. Update and install:
```sh
sudo yum install -y etcher-electron
```
or
```sh
sudo dnf install -y etcher-electron
```
##### Uninstall
```
sudo yum remove -y etcher-electron
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
sudo yum clean all
sudo yum makecache fast
```
or
```
sudo dnf remove -y etcher-electron
sudo rm /etc/yum.repos.d/bintray-resin-io-redhat.repo
sudo dnf clean all
sudo dnf makecache
```
#### Arch/Manjaro Linux (GNU/Linux x64)
#### Brew Cask (macOS)
Etcher is offered through the Arch User Repository and can be installed on both Manjaro and Arch systems. You can compile it from the source code in this repository using [`balena-etcher`](https://aur.archlinux.org/packages/balena-etcher/). The following example uses a common AUR helper to install the latest release:
Note that the Etcher Cask has to be updated manually to point to new versions,
so it might not refer to the latest version immediately after an Etcher
release.
```sh
yay -S balena-etcher
brew cask install etcher
```
##### Uninstall
```sh
yay -R balena-etcher
brew cask uninstall etcher
```
#### WinGet (Windows)
This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically.
```sh
winget install balenaEtcher #or Balena.Etcher
```
##### Uninstall
```sh
winget uninstall balenaEtcher
```
#### Chocolatey (Windows)
### Chocolatey (Windows)
This package is maintained by [@majkinetor](https://github.com/majkinetor), and
is kept up to date automatically.
@ -93,28 +126,25 @@ is kept up to date automatically.
choco install etcher
```
##### Uninstall
Support
-------
```sh
choco uninstall etcher
```
If you're having any problem, please [raise an issue][newissue] on GitHub and
the resin.io team will be happy to help.
## Support
License
-------
If you're having any problem, please [raise an issue][newissue] on GitHub, and
the balena.io team will be happy to help.
## License
Etcher is free software and may be redistributed under the terms specified in
Etcher is free software, and may be redistributed under the terms specified in
the [license].
[etcher]: https://balena.io/etcher
[electron]: https://electronjs.org/
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
[support]: https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md
[contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md
[user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[milestones]: https://github.com/balena-io/etcher/milestones
[newissue]: https://github.com/balena-io/etcher/issues/new
[license]: https://github.com/balena-io/etcher/blob/master/LICENSE
[etcher]: https://etcher.io
[electron]: http://electron.atom.io
[electron-supported-platforms]: http://electron.atom.io/docs/tutorial/supported-platforms/
[SUPPORT]: https://github.com/resin-io/etcher/blob/master/SUPPORT.md
[CONTRIBUTING]: https://github.com/resin-io/etcher/blob/master/docs/CONTRIBUTING.md
[CLI]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
[USER-DOCUMENTATION]: https://github.com/resin-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[milestones]: https://github.com/resin-io/etcher/milestones
[newissue]: https://github.com/resin-io/etcher/issues/new
[license]: https://github.com/resin-io/etcher/blob/master/LICENSE

34
SUPPORT.md Normal file
View File

@ -0,0 +1,34 @@
Getting help with Etcher
========================
There are various ways to get support for Etcher if you experience an issue or
have an idea you'd like to share with us.
Forums
------
We have a [Discourse forum][discourse] which is open to everyone, so please
come join us :). Drop us a line there and the resin.io staff and community
users will be happy to assist. Your question might already be answered, so take
a look at the existing threads before opening a new one!
Make sure to mention the following information to help us provide better
support:
- The Etcher version you're running.
- The operating system you're running Etcher in.
- Relevant logging output, if any, from DevTools, which you can open by
pressing `Ctrl+Alt+I` or `Cmd+Alt+I` depending on your platform.
GitHub
------
If you encounter an issue or have a suggestion, head on over to Etcher's [issue
tracker][issues] and if there isn't a ticket covering it, [create
one][new-issue].
[discourse]: https://forums.resin.io/c/etcher
[issues]: https://github.com/resin-io/etcher/issues
[new-issue]: https://github.com/resin-io/etcher/issues/new

View File

@ -1,11 +0,0 @@
#!/bin/bash
# Link to the binary
# Must hardcode balenaEtcher directory; no variable available
ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}'
# SUID chrome-sandbox for Electron 5+
chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true
update-mime-database /usr/share/mime || true
update-desktop-database /usr/share/applications || true

50
appveyor.yml Normal file
View File

@ -0,0 +1,50 @@
# appveyor file
# http://www.appveyor.com/docs/appveyor-yml
image: Visual Studio 2015
# See https://github.com/electron/spectron#on-appveyor
os: unstable
cache:
- C:\Users\appveyor\.node-gyp
- '%LOCALAPPDATA%\electron\Cache'
- '%LOCALAPPDATA%\electron-builder\cache'
- '%AppData%\npm-cache'
- '%USERPROFILE%\.pkg-cache'
- node_modules -> npm-shrinkwrap.json
- C:\ProgramData\chocolatey\bin -> appveyor.yml
- C:\ProgramData\chocolatey\lib -> appveyor.yml
- C:\Users\appveyor\AppData\Local\Temp\chocolatey -> appveyor.yml
# what combinations to test
environment:
global:
ELECTRON_NO_ATTACH_CONSOLE: true
nodejs_version: "6.10.3"
platform:
- x86
- x64
matrix:
fast_finish: true
install:
- ps: Update-NodeJsInstallation $env:nodejs_version $env:Platform
- set PATH=C:\Program Files (x86)\Windows Kits\8.1\bin\x86;%PATH%
- set PATH=C:\Program Files (x86)\NSIS;%PATH%
- set PATH=C:\MinGW\bin;%PATH%
- set PATH=C:\MinGW\msys\1.0\bin;%PATH%
- bash .\scripts\ci\install.sh -o win32 -r %Platform%
build: off
test_script:
- node --version
- npm --version
- bash .\scripts\ci\test.sh -o win32 -r %Platform%
- bash .\scripts\ci\build-installers.sh -o win32 -r %Platform%
deploy_script:
- if %APPVEYOR_REPO_BRANCH%==master (bash .\scripts\ci\deploy.sh -o win32 -r %Platform%)

BIN
assets/dmg/background.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/dmg/background.tiff Executable file → Normal file

Binary file not shown.

BIN
assets/dmg/background@2x.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/icon.icns Executable file → Normal file

Binary file not shown.

BIN
assets/icon.ico Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

BIN
assets/icon.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 881 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 802 B

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 146 KiB

2
bin/etcher Executable file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
require('../lib/cli/etcher');

25
binding.gyp Normal file
View File

@ -0,0 +1,25 @@
{
"targets": [
{
"target_name": "elevator",
"include_dirs" : [
"src",
"<!(node -e \"require('nan')\")"
],
'conditions': [
[ 'OS=="win"', {
"sources": [
"src/utils/v8utils.cpp",
"src/os/win32/elevate.cpp",
"src/elevator_init.cpp",
],
"libraries": [
"-lShell32.lib",
],
} ]
],
}
],
}

9
dictionary.txt Normal file
View File

@ -0,0 +1,9 @@
boolen->boolean
aknowledge->acknowledge
seleted->selected
reming->remind
locl->local
subsribe->subscribe
unsubsribe->unsubscribe
calcluate->calculate
dictionaty->dictionary

View File

@ -12,9 +12,12 @@ technologies used in Etcher that you should become familiar with:
- [Electron][electron]
- [NodeJS][nodejs]
- [AngularJS][angularjs]
- [Redux][redux]
- [ImmutableJS][immutablejs]
- [Bootstrap][bootstrap]
- [Sass][sass]
- [Flexbox Grid][flexbox-grid]
- [Mocha][mocha]
- [JSDoc][jsdoc]
@ -37,18 +40,62 @@ to submit their work or bug reports.
These are the main Etcher components, in a nutshell:
- [Drivelist](https://github.com/balena-io-modules/drivelist)
- [Etcher Image Write][etcher-image-write]
This is the repository that implements the actual procedures to write an image
to a raw device and the place where image validation resides. Its main purpose
is to abstract the messy details of interacting with raw devices in all major
operating systems.
- [Etcher Image Stream](../lib/image-stream)
> (Moved from a separate repository into the main Etcher codebase)
This module converts any kind of input into a readable stream
representing the image so it can be plugged to [etcher-image-write]. Inputs
that this module might handle could be, for example: a simple image file, a URL
to an image, a compressed image, an image inside a ZIP archive, etc. Together
with [etcher-image-write], these modules are the building blocks needed to take
an image representation to the user's device, the "Etcher's backend".
- [Drivelist](https://github.com/resin-io-modules/drivelist)
As the name implies, this module's duty is to detect the connected drives
uniformly in all major operating systems, along with valuable metadata, like if
a drive is removable or not, to prevent users from trying to write an image to
a system drive.
- [Etcher](https://github.com/balena-io/etcher)
- [Etcher](https://github.com/resin-io/etcher)
This is the *"main repository"*, from which you're reading this from, which is
basically the front-end and glue for all previously listed projects.
Front-ends
----------
The main repository consists of the implementation of the Etcher CLI and the
Etcher GUI (the desktop application), located at [`lib/cli/`][cli-dir] and
[`lib/gui/`][gui-dir], respectively.
In fact, the only front-end that interacts directly with Etcher's backend is
the CLI. The GUI merely forks the CLI and communicates with its child process
to get state information.
In this sense, you can consider the GUI as being the front-end to the CLI,
which is in turn the front-end to the actual image writing functionality.
As a way to simplify how the GUI forks the CLI in a packaged and distributed
context, both the CLI and GUI share the same application entry point. This
means that the same Etcher binary can behave as CLI or GUI as needed.
## Process communication
As mentioned before, the Etcher GUI forks the CLI and retrieves information
from it to update its state. In order to accomplish this, the Etcher CLI
contains certain features to ease communication:
- [Well-documented exit codes.][exit-codes]
Summary
-------
@ -59,12 +106,17 @@ 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
[gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui
[etcher-image-write]: https://github.com/resin-io-modules/etcher-image-write
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js
[cli-dir]: https://github.com/resin-io/etcher/tree/master/lib/cli
[gui-dir]: https://github.com/resin-io/etcher/tree/master/lib/gui
[electron]: http://electron.atom.io
[nodejs]: https://nodejs.org
[angularjs]: https://angularjs.org
[redux]: http://redux.js.org
[immutablejs]: http://facebook.github.io/immutable-js/
[bootstrap]: http://getbootstrap.com
[sass]: http://sass-lang.com
[flexbox-grid]: http://flexboxgrid.com
[mocha]: http://mochajs.org
[jsdoc]: http://usejsdoc.org

61
docs/CLI-INSTALLATION.md Normal file
View File

@ -0,0 +1,61 @@
### macOS and GNU/Linux
- Extract the `.tar.gz` package by running:
```sh
tar fvx path/to/cli.tar.gz
```
- Move the resulting directory to `/opt/etcher-cli`
- Add `/opt/etcher-cli` to the `PATH`. For example, add the following to
`.bashrc` or `.zshrc`:
```sh
export PATH="$PATH:/opt/etcher-cli"
```
### Windows
- Unzip the `.zip` package by right-clicking on it and selecting "Extract All"
- Move the resulting directory to `C:\etcher-cli`
- Add `C:\etcher-cli` to the `%PATH%`
- On Windows 10 and Windows 8
- Open *Control Panel*
- Open *System*
- Click the *Advanced system settings* link
- Click *Environment Variables*
- Find the `PATH` environment variable, and click *Edit*
- Append `;C:\etcher-cli` to the environment variable value
- Click *OK*
- On Windows 7
- Right-click the *My Computer* icon
- Open the *Properties* menu
- Open the *Advanced* tab
- Click *Environment Variables*
- Find the `PATH` environment variable, and click *Edit*
- Append `;C:\etcher-cli` to the environment variable value
- Click *OK*
- Re-open `cmd.exe`, or PowerShell
### Running
```sh
etcher -v
```
### Options
```
--help, -h show help
--version, -v show version number
--drive, -d drive
--check, -c validate write
--yes, -y confirm non-interactively
--unmount, -u unmount on success
```

42
docs/CLI.md Normal file
View File

@ -0,0 +1,42 @@
Etcher CLI
==========
The Etcher CLI is a command-line tool that aims to provide all the benefits of
the Etcher desktop application in a way that can be run from a terminal, or
even used from a script.
In fact, the Etcher desktop application is simply a wrapper around the CLI,
which is the place where the actual writing logic takes place.
Installing
----------
Head over to [etcher.io/cli][etcher-cli], download the package that corresponds to
your operating system, and then follow the installation instructions there.
Running
-------
```sh
etcher -v
```
Options
-------
```
--help, -h show help
--version, -v show version number
--drive, -d drive
--check, -c validate write
--yes, -y confirm non-interactively
--unmount, -u unmount on success
```
Debug mode
----------
You can set the `ETCHER_CLI_DEBUG` environment variable to make the Etcher CLI
print error stack traces.
[etcher-cli]: https://etcher.io/cli

View File

@ -12,29 +12,72 @@ over the commit history.
- Be able to automatically reference relevant changes from a dependency
upgrade.
The guidelines are inspired by the [AngularJS git commit
guidelines][angular-commit-guidelines].
Commit structure
----------------
Each commit message needs to specify the semver-type. Which can be `patch|minor|major`.
See the [Semantic Versioning][semver] specification for a more detailed explanation of the meaning of these types.
See balena commit guidelines for more info about the whole commit structure.
Each commit message consists of a header, a body and a footer. The header has a
special format that includes a type, a scope and a subject.
```
<semver-type>: <subject>
```
or
```
<subject>
<type>(<scope>): <subject>
<BLANK LINE>
<details>
<body>
<BLANK LINE>
Change-Type: <semver-type>
<footer>
```
The subject should not contain more than 70 characters, including the type and
scope, and the body should be wrapped at 72 characters.
Type
----
Must be one of the following:
- `feat`: A new feature.
- `fix`: A bug fix.
- `minifix`: A minimal fix that doesn't warrant an entry in the CHANGELOG.
- `docs`: Documentation only changes.
- `style`: Changes that do not affect the meaning of the code (white-space,
formatting, missing semi-colons, JSDoc annotations, comments, etc).
- `refactor`: A code change that neither fixes a bug nor adds a feature.
- `perf`: A code change that improves performance.
- `test`: Adding missing tests.
- `chore`: Changes to the build process or auxiliary tools and libraries.
- `upgrade`: A version upgrade of a project dependency.
Scope
-----
The scope is required for types that make sense, such as `feat`, `fix`,
`test`, etc. Certain commit types, such as `chore` might not have a clearly
defined scope, in which case its better to omit it.
When it applies, the scope must be either `GUI` or `CLI`.
A commit that takes part in both the GUI and CLI scopes, and makes more logical
sense that way, might entirely omit the scope.
Subject
-------
The subject should contain a short description of the change:
- Use the imperative, present tense.
- Don't capitalize the first letter.
- No dot (.) at the end.
Footer
------
The footer contains extra information about the commit, such as tags.
**Breaking Changes** should start with the word BREAKING CHANGE: with a space
or two newlines. The rest of the commit message is then used for this.
Tags
----
@ -79,8 +122,129 @@ A commit can include multiple instances of this tag.
Examples:
```
Closes: https://github.com/balena-io/etcher/issues/XXX
Fixes: https://github.com/balena-io/etcher/issues/XXX
Closes: https://github.com/resin-io/etcher/issues/XXX
Fixes: https://github.com/resin-io/etcher/issues/XXX
```
### `Change-Type: <type>`
This tag is used to determine the change type that a commit introduces. The
following types are supported:
- `major`
- `minor`
- `patch`
This tag can be omitted for commits that don't change the application from the
user's point of view, such as for refactoring commits.
Examples:
```
Change-Type: major
Change-Type: minor
Change-Type: patch
```
See the [Semantic Versioning][semver] specification for a more detailed
explanation of the meaning of these types.
### `Changelog-Entry: <message>`
This tag is used to describe the changes introduced by the commit in a more
human style that would fit the `CHANGELOG.md` better.
If the commit type is either `fix` or `feat`, the commit will take part in the
CHANGELOG. If this tag is not defined, then the commit subject will be used
instead.
You explicitly can use this tag to make a commit whose type is not `fix` nor
`feat` appear in the `CHANGELOG.md`.
Since whatever your write here will be shown *as it is* in the `CHANGELOG.md`,
take some time to write a decent entry. Consider the following guidelines:
- Use the imperative, present tense.
- Capitalize the first letter.
There is no fixed length limit for the contents of this tag, but always strive
to make as short as possible without compromising its quality.
Examples:
```
Changelog-Entry: Fix EPERM errors when flashing to a GPT drive.
```
Complete examples
-----------------
```
fix(GUI): ignore extensions before the first non-compressed extension
Currently, we extract all the extensions from an image path and report back
that the image is invalid if *any* of the extensions is not valid , however
this can cause trouble with images including information between dots that are
not strictly extensions, for example:
elementaryos-0.3.2-stable-i386.20151209.iso
Etcher will consider `20151209` to be an invalid extension and therefore
will prevent such image from being selected at all.
As a way to allow these corner cases but still make use of our enforced check
controls, the validation routine now only consider extensions starting from the
first non compressed extension.
Change-Type: patch
Changelog-Entry: Don't interpret image file name information between dots as image extensions.
Fixes: https://github.com/resin-io/etcher/issues/492
```
***
```
upgrade: etcher-image-write to v5.0.2
This version contains a fix to an `EPERM` issue happening to some Windows user,
triggered by the `write` system call during the first ~5% of a flash given that
the operating system still thinks the drive has a file system.
Change-Type: patch
Changelog-Entry: Upgrade `etcher-image-write` to v5.0.2.
Link: https://github.com/resin-io-modules/etcher-image-write/blob/master/CHANGELOG.md#502---2016-06-27
Fixes: https://github.com/resin-io/etcher/issues/531
```
***
```
feat(GUI): implement update notifier functionality
Auto-update functionality is not ready for usage. As a workaround to
prevent users staying with older versions, we now check for updates at
startup, and if the user is not running the latest version, we present a
modal informing the user of the availiblity of a new version, and
provide a call to action to open the Etcher website in his web browser.
Extra features:
- The user can skip the update, and tell the program to delay the
notification for 7 days.
Misc changes:
- Center modal with flexbox, to allow more flexibility on the modal height.
interacting with the S3 server.
- Implement `ManifestBindService`, which now serves as a backend for the
`manifest-bind` directive to allow the directive's functionality to be
re-used by other services.
- Namespace checkbox styles that are specific to the settings page.
Change-Type: minor
Changelog-Entry: Check for updates and show a modal prompting the user to download the latest version.
Closes: https://github.com/resin-io/etcher/issues/396
```
[angular-commit-guidelines]: https://github.com/angular/angular.js/blob/master/CONTRIBUTING.md#commit
[semver]: http://semver.org

View File

@ -17,11 +17,10 @@ Developing
#### Common
- [NodeJS](https://nodejs.org) (at least v16.11)
- [Python 3](https://www.python.org)
- [NodeJS](https://nodejs.org) (at least v6)
- [Python 2.7](https://www.python.org)
- [jq](https://stedolan.github.io/jq/)
- [curl](https://curl.haxx.se/)
- [npm](https://www.npmjs.com/)
```sh
pip install -r requirements.txt
@ -33,16 +32,16 @@ You might need to run this with `sudo` or administrator permissions.
- [NSIS v2.51](http://nsis.sourceforge.net/Main_Page) (v3.x won't work)
- Either one of the following:
- [Visual C++ 2019 Build Tools](https://visualstudio.microsoft.com/vs/features/cplusplus/) containing standalone compilers, libraries and scripts
- The [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools#windows-build-tools) should be installed along with NodeJS
- [Visual Studio Community 2019](https://visualstudio.microsoft.com/vs/) (free) (other editions, like Professional and Enterprise, should work too)
**NOTE:** Visual Studio doesn't install C++ by default. You have to rerun the
- [Visual C++ 2015 Build Tools](http://landinghub.visualstudio.com/visual-cpp-build-tools) containing standalone compilers, libraries and scripts
- Install the [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) via npm with `npm install --global windows-build-tools`
- [Visual Studio Community 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48146) (free) (other editions, like Professional and Enterprise, should work too)
**NOTE:** Visual Studio 2015 doesn't install C++ by default. You have to rerun the
setup, select "Modify" and then check `Visual C++ -> Common Tools for Visual
C++` (see http://stackoverflow.com/a/31955339)
C++ 2015` (see http://stackoverflow.com/a/31955339)
- [MinGW](http://www.mingw.org)
You might need to `npm config set msvs_version 2019` for node-gyp to correctly detect
the version of Visual Studio you're using (in this example VS2019).
You might need to `npm config set msvs_version 2015` for node-gyp to correctly detect
the version of Visual Studio you're using (in this example VS2015).
The following MinGW packages are required:
@ -52,33 +51,54 @@ The following MinGW packages are required:
- `msys-bash`
- `msys-coreutils`
#### macOS
#### OS X
- [Xcode](https://developer.apple.com/xcode/)
It's not enough to have [Xcode Command Line Tools] installed. Xcode must be installed
as well.
- [XCode](https://developer.apple.com/xcode/) or [XCode Command Line Tools],
which can be installed by running `xcode-select --install`.
#### Linux
- `libudev-dev` for libusb (for example install with `sudo apt install libudev-dev`, or on fedora `systemd-devel` contains the required package)
- `libudev-dev` for libusb (install with `sudo apt install libudev-dev` for example)
### Cloning the project
```sh
git clone --recursive https://github.com/balena-io/etcher
git clone https://github.com/resin-io/etcher
cd etcher
```
### Installing npm dependencies
**NOTE:** Please make use of the following command to install npm dependencies rather
than simply running `npm install` given that we need to do extra configuration
to make sure native dependencies are correctly compiled for Electron, otherwise
the application might not run successfully.
If you're on Windows, **run the command from the _Developer Command Prompt for
VS2015_**, to ensure all Visual Studio command utilities are available in the
`%PATH%`.
```sh
make electron-develop
```
### Running the application
#### GUI
```sh
# Build and start application
# Build the GUI
make webpack
# Start Electron
npm start
```
#### CLI
```sh
node bin/etcher
```
Testing
-------
@ -102,6 +122,11 @@ systems as they can before sending a pull request.
*The test suite is run automatically by CI servers when you send a pull
request.*
We also rely on various `make` targets to perform some common tasks:
- `make lint`: Run the linter.
- `make sass`: Compile SCSS files.
We make use of [EditorConfig] to communicate indentation, line endings and
other text editing default. We encourage you to install the relevant plugin in
your text editor of choice to avoid having to fix any issues during the review
@ -110,7 +135,19 @@ process.
Updating a dependency
---------------------
- Install new version of dependency using npm
Given we use [npm shrinkwrap][shrinkwrap], we have to take extra steps to make
sure the `npm-shrinkwrap.json` file gets updated correctly when we update a
dependency.
Use the following steps to ensure everything goes flawlessly:
- Run `make electron-develop` to ensure you don't have extraneous dependencies
you might have brought during development, or you are running older
dependencies because you come from another branch or reference.
- Install the new version of the dependency. For example: `npm install --save
<package>@<version>`. This will update the `npm-shrinkwrap.json` file.
- Commit *both* `package.json` and `npm-shrinkwrap.json`.
Diffing Binaries
@ -167,7 +204,7 @@ Before your pull request can be merged, the following conditions must hold:
- The linter doesn't throw any warning.
- All the tests pass.
- All the tests passes.
- The coding style aligns with the project's convention.
@ -176,9 +213,9 @@ systems we support.
Don't hesitate to get in touch if you have any questions or need any help!
[ARCHITECTURE]: https://github.com/balena-io/etcher/blob/master/docs/ARCHITECTURE.md
[COMMIT-GUIDELINES]: https://github.com/balena-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
[ARCHITECTURE]: https://github.com/resin-io/etcher/blob/master/docs/ARCHITECTURE.md
[COMMIT-GUIDELINES]: https://github.com/resin-io/etcher/blob/master/docs/COMMIT-GUIDELINES.md
[EditorConfig]: http://editorconfig.org
[shrinkwrap]: https://docs.npmjs.com/cli/shrinkwrap
[hxd]: https://github.com/jhermsmeier/hxd
[Xcode Command Line Tools]: https://developer.apple.com/library/content/technotes/tn2339/_index.html
[XCode Command Line Tools]: https://developer.apple.com/library/content/technotes/tn2339/_index.html

View File

@ -1,52 +0,0 @@
## Why is my drive not bootable?
Etcher copies images to drives byte by byte, without doing any transformation to the final device, which means images that require special treatment to be made bootable, like Windows images, will not work out of the box. In these cases, the general advice is to use software specific to those kind of images, usually available from the image publishers themselves. You can find more information [here](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#why-is-my-drive-not-bootable).
## How can I configure persistent storage?
Some programs, usually oriented at making GNU/Linux live USB drives, include an option to set persistent storage. This is currently not supported by Etcher, so if you require this functionality, we advise to fallback to [UNetbootin](https://unetbootin.github.io/).
## How do I flash Ubuntu ISOs
Ubuntu images (and potentially some other related GNU/Linux distributions) have a peculiar format that allows the image to boot without any further modification from both CDs and USB drives.
A consequence of this enhancement is that some programs, like parted get confused about the drive's format and partition table, printing warnings such as:
> /dev/xxx contains GPT signatures, indicating that it has a GPT table. However, it does not have a valid fake msdos partition table, as it should. Perhaps it was corrupted -- possibly by a program that doesn't understand GPT partition tables. Or perhaps you deleted the GPT table, and are now using an msdos partition table. Is this a GPT partition table? Both the primary and backup GPT tables are corrupt. Try making a fresh table, and using Parted's rescue feature to recover partitions.
> Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes.
All these warnings are safe to ignore, and your drive should be able to boot without any problems.
Refer to [the following message from Ubuntu's mailing list](https://lists.ubuntu.com/archives/ubuntu-devel/2011-June/033495.html) if you want to learn more.
## How do I run Etcher on Wayland?
The XWayland Server provides backwards compatibility to run any X client on Wayland, including Etcher.
This usually works out of the box on mainstream GNU/Linux distributions that properly support Wayland. If it doesn't, make sure the xwayland.so module is being loaded by declaring it in your [weston.ini](http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html):
```
[core]
modules=xwayland.so
```
## What are the runtime GNU/LINUX dependencies?
[This entry](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#runtime-gnulinux-dependencies) aims to provide an up to date list of runtime dependencies needed to run Etcher on a GNU/Linux system.
## How can I recover the broken drive?
Sometimes, things might go wrong, and you end up with a half-flashed drive that is unusable by your operating systems, and common graphical tools might even refuse to get it back to a normal state.
To solve these kinds of problems, we've collected [a list of fail-proof methods](https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md#recovering-broken-drives) to completely erase your drive in major operating systems.
## I receive "No polkit authentication agent found" error in GNU/Linux
Etcher requires an available [polkit authentication agent](https://wiki.archlinux.org/index.php/Polkit#Authentication_agents) in your system in order to show a secure password prompt dialog to perform elevation. Make sure you have one installed for the desktop environment of your choice.
## May I run Etcher in older macOS versions?
Etcher GUI is based on the [Electron](http://electron.atom.io/) framework, [which only supports macOS 10.10 and newer versions](https://github.com/electron/electron/blob/master/docs/tutorial/support.md#supported-platforms).
## Can I use the Flash With Etcher button on my site?
You can use the Flash with Etcher button on your site or blog, if you have an OS that you want your users to be able to easily flash using Etcher, add the following code where you want to button to be:
`<a href="https://efp.balena.io/open-image-url?imageUrl=<your image URL>"><img src="http://balena.io/flash-with-etcher.png" /></a>`

View File

@ -8,20 +8,16 @@ Releasing
### Release Types
- **draft**: A continues snapshot of current master, made by the CI services
- **pre-release** (default): A continues snapshot of current master, made by the CI services
- **release**: Full releases
Draft release is created from each PR, tagged with the branch name.
All merged PR will generate a new tag/version as a *pre-release*.
Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary.
- **snapshot** (default): A continues snapshot of current master, made by the CI services
- **production**: Full releases
### Flight Plan
#### Preparation
- [Prepare the new version](#preparing-a-new-version)
- [Generate build artifacts](#generating-binaries) (binaries, archives, etc.)
- [Draft a release on GitHub](https://github.com/balena-io/etcher/releases)
- [Draft a release on GitHub](https://github.com/resin-io/etcher/releases)
- Upload build artifacts to GitHub release draft
#### Testing
@ -31,18 +27,51 @@ Mark the pre-release as final when it is necessary, then distribute the packages
#### Publishing
- [Publish release draft on GitHub](https://github.com/balena-io/etcher/releases)
- [Post release note to forums](https://forums.balena.io/c/etcher)
- [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec)
- [Update the website](https://github.com/balena-io/etcher-homepage)
- Wait 2-3 hours for analytics (Sentry, Amplitude) to trickle in and check for elevated error rates, or regressions
- [Publish release draft on GitHub](https://github.com/resin-io/etcher/releases)
- [Post release note to forums](https://forums.resin.io/c/etcher)
- [Update the website](https://github.com/resin-io/etcher-homepage)
- Wait 2-3 hours for analytics (Sentry, Mixpanel) to trickle in and check for elevated error rates, or regressions
- If regressions arise; pull the release, and release a patched version, else:
- [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront)
- Post changelog with `#release-notes` tag on internal chat
- [Upload deb & rpm packages to Bintray](#uploading-packages-to-bintray)
- [Upload build artifacts to Amazon S3](#uploading-binaries-to-amazon-s3)
- Post changelog with `#release-notes` tag on Flowdock
- If this release packs noteworthy major changes:
- Write a blog post about it, and / or
- Write about it to the Etcher mailing list
### Preparing a New Version
- Create & hop onto a new release branch, i.e. `release-1.0.0`
- Bump the version number in the `package.json`'s `version` property.
- Bump the version number in the `npm-shrinkwrap.json`'s `version` property
- Add a new entry to `CHANGELOG.md` by running `make changelog`
- Manually revise the `CHANGELOG.md` versionist output
- Update `screenshot.png` so it displays the latest version in the bottom
right corner
- Revise the `updates.semverRange` version in `package.json`
- Commit the changes with the version number as the commit title, including the `v` prefix, to `master`. For example:
**NOTE:** The version **MUST** be prefixed with a "v"
```bash
git commit -m "v1.0.0" # not 1.0.0
```
- Create an annotated tag for the new version. The commit title should equal the annotated tag name. For example:
```bash
git tag -a v1.0.0 -m "v1.0.0"
```
- Push the commit and the annotated tag.
```bash
git push
git push --tags
```
- Open a pull request against `master` titled "Release v1.0.0"
### Generating binaries
**Environment**
@ -51,30 +80,106 @@ Make sure to set the analytics tokens when generating production release binarie
```bash
export ANALYTICS_SENTRY_TOKEN="xxxxxx"
export ANALYTICS_AMPLITUDE_TOKEN="xxxxxx"
export ANALYTICS_MIXPANEL_TOKEN="xxxxxx"
```
#### Linux
##### Clean dist folder
Delete `.webpack` and `out/`.
**NOTE:** Make sure to adjust the path as necessary (here the Etcher repository has been cloned to `/home/$USER/code/etcher`)
```bash
./scripts/build/docker/run-command.sh -r x64 -s . -c "make distclean"
```
##### Generating artifacts
The artifacts are generated by the CI and published as draft-release or pre-release.
Etcher is built with electron-forge. Run:
```bash
# x64
```
npm run make
# Build Debian packages
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
# Build RPM packages
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
# Build AppImages
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
# Build CLI
./scripts/build/docker/run-command.sh -r x64 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
# x86
# Build Debian packages
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-debian"
# Build RPM packages
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-redhat"
# Build AppImages
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production electron-installer-appimage"
# Build CLI
./scripts/build/docker/run-command.sh -r x86 -s . -c "make electron-develop && make RELEASE_TYPE=production cli-installer-tar-gz"
```
Our CI will appropriately sign artifacts for macOS and some Windows targets.
#### Mac OS
**ATTENTION:** For production releases you'll need the code-signing key,
and set `CSC_NAME` to generate signed binaries on Mac OS.
### Uploading packages to Cloudfront
**NOTE:** The CLI is not code-signed for either at this time.
Log in to cloudfront and upload the `rpm` and `deb` files.
```bash
make electron-develop
# Build the CLI
make RELEASE_TYPE=production cli-installer-tar-gz
# Build the zip
make RELEASE_TYPE=production electron-installer-app-zip
# Build the dmg
make RELEASE_TYPE=production electron-installer-dmg
```
#### Windows
**ATTENTION:** For production releases you'll need the code-signing key,
and set `CSC_LINK`, and `CSC_KEY_PASSWORD` to generate signed binaries on Windows.
**NOTE:**
- The CLI is not code-signed for either at this time.
- Keep in mind to also generate artifacts for x86, with `TARGET_ARCH=x86`.
```bash
make electron-develop
# Build the CLI
make RELEASE_TYPE=production cli-installer-zip
# Build the Portable version
make RELEASE_TYPE=production electron-installer-portable
# Build the Installer
make RELEASE_TYPE=production electron-installer-nsis
```
### Uploading packages to Bintray
```bash
export BINTRAY_USER="username@account"
export BINTRAY_API_KEY="youruserapikey"
```
```bash
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x64" -f "dist/etcher-electron_1.2.1_amd64.deb"
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "debian" -y "debian" -r "x86" -f "dist/etcher-electron_1.2.1_i386.deb"
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x64" -f "dist/etcher-electron-1.2.1.x86_64.rpm"
./scripts/publish/bintray.sh -c "etcher" -t "production" -v "1.2.1" -o "resin-io" -p "redhat" -y "redhat" -r "x86" -f "dist/etcher-electron-1.2.1.i686.rpm"
```
### Uploading binaries to Amazon S3
```bash
export S3_KEY="..."
```
```bash
./scripts/publish/aws-s3.sh -b "resin-production-downloads" -v "1.2.1" -p "etcher" -f "dist/<filename>"
```
### Dealing with a Problematic Release
@ -84,7 +189,7 @@ revert the problematic release as soon as possible, until the bugs are fixed.
You can revert a version by deleting its builds from the S3 bucket and Bintray.
Refer to the `Makefile` for the up to date information about the S3 bucket
where we push builds to, and get in touch with the balena.io operations team to
where we push builds to, and get in touch with the resin.io operations team to
get write access to it.
The Etcher update notifier dialog and the website only show the a certain
@ -98,15 +203,3 @@ aws s3api delete-object --bucket <bucket name> --key <file name>
```
The Bintray dashboard provides an easy way to delete a version's files.
### Submitting binaries to Symantec
- [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/)
- Fill out form:
- **Select Submission Type:** "Provide a direct download URL"
- **Name of the software being detected:** Etcher
- **Name of detection given by Symantec product:** WS.Reputation.1
- **Contact name:** Balena.io Ltd
- **E-mail address:** hello@etcher.io
- **Are you the creator or distributor of the software in question?** Yes

View File

@ -112,4 +112,4 @@ Analytics
- [ ] Disable analytics, open DevTools Network pane or a packet sniffer, and
check that no request is sent
- [ ] **Disable analytics, refresh application from DevTools (using Cmd-R or
F5), and check that initial events are not sent to Amplitude**
F5), and check that initial events are not sent to Mixpanel**

View File

@ -7,9 +7,44 @@ systems.
Release Types
-------------
Etcher supports **pre-release** and **final** release types as does Github. Each is
published to Github releases.
The release version is generated automatically from the commit messasges.
Etcher supports **production** and **snapshot** release types. Each is
published to a different S3 bucket, and production release types are code
signed, while snapshot release types aren't and include a short git commit-hash
as a build number. For example, `1.0.0-beta.19` is a production release type,
while `1.0.0-beta.19+531ab82` is a snapshot release type.
In terms of comparison: `1.0.0-beta.19` (production) < `1.0.0-beta.19+531ab82`
(snapshot) < `1.0.0-rc.1` (production) < `1.0.0-rc.1+7fde24a` (snapshot) <
`1.0.0` (production) < `1.0.0+2201e5f` (snapshot). Keep in mind that if you're
running a production release type, you'll only be prompted to update to
production release types, and if you're running a snapshot release type, you'll
only be prompted to update to other snapshot release types.
The build system creates (and publishes) snapshot release types by default, but
you can build a specific release type by setting the `RELEASE_TYPE` make
variable. For example:
```sh
make <target> RELEASE_TYPE=snapshot
make <target> RELEASE_TYPE=production
```
We can control the version range a specific Etcher version will consider when
showing the update notification dialog by tweaking the `updates.semverRange`
property of `package.json`.
Update Channels
---------------
Etcher has a setting to include the unstable update channel. If this option is
set, Etcher will consider both stable and unstable versions when showing the
update notifier dialog. Unstable versions are the ones that contain a `beta`
pre-release tag. For example:
- Production unstable version: `1.4.0-beta.1`
- Snapshot unstable version: `1.4.0-beta.1+7fde24a`
- Production stable version: `1.4.0`
- Snapshot stable version: `1.4.0+7fde24a`
Signing
-------
@ -17,7 +52,7 @@ Signing
### OS X
1. Get our Apple Developer ID certificate for signing applications distributed
outside the Mac App Store from the balena.io Apple account.
outside the Mac App Store from the resin.io Apple account.
2. Install the Developer ID certificate to your Mac's Keychain by double
clicking on the certificate file.
@ -27,7 +62,7 @@ packaging for OS X.
### Windows
1. Get access to our code signing certificate and decryption key as a balena.io
1. Get access to our code signing certificate and decryption key as a resin.io
employee by asking for it from the relevant people.
2. Place the certificate in the root of the Etcher repository naming it
@ -36,24 +71,65 @@ employee by asking for it from the relevant people.
Packaging
---------
Run the following command on each platform:
The resulting installers will be saved to `dist/out`.
Run the following commands:
### OS X
```sh
npm run make
make electron-installer-dmg
make electron-installer-app-zip
```
This will produce all targets (eg. zip, dmg) specified in forge.config.ts for the
host platform and architecture.
### GNU/Linux
The resulting artifacts can be found in `out/make`.
```sh
make electron-installer-appimage
make electron-installer-debian
```
### Windows
Publishing to Cloudfront
```sh
make electron-installer-zip
make electron-installer-nsis
```
Publishing to Bintray
---------------------
We publish GNU/Linux Debian packages to [Cloudfront][cloudfront].
We publish GNU/Linux Debian packages to [Bintray][bintray].
Log in to cloudfront and upload the `rpm` and `deb` files.
Make sure you set the following environment variables:
- `BINTRAY_USER`
- `BINTRAY_API_KEY`
Run the following command:
```sh
make publish-bintray-debian
```
Publishing to S3
----------------
- [AWS CLI][aws-cli]
Make sure you have the [AWS CLI tool][aws-cli] installed and configured to
access resin.io's production or snapshot S3 bucket.
Run the following command to publish all files for the current combination of
_platform_ and _arch_ (building them if necessary):
```sh
make publish-aws-s3
```
Also add links to each AWS S3 file in [GitHub Releases][github-releases]. See
[`v1.0.0-beta.17`](https://github.com/resin-io/etcher/releases/tag/v1.0.0-beta.17)
as an example.
Publishing to Homebrew Cask
---------------------------
@ -67,16 +143,12 @@ Publishing to Homebrew Cask
Announcing
----------
Post messages to the [Etcher forum][balena-forum-etcher] announcing the new version
Post messages to the [Etcher forum][resin-forum-etcher] announcing the new version
of Etcher, and including the relevant section of the Changelog.
[aws-cli]: https://aws.amazon.com/cli
[cloudfront]: https://cloudfront.com
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb
[bintray]: https://bintray.com
[etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/etcher.rb
[homebrew-cask]: https://github.com/caskroom/homebrew-cask
[balena-forum-etcher]: https://forums.balena.io/c/etcher
[github-releases]: https://github.com/balena-io/etcher/releases
Updating EFP / Success-Banner
-----------------------------
Etcher Featured Project is automatically run based on an algorithm which promoted projects from the balena marketplace which have been contributed by the community, the algorithm prioritises projects which give uses the best experience. Editing both EFP and the Etcher Success-Banner can only be done by someone from balena, instruction are on the [Etcher-EFP repo (private)](https://github.com/balena-io/etcher-efp)
[resin-forum-etcher]: https://forums.resin.io/c/etcher
[github-releases]: https://github.com/resin-io/etcher/releases

View File

@ -1,43 +0,0 @@
Getting help with BalenaEtcher
===============================
There are various ways to get support for Etcher if you experience an issue or
have an idea you'd like to share with us.
Documentation
------
We have answers to a variety of frequently asked questions in the [user
documentation][documentation] and also in the [FAQs][faq] on the Etcher website.
Forums
------
We have a [Discourse forum][discourse] which is open to everyone, so please
come join us :). Drop us a line there and the balena.io staff and community
users will be happy to assist. Your question might already be answered, so take
a look at the existing threads before opening a new one!
Make sure to mention the following information to help us provide better
support:
- The BalenaEtcher version you're running.
- The operating system you're running Etcher in.
- Relevant logging output, if any, from DevTools, which you can open by
pressing `Ctrl+Shift+I` or `Cmd+Alt+I` depending on your platform.
GitHub
------
If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue
tracker][issues] and if there isn't a ticket covering it, [create
one][new-issue].
[discourse]: https://forums.balena.io/c/etcher
[issues]: https://github.com/balena-io/etcher/issues
[new-issue]: https://github.com/balena-io/etcher/issues/new
[documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md
[faq]: https://etcher.io

View File

@ -3,11 +3,6 @@ Etcher User Documentation
This document contains how-tos and FAQs oriented to Etcher users.
Config
------
Etcher's configuration is saved to the `config.json` file in the apps folder.
Not all the options are surfaced to the UI. You may edit this file to tweak settings even before launching the app.
Why is my drive not bootable?
-----------------------------
@ -35,7 +30,7 @@ if you require this functionality, we advise to fallback to
Deactivate desktop shortcut prompt on GNU/Linux
-----------------------------------------------
This is a feature provided by [AppImages][appimage], where the applications
This is a feature provided by [AppImages](appimage), where the applications
prompts the user to automatically register a desktop shortcut to easily access
the application.
@ -122,6 +117,7 @@ run Etcher on a GNU/Linux system.
- xrender
- xtst
- xscrnsaver
- gconf-2.0
- gmodule-2.0
- nss
@ -134,6 +130,21 @@ run Etcher on a GNU/Linux system.
- liblzma (for xz decompression)
Simulate an update alert
------------------------
You can set the `ETCHER_FAKE_S3_LATEST_VERSION` environment variable to a valid
semver version (greater than the current version) to trick the application into
thinking that what you put there is the latest available version, therefore
causing the update notification dialog to be presented at startup.
Note that the value of the variable will be ignored if it doesn't match the
release type of the current application version. For example, setting the
variable to a production version (e.g. `ETCHER_FAKE_S3_LATEST_VERSION=2.0.0`)
will be ignored if you're running a snapshot build, and vice-versa.
See [`PUBLISHING.md`][publishing] for more details about release types.
Recovering broken drives
------------------------
@ -163,18 +174,6 @@ pre-installed in all modern Windows versions.
- Run `clean`. This command will completely clean your drive by erasing any
existent filesystem.
- Run `create partition primary`. This command will create a new partition.
- Run `active`. This command will active the partition.
- Run `list partition`. This command will show available partition.
- Run `select partition N`, where `N` corresponds to the id of the newly available partition.
- Run `format override quick`. This command will format the partition. You can choose a specific formatting by adding `FS=xx` where `xx` could be `NTFS or FAT or FAT32` after `format`. Example : `format FS=NTFS override quick`
- Run `exit` to quit diskpart.
### OS X
@ -182,7 +181,7 @@ Run the following command in `Terminal.app`, replacing `N` by the corresponding
disk number, which you can find by running `diskutil list`:
```sh
diskutil eraseDisk FAT32 UNTITLED MBRFormat /dev/diskN
diskutil eraseDisk free UNTITLED /dev/diskN
```
### GNU/Linux
@ -207,20 +206,22 @@ Running in older macOS versions
-------------------------------
Etcher GUI is based on the [Electron][electron] framework, [which only supports
macOS 10.10 (Yosemite) and newer versions][electron-supported-platforms].
macOS 10.9 and newer versions][electron-supported-platforms].
[balena.io]: https://balena.io
You can however, run the [Etcher CLI][etcher-cli], which should work in older
platforms.
[resin.io]: https://resin.io
[appimage]: http://appimage.org
[xwayland]: https://wayland.freedesktop.org/xserver.html
[weston.ini]: http://manpages.ubuntu.com/manpages/wily/man5/weston.ini.5.html
[diskpart]: https://technet.microsoft.com/en-us/library/cc770877(v=ws.11).aspx
[electron]: https://electronjs.org/
[electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms
[publishing]: https://github.com/balena-io/etcher/blob/master/docs/PUBLISHING.md
[electron]: http://electron.atom.io
[electron-supported-platforms]: https://github.com/electron/electron/blob/master/docs/tutorial/supported-platforms.md
[etcher-cli]: https://github.com/resin-io/etcher/blob/master/docs/CLI.md
[publishing]: https://github.com/resin-io/etcher/blob/master/docs/PUBLISHING.md
[windows-usb-tool]: https://www.microsoft.com/en-us/download/windows-usb-dvd-download-tool
[rufus]: https://rufus.akeo.ie
[unetbootin]: https://unetbootin.github.io
[windows-iot-dashboard]: https://developer.microsoft.com/en-us/windows/iot/downloads
[woeusb]: https://github.com/slacka/WoeUSB
See [PUBLISHING](/docs/PUBLISHING.md) for more details about release types.

1
docs/_config.yml Normal file
View File

@ -0,0 +1 @@
theme: jekyll-theme-minimal

92
electron-builder.yml Normal file
View File

@ -0,0 +1,92 @@
appId: io.resin.etcher
copyright: Copyright 2016-2018 Resinio Ltd
productName: Etcher
npmRebuild: false
nodeGypRebuild: false
publish: null
files:
- lib
- "!lib/gui/app"
- lib/gui/app/index.html
- generated
- build/**/*.node
- assets/icon.png
- node_modules/**/*
mac:
icon: assets/icon.icns
category: public.app-category.developer-tools
dmg:
background: assets/dmg/background.tiff
icon: assets/icon.icns
iconSize: 110
contents:
- x: 140
y: 225
- x: 415
y: 225
type: link
path: /Applications
window:
width: 540
height: 405
win:
icon: assets/icon.ico
nsis:
oneClick: true
runAfterFinish: true
installerIcon: assets/icon.ico
uninstallerIcon: assets/icon.ico
deleteAppDataOnUninstall: true
license: LICENSE
artifactName: "${productName}-Setup-${version}-${env.TARGET_ARCH}.${ext}"
portable:
artifactName: "${productName}-Portable-${version}-${env.TARGET_ARCH}.${ext}"
requestExecutionLevel: user
linux:
category: Utility
packageCategory: utils
executableName: etcher-electron
synopsis: 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.
icon: assets/iconset
deb:
priority: optional
depends:
- gconf2
- gconf-service
- libappindicator1
- libasound2
- libatk1.0-0
- libc6
- libcairo2
- libcups2
- libdbus-1-3
- libexpat1
- libfontconfig1
- libfreetype6
- libgcc1
- libgconf-2-4
- libgdk-pixbuf2.0-0
- libglib2.0-0
- libgtk2.0-0
- liblzma5
- libnotify4
- libnspr4
- libnss3
- libpango1.0-0
- libstdc++6
- libx11-6
- libxcomposite1
- libxcursor1
- libxdamage1
- libxext6
- libxfixes3
- libxi6
- libxrandr2
- libxrender1
- libxss1
- libxtst6
- polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1
rpm:
depends:
- lsb
- libXScrnSaver

View File

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.device.usb</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
</dict>
</plist>

View File

@ -1,159 +0,0 @@
import type { ForgeConfig } from '@electron-forge/shared-types';
import { MakerSquirrel } from '@electron-forge/maker-squirrel';
import { MakerZIP } from '@electron-forge/maker-zip';
import { MakerDeb } from '@electron-forge/maker-deb';
import { MakerRpm } from '@electron-forge/maker-rpm';
import { MakerDMG } from '@electron-forge/maker-dmg';
import { MakerAppImage } from '@reforged/maker-appimage';
import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives';
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
import { exec } from 'child_process';
import { mainConfig, rendererConfig } from './webpack.config';
import * as sidecar from './forge.sidecar';
import { hostDependencies, productDescription } from './package.json';
const osxSigningConfig: any = {};
let winSigningConfig: any = {};
if (process.env.NODE_ENV === 'production') {
osxSigningConfig.osxNotarize = {
tool: 'notarytool',
appleId: process.env.XCODE_APP_LOADER_EMAIL,
appleIdPassword: process.env.XCODE_APP_LOADER_PASSWORD,
teamId: process.env.XCODE_APP_LOADER_TEAM_ID,
};
winSigningConfig = {
signWithParams: `-sha1 ${process.env.SM_CODE_SIGNING_CERT_SHA1_HASH} -tr ${process.env.TIMESTAMP_SERVER} -td sha256 -fd sha256 -d balena-etcher`,
};
}
const config: ForgeConfig = {
packagerConfig: {
asar: true,
icon: './assets/icon',
executableName:
process.platform === 'linux' ? 'balena-etcher' : 'balenaEtcher',
appBundleId: 'io.balena.etcher',
appCategoryType: 'public.app-category.developer-tools',
appCopyright: 'Copyright 2016-2023 Balena Ltd',
darwinDarkModeSupport: true,
protocols: [{ name: 'etcher', schemes: ['etcher'] }],
extraResource: [
'lib/shared/sudo/sudo-askpass.osascript-zh.js',
'lib/shared/sudo/sudo-askpass.osascript-en.js',
],
osxSign: {
optionsForFile: () => ({
entitlements: './entitlements.mac.plist',
hardenedRuntime: true,
}),
},
...osxSigningConfig,
},
rebuildConfig: {
onlyModules: [], // prevent rebuilding *any* native modules as they won't be used by electron but by the sidecar
},
makers: [
new MakerZIP(),
new MakerSquirrel({
setupIcon: 'assets/icon.ico',
loadingGif: 'assets/icon.png',
...winSigningConfig,
}),
new MakerDMG({
background: './assets/dmg/background.tiff',
icon: './assets/icon.icns',
iconSize: 110,
contents: ((opts: { appPath: string }) => {
return [
{ x: 140, y: 250, type: 'file', path: opts.appPath },
{ x: 415, y: 250, type: 'link', path: '/Applications' },
];
}) as any, // type of MakerDMGConfig omits `appPath`
additionalDMGOptions: {
window: {
size: {
width: 540,
height: 425,
},
position: {
x: 400,
y: 500,
},
},
},
}),
new MakerAppImage({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
},
}),
new MakerRpm({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
productDescription,
requires: ['util-linux'],
},
}),
new MakerDeb({
options: {
icon: './assets/icon.png',
categories: ['Utility'],
section: 'utils',
priority: 'optional',
productDescription,
scripts: {
postinst: './after-install.tpl',
},
depends: hostDependencies['debian'],
},
}),
],
plugins: [
new AutoUnpackNativesPlugin({}),
new WebpackPlugin({
mainConfig,
renderer: {
config: rendererConfig,
nodeIntegration: true,
entryPoints: [
{
html: './lib/gui/app/index.html',
js: './lib/gui/app/renderer.ts',
name: 'main_window',
preload: {
js: './lib/gui/app/preload.ts',
},
},
],
},
}),
new sidecar.SidecarPlugin(),
],
hooks: {
postPackage: async (_forgeConfig, options) => {
if (options.platform === 'linux') {
// symlink the etcher binary from balena-etcher to balenaEtcher to ensure compatibility with the wdio suite and the old name
await new Promise<void>((resolve, reject) => {
exec(
`ln -s "${options.outputPaths}/balena-etcher" "${options.outputPaths}/balenaEtcher"`,
(err) => {
if (err) {
reject(err);
} else {
resolve();
}
},
);
});
}
},
},
};
export default config;

View File

@ -1,168 +0,0 @@
import { PluginBase } from '@electron-forge/plugin-base';
import type {
ForgeHookMap,
ResolvedForgeConfig,
} from '@electron-forge/shared-types';
import { WebpackPlugin } from '@electron-forge/plugin-webpack';
import { DefinePlugin } from 'webpack';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as d from 'debug';
const debug = d('sidecar');
function isStartScrpt(): boolean {
return process.env.npm_lifecycle_event === 'start';
}
function addWebpackDefine(
config: ResolvedForgeConfig,
defineName: string,
binDir: string,
binName: string,
): ResolvedForgeConfig {
config.plugins.forEach((plugin) => {
if (plugin.name !== 'webpack' || !(plugin instanceof WebpackPlugin)) {
return;
}
const { mainConfig } = plugin.config as any;
if (mainConfig.plugins == null) {
mainConfig.plugins = [];
}
const value = isStartScrpt()
? // on `npm start`, point directly to the binary
path.resolve(binDir, binName)
: // otherwise point relative to the resources folder of the bundled app
binName;
debug(`define '${defineName}'='${value}'`);
mainConfig.plugins.push(
new DefinePlugin({
// expose path to helper via this webpack define
[defineName]: JSON.stringify(value),
}),
);
});
return config;
}
function build(
sourcesDir: string,
buildForArchs: string,
binDir: string,
binName: string,
) {
const commands: Array<[string, string[], object?]> = [
['tsc', ['--project', 'tsconfig.sidecar.json', '--outDir', sourcesDir]],
];
buildForArchs.split(',').forEach((arch) => {
const binPath = isStartScrpt()
? // on `npm start`, we don't know the arch we're building for at the time we're
// adding the webpack define, so we just build under binDir
path.resolve(binDir, binName)
: // otherwise build in arch-specific directory within binDir
path.resolve(binDir, arch, binName);
// FIXME: rebuilding mountutils shouldn't be necessary, but it is.
// It's coming from etcher-sdk, a fix has been upstreamed but to use
// the latest etcher-sdk we need to upgrade axios at the same time.
commands.push(['npm', ['rebuild', 'mountutils', `--arch=${arch}`]]);
commands.push([
'pkg',
[
path.join(sourcesDir, 'util', 'api.js'),
'-c',
'pkg-sidecar.json',
// `--no-bytecode` so that we can cross-compile for arm64 on x64
'--no-bytecode',
'--public',
'--public-packages',
'"*"',
// always build for host platform and node version
// https://github.com/vercel/pkg-fetch/releases
'--target',
`node20-${arch}`,
'--output',
binPath,
],
]);
});
commands.forEach(([cmd, args, opt]) => {
debug('running command:', cmd, args.join(' '));
execFileSync(cmd, args, { shell: true, stdio: 'inherit', ...opt });
});
}
function copyArtifact(
buildPath: string,
arch: string,
binDir: string,
binName: string,
) {
const binPath = isStartScrpt()
? // on `npm start`, we don't know the arch we're building for at the time we're
// adding the webpack define, so look for the binary directly under binDir
path.resolve(binDir, binName)
: // otherwise look into arch-specific directory within binDir
path.resolve(binDir, arch, binName);
// buildPath points to appPath, which is inside resources dir which is the one we actually want
const resourcesPath = path.dirname(buildPath);
const dest = path.resolve(resourcesPath, path.basename(binPath));
debug(`copying '${binPath}' to '${dest}'`);
fs.copyFileSync(binPath, dest);
}
export class SidecarPlugin extends PluginBase<void> {
name = 'sidecar';
constructor() {
super();
this.getHooks = this.getHooks.bind(this);
debug('isStartScript:', isStartScrpt());
}
getHooks(): ForgeHookMap {
const DEFINE_NAME = 'ETCHER_UTIL_BIN_PATH';
const BASE_DIR = path.join('out', 'sidecar');
const SRC_DIR = path.join(BASE_DIR, 'src');
const BIN_DIR = path.join(BASE_DIR, 'bin');
const BIN_NAME = `etcher-util${process.platform === 'win32' ? '.exe' : ''}`;
return {
resolveForgeConfig: async (currentConfig) => {
debug('resolveForgeConfig');
return addWebpackDefine(currentConfig, DEFINE_NAME, BIN_DIR, BIN_NAME);
},
generateAssets: async (_config, platform, arch) => {
debug('generateAssets', { platform, arch });
build(SRC_DIR, arch, BIN_DIR, BIN_NAME);
},
packageAfterCopy: async (
_config,
buildPath,
electronVersion,
platform,
arch,
) => {
debug('packageAfterCopy', {
buildPath,
electronVersion,
platform,
arch,
});
copyArtifact(buildPath, arch, BIN_DIR, BIN_NAME);
},
};
}
}

21
lib/cli/README.md Normal file
View File

@ -0,0 +1,21 @@
Etcher CLI
==========
The Etcher CLI is a command line interface to the Etcher writer backend, and
currently the only module in the "Etcher" umbrella that makes use of this
backend directly.
This module also has the task of unmounting the drives before and after
flashing.
Notice the Etcher CLI is not worried about elevation, and assumes it has enough
permissions to continue, throwing an error otherwise.
Exit codes
----------
The Etcher CLI uses certain exit codes to signal the result of the operation.
These are documented in [`lib/shared/exit-codes.js`][exit-codes] and are also
printed on the Etcher CLI help page.
[exit-codes]: https://github.com/resin-io/etcher/blob/master/lib/shared/exit-codes.js

124
lib/cli/diskpart.js Normal file
View File

@ -0,0 +1,124 @@
/*
* 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.
*/
'use strict'
const os = require('os')
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const childProcess = require('child_process')
const debug = require('debug')('etcher:cli:diskpart')
const Promise = require('bluebird')
const retry = require('bluebird-retry')
const TMP_RANDOM_BYTES = 6
const DISKPART_DELAY = 2000
const DISKPART_RETRIES = 5
/**
* @summary Generate a tmp filename with full path of OS' tmp dir
* @function
* @private
*
* @param {String} extension - temporary file extension
* @returns {String} filename
*
* @example
* const filename = tmpFilename('.sh');
*/
const tmpFilename = (extension) => {
const random = crypto.randomBytes(TMP_RANDOM_BYTES).toString('hex')
const filename = `etcher-diskpart-${random}${extension}`
return path.join(os.tmpdir(), filename)
}
/**
* @summary Run a diskpart script
* @param {Array<String>} commands - list of commands to run
* @param {Function} callback - callback(error)
* @example
* runDiskpart(['rescan'], (error) => {
* ...
* })
*/
const runDiskpart = (commands, callback) => {
if (os.platform() !== 'win32') {
callback()
return
}
const filename = tmpFilename('')
const script = commands.join('\r\n')
fs.writeFile(filename, script, {
mode: 0o755
}, (writeError) => {
debug('write %s:', filename, writeError || 'OK')
childProcess.exec(`diskpart /s ${filename}`, (execError, stdout, stderr) => {
debug('stdout:', stdout)
debug('stderr:', stderr)
fs.unlink(filename, (unlinkError) => {
debug('unlink %s:', filename, unlinkError || 'OK')
callback(execError)
})
})
})
}
module.exports = {
/**
* @summary Clean a device's partition tables
* @param {String} device - device path
* @example
* diskpart.clean('\\\\.\\PhysicalDrive2')
* .then(...)
* .catch(...)
* @returns {Promise}
*/
clean (device) {
if (os.platform() !== 'win32') {
return Promise.resolve()
}
debug('clean', device)
const pattern = /PHYSICALDRIVE(\d+)/i
if (pattern.test(device)) {
const deviceId = device.match(pattern).pop()
return retry(() => {
return new Promise((resolve, reject) => {
runDiskpart([ `select disk ${deviceId}`, 'clean', 'rescan' ], (error) => {
return error ? reject(error) : resolve()
})
}).delay(DISKPART_DELAY)
}, {
/* eslint-disable camelcase */
max_tries: DISKPART_RETRIES
/* eslint-enable camelcase */
}).catch((error) => {
throw new Error(`Couldn't clean the drive, ${error.failure.message} (code ${error.failure.code})`)
})
}
return Promise.reject(new Error(`Invalid device: "${device}"`))
}
}

153
lib/cli/etcher.js Normal file
View File

@ -0,0 +1,153 @@
/*
* 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 _ = require('lodash')
const Bluebird = require('bluebird')
const visuals = require('resin-cli-visuals')
const form = require('resin-cli-form')
const bytes = require('pretty-bytes')
const ImageWriter = require('../sdk/writer')
const utils = require('./utils')
const options = require('./options')
const messages = require('../shared/messages')
const EXIT_CODES = require('../shared/exit-codes')
const errors = require('../shared/errors')
const permissions = require('../shared/permissions')
/* eslint-disable no-magic-numbers */
const ARGV_IMAGE_PATH_INDEX = 0
const imagePath = options._[ARGV_IMAGE_PATH_INDEX]
permissions.isElevated().then((elevated) => {
if (!elevated) {
throw errors.createUserError({
title: messages.error.elevationRequired(),
description: 'This tool requires special permissions to write to external drives'
})
}
return form.run([
{
message: 'Select drive',
type: 'drive',
name: 'drive'
},
{
message: 'This will erase the selected drive. Are you sure?',
type: 'confirm',
name: 'yes',
default: false
}
], {
override: {
drive: options.drive,
// If `options.yes` is `false`, pass `null`,
// otherwise the question will not be asked because
// `false` is a defined value.
yes: options.yes || null
}
})
}).then((answers) => {
if (!answers.yes) {
throw errors.createUserError({
title: 'Aborted',
description: 'We can\'t proceed without confirmation'
})
}
const progressBars = {
write: new visuals.Progress('Flashing'),
check: new visuals.Progress('Validating')
}
return new Bluebird((resolve, reject) => {
/**
* @summary Progress update handler
* @param {Object} state - progress state
* @private
* @example
* writer.on('progress', onProgress)
*/
const onProgress = (state) => {
state.message = state.active > 1
? `${bytes(state.totalSpeed)}/s total, ${bytes(state.speed)}/s x ${state.active}`
: `${bytes(state.totalSpeed)}/s`
state.message = `${state.type === 'write' ? 'Flashing' : 'Validating'}: ${state.message}`
// Update progress bar
progressBars[state.type].update(state)
}
const writer = new ImageWriter({
verify: options.check,
unmountOnSuccess: options.unmount,
checksumAlgorithms: options.check ? [ 'sha512' ] : []
})
/**
* @summary Finish handler
* @private
* @example
* writer.on('finish', onFinish)
*/
const onFinish = function () {
resolve(Array.from(writer.destinations.values()))
}
writer.on('progress', onProgress)
writer.on('error', reject)
writer.on('finish', onFinish)
// NOTE: Drive can be (String|Array)
const destinations = [].concat(answers.drive)
writer.write(imagePath, destinations)
})
}).then((results) => {
let exitCode = EXIT_CODES.SUCCESS
if (options.check) {
console.log('')
console.log('Checksums:')
_.forEach(results, (result) => {
if (result.error) {
exitCode = EXIT_CODES.GENERAL_ERROR
console.log(` - ${result.device.device}: ${result.error.message}`)
} else {
console.log(` - ${result.device.device}: ${result.checksum.sha512}`)
}
})
}
process.exit(exitCode)
}).catch((error) => {
return Bluebird.try(() => {
utils.printError(error)
return Bluebird.resolve()
}).then(() => {
if (error.code === 'EVALIDATION') {
process.exit(EXIT_CODES.VALIDATION_ERROR)
}
process.exit(EXIT_CODES.GENERAL_ERROR)
})
})

166
lib/cli/options.js Normal file
View File

@ -0,0 +1,166 @@
/*
* 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 _ = require('lodash')
const fs = require('fs')
const yargs = require('yargs')
const utils = require('./utils')
const EXIT_CODES = require('../shared/exit-codes')
const errors = require('../shared/errors')
const packageJSON = require('../../package.json')
/**
* @summary The minimum required number of CLI arguments
* @constant
* @private
* @type {Number}
*/
const MINIMUM_NUMBER_OF_ARGUMENTS = 1
/**
* @summary The index of the image argument
* @constant
* @private
* @type {Number}
*/
const IMAGE_PATH_ARGV_INDEX = 0
/**
* @summary The first index that represents an actual option argument
* @constant
* @private
* @type {Number}
*
* @description
* The first arguments are usually the program executable itself, etc.
*/
const OPTIONS_INDEX_START = 2
/**
* @summary Parsed CLI options and arguments
* @type {Object}
* @public
*/
module.exports = yargs
// Don't wrap at all
.wrap(null)
.demand(MINIMUM_NUMBER_OF_ARGUMENTS, 'Missing image')
// Usage help
.usage('Usage: $0 [options] <image>')
.epilogue([
'Exit codes:',
_.map(EXIT_CODES, (value, key) => {
const reason = _.map(_.split(key, '_'), _.capitalize).join(' ')
return ` ${value} - ${reason}`
}).join('\n'),
'',
'If you need help, don\'t hesitate in contacting us at:',
'',
' GitHub: https://github.com/resin-io/etcher/issues/new',
' Forums: https://forums.resin.io/c/etcher'
].join('\n'))
// Examples
.example('$0 raspberry-pi.img')
.example('$0 --no-check raspberry-pi.img')
.example('$0 -d /dev/disk2 ubuntu.iso')
.example('$0 -d /dev/disk2 -y rpi.img')
// Help option
.help()
// Version option
.version(packageJSON.version)
// Error reporting
.fail((message, error) => {
const errorObject = error || errors.createUserError({
title: message
})
yargs.showHelp()
utils.printError(errorObject)
process.exit(EXIT_CODES.GENERAL_ERROR)
})
// Assert that image exists
.check((argv) => {
const imagePath = argv._[IMAGE_PATH_ARGV_INDEX]
try {
fs.accessSync(imagePath)
} catch (error) {
throw errors.createUserError({
title: 'Unable to access file',
description: `The image ${imagePath} is not accessible`
})
}
return true
})
// Assert that if the `yes` flag is provided, the `drive` flag is also provided.
.check((argv) => {
if (argv.yes && !argv.drive) {
throw errors.createUserError({
title: 'Missing drive',
description: 'You need to explicitly pass a drive when disabling interactively'
})
}
return true
})
.options({
help: {
describe: 'show help',
boolean: true,
alias: 'h'
},
version: {
describe: 'show version number',
boolean: true,
alias: 'v'
},
drive: {
describe: 'drive',
string: true,
alias: 'd'
},
check: {
describe: 'validate write',
boolean: true,
alias: 'c',
default: true
},
yes: {
describe: 'confirm non-interactively',
boolean: true,
alias: 'y'
},
unmount: {
describe: 'unmount on success',
boolean: true,
alias: 'u',
default: true
}
})
.parse(process.argv.slice(OPTIONS_INDEX_START))

47
lib/cli/utils.js Normal file
View File

@ -0,0 +1,47 @@
/*
* 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 chalk = require('chalk')
const errors = require('../shared/errors')
/**
* @summary Print an error to stderr
* @function
* @public
*
* @param {Error} error - error
*
* @example
* utils.printError(new Error('Oops!'));
*/
exports.printError = (error) => {
const title = errors.getTitle(error)
const description = errors.getDescription(error, {
userFriendlyDescriptionsOnly: true
})
console.error(chalk.red(title))
if (description) {
console.error(`\n${chalk.red(description)}`)
}
if (process.env.ETCHER_CLI_DEBUG && error.stack) {
console.error(`\n${chalk.red(error.stack)}`)
}
}

367
lib/gui/app/app.js Normal file
View File

@ -0,0 +1,367 @@
/*
* 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.
*/
/**
* @module Etcher
*/
'use strict'
/* eslint-disable no-var */
var angular = require('angular')
/* eslint-enable no-var */
const electron = require('electron')
const Bluebird = require('bluebird')
const semver = require('semver')
const EXIT_CODES = require('../../shared/exit-codes')
const messages = require('../../shared/messages')
const s3Packages = require('../../shared/s3-packages')
const release = require('../../shared/release')
const store = require('../../shared/store')
const errors = require('../../shared/errors')
const packageJSON = require('../../../package.json')
const flashState = require('../../shared/models/flash-state')
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('../../shared/models/available-drives')
const selectionState = require('../../shared/models/selection-state')
const driveScanner = require('./modules/drive-scanner')
const osDialog = require('./os/dialog')
const exceptionReporter = require('./modules/exception-reporter')
// Enable debug information from all modules that use `debug`
// See https://github.com/visionmedia/debug#browser-support
//
// Enable drivelist debugging information
// See https://github.com/resin-io-modules/drivelist
process.env.DRIVELIST_DEBUG = /drivelist|^\*$/i.test(process.env.DEBUG) ? '1' : ''
window.localStorage.debug = process.env.DEBUG
const app = angular.module('Etcher', [
require('angular-ui-router'),
require('angular-ui-bootstrap'),
require('angular-if-state'),
// Components
require('./components/svg-icon'),
require('./components/warning-modal/warning-modal'),
require('./components/safe-webview'),
// Pages
require('./pages/main/main'),
require('./pages/finish/finish'),
require('./pages/settings/settings'),
// OS
require('./os/open-external/open-external'),
require('./os/dropzone/dropzone'),
// Utils
require('./utils/manifest-bind/manifest-bind')
])
app.run(() => {
console.log([
' _____ _ _',
'| ___| | | |',
'| |__ | |_ ___| |__ ___ _ __',
'| __|| __/ __| \'_ \\ / _ \\ \'__|',
'| |___| || (__| | | | __/ |',
'\\____/ \\__\\___|_| |_|\\___|_|',
'',
'Interested in joining the Etcher team?',
'Drop us a line at join+etcher@resin.io',
'',
`Version = ${packageJSON.version}, Type = ${packageJSON.packageType}`
].join('\n'))
})
app.run(() => {
const currentVersion = packageJSON.version
analytics.logEvent('Application start', {
packageType: packageJSON.packageType,
version: currentVersion
})
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
})
return Bluebird.resolve()
}
const updateSemverRange = packageJSON.updates.semverRange
const includeUnstableChannel = settings.get('includeUnstableUpdateChannel')
analytics.logEvent('Checking for updates', {
currentVersion,
stable: isStableRelease,
updateSemverRange,
includeUnstableChannel
})
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'
})
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'
})
return Bluebird.resolve()
}
analytics.logEvent('Notifying update', {
latestVersion
})
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)
})
}).catch(exceptionReporter.report)
})
app.run(() => {
store.subscribe(() => {
if (!flashState.isFlashing()) {
return
}
const currentFlashState = flashState.getFlashState()
const stateType = !currentFlashState.flashing && currentFlashState.verifying
? `Verifying ${currentFlashState.verifying}`
: `Flashing ${currentFlashState.flashing}`
// NOTE: There is usually a short time period between the `isFlashing()`
// property being set, and the flashing actually starting, which
// might cause some non-sense flashing state logs including
// `undefined` values.
analytics.logDebug(
`${stateType} devices, ` +
`${currentFlashState.percentage}% at ${currentFlashState.speed} MB/s ` +
`(total ${currentFlashState.totalSpeed} MB/s) ` +
`eta in ${currentFlashState.eta}s ` +
`with ${currentFlashState.failed} failed devices`
)
windowProgress.set(currentFlashState)
})
})
app.run(($timeout) => {
driveScanner.on('devices', (drives) => {
// Safely trigger a digest cycle.
// In some cases, AngularJS doesn't acknowledge that the
// available drives list has changed, and incorrectly
// keeps asking the user to "Connect a drive".
$timeout(() => {
availableDrives.setDrives(drives)
})
})
driveScanner.on('error', (error) => {
// Stop the drive scanning loop in case of errors,
// otherwise we risk presenting the same error over
// and over again to the user, while also heavily
// spamming our error reporting service.
driveScanner.stop()
return exceptionReporter.report(error)
})
driveScanner.start()
})
app.run(($window) => {
let popupExists = false
$window.addEventListener('beforeunload', (event) => {
if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', {
isFlashing: flashState.isFlashing()
})
return
}
// Don't close window while flashing
event.returnValue = false
// Don't open any more popups
popupExists = true
analytics.logEvent('Close attempt while flashing')
osDialog.showWarning({
confirmationLabel: 'Yes, quit',
rejectionLabel: 'Cancel',
title: 'Are you sure you want to close Etcher?',
description: messages.warning.exitWhileFlashing()
}).then((confirmed) => {
if (confirmed) {
analytics.logEvent('Close confirmed while flashing', {
uuid: flashState.getFlashUuid()
})
// This circumvents the 'beforeunload' event unlike
// electron.remote.app.quit() which does not.
electron.remote.process.exit(EXIT_CODES.SUCCESS)
}
analytics.logEvent('Close rejected while flashing')
popupExists = false
}).catch(exceptionReporter.report)
})
})
app.run(($rootScope) => {
$rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
// Ignore first navigation
if (!fromState.name) {
return
}
analytics.logEvent('Navigate', {
to: toState.name,
from: fromState.name
})
})
})
app.config(($urlRouterProvider) => {
$urlRouterProvider.otherwise('/main')
})
app.config(($provide) => {
$provide.decorator('$exceptionHandler', ($delegate) => {
return (exception, cause) => {
exceptionReporter.report(exception)
$delegate(exception, cause)
}
})
})
app.controller('HeaderController', function (OSOpenExternalService) {
/**
* @summary Open help page
* @function
* @public
*
* @description
* This application will open either the image's support url, declared
* in the archive `manifest.json`, or the default Etcher help page.
*
* @example
* HeaderController.openHelpPage();
*/
this.openHelpPage = () => {
const DEFAULT_SUPPORT_URL = 'https://github.com/resin-io/etcher/blob/master/SUPPORT.md'
const supportUrl = selectionState.getImageSupportUrl() || DEFAULT_SUPPORT_URL
OSOpenExternalService.open(supportUrl)
}
})
app.controller('StateController', function ($rootScope, $scope) {
const unregisterStateChange = $rootScope.$on('$stateChangeSuccess', (event, toState, toParams, fromState) => {
this.previousName = fromState.name
this.currentName = toState.name
})
$scope.$on('$destroy', unregisterStateChange)
/**
* @summary Get the previous state name
* @function
* @public
*
* @returns {String} previous state name
*
* @example
* if (StateController.previousName === 'main') {
* console.log('We left the main screen!');
* }
*/
this.previousName = null
/**
* @summary Get the current state name
* @function
* @public
*
* @returns {String} current state name
*
* @example
* if (StateController.currentName === 'main') {
* console.log('We are on the main screen!');
* }
*/
this.currentName = null
})
// Handle keyboard shortcut to open the settings
app.run(($state) => {
electron.ipcRenderer.on('menu:preferences', () => {
$state.go('settings')
})
})
// Ensure user settings are loaded before
// we bootstrap the Angular.js application
angular.element(document).ready(() => {
settings.load().then(() => {
angular.bootstrap(document, [ 'Etcher' ])
}).catch(exceptionReporter.report)
})

View File

@ -1,238 +0,0 @@
/*
* Copyright 2016 balena.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 electron from 'electron';
import * as remote from '@electron/remote';
import type { Dictionary } from 'lodash';
import { debounce, capitalize, values } from 'lodash';
import outdent from 'outdent';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import type { DrivelistDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
import { spawnChildAndConnect } from './modules/api';
import * as exceptionReporter from './modules/exception-reporter';
import * as osDialog from './os/dialog';
import * as windowProgress from './os/window-progress';
import MainPage from './pages/main/MainPage';
import './css/main.css';
import * as i18next from 'i18next';
import type { SourceMetadata } from '../../shared/typings/source-selector';
window.addEventListener(
'unhandledrejection',
(event: PromiseRejectionEvent | any) => {
// Promise: event.reason
// Anything else: event
const error = event.reason || event;
analytics.logException(error);
event.preventDefault();
},
);
// Set application session UUID
store.dispatch({
type: Actions.SET_APPLICATION_SESSION_UUID,
data: uuidV4(),
});
// Set first flashing workflow UUID
store.dispatch({
type: Actions.SET_FLASHING_WORKFLOW_UUID,
data: uuidV4(),
});
const applicationSessionUuid = store.getState().toJS().applicationSessionUuid;
const flashingWorkflowUuid = store.getState().toJS().flashingWorkflowUuid;
console.log(outdent`
${outdent}
_____ _ _
| ___| | | |
| |__ | |_ ___| |__ ___ _ __
| __|| __/ __| '_ \\ / _ \\ '__|
| |___| || (__| | | | __/ |
\\____/ \\__\\___|_| |_|\\___|_|
Interested in joining the Etcher team?
Drop us a line at join+etcher@balena.io
Version = ${packageJSON.version}, Type = ${packageJSON.packageType}
`);
const currentVersion = packageJSON.version;
analytics.logEvent('Application start', {
packageType: packageJSON.packageType,
version: currentVersion,
});
const debouncedLog = debounce(console.log, 1000, { maxWait: 1000 });
function pluralize(word: string, quantity: number) {
return `${quantity} ${word}${quantity === 1 ? '' : 's'}`;
}
observe(() => {
if (!flashState.isFlashing()) {
return;
}
const currentFlashState = flashState.getFlashState();
windowProgress.set(currentFlashState);
let eta = '';
if (currentFlashState.eta !== undefined) {
eta = `eta in ${currentFlashState.eta.toFixed(0)}s`;
}
let active = '';
if (currentFlashState.type !== 'decompressing') {
active = pluralize('device', currentFlashState.active);
}
// NOTE: There is usually a short time period between the `isFlashing()`
// property being set, and the flashing actually starting, which
// might cause some non-sense flashing state logs including
// `undefined` values.
debouncedLog(outdent({ newline: ' ' })`
${capitalize(currentFlashState.type)}
${active},
${currentFlashState.percentage}%
at
${(currentFlashState.speed || 0).toFixed(2)}
MB/s
(total ${(currentFlashState.speed * currentFlashState.active).toFixed(2)} MB/s)
${eta}
with
${pluralize('failed device', currentFlashState.failed)}
`);
});
function setDrives(drives: Dictionary<DrivelistDrive>) {
// prevent setting drives while flashing otherwise we might lose some while we unmount them
if (!flashState.isFlashing()) {
availableDrives.setDrives(values(drives));
}
}
// Spawning the child process without privileges to get the drives list
// TODO: clean up this mess of exports
export let requestMetadata: any;
// start the api and spawn the child process
spawnChildAndConnect({
withPrivileges: false,
})
.then(({ emit, registerHandler }) => {
// start scanning
emit('scan', {});
// make the sourceMetada awaitable to be used on source selection
requestMetadata = async (params: any): Promise<SourceMetadata> => {
emit('sourceMetadata', JSON.stringify(params));
return new Promise((resolve) =>
registerHandler('sourceMetadata', (data: any) => {
resolve(JSON.parse(data));
}),
);
};
registerHandler('drives', (data: any) => {
setDrives(JSON.parse(data));
});
})
.catch((error: any) => {
throw new Error(`Failed to start the flasher process. error: ${error}`);
});
let popupExists = false;
analytics.initAnalytics();
window.addEventListener('beforeunload', async (event) => {
if (!flashState.isFlashing() || popupExists) {
analytics.logEvent('Close application', {
isFlashing: flashState.isFlashing(),
});
return;
}
// Don't close window while flashing
event.returnValue = false;
// Don't open any more popups
popupExists = true;
analytics.logEvent('Close attempt while flashing');
try {
const confirmed = await osDialog.showWarning({
confirmationLabel: i18next.t('yesExit'),
rejectionLabel: i18next.t('cancel'),
title: i18next.t('reallyExit'),
description: messages.warning.exitWhileFlashing(),
});
if (confirmed) {
analytics.logEvent('Close confirmed while flashing', {
flashInstanceUuid: flashState.getFlashUuid(),
});
// This circumvents the 'beforeunload' event unlike
// remote.app.quit() which does not.
remote.process.exit(EXIT_CODES.SUCCESS);
}
analytics.logEvent('Close rejected while flashing', {
applicationSessionUuid,
flashingWorkflowUuid,
});
popupExists = false;
} catch (error: any) {
exceptionReporter.report(error);
}
});
export async function main() {
try {
const { init: ledsInit } = require('./models/leds');
await ledsInit();
} catch (error: any) {
exceptionReporter.report(error);
}
ReactDOM.render(
React.createElement(MainPage),
document.getElementById('main'),
// callback to set the correct zoomFactor for webviews as well
async () => {
const fullscreen = await settings.get('fullscreen');
const width = fullscreen ? window.screen.width : window.outerWidth;
try {
electron.webFrame.setZoomFactor(width / settings.DEFAULT_WIDTH);
} catch (err) {
// noop
}
},
);
}

View File

@ -0,0 +1,212 @@
/*
* 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 angular = require('angular')
const _ = require('lodash')
const constraints = require('../../../../../shared/drive-constraints')
const analytics = require('../../../modules/analytics')
const availableDrives = require('../../../../../shared/models/available-drives')
const selectionState = require('../../../../../shared/models/selection-state')
const utils = require('../../../../../shared/utils')
module.exports = function (
$q,
$uibModalInstance
) {
/**
* @summary The drive selector state
* @type {Object}
* @public
*/
this.state = selectionState
/**
* @summary Static methods to check a drive's properties
* @type {Object}
* @public
*/
this.constraints = constraints
/**
* @summary The drives model
* @type {Object}
* @public
*
* @description
* We expose the whole service instead of the `.drives`
* property, which is the one we're interested in since
* this allows the property to be automatically updated
* when `availableDrives` detects a change in the drives.
*/
this.drives = availableDrives
/**
* @summary Determine if we can change a drive's selection state
* @function
* @private
*
* @param {Object} drive - drive
* @returns {Promise}
*
* @example
* DriveSelectorController.shouldChangeDriveSelectionState(drive)
* .then((shouldChangeDriveSelectionState) => {
* if (shouldChangeDriveSelectionState) doSomething();
* });
*/
const shouldChangeDriveSelectionState = (drive) => {
return $q.resolve(constraints.isDriveValid(drive, selectionState.getImage()))
}
/**
* @summary Toggle a drive selection
* @function
* @public
*
* @param {Object} drive - drive
* @returns {Promise} - resolved promise
*
* @example
* DriveSelectorController.toggleDrive({
* device: '/dev/disk2',
* size: 999999999,
* name: 'Cruzer USB drive'
* });
*/
this.toggleDrive = (drive) => {
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
if (canChangeDriveSelectionState) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: selectionState.isCurrentDrive(drive.device)
})
selectionState.toggleDrive(drive.device)
}
})
}
/**
* @summary Close the modal and resolve the selected drive
* @function
* @public
*
* @example
* DriveSelectorController.closeModal();
*/
this.closeModal = () => {
const selectedDrive = selectionState.getCurrentDrive()
// Sanity check to cover the case where a drive is selected,
// the drive is then unplugged from the computer and the modal
// is resolved with a non-existent drive.
if (!selectedDrive || !_.includes(this.drives.getDrives(), selectedDrive)) {
$uibModalInstance.close()
} else {
$uibModalInstance.close(selectedDrive)
}
}
/**
* @summary Select a drive and close the modal
* @function
* @public
*
* @param {Object} drive - drive
* @returns {Promise} - resolved promise
*
* @example
* DriveSelectorController.selectDriveAndClose({
* device: '/dev/disk2',
* size: 999999999,
* name: 'Cruzer USB drive'
* });
*/
this.selectDriveAndClose = (drive) => {
return shouldChangeDriveSelectionState(drive).then((canChangeDriveSelectionState) => {
if (canChangeDriveSelectionState) {
selectionState.selectDrive(drive.device)
analytics.logEvent('Drive selected (double click)')
this.closeModal()
}
})
}
/**
* @summary Memoized getDrives function
* @function
* @public
*
* @returns {Array<Object>} - memoized list of drives
*
* @example
* const drives = DriveSelectorController.getDrives()
* // Do something with drives
*/
this.getDrives = utils.memoize(this.drives.getDrives, angular.equals)
/**
* @summary Get a drive's compatibility status object(s)
* @function
* @public
*
* @description
* Given a drive, return its compatibility status with the selected image,
* containing the status type (ERROR, WARNING), and accompanying
* status message.
*
* @returns {Object[]} list of objects containing statuses
*
* @example
* const statuses = DriveSelectorController.getDriveStatuses(drive);
*
* for ({ type, message } of statuses) {
* // do something
* }
*/
this.getDriveStatuses = utils.memoize((drive) => {
return this.constraints.getDriveImageCompatibilityStatuses(drive, this.state.getImage())
}, angular.equals)
/**
* @summary Keyboard event drive toggling
* @function
* @public
*
* @description
* Keyboard-event specific entry to the toggleDrive function.
*
* @param {Object} drive - drive
* @param {Object} $event - event
*
* @example
* <div tabindex="1" ng-keypress="this.keyboardToggleDrive(drive, $event)">
* Tab-select me and press enter or space!
* </div>
*/
this.keyboardToggleDrive = (drive, $event) => {
console.log($event.keyCode)
const ENTER = 13
const SPACE = 32
if (_.includes([ ENTER, SPACE ], $event.keyCode)) {
this.toggleDrive(drive)
}
}
}

View File

@ -0,0 +1,33 @@
/*
* 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.Components.DriveSelector
*/
const angular = require('angular')
const MODULE_NAME = 'Etcher.Components.DriveSelector'
const DriveSelector = angular.module(MODULE_NAME, [
require('../modal/modal'),
require('../../utils/byte-size/byte-size')
])
DriveSelector.controller('DriveSelectorController', require('./controllers/drive-selector'))
DriveSelector.service('DriveSelectorService', require('./services/drive-selector'))
module.exports = MODULE_NAME

View File

@ -1,572 +0,0 @@
/*
* Copyright 2019 balena.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 ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import type * as sourceDestination from 'etcher-sdk/build/source-destination/';
import * as React from 'react';
import type { ModalProps, TableColumn } from 'rendition';
import { Flex, Txt, Badge, Link } from 'rendition';
import styled from 'styled-components';
import type {
DriveStatus,
DrivelistDrive,
} from '../../../../shared/drive-constraints';
import {
getDriveImageCompatibilityStatuses,
isDriveValid,
isDriveSizeLarge,
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import prettyBytes from 'pretty-bytes';
import { getDrives, hasAvailableDrives } from '../../models/available-drives';
import { getImage, isDriveSelected } from '../../models/selection-state';
import { store } from '../../models/store';
import { logEvent, logException } from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import type { GenericTableProps } from '../../styled-components';
import { Alert, Modal, Table } from '../../styled-components';
import type { SourceMetadata } from '../../../../shared/typings/source-selector';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
progress: number;
}
interface DriverlessDrive {
displayName: string; // added in app.ts
description: string;
link: string;
linkTitle: string;
linkMessage: string;
linkCTA: string;
}
type Drive = DrivelistDrive | DriverlessDrive | UsbbootDrive;
function isUsbbootDrive(drive: Drive): drive is UsbbootDrive {
return (drive as UsbbootDrive).progress !== undefined;
}
function isDriverlessDrive(drive: Drive): drive is DriverlessDrive {
return (drive as DriverlessDrive).link !== undefined;
}
function isDrivelistDrive(drive: Drive): drive is DrivelistDrive {
return typeof (drive as DrivelistDrive).size === 'number';
}
const DrivesTable = styled((props: GenericTableProps<Drive>) => (
<Table<Drive> {...props} />
))`
[data-display='table-head'],
[data-display='table-body'] {
> [data-display='table-row'] > [data-display='table-cell'] {
&:nth-child(2) {
width: 32%;
}
&:nth-child(3) {
width: 15%;
}
&:nth-child(4) {
width: 15%;
}
&:nth-child(5) {
width: 32%;
}
}
}
`;
function badgeShadeFromStatus(status: string) {
switch (status) {
case compatibility.containsImage():
return 16;
case compatibility.system():
case compatibility.tooSmall():
return 5;
default:
return 14;
}
}
const InitProgress = styled(
({
value,
...props
}: {
value: number;
props?: React.ProgressHTMLAttributes<Element>;
}) => {
return <progress max="100" value={value} {...props} />;
},
)`
/* Reset the default appearance */
appearance: none;
::-webkit-progress-bar {
width: 130px;
height: 4px;
background-color: #dde1f0;
border-radius: 14px;
}
::-webkit-progress-value {
background-color: #1496e1;
border-radius: 14px;
}
`;
export interface DriveSelectorProps
extends Omit<ModalProps, 'done' | 'cancel' | 'onSelect'> {
write: boolean;
multipleSelection: boolean;
showWarnings?: boolean;
cancel: (drives: DrivelistDrive[]) => void;
done: (drives: DrivelistDrive[]) => void;
titleLabel: string;
emptyListLabel: string;
emptyListIcon: JSX.Element;
selectedList?: DrivelistDrive[];
updateSelectedList?: () => DrivelistDrive[];
onSelect?: (drive: DrivelistDrive) => void;
}
interface DriveSelectorState {
drives: Drive[];
image?: SourceMetadata;
missingDriversModal: { drive?: DriverlessDrive };
selectedList: DrivelistDrive[];
showSystemDrives: boolean;
}
function isSystemDrive(drive: Drive) {
return isDrivelistDrive(drive) && drive.isSystem;
}
export class DriveSelector extends React.Component<
DriveSelectorProps,
DriveSelectorState
> {
private unsubscribe: (() => void) | undefined;
tableColumns: Array<TableColumn<Drive>>;
originalList: DrivelistDrive[];
constructor(props: DriveSelectorProps) {
super(props);
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = this.props.selectedList || [];
this.originalList = [...(this.props.selectedList || [])];
this.state = {
drives: getDrives(),
image: getImage(),
missingDriversModal: defaultMissingDriversModalState,
selectedList,
showSystemDrives: false,
};
this.tableColumns = [
{
field: 'description',
label: i18next.t('drives.name'),
render: (description: string, drive: Drive) => {
if (isDrivelistDrive(drive)) {
const isLargeDrive = isDriveSizeLarge(drive);
const hasWarnings =
this.props.showWarnings && (isLargeDrive || drive.isSystem);
return (
<Flex alignItems="center">
{hasWarnings && (
<ExclamationTriangleSvg
height="1em"
fill={drive.isSystem ? '#fca321' : '#8f9297'}
/>
)}
<Txt ml={(hasWarnings && 8) || 0}>
{middleEllipsis(description, 32)}
</Txt>
</Flex>
);
}
return <Txt>{description}</Txt>;
},
},
{
field: 'description',
key: 'size',
label: i18next.t('drives.size'),
render: (_description: string, drive: Drive) => {
if (isDrivelistDrive(drive) && drive.size !== null) {
return prettyBytes(drive.size);
}
},
},
{
field: 'description',
key: 'link',
label: i18next.t('drives.location'),
render: (_description: string, drive: Drive) => {
return (
<Txt>
{drive.displayName}
{isDriverlessDrive(drive) && (
<>
{' '}
-{' '}
<b>
<a onClick={() => this.installMissingDrivers(drive)}>
{drive.linkCTA}
</a>
</b>
</>
)}
</Txt>
);
},
},
{
field: 'description',
key: 'extra',
// We use an empty React fragment otherwise it uses the field name as label
label: <></>,
render: (_description: string, drive: Drive) => {
if (isUsbbootDrive(drive)) {
return this.renderProgress(drive.progress);
} else if (isDrivelistDrive(drive)) {
return this.renderStatuses(drive);
}
},
},
];
}
private driveShouldBeDisabled(drive: Drive, image?: SourceMetadata) {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
!isDriveValid(drive, image, this.props.write) ||
(this.props.write && drive.isReadOnly)
);
}
private getDisplayedDrives(drives: Drive[]): Drive[] {
return drives.filter((drive) => {
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
isDriveSelected(drive.device) ||
this.state.showSystemDrives ||
!drive.isSystem
);
});
}
private getDisabledDrives(drives: Drive[], image?: SourceMetadata): string[] {
return drives
.filter((drive) => this.driveShouldBeDisabled(drive, image))
.map((drive) => drive.displayName);
}
private renderProgress(progress: number) {
return (
<Flex flexDirection="column">
<Txt fontSize={12}>Initializing device</Txt>
<InitProgress value={progress} />
</Flex>
);
}
private warningFromStatus(
status: string,
drive: { device: string; size: number },
) {
switch (status) {
case compatibility.containsImage():
return warning.sourceDrive();
case compatibility.largeDrive():
return warning.largeDriveSize();
case compatibility.system():
return warning.systemDrive();
case compatibility.tooSmall():
return warning.tooSmall(
{
size:
this.state.image?.recommendedDriveSize ||
this.state.image?.size ||
0,
},
drive,
);
default:
return '';
}
}
private renderStatuses(drive: DrivelistDrive) {
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
drive,
this.state.image,
this.props.write,
).slice(0, 2);
return (
// the column render fn expects a single Element
<>
{statuses.map((status) => {
const badgeShade = badgeShadeFromStatus(status.message);
const warningMessage = this.warningFromStatus(status.message, {
device: drive.device,
size: drive.size || 0,
});
return (
<Badge
key={status.message}
shade={badgeShade}
mr="8px"
tooltip={this.props.showWarnings ? warningMessage : ''}
>
{status.message}
</Badge>
);
})}
</>
);
}
private installMissingDrivers(drive: DriverlessDrive) {
if (drive.link) {
logEvent('Open driver link modal', {
url: drive.link,
});
this.setState({ missingDriversModal: { drive } });
}
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const drives = getDrives();
const image = getImage();
this.setState({
drives,
image,
selectedList:
(this.props.updateSelectedList && this.props.updateSelectedList()) ||
[],
});
});
}
componentWillUnmount() {
this.unsubscribe?.();
}
render() {
const { cancel, done, ...props } = this.props;
const { selectedList, drives, image, missingDriversModal } = this.state;
const displayedDrives = this.getDisplayedDrives(drives);
const disabledDrives = this.getDisabledDrives(drives, image);
const numberOfSystemDrives = drives.filter(isSystemDrive).length;
const numberOfDisplayedSystemDrives =
displayedDrives.filter(isSystemDrive).length;
const numberOfHiddenSystemDrives =
numberOfSystemDrives - numberOfDisplayedSystemDrives;
const hasSystemDrives = selectedList.filter(isSystemDrive).length;
const showWarnings = this.props.showWarnings && hasSystemDrives;
return (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
{this.props.titleLabel}
</Txt>
<Txt
fontSize={11}
ml={12}
color="#5b82a7"
style={{ fontWeight: 600 }}
>
{i18next.t('drives.find', { length: drives.length })}
</Txt>
</Flex>
}
titleDetails={<Txt fontSize={11}>{getDrives().length} found</Txt>}
cancel={() => cancel(this.originalList)}
done={() => done(selectedList)}
action={i18next.t('drives.select', { select: selectedList.length })}
primaryButtonProps={{
primary: !showWarnings,
warning: showWarnings,
disabled: !hasAvailableDrives(),
}}
{...props}
>
{!hasAvailableDrives() ? (
<Flex
flexDirection="column"
justifyContent="center"
alignItems="center"
width="100%"
>
{this.props.emptyListIcon}
<b>{this.props.emptyListLabel}</b>
</Flex>
) : (
<>
<DrivesTable
refFn={() => {
// noop
}}
checkedItems={selectedList}
checkedRowsNumber={selectedList.length}
multipleSelection={this.props.multipleSelection}
columns={this.tableColumns}
data={displayedDrives}
disabledRows={disabledDrives}
getRowClass={(row: Drive) =>
isDrivelistDrive(row) && row.isSystem ? ['system'] : []
}
rowKey="displayName"
onCheck={(rows) => {
if (rows == null) {
rows = [];
}
let newSelection = rows.filter(isDrivelistDrive);
if (this.props.multipleSelection) {
if (rows.length === 0) {
newSelection = [];
}
const deselecting = selectedList.filter(
(selected) =>
newSelection.filter(
(row) => row.device === selected.device,
).length === 0,
);
const selecting = newSelection.filter(
(row) =>
selectedList.filter(
(selected) => row.device === selected.device,
).length === 0,
);
deselecting.concat(selecting).forEach((row) => {
if (this.props.onSelect) {
this.props.onSelect(row);
}
});
this.setState({
selectedList: newSelection,
});
return;
}
if (this.props.onSelect) {
this.props.onSelect(newSelection[newSelection.length - 1]);
}
this.setState({
selectedList: newSelection.slice(newSelection.length - 1),
});
}}
onRowClick={(row: Drive) => {
if (
!isDrivelistDrive(row) ||
this.driveShouldBeDisabled(row, image)
) {
return;
}
if (this.props.onSelect) {
this.props.onSelect(row);
}
const index = selectedList.findIndex(
(d) => d.device === row.device,
);
const newList = this.props.multipleSelection
? [...selectedList]
: [];
if (index === -1) {
newList.push(row);
} else {
// Deselect if selected
newList.splice(index, 1);
}
this.setState({
selectedList: newList,
});
}}
/>
{numberOfHiddenSystemDrives > 0 && (
<Link
mt={15}
mb={15}
fontSize="14px"
onClick={() => this.setState({ showSystemDrives: true })}
>
<Flex alignItems="center">
<ChevronDownSvg height="1em" fill="currentColor" />
<Txt ml={8}>
{i18next.t('drives.showHidden', {
num: numberOfHiddenSystemDrives,
})}
</Txt>
</Flex>
</Link>
)}
</>
)}
{this.props.showWarnings && hasSystemDrives ? (
<Alert className="system-drive-alert" style={{ width: '67%' }}>
{i18next.t('drives.systemDriveDanger')}
</Alert>
) : null}
{missingDriversModal.drive !== undefined && (
<Modal
width={400}
title={missingDriversModal.drive.linkTitle}
cancel={() => this.setState({ missingDriversModal: {} })}
done={() => {
try {
if (missingDriversModal.drive !== undefined) {
openExternal(missingDriversModal.drive.link);
}
} catch (error: any) {
logException(error);
} finally {
this.setState({ missingDriversModal: {} });
}
}}
action={i18next.t('yesContinue')}
cancelButtonProps={{
children: i18next.t('cancel'),
}}
children={
missingDriversModal.drive.linkMessage ||
i18next.t('drives.openInBrowser', {
link: missingDriversModal.drive.link,
})
}
/>
)}
</Modal>
);
}
}

View File

@ -0,0 +1,66 @@
/*
* 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.exports = function (ModalService, $q) {
let modal = null
/**
* @summary Open the drive selector widget
* @function
* @public
*
* @fulfil {(Object|Undefined)} - selected drive
* @returns {Promise}
*
* @example
* DriveSelectorService.open().then((drive) => {
* console.log(drive);
* });
*/
this.open = () => {
modal = ModalService.open({
name: 'drive-selector',
template: require('../templates/drive-selector-modal.tpl.html'),
controller: 'DriveSelectorController as modal',
size: 'drive-selector-modal'
})
return modal.result
}
/**
* @summary Close the drive selector widget
* @function
* @public
*
* @fulfil {Undefined}
* @returns {Promise}
*
* @example
* DriveSelectorService.close();
*/
this.close = () => {
if (modal) {
return modal.close()
}
// Resolve `undefined` if the modal
// was already closed for consistency
return $q.resolve()
}
}

View File

@ -0,0 +1,110 @@
/*
* 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.
*/
.modal-drive-selector-modal .modal-content {
width: 315px;
height: 320px;
}
.modal-drive-selector-modal .modal-body {
padding-top: 0;
padding-bottom: 0;
}
.modal-drive-selector-modal .list-group-item[disabled] {
cursor: not-allowed;
}
.modal-drive-selector-modal {
.list-group-item-footer:has(span) {
margin-top: 8px;
}
.list-group-item-heading,
.list-group-item-text {
word-break: break-all;
}
.list-group {
margin-bottom: 0;
}
.list-group-item {
display: flex;
align-items: center;
border-left: 0;
border-right: 0;
border-radius: 0;
border-color: darken($palette-theme-light-background, 7%);
padding: 12px 0;
.list-group-item-section-expanded {
flex-grow: 1;
}
.list-group-item-section + .list-group-item-section {
margin-left: 10px;
}
> .tick {
font-size: 11px;
}
&:first-child {
border-top: 0;
}
&[disabled] .list-group-item-heading {
color: $palette-theme-light-soft-foreground;
}
progress {
appearance: none;
width: 100%;
height: 2.5px;
border: none;
border-radius: 50% 50%;
}
progress::-webkit-progress-bar {
background-color: $palette-theme-default-background;
border: none;
outline: none;
}
progress::-webkit-progress-value {
border-bottom: 1px solid darken($palette-theme-primary-background, 15);
background-color: $palette-theme-primary-background;
}
}
.list-group-item-heading {
font-size: 13px;
}
.list-group-item-text {
line-height: 1;
font-size: 11px;
color: $palette-theme-light-soft-foreground;
}
.word-keep {
word-break: keep-all;
}
}

View File

@ -0,0 +1,61 @@
<div class="modal-header">
<h4 class="modal-title">Select a Drive</h4>
<button tabindex="14" class="close" ng-click="modal.closeModal()">&times;</button>
</div>
<div class="modal-body">
<ul class="list-group">
<li class="list-group-item" ng-repeat="drive in modal.getDrives() track by drive.device"
ng-disabled="!modal.constraints.isDriveValid(drive, modal.state.getImage())"
ng-dblclick="modal.selectDriveAndClose(drive)"
ng-click="modal.toggleDrive(drive)">
<img class="list-group-item-section" alt="Drive device type logo"
ng-if="drive.icon"
ng-src="../assets/{{drive.icon}}.svg"
width="25"
height="30">
<div
class="list-group-item-section list-group-item-section-expanded"
tabindex="{{ 15 + $index }}"
ng-keypress="modal.keyboardToggleDrive(drive, $event)">
<h4 class="list-group-item-heading">{{ drive.description }}
<span class="word-keep"
ng-show="drive.size"> - {{ drive.size | closestUnit }}</span>
</h4>
<p class="list-group-item-text">{{ drive.displayName }}</p>
<footer class="list-group-item-footer">
<span class="label" ng-repeat="status in modal.getDriveStatuses(drive)"
ng-class="{
'label-warning': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.WARNING,
'label-danger': status.type === modal.constraints.COMPATIBILITY_STATUS_TYPES.ERROR
}">{{ status.message }}</span>
</footer>
<progress ng-if="drive.progress" value="{{ drive.progress }}" max="100"></progress>
</div>
<span class="list-group-item-section tick tick--success"
ng-show="modal.constraints.isDriveValid(drive, modal.state.getImage())"
ng-disabled="!modal.state.isDriveSelected(drive.device)"></span>
</li>
<li class="list-group-item"
ng-show="!modal.drives.hasAvailableDrives()">
<div>
<b>Connect a drive!</b>
<div>No removable drive detected.</div>
</div>
</li>
</ul>
</div>
<div class="modal-footer">
<button class="button button-primary button-block"
tabindex="{{ 15 + modal.getDrives().length }}"
ng-class="{
'button-warning': modal.constraints.hasListDriveImageCompatibilityStatus(modal.state.getSelectedDrives(), modal.state.getImage())
}"
ng-click="modal.closeModal()"
ng-disabled="!modal.state.hasDrive()">Continue</button>
</div>

View File

@ -1,83 +0,0 @@
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import * as React from 'react';
import type { ModalProps } from 'rendition';
import { Badge, Flex, Txt } from 'rendition';
import { Modal, ScrollableFlex } from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import prettyBytes from 'pretty-bytes';
import type { DriveWithWarnings } from '../../pages/main/Flash';
import * as i18next from 'i18next';
const DriveStatusWarningModal = ({
done,
cancel,
isSystem,
drivesWithWarnings,
}: ModalProps & {
isSystem: boolean;
drivesWithWarnings: DriveWithWarnings[];
}) => {
let warningSubtitle = i18next.t('drives.largeDriveWarning');
let warningCta = i18next.t('drives.largeDriveWarningMsg');
if (isSystem) {
warningSubtitle = i18next.t('drives.systemDriveWarning');
warningCta = i18next.t('drives.systemDriveWarningMsg');
}
return (
<Modal
footerShadow={false}
reverseFooterButtons={true}
done={done}
cancel={cancel}
cancelButtonProps={{
primary: false,
warning: true,
children: i18next.t('drives.changeTarget'),
}}
action={i18next.t('sure')}
primaryButtonProps={{
primary: false,
outline: true,
}}
>
<Flex
flexDirection="column"
alignItems="center"
justifyContent="center"
width="100%"
>
<Flex flexDirection="column">
<ExclamationTriangleSvg height="2em" fill="#fca321" />
<Txt fontSize="24px" color="#fca321">
{i18next.t('warning')}
</Txt>
</Flex>
<Txt fontSize="24px">{warningSubtitle}</Txt>
<ScrollableFlex
flexDirection="column"
backgroundColor="#fff5e6"
m="2em 0"
p="1em 2em"
width="420px"
maxHeight="100px"
>
{drivesWithWarnings.map((drive, i, array) => (
<>
<Flex justifyContent="space-between" alignItems="baseline">
<strong>{middleEllipsis(drive.description, 28)}</strong>{' '}
{drive.size && prettyBytes(drive.size) + ' '}
<Badge shade={5}>{drive.statuses[0].message}</Badge>
</Flex>
{i !== array.length - 1 ? <hr style={{ width: '100%' }} /> : null}
</>
))}
</ScrollableFlex>
<Txt style={{ fontWeight: 600 }}>{warningCta}</Txt>
</Flex>
</Modal>
);
};
export default DriveStatusWarningModal;

View File

@ -1,125 +0,0 @@
/*
* Copyright 2019 balena.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 React from 'react';
import { Flex } from 'rendition';
import { v4 as uuidV4 } from 'uuid';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { FlashAnother } from '../flash-another/flash-another';
import type { FlashError } from '../flash-results/flash-results';
import { FlashResults } from '../flash-results/flash-results';
import { SafeWebview } from '../safe-webview/safe-webview';
function restart(goToMain: () => void) {
selectionState.deselectAllDrives();
analytics.logEvent('Restart');
// Reset the flashing workflow uuid
store.dispatch({
type: Actions.SET_FLASHING_WORKFLOW_UUID,
data: uuidV4(),
});
goToMain();
}
async function getSuccessBannerURL() {
return (
(await settings.get('successBannerURL')) ??
'https://efp.balena.io/success-banner?borderTop=false&darkBackground=true'
);
}
function FinishPage({ goToMain }: { goToMain: () => void }) {
const [webviewShowing, setWebviewShowing] = React.useState(false);
const [successBannerURL, setSuccessBannerURL] = React.useState('');
(async () => {
setSuccessBannerURL(await getSuccessBannerURL());
})();
const flashResults = flashState.getFlashResults();
const errors: FlashError[] = (
store.getState().toJS().failedDeviceErrors || []
).map(([, error]: [string, FlashError]) => ({
...error,
}));
const { averageSpeed, blockmappedSize, bytesWritten, failed, size } =
flashState.getFlashState();
const {
skip,
results = {
bytesWritten,
sourceMetadata: {
size,
blockmappedSize,
},
averageFlashingSpeed: averageSpeed,
devices: { failed, successful: 0 },
},
} = flashResults;
return (
<Flex height="100%" justifyContent="space-between">
<Flex
width={webviewShowing ? '36.2vw' : '100vw'}
height="100vh"
alignItems="center"
justifyContent="center"
flexDirection="column"
style={{
position: 'absolute',
top: 0,
zIndex: 1,
boxShadow: '0 2px 15px 0 rgba(0, 0, 0, 0.2)',
}}
>
<FlashResults
image={selectionState.getImage()?.name}
results={results}
skip={skip}
errors={errors}
mb="32px"
goToMain={goToMain}
/>
<FlashAnother
onClick={() => {
restart(goToMain);
}}
/>
</Flex>
{successBannerURL.length && (
<SafeWebview
src={successBannerURL}
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
</Flex>
);
}
export default FinishPage;

View File

@ -0,0 +1,31 @@
/*
* 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.Components.FlashErrorModal
*/
const angular = require('angular')
const MODULE_NAME = 'Etcher.Components.FlashErrorModal'
const FlashErrorModal = angular.module(MODULE_NAME, [
require('../warning-modal/warning-modal')
])
FlashErrorModal.service('FlashErrorModalService', require('./services/flash-error-modal'))
module.exports = MODULE_NAME

View File

@ -0,0 +1,49 @@
/*
* 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 flashState = require('../../../../../shared/models/flash-state')
const selectionState = require('../../../../../shared/models/selection-state')
const analytics = require('../../../modules/analytics')
module.exports = function (WarningModalService) {
/**
* @summary Open the flash error modal
* @function
* @public
*
* @param {String} message - flash error message
* @returns {Promise}
*
* @example
* FlashErrorModalService.show('The drive is not large enough!');
*/
this.show = (message) => {
return WarningModalService.display({
confirmationLabel: 'Retry',
description: message
}).then((confirmed) => {
flashState.resetState()
if (confirmed) {
analytics.logEvent('Restart after failure')
} else {
selectionState.clear()
}
})
}
}

View File

@ -1,244 +0,0 @@
/*
* Copyright 2019 balena.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 CircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle.svg';
import CheckCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-check.svg';
import TimesCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/circle-xmark.svg';
import * as React from 'react';
import type { FlexProps, TableColumn } from 'rendition';
import { Flex, Link, Txt } from 'rendition';
import styled from 'styled-components';
import { progress } from '../../../../shared/messages';
import { bytesToMegabytes } from '../../../../shared/units';
import FlashSvg from '../../../assets/flash.svg';
import { getDrives } from '../../models/available-drives';
import { resetState } from '../../models/flash-state';
import * as selection from '../../models/selection-state';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { Modal, Table } from '../../styled-components';
import * as i18next from 'i18next';
const ErrorsTable = styled((props) => <Table<FlashError> {...props} />)`
&&& [data-display='table-head'],
&&& [data-display='table-body'] {
> [data-display='table-row'] {
> [data-display='table-cell'] {
&:first-child {
width: 30%;
}
&:nth-child(2) {
width: 20%;
}
&:last-child {
width: 50%;
}
}
}
}
`;
const DoneIcon = (props: {
skipped: boolean;
color: string;
allFailed: boolean;
}) => {
const svgProps = {
width: '28px',
fill: props.color,
style: {
marginTop: '-25px',
marginLeft: '13px',
zIndex: 1,
},
};
return props.allFailed && !props.skipped ? (
<TimesCircleSvg {...svgProps} />
) : (
<CheckCircleSvg {...svgProps} />
);
};
export interface FlashError extends Error {
description: string;
device: string;
code: string;
}
function formattedErrors(errors: FlashError[]) {
return errors
.map((error) => `${error.device}: ${error.message || error.code}`)
.join('\n');
}
const columns: Array<TableColumn<FlashError>> = [
{
field: 'description',
label: i18next.t('flash.target'),
},
{
field: 'device',
label: i18next.t('flash.location'),
},
{
field: 'message',
label: i18next.t('flash.error'),
render: (message: string, { code }: FlashError) => {
return message ?? code;
},
},
];
function getEffectiveSpeed(results: {
sourceMetadata: {
size: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
}) {
const flashedSize =
results.sourceMetadata.blockmappedSize ?? results.sourceMetadata.size;
const timeSpent = flashedSize / results.averageFlashingSpeed;
return results.sourceMetadata.size / timeSpent;
}
export function FlashResults({
goToMain,
image = '',
errors,
results,
skip,
...props
}: {
goToMain: () => void;
image?: string;
errors: FlashError[];
skip: boolean;
results: {
sourceMetadata: {
size: number;
blockmappedSize?: number;
};
averageFlashingSpeed: number;
devices: { failed: number; successful: number };
};
} & FlexProps) {
const [showErrorsInfo, setShowErrorsInfo] = React.useState(false);
const allFailed = !skip && results?.devices?.successful === 0;
const someFailed = results?.devices?.failed !== 0 || errors?.length !== 0;
const effectiveSpeed = bytesToMegabytes(getEffectiveSpeed(results)).toFixed(
1,
);
return (
<Flex flexDirection="column" {...props}>
<Flex alignItems="center" flexDirection="column">
<Flex
alignItems="center"
mt="50px"
mb="32px"
color="#7e8085"
flexDirection="column"
>
<FlashSvg width="40px" height="40px" className="disabled" />
<DoneIcon
skipped={skip}
allFailed={allFailed}
color={allFailed || someFailed ? '#c6c8c9' : '#1ac135'}
/>
<Txt>{middleEllipsis(image, 24)}</Txt>
</Flex>
<Txt fontSize={24} color="#fff" mb="17px">
{allFailed
? i18next.t('flash.flashFailed')
: i18next.t('flash.flashCompleted')}
</Txt>
{skip ? <Txt color="#7e8085">{i18next.t('flash.skip')}</Txt> : null}
</Flex>
<Flex flexDirection="column" color="#7e8085">
{results.devices.successful !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#1ac135" />
<Txt ml="10px" color="#fff">
{results.devices.successful}
</Txt>
<Txt ml="10px">
{progress.successful(results.devices.successful)}
</Txt>
</Flex>
) : null}
{errors.length !== 0 ? (
<Flex alignItems="center">
<CircleSvg width="14px" fill="#ff4444" />
<Txt ml="10px" color="#fff">
{errors.length}
</Txt>
<Txt ml="10px" tooltip={formattedErrors(errors)}>
{progress.failed(errors.length)}
</Txt>
<Link ml="10px" onClick={() => setShowErrorsInfo(true)}>
{i18next.t('flash.moreInfo')}
</Link>
</Flex>
) : null}
{!allFailed && (
<Txt
fontSize="10px"
style={{
fontWeight: 500,
textAlign: 'center',
}}
tooltip={i18next.t('flash.speedTip')}
>
{i18next.t('flash.speed', { speed: effectiveSpeed })}
</Txt>
)}
</Flex>
{showErrorsInfo && (
<Modal
titleElement={
<Flex alignItems="baseline" mb={18}>
<Txt fontSize={24} align="left">
{i18next.t('failedTarget')}
</Txt>
</Flex>
}
action={i18next.t('failedRetry')}
cancel={() => setShowErrorsInfo(false)}
done={() => {
setShowErrorsInfo(false);
resetState();
getDrives()
.map((drive) => {
selection.deselectDrive(drive.device);
return drive.device;
})
.filter((driveDevice) =>
errors.some((error) => error.device === driveDevice),
)
.forEach((driveDevice) => selection.selectDrive(driveDevice));
goToMain();
}}
>
<ErrorsTable columns={columns} data={errors} />
</Modal>
)}
</Flex>
);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016 balena.io
* 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.
@ -14,14 +14,18 @@
* limitations under the License.
*/
import { expect } from 'chai';
import { bytesToMegabytes } from '../../lib/shared/units';
'use strict'
describe('Shared: Units', function () {
describe('.bytesToMegabytes()', function () {
it('should convert bytes to megabytes', function () {
expect(bytesToMegabytes(1.2e7)).to.equal(12);
expect(bytesToMegabytes(332000)).to.equal(0.332);
});
});
});
/**
* @module Etcher.Components.Modal
*/
const angular = require('angular')
const MODULE_NAME = 'Etcher.Components.Modal'
const Modal = angular.module(MODULE_NAME, [
require('angular-ui-bootstrap')
])
Modal.service('ModalService', require('./services/modal'))
module.exports = MODULE_NAME

View File

@ -0,0 +1,90 @@
/*
* 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 _ = require('lodash')
const analytics = require('../../../modules/analytics')
module.exports = function ($uibModal, $q) {
/**
* @summary Open a modal
* @function
* @public
*
* @param {Object} options - options
* @param {String} options.template - template contents
* @param {String} options.controller - controller
* @param {String} [options.size='sm'] - modal size
* @param {Object} options.resolve - modal resolves
* @returns {Object} modal
*
* @example
* ModalService.open({
* name: 'my modal',
* template: require('./path/to/modal.tpl.html'),
* controller: 'DriveSelectorController as modal',
* });
*/
this.open = (options = {}) => {
_.defaults(options, {
size: 'sm'
})
analytics.logEvent('Open modal', {
name: options.name
})
const modal = $uibModal.open({
animation: true,
template: options.template,
controller: options.controller,
size: options.size,
resolve: options.resolve
})
return {
close: modal.close,
result: $q((resolve, reject) => {
modal.result.then((value) => {
analytics.logEvent('Modal accepted', {
name: options.name,
value
})
resolve(value)
}).catch((error) => {
// Bootstrap doesn't 'resolve' these but cancels the dialog
if (error === 'escape key press' || error === 'backdrop click') {
analytics.logEvent('Modal rejected', {
name: options.name,
method: error
})
return resolve()
}
analytics.logEvent('Modal rejected', {
name: options.name,
value: error
})
return reject(error)
})
})
}
}
}

View File

@ -0,0 +1,105 @@
/*
* 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.
*/
.modal-content {
background-color: $palette-theme-light-background;
display: flex;
flex-direction: column;
margin: 0 auto;
height: auto;
}
.modal-header {
display: flex;
align-items: baseline;
font-size: 12px;
color: $palette-theme-light-soft-foreground;
padding: 11px 20px;
flex-grow: 0;
}
.modal-title {
font-size: inherit;
flex-grow: 1;
}
.modal-body {
flex-grow: 1;
color: $palette-theme-light-foreground;
padding: 20px;
max-height: 250px;
overflow: auto;
a {
color: $palette-theme-primary-background;
}
> p {
white-space: pre-line;
}
> p:last-child {
margin-bottom: 0;
}
}
.modal-menu {
display: flex;
> * {
flex-basis: auto;
}
}
// UI Bootstrap adds the `.modal-open` class to the <body>
// element and sets its right padding to the width of the
// window, causing the window content to overflow and get
// pushed to the bottom.
// The `!important` flag is needed since UI Bootstrap inlines
// the styles programmatically to the element.
.modal-open {
padding-right: 0 !important;
}
// Disable modal opacity
.modal-backdrop.in {
opacity: 0;
}
.modal-footer {
flex-grow: 0;
border: 0;
}
.modal {
// Center the modal using Flexbox so we can
// freely use any height.
display: flex !important;
justify-content: center;
align-items: center;
.button[disabled] {
background-color: $palette-theme-light-disabled-background;
color: $palette-theme-light-disabled-foreground;
}
}
.modal-dialog {
margin: 0;
position: initial;
max-width: 50%;
}

View File

@ -0,0 +1,44 @@
/*
* 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'
/**
* @summary ProgressButton directive
* @function
* @public
*
* @description
* This directive provides a button containing a progress bar inside.
* The button is styled by default as a primary button.
*
* @returns {Object} directive
*
* @example
* <progress-button percentage="{{ 40 }}" striped>My Progress Button</progress-button>
*/
module.exports = () => {
return {
template: require('../templates/progress-button.tpl.html'),
restrict: 'E',
replace: true,
transclude: true,
scope: {
percentage: '=',
striped: '@'
}
}
}

View File

@ -0,0 +1,28 @@
/*
* 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.Components.ProgressButton
*/
const angular = require('angular')
const MODULE_NAME = 'Etcher.Components.ProgressButton'
const ProgressButton = angular.module(MODULE_NAME, [])
ProgressButton.directive('progressButton', require('./directives/progress-button'))
module.exports = MODULE_NAME

View File

@ -1,136 +0,0 @@
/*
* Copyright 2016 balena.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 React from 'react';
import { Flex, Button, ProgressBar, Txt } from 'rendition';
import { default as styled } from 'styled-components';
import { fromFlashState } from '../../modules/progress-status';
import { StepButton } from '../../styled-components';
import * as i18next from 'i18next';
const FlashProgressBar = styled(ProgressBar)`
> div {
width: 100%;
height: 12px;
color: white !important;
text-shadow: none !important;
transition-duration: 0s;
> div {
transition-duration: 0s;
}
}
width: 100%;
height: 12px;
margin-bottom: 6px;
border-radius: 14px;
font-size: 16px;
line-height: 48px;
background: #2f3033;
`;
interface ProgressButtonProps {
type: 'decompressing' | 'flashing' | 'verifying';
active: boolean;
percentage: number;
position: number;
disabled: boolean;
cancel: (type: string) => void;
callback: () => void;
warning?: boolean;
}
const colors = {
decompressing: '#00aeef',
flashing: '#da60ff',
verifying: '#1ac135',
} as const;
const CancelButton = styled(({ type, onClick, ...props }) => {
const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel');
return (
<Button plain onClick={() => onClick(status)} {...props}>
{status}
</Button>
);
})`
font-weight: 600;
&&& {
width: auto;
height: auto;
font-size: 14px;
}
`;
export class ProgressButton extends React.PureComponent<ProgressButtonProps> {
public render() {
const percentage = this.props.percentage;
const warning = this.props.warning;
const { status, position } = fromFlashState({
type: this.props.type,
percentage,
position: this.props.position,
});
const type = this.props.type || 'default';
if (this.props.active) {
return (
<>
<Flex
alignItems="baseline"
justifyContent="space-between"
width="100%"
style={{
marginTop: 42,
marginBottom: '6px',
fontSize: 16,
fontWeight: 600,
}}
>
<Flex>
<Txt color="#fff">{status}&nbsp;</Txt>
<Txt color={colors[type]}>{position}</Txt>
</Flex>
{type && (
<CancelButton
type={type}
onClick={this.props.cancel}
color="#00aeef"
/>
)}
</Flex>
<FlashProgressBar background={colors[type]} value={percentage} />
</>
);
}
return (
<StepButton
primary={!warning}
warning={warning}
onClick={this.props.callback}
disabled={this.props.disabled}
style={{
marginTop: 30,
}}
>
{i18next.t('flash.flashNow')}
</StepButton>
);
}
}

View File

@ -0,0 +1,126 @@
/*
* 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.
*/
/**
* A button with a progress bar inside.
*
* From http://tympanus.net/Development/ProgressButtonStyles/
*
* The state of the progress bar is controller by the width, in percentage,
* of `.progress-button__bar`.
*
* If there is an action in place, the `active` attribute must be set to `true`.
* This is useful to determine if the progress bar is paused from the point of view
* of the styling.
*
* You can optionally pass the `.progress-button--striped` modified to get a striped
* progress bar.
*
* The stripe implementation idea was taken from:
*
* https://css-tricks.com/css3-progress-bars/
*
* Usage:
*
* <button class="progress-button" active="true">
* <span class="progress-button__content">Button text</span>
* <span class="progress-button__bar" style="width: 50%;"></span>
* </button>
*/
$progress-button-stripes-width: 20px;
$progress-button-stripes-animation-duration: 1s;
.progress-button {
@extend .button;
@extend .button-primary;
overflow: hidden;
&[active="true"] {
background-color: $palette-theme-warning-background;
}
.progress-button__bar {
background-color: lighten($palette-theme-warning-background, 5%);
}
&.progress-button--striped {
$progress-button-stripes-background-color: desaturate($palette-theme-primary-background, 5%);
$progress-button-stripes-foreground-color: desaturate(darken($palette-theme-primary-background, 18%), 20%);
// Notice that we add `0.01` to certain gradient stop positions.
// That workarounds a Chrome rendering issue where diagonal
// lines look spiky.
// See https://github.com/resin-io/etcher/issues/472
background-image: -webkit-gradient(linear, 0 0, 100% 100%,
color-stop(0.25, $progress-button-stripes-foreground-color),
color-stop(0.25 + 0.01, $progress-button-stripes-background-color),
color-stop(0.50, $progress-button-stripes-background-color),
color-stop(0.50 + 0.01, $progress-button-stripes-foreground-color),
color-stop(0.75, $progress-button-stripes-foreground-color),
color-stop(0.75 + 0.01, $progress-button-stripes-background-color),
to($progress-button-stripes-background-color));
.progress-button__bar {
background-color: lighten($palette-theme-primary-background, 5%);
}
}
}
// Prevent the button from being clickable
// when it has an active progress bar.
.progress-button[active="true"] {
@extend .button-no-hover;
}
.progress-button__content {
position: relative;
z-index: 10;
}
.progress-button__bar {
position: absolute;
left: 0;
top: 0;
width: 0;
height: 100%;
// Subtle progress bar animation
transition: width 0.3s;
}
.progress-button--striped {
background-size: $progress-button-stripes-width $progress-button-stripes-width;
animation: progress-button-stripes $progress-button-stripes-animation-duration linear infinite;
overflow: hidden;
}
@keyframes progress-button-stripes {
0% {
background-position: 0 0;
}
100% {
background-position: $progress-button-stripes-width $progress-button-stripes-width;
}
}

View File

@ -0,0 +1,7 @@
<button class="progress-button"
ng-class="{
'progress-button--striped': striped && striped != 'false'
}">
<span class="progress-button__content" ng-transclude></span>
<span class="progress-button__bar" ng-style="{ width: (percentage > 100 ? 100 : percentage) + '%' }"></span>
</button>

View File

@ -1,74 +0,0 @@
/*
* Copyright 2016 balena.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 React from 'react';
import { Flex, Txt } from 'rendition';
import DriveSvg from '../../../assets/drive.svg';
import ImageSvg from '../../../assets/image.svg';
import { SVGIcon } from '../svg-icon/svg-icon';
import { middleEllipsis } from '../../utils/middle-ellipsis';
interface ReducedFlashingInfosProps {
imageLogo?: string;
imageName?: string;
imageSize: string;
driveTitle: string;
driveLabel: string;
style?: React.CSSProperties;
}
export class ReducedFlashingInfos extends React.Component<ReducedFlashingInfosProps> {
constructor(props: ReducedFlashingInfosProps) {
super(props);
this.state = {};
}
public render() {
const { imageName = '' } = this.props;
return (
<Flex
flexDirection="column"
style={this.props.style ? this.props.style : undefined}
>
<Flex mb={16}>
<SVGIcon
disabled
width="21px"
height="21px"
contents={this.props.imageLogo}
fallback={ImageSvg}
style={{ marginRight: '9px' }}
/>
<Txt
style={{ marginRight: '9px' }}
tooltip={{ text: imageName, placement: 'right' }}
>
{middleEllipsis(imageName, 16)}
</Txt>
<Txt color="#7e8085">{this.props.imageSize}</Txt>
</Flex>
<Flex>
<DriveSvg width="21px" height="21px" style={{ marginRight: '9px' }} />
<Txt tooltip={{ text: this.props.driveLabel, placement: 'right' }}>
{middleEllipsis(this.props.driveTitle, 16)}
</Txt>
</Flex>
</Flex>
);
}
}

View File

@ -0,0 +1,266 @@
/*
* 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.
*/
'use strict'
/* eslint-disable jsdoc/require-example */
const _ = require('lodash')
const electron = require('electron')
const angular = require('angular')
const react = require('react')
const propTypes = require('prop-types')
const { react2angular } = require('react2angular')
const analytics = require('../modules/analytics')
const packageJSON = require('../../../../package.json')
const MODULE_NAME = 'Etcher.Components.SafeWebview'
const angularSafeWebview = angular.module(MODULE_NAME, [])
/**
* @summary Electron session identifier
* @constant
* @private
* @type {String}
*/
const ELECTRON_SESSION = 'persist:success-banner'
/**
* @summary Etcher version search-parameter key
* @constant
* @private
* @type {String}
*/
const ETCHER_VERSION_PARAM = 'etcher-version'
/**
* @summary API version search-parameter key
* @constant
* @private
* @type {String}
*/
const API_VERSION_PARAM = 'api-version'
/**
* @summary Webview API version
* @constant
* @private
* @type {String}
*
* @description
* Changing this number represents a departure from an older API and as such
* should only be changed when truly necessary as it introduces breaking changes.
* This version number is exposed to the banner such that it can determine what
* features are safe to utilize.
*/
const API_VERSION = 1
/**
* @summary Webviews that hide/show depending on the HTTP status returned
* @type {Object}
* @public
*
* @example
* <safe-webview src="https://etcher.io/"></safe-webview>
*/
class SafeWebview extends react.PureComponent {
/**
* @param {Object} props - React element properties
*/
constructor (props) {
super(props)
this.state = {
shouldShow: true
}
const url = new window.URL(props.src)
// We set the version GET parameters here.
url.searchParams.set(ETCHER_VERSION_PARAM, packageJSON.version)
url.searchParams.set(API_VERSION_PARAM, API_VERSION)
this.entryHref = url.href
// Events steal 'this'
this.didFailLoad = _.bind(this.didFailLoad, this)
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this)
this.eventTuples = [
[ 'did-fail-load', this.didFailLoad ],
[ 'did-get-response-details', this.didGetResponseDetails ],
[ 'new-window', this.constructor.newWindow ],
[ 'console-message', this.constructor.consoleMessage ]
]
// Make a persistent electron session for the webview
electron.remote.session.fromPartition(ELECTRON_SESSION, {
// Disable the cache for the session such that new content shows up when refreshing
cache: false
})
}
/**
* @returns {react.Element}
*/
render () {
return react.createElement('webview', {
ref: 'webview',
style: {
flex: this.state.shouldShow ? null : '0 1',
width: this.state.shouldShow ? null : '0',
height: this.state.shouldShow ? null : '0'
}
}, [])
}
/**
* @summary Add the Webview events
*/
componentDidMount () {
// Events React is unaware of have to be handled manually
_.map(this.eventTuples, (tuple) => {
this.refs.webview.addEventListener(...tuple)
})
// Use the 'success-banner' session
this.refs.webview.partition = ELECTRON_SESSION
// It's important that this comes after the partition setting, otherwise it will
// use another session and we can't change it without destroying the element again
this.refs.webview.src = this.entryHref
}
/**
* @summary Remove the Webview events
*/
componentWillUnmount () {
// Events that React is unaware of have to be handled manually
_.map(this.eventTuples, (tuple) => {
this.refs.webview.removeEventListener(...tuple)
})
}
/**
* @summary Refresh the webview if we are navigating away from the success page
* @param {Object} nextProps - upcoming properties
*/
componentWillReceiveProps (nextProps) {
if (nextProps.refreshNow && !this.props.refreshNow) {
// Reload the page if it hasn't changed, otherwise reset the source URL,
// because reload interferes with 'src' setting, resetting the 'src' attribute
// to what it was was just prior.
if (this.refs.webview.src === this.entryHref) {
this.refs.webview.reload()
} else {
this.refs.webview.src = this.entryHref
}
this.setState({
shouldShow: true
})
}
}
/**
* @summary Set the element state to hidden
*/
didFailLoad () {
this.setState({
shouldShow: false
})
}
/**
* @summary Set the element state depending on the HTTP response code
* @param {Event} event - Event object
*/
didGetResponseDetails (event) {
// This seems to pick up all requests related to the webview,
// only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200
analytics.logEvent(event)
this.setState({
shouldShow: event.httpResponseCode === HTTP_OK
})
}
}
/**
* @summary Open link in browser if it's opened as a 'foreground-tab'
* @param {Event} event - event object
*/
static newWindow (event) {
const url = new window.URL(event.url)
if (_.every([
url.protocol === 'http:' || url.protocol === 'https:',
event.disposition === 'foreground-tab'
])) {
electron.shell.openExternal(url.href)
}
}
/**
* @summary Forward specially-formatted console messages from the webview
* @param {Event} event - event object
*
* @example
*
* // In the webview
* console.log('Good night!')
*/
static consoleMessage (event) {
if (_.isNil(event.message)) {
return
}
let message = event.message
try {
message = JSON.parse(event.message)
} catch (error) {
// Ignore
}
if (message.command === 'error') {
analytics.logException(message.data)
} else {
analytics.logEvent(message.data || message)
}
}
}
SafeWebview.propTypes = {
/**
* @summary The website source URL
*/
src: propTypes.string.isRequired,
/**
* @summary Refresh the webview
*/
refreshNow: propTypes.bool
}
angularSafeWebview.component('safeWebview', react2angular(SafeWebview))
module.exports = MODULE_NAME

View File

@ -1,211 +0,0 @@
/*
* Copyright 2017 balena.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 electron from 'electron';
import * as remote from '@electron/remote';
import * as _ from 'lodash';
import * as React from 'react';
import * as packageJSON from '../../../../../package.json';
import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
/**
* @summary Electron session identifier
*/
const ELECTRON_SESSION = 'persist:success-banner';
/**
* @summary Etcher version search-parameter key
*/
const ETCHER_VERSION_PARAM = 'etcher-version';
/**
* @summary API version search-parameter key
*/
const API_VERSION_PARAM = 'api-version';
/**
* @summary Opt-out analytics search-parameter key
*/
const OPT_OUT_ANALYTICS_PARAM = 'optOutAnalytics';
/**
* @summary Webview API version
*
* @description
* Changing this number represents a departure from an older API and as such
* should only be changed when truly necessary as it introduces breaking changes.
* This version number is exposed to the banner such that it can determine what
* features are safe to utilize.
*
* See `git blame -L n` where n is the line below for the history of version changes.
*/
const API_VERSION = '2';
interface SafeWebviewProps {
// The website source URL
src: string;
// Webview lifecycle event
onWebviewShow?: (isWebviewShowing: boolean) => void;
style?: React.CSSProperties;
}
interface SafeWebviewState {
shouldShow: boolean;
}
/**
* @summary Webviews that hide/show depending on the HTTP status returned
*/
export class SafeWebview extends React.PureComponent<
SafeWebviewProps,
SafeWebviewState
> {
private entryHref: string;
private session: electron.Session;
private webviewRef: React.RefObject<electron.WebviewTag>;
constructor(props: SafeWebviewProps) {
super(props);
this.webviewRef = React.createRef();
this.state = {
shouldShow: true,
};
const url = new window.URL(this.props.src);
// 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.getSync('errorReporting')).toString(),
);
this.entryHref = url.href;
// Events steal 'this'
this.handleDomReady = _.bind(this.handleDomReady, this);
this.didFailLoad = _.bind(this.didFailLoad, this);
this.didGetResponseDetails = _.bind(this.didGetResponseDetails, this);
// Make a persistent electron session for the webview
this.session = remote.session.fromPartition(ELECTRON_SESSION, {
// Disable the cache for the session such that new content shows up when refreshing
cache: false,
});
}
private static logWebViewMessage(event: electron.ConsoleMessageEvent) {
console.log('Message from SafeWebview:', event.message);
}
public render() {
const {
style = {
flex: this.state.shouldShow ? undefined : '0 1',
width: this.state.shouldShow ? undefined : '0',
height: this.state.shouldShow ? undefined : '0',
},
} = this.props;
return (
<webview
ref={this.webviewRef}
partition={ELECTRON_SESSION}
style={style}
// @ts-ignore
allowpopups="true"
/>
);
}
// Add the Webview events
public componentDidMount() {
// Events React is unaware of have to be handled manually
if (this.webviewRef.current !== null) {
this.webviewRef.current.addEventListener(
'did-fail-load',
this.didFailLoad,
);
this.webviewRef.current.addEventListener(
'dom-ready',
this.handleDomReady,
);
this.webviewRef.current.addEventListener(
'console-message',
SafeWebview.logWebViewMessage,
);
this.session.webRequest.onCompleted(this.didGetResponseDetails);
// It's important that this comes after the partition setting, otherwise it will
// use another session and we can't change it without destroying the element again
this.webviewRef.current.src = this.entryHref;
}
}
// Remove the Webview events
public componentWillUnmount() {
// Events that React is unaware of have to be handled manually
if (this.webviewRef.current !== null) {
this.webviewRef.current.removeEventListener(
'did-fail-load',
this.didFailLoad,
);
this.webviewRef.current.removeEventListener(
'dom-ready',
this.handleDomReady,
);
this.webviewRef.current.removeEventListener(
'console-message',
SafeWebview.logWebViewMessage,
);
}
this.session.webRequest.onCompleted(null);
}
handleDomReady() {
const webview = this.webviewRef.current;
if (webview == null) {
return;
}
const id = webview.getWebContentsId();
electron.ipcRenderer.send('webview-dom-ready', id);
}
// Set the element state to hidden
public didFailLoad() {
this.setState({
shouldShow: false,
});
if (this.props.onWebviewShow) {
this.props.onWebviewShow(false);
}
}
// Set the element state depending on the HTTP response code
public didGetResponseDetails(event: electron.OnCompletedListenerDetails) {
// This seems to pick up all requests related to the webview,
// only care about this event if it's a request for the main frame
if (event.resourceType === 'mainFrame') {
const HTTP_OK = 200;
const { webContents, ...webviewEvent } = event;
analytics.logEvent('SafeWebview loaded', {
...webviewEvent,
});
this.setState({
shouldShow: event.statusCode === HTTP_OK,
});
if (this.props.onWebviewShow) {
this.props.onWebviewShow(event.statusCode === HTTP_OK);
}
}
}
}

View File

@ -1,158 +0,0 @@
/*
* Copyright 2019 balena.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 GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg';
import * as _ from 'lodash';
import * as React from 'react';
import { Box, Checkbox, Flex, Txt } from 'rendition';
import { version, packageType } from '../../../../../package.json';
import * as settings from '../../models/settings';
import * as analytics from '../../modules/analytics';
import { open as openExternal } from '../../os/open-external/services/open-external';
import { Modal } from '../../styled-components';
import * as i18next from 'i18next';
import { etcherProInfo } from '../../utils/etcher-pro-specific';
interface Setting {
name: string;
label: string | JSX.Element;
}
async function getSettingsList(): Promise<Setting[]> {
const list: Setting[] = [
{
name: 'errorReporting',
label: i18next.t('settings.errorReporting'),
},
{
name: 'autoBlockmapping',
label: i18next.t('settings.trimExtPartitions'),
},
];
if (['appimage', 'nsis', 'dmg'].includes(packageType)) {
list.push({
name: 'updatesEnabled',
label: i18next.t('settings.autoUpdate'),
});
}
return list;
}
interface SettingsModalProps {
toggleModal: (value: boolean) => void;
}
const EPInfo = etcherProInfo();
const InfoBox = (props: any) => (
<Box fontSize={14}>
<Txt>{props.label}</Txt>
<Txt code copy={props.value}>
{props.value}{' '}
</Txt>
</Box>
);
export function SettingsModal({ toggleModal }: SettingsModalProps) {
const [settingsList, setCurrentSettingsList] = React.useState<Setting[]>([]);
React.useEffect(() => {
(async () => {
if (settingsList.length === 0) {
setCurrentSettingsList(await getSettingsList());
}
})();
});
const [currentSettings, setCurrentSettings] = React.useState<
_.Dictionary<boolean>
>({});
React.useEffect(() => {
(async () => {
if (_.isEmpty(currentSettings)) {
setCurrentSettings(await settings.getAll());
}
})();
});
const toggleSetting = async (setting: string) => {
const value = currentSettings[setting];
analytics.logEvent('Toggle setting', { setting, value });
await settings.set(setting, !value);
setCurrentSettings({
...currentSettings,
[setting]: !value,
});
};
return (
<Modal
titleElement={
<Txt fontSize={24} mb={24}>
{i18next.t('settings.settings')}
</Txt>
}
done={() => toggleModal(false)}
>
<Flex flexDirection="column">
{settingsList.map((setting: Setting, i: number) => {
return (
<Flex key={setting.name} mb={14}>
<Checkbox
toggle
tabIndex={6 + i}
label={setting.label}
checked={currentSettings[setting.name]}
onChange={() => toggleSetting(setting.name)}
/>
</Flex>
);
})}
{EPInfo !== undefined && (
<Flex flexDirection="column">
<Txt fontSize={24}>{i18next.t('settings.systemInformation')}</Txt>
{EPInfo.get_serial() === undefined ? (
<InfoBox label="UUID" value={EPInfo.uuid} />
) : (
<InfoBox label="Serial" value={EPInfo.get_serial()} />
)}
</Flex>
)}
<Flex
mt={18}
alignItems="center"
color="#00aeef"
style={{
width: 'fit-content',
cursor: 'pointer',
fontSize: 14,
}}
onClick={() =>
openExternal(
'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md',
)
}
>
<GithubSvg
height="1em"
fill="currentColor"
style={{ marginRight: 8 }}
/>
<Txt style={{ borderBottom: '1px solid #00aeef' }}>{version}</Txt>
</Flex>
</Flex>
</Modal>
);
}

View File

@ -1,814 +0,0 @@
/*
* Copyright 2016 balena.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 CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import ChevronDownSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-down.svg';
import ChevronRightSvg from '@fortawesome/fontawesome-free/svgs/solid/chevron-right.svg';
import type { IpcRendererEvent } from 'electron';
import { ipcRenderer } from 'electron';
import { uniqBy, isNil } from 'lodash';
import * as path from 'path';
import prettyBytes from 'pretty-bytes';
import * as React from 'react';
import { requestMetadata } from '../../app';
import type { ButtonProps } from 'rendition';
import {
Flex,
Modal as SmallModal,
Txt,
Card as BaseCard,
Input,
Spinner,
Link,
} from 'rendition';
import styled from 'styled-components';
import * as errors from '../../../../shared/errors';
import * as messages from '../../../../shared/messages';
import * as supportedFormats from '../../../../shared/supported-formats';
import * as selectionState from '../../models/selection-state';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import * as exceptionReporter from '../../modules/exception-reporter';
import * as osDialog from '../../os/dialog';
import {
ChangeButton,
DetailsText,
Modal,
StepButton,
StepNameButton,
ScrollableFlex,
} from '../../styled-components';
import { colors } from '../../theme';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import { SVGIcon } from '../svg-icon/svg-icon';
import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
import type { DrivelistDrive } from '../../../../shared/drive-constraints';
import { isJson } from '../../../../shared/utils';
import type {
SourceMetadata,
Authentication,
Source,
} from '../../../../shared/typings/source-selector';
import * as i18next from 'i18next';
const recentUrlImagesKey = 'recentUrlImages';
function normalizeRecentUrlImages(urls: any[]): URL[] {
if (!Array.isArray(urls)) {
urls = [];
}
urls = urls
.map((url) => {
try {
return new URL(url);
} catch (error: any) {
// Invalid URL, skip
}
})
.filter((url) => url !== undefined);
urls = uniqBy(urls, (url) => url.href);
return urls.slice(urls.length - 5);
}
function getRecentUrlImages(): URL[] {
let urls = [];
try {
urls = JSON.parse(localStorage.getItem(recentUrlImagesKey) || '[]');
} catch {
// noop
}
return normalizeRecentUrlImages(urls);
}
function setRecentUrlImages(urls: URL[]) {
const normalized = normalizeRecentUrlImages(urls.map((url: URL) => url.href));
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
}
const isURL = (imagePath: string) =>
imagePath.startsWith('https://') || imagePath.startsWith('http://');
const Card = styled(BaseCard)`
hr {
margin: 5px 0;
}
`;
// TODO move these styles to rendition
const ModalText = styled.p`
a {
color: rgb(0, 174, 239);
&:hover {
color: rgb(0, 139, 191);
}
}
`;
function getState() {
const image = selectionState.getImage();
return {
hasImage: selectionState.hasImage(),
imageName: image?.name,
imageSize: image?.size,
};
}
function isString(value: any): value is string {
return typeof value === 'string';
}
const URLSelector = ({
done,
cancel,
}: {
done: (imageURL: string, auth?: Authentication) => void;
cancel: () => void;
}) => {
const [imageURL, setImageURL] = React.useState('');
const [recentImages, setRecentImages] = React.useState<URL[]>([]);
const [loading, setLoading] = React.useState(false);
const [showBasicAuth, setShowBasicAuth] = React.useState(false);
const [username, setUsername] = React.useState('');
const [password, setPassword] = React.useState('');
React.useEffect(() => {
const fetchRecentUrlImages = async () => {
const recentUrlImages: URL[] = await getRecentUrlImages();
setRecentImages(recentUrlImages);
};
fetchRecentUrlImages();
}, []);
return (
<Modal
cancel={cancel}
primaryButtonProps={{
disabled: loading || !imageURL,
}}
action={loading ? <Spinner /> : i18next.t('ok')}
done={async () => {
setLoading(true);
const urlStrings = recentImages.map((url: URL) => url.href);
const normalizedRecentUrls = normalizeRecentUrlImages([
...urlStrings,
imageURL,
]);
setRecentUrlImages(normalizedRecentUrls);
const auth = username ? { username, password } : undefined;
await done(imageURL, auth);
}}
>
<Flex flexDirection="column">
<Flex mb={15} style={{ width: '100%' }} flexDirection="column">
<Txt mb="10px" fontSize="24px">
{i18next.t('source.useSourceURL')}
</Txt>
<Input
value={imageURL}
placeholder={i18next.t('source.enterValidURL')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setImageURL(evt.target.value)
}
/>
<Link
mt={15}
mb={15}
fontSize="14px"
onClick={() => {
if (showBasicAuth) {
setUsername('');
setPassword('');
}
setShowBasicAuth(!showBasicAuth);
}}
>
<Flex alignItems="center">
{showBasicAuth && (
<ChevronDownSvg height="1em" fill="currentColor" />
)}
{!showBasicAuth && (
<ChevronRightSvg height="1em" fill="currentColor" />
)}
<Txt ml={8}>{i18next.t('source.auth')}</Txt>
</Flex>
</Link>
{showBasicAuth && (
<React.Fragment>
<Input
mb={15}
value={username}
placeholder={i18next.t('source.username')}
type="text"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setUsername(evt.target.value)
}
/>
<Input
value={password}
placeholder={i18next.t('source.password')}
type="password"
onChange={(evt: React.ChangeEvent<HTMLInputElement>) =>
setPassword(evt.target.value)
}
/>
</React.Fragment>
)}
</Flex>
{recentImages.length > 0 && (
<Flex flexDirection="column" height="78.6%">
<Txt fontSize={18}>Recent</Txt>
<ScrollableFlex flexDirection="column">
<Card
p="10px 15px"
rows={recentImages
.map((recent) => (
<Txt
key={recent.href}
onClick={() => {
setImageURL(recent.href);
}}
style={{
overflowWrap: 'break-word',
}}
>
{recent.pathname.split('/').pop()} - {recent.href}
</Txt>
))
.reverse()}
/>
</ScrollableFlex>
</Flex>
)}
</Flex>
</Modal>
);
};
interface Flow {
icon?: JSX.Element;
onClick: (evt: React.MouseEvent) => void;
label: string;
}
const FlowSelector = styled(
({ flow, ...props }: { flow: Flow } & ButtonProps) => (
<StepButton
plain={!props.primary}
primary={props.primary}
onClick={(evt: React.MouseEvent<Element, MouseEvent>) =>
flow.onClick(evt)
}
icon={flow.icon}
{...props}
>
{flow.label}
</StepButton>
),
)`
border-radius: 24px;
color: rgba(255, 255, 255, 0.7);
:enabled:focus,
:enabled:focus svg {
color: ${colors.primary.foreground} !important;
}
:enabled:hover {
background-color: ${colors.primary.background};
color: ${colors.primary.foreground};
font-weight: 600;
svg {
color: ${colors.primary.foreground} !important;
}
}
`;
interface SourceSelectorProps {
flashing: boolean;
hideAnalyticsAlert: () => void;
}
interface SourceSelectorState {
hasImage: boolean;
imageName?: string;
imageSize?: number;
warning: { message: string; title: string | null } | null;
showImageDetails: boolean;
showURLSelector: boolean;
showDriveSelector: boolean;
defaultFlowActive: boolean;
imageSelectorOpen: boolean;
imageLoading: boolean;
}
export class SourceSelector extends React.Component<
SourceSelectorProps,
SourceSelectorState
> {
private unsubscribe: (() => void) | undefined;
constructor(props: SourceSelectorProps) {
super(props);
this.state = {
...getState(),
warning: null,
showImageDetails: false,
showURLSelector: false,
showDriveSelector: false,
defaultFlowActive: true,
imageSelectorOpen: false,
imageLoading: false,
};
// Bind `this` since it's used in an event's callback
this.onSelectImage = this.onSelectImage.bind(this);
}
public componentDidMount() {
this.unsubscribe = observe(() => {
this.setState(getState());
});
ipcRenderer.on('select-image', this.onSelectImage);
ipcRenderer.send('source-selector-ready');
}
public componentWillUnmount() {
this.unsubscribe?.();
ipcRenderer.removeListener('select-image', this.onSelectImage);
}
public componentDidUpdate(
_prevProps: Readonly<SourceSelectorProps>,
prevState: Readonly<SourceSelectorState>,
) {
if (
(!prevState.showDriveSelector && this.state.showDriveSelector) ||
(!prevState.showURLSelector && this.state.showURLSelector) ||
(!prevState.showImageDetails && this.state.showImageDetails) ||
(!prevState.imageSelectorOpen && this.state.imageSelectorOpen)
) {
this.props.hideAnalyticsAlert();
}
}
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
this.setState({ imageLoading: true });
await this.selectSource(
imagePath,
isURL(this.normalizeImagePath(imagePath)) ? 'Http' : 'File',
).promise;
this.setState({ imageLoading: false });
}
public normalizeImagePath(imgPath: string) {
const decodedPath = decodeURIComponent(imgPath);
if (isJson(decodedPath)) {
return JSON.parse(decodedPath).url ?? decodedPath;
}
return decodedPath;
}
private reselectSource() {
analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(),
});
selectionState.deselectImage();
this.props.hideAnalyticsAlert();
}
private selectSource(
selected: string | DrivelistDrive,
SourceType: Source,
auth?: Authentication,
): { promise: Promise<void>; cancel: () => void } {
return {
cancel: () => {
// noop
},
promise: (async () => {
const sourcePath = isString(selected) ? selected : selected.device;
let metadata: SourceMetadata | undefined;
if (isString(selected)) {
if (
SourceType === 'Http' &&
!isURL(this.normalizeImagePath(selected))
) {
this.handleError(
i18next.t('source.unsupportedProtocol'),
selected,
messages.error.unsupportedProtocol(),
);
return;
}
if (supportedFormats.looksLikeWindowsImage(selected)) {
analytics.logEvent('Possibly Windows image', { image: selected });
this.setState({
warning: {
message: messages.warning.looksLikeWindowsImage(),
title: i18next.t('source.windowsImage'),
},
});
}
try {
// this will send an event down the ipcMain asking for metadata
// we'll get the response through an event
// FIXME: This is a poor man wait while loading to prevent a potential race condition without completely blocking the interface
// This should be addressed when refactoring the GUI
let retriesLeft = 10;
while (requestMetadata === undefined && retriesLeft > 0) {
await new Promise((resolve) => setTimeout(resolve, 1050)); // api is trying to connect every 1000, this is offset to make sure we fall between retries
retriesLeft--;
}
metadata = await requestMetadata({ selected, SourceType, auth });
if (!metadata?.hasMBR && this.state.warning === null) {
analytics.logEvent('Missing partition table', { metadata });
this.setState({
warning: {
message: messages.warning.missingPartitionTable(),
title: i18next.t('source.partitionTable'),
},
});
}
} catch (error: any) {
this.handleError(
i18next.t('source.errorOpen'),
sourcePath,
messages.error.openSource(sourcePath, error.message),
error,
);
}
} else {
if (selected.partitionTableType === null) {
analytics.logEvent('Missing partition table', { selected });
this.setState({
warning: {
message: messages.warning.driveMissingPartitionTable(),
title: i18next.t('source.partitionTable'),
},
});
}
metadata = {
path: selected.device,
displayName: selected.displayName,
description: selected.displayName,
size: selected.size as SourceMetadata['size'],
SourceType: 'BlockDevice',
drive: selected,
};
}
if (metadata !== undefined) {
metadata.auth = auth;
metadata.SourceType = SourceType;
selectionState.selectSource(metadata);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
// certain features without printing pages of text to DevTools.
image: {
...metadata,
logo: Boolean(metadata.logo),
blockMap: Boolean(metadata.blockMap),
},
});
}
})(),
};
}
private handleError(
title: string,
sourcePath: string,
description: string,
error?: Error,
) {
const imageError = errors.createUserError({
title,
description,
});
osDialog.showError(imageError);
if (error) {
analytics.logException(error);
return;
}
analytics.logEvent(title, { path: sourcePath });
}
private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true });
try {
const imagePath = await osDialog.selectImage();
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
if (!imagePath) {
analytics.logEvent('Image selector closed');
return;
}
await this.selectSource(imagePath, 'File').promise;
} catch (error: any) {
exceptionReporter.report(error);
} finally {
this.setState({ imageSelectorOpen: false });
}
}
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const file = event.dataTransfer.files.item(0);
if (file != null) {
await this.selectSource(file.path, 'File').promise;
}
}
private openURLSelector() {
analytics.logEvent('Open image URL selector');
this.setState({
showURLSelector: true,
});
}
private openDriveSelector() {
analytics.logEvent('Open drive selector');
this.setState({
showDriveSelector: true,
});
}
private onDragOver(event: React.DragEvent<HTMLDivElement>) {
// Needed to get onDrop events on div elements
event.preventDefault();
}
private onDragEnter(event: React.DragEvent<HTMLDivElement>) {
// Needed to get onDrop events on div elements
event.preventDefault();
}
private showSelectedImageDetails() {
analytics.logEvent('Show selected image tooltip', {
imagePath: selectionState.getImage()?.path,
});
this.setState({
showImageDetails: true,
});
}
private setDefaultFlowActive(defaultFlowActive: boolean) {
this.setState({ defaultFlowActive });
}
private closeModal() {
this.setState({
showDriveSelector: false,
});
}
// TODO add a visual change when dragging a file over the selector
public render() {
const { flashing } = this.props;
const {
showImageDetails,
showURLSelector,
showDriveSelector,
imageLoading,
} = this.state;
const selectionImage = selectionState.getImage();
let image =
selectionImage !== undefined ? selectionImage : ({} as SourceMetadata);
image = image.drive ?? image;
let cancelURLSelection = () => {
// noop
};
image.name = image.description || image.name;
const imagePath = image.path || image.displayName || '';
const imageBasename = path.basename(imagePath);
const imageName = image.name || '';
const imageSize = image.size;
const imageLogo = image.logo || '';
return (
<>
<Flex
flexDirection="column"
alignItems="center"
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
this.onDragEnter(evt)
}
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
this.onDragOver(evt)
}
>
<SVGIcon
contents={imageLogo}
fallback={ImageSvg}
style={{
marginBottom: 30,
}}
/>
{selectionImage !== undefined || imageLoading ? (
<>
<StepNameButton
plain
onClick={() => this.showSelectedImageDetails()}
tooltip={imageName || imageBasename}
>
<Spinner show={imageLoading}>
{middleEllipsis(imageName || imageBasename, 20)}
</Spinner>
</StepNameButton>
{!flashing && !imageLoading && (
<ChangeButton
plain
mb={14}
onClick={() => this.reselectSource()}
>
{i18next.t('cancel')}
</ChangeButton>
)}
{!isNil(imageSize) && !imageLoading && (
<DetailsText>{prettyBytes(imageSize)}</DetailsText>
)}
</>
) : (
<>
<FlowSelector
disabled={this.state.imageSelectorOpen}
primary={this.state.defaultFlowActive}
key="Flash from file"
flow={{
onClick: () => this.openImageSelector(),
label: i18next.t('source.fromFile'),
icon: <FileSvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
<FlowSelector
key="Flash from URL"
flow={{
onClick: () => this.openURLSelector(),
label: i18next.t('source.fromURL'),
icon: <LinkSvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
<FlowSelector
key="Clone drive"
flow={{
onClick: () => this.openDriveSelector(),
label: i18next.t('source.clone'),
icon: <CopySvg height="1em" fill="currentColor" />,
}}
onMouseEnter={() => this.setDefaultFlowActive(false)}
onMouseLeave={() => this.setDefaultFlowActive(true)}
/>
</>
)}
</Flex>
{this.state.warning != null && (
<SmallModal
style={{
boxShadow: '0 3px 7px rgba(0, 0, 0, 0.3)',
}}
title={
<span>
<ExclamationTriangleSvg fill="#fca321" height="1em" />{' '}
<span>{this.state.warning.title}</span>
</span>
}
action={i18next.t('continue')}
cancel={() => {
this.setState({ warning: null });
this.reselectSource();
}}
done={() => {
this.setState({ warning: null });
}}
primaryButtonProps={{ warning: true, primary: false }}
>
<ModalText
dangerouslySetInnerHTML={{ __html: this.state.warning.message }}
/>
</SmallModal>
)}
{showImageDetails && (
<SmallModal
title={i18next.t('source.image')}
done={() => {
this.setState({ showImageDetails: false });
}}
>
<Txt.p>
<Txt.span bold>{i18next.t('source.name')}</Txt.span>
<Txt.span>{imageName || imageBasename}</Txt.span>
</Txt.p>
<Txt.p>
<Txt.span bold>{i18next.t('source.path')}</Txt.span>
<Txt.span>{imagePath}</Txt.span>
</Txt.p>
</SmallModal>
)}
{showURLSelector && (
<URLSelector
cancel={() => {
cancelURLSelection();
this.setState({
showURLSelector: false,
});
}}
done={async (imageURL: string, auth?: Authentication) => {
// Avoid analytics and selection state changes
// if no file was resolved from the dialog.
if (!imageURL) {
analytics.logEvent('URL selector closed');
} else {
let promise;
({ promise, cancel: cancelURLSelection } = this.selectSource(
imageURL,
'Http',
auth,
));
await promise;
}
this.setState({
showURLSelector: false,
});
}}
/>
)}
{showDriveSelector && (
<DriveSelector
write={false}
multipleSelection={false}
titleLabel={i18next.t('source.selectSource')}
emptyListLabel={i18next.t('source.plugSource')}
emptyListIcon={<SrcSvg width="40px" />}
cancel={(originalList) => {
if (originalList.length) {
const originalSource = originalList[0];
if (selectionImage?.drive?.device !== originalSource.device) {
this.selectSource(originalSource, 'BlockDevice');
}
} else {
selectionState.deselectImage();
}
this.closeModal();
}}
done={() => this.closeModal()}
onSelect={(drive) => {
if (drive) {
if (
selectionState.getImage()?.drive?.device === drive?.device
) {
return selectionState.deselectImage();
}
this.selectSource(drive, 'BlockDevice');
}
}}
/>
)}
</>
);
}
}

View File

@ -0,0 +1,183 @@
/*
* 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'
/* eslint-disable jsdoc/require-example */
/**
* @module Etcher.Components.SVGIcon
*/
const _ = require('lodash')
const angular = require('angular')
const react = require('react')
const propTypes = require('prop-types')
const react2angular = require('react2angular').react2angular
const path = require('path')
const fs = require('fs')
const analytics = require('../modules/analytics')
const MODULE_NAME = 'Etcher.Components.SVGIcon'
const angularSVGIcon = angular.module(MODULE_NAME, [])
const DEFAULT_SIZE = '40px'
const domParser = new window.DOMParser()
/**
* @summary Try to parse SVG contents and return it data encoded
*
* @param {String} contents - SVG XML contents
* @returns {String|null}
*
* @example
* const encodedSVG = tryParseSVGContents('<svg><path></path></svg>')
*
* img.src = encodedSVG
*/
const tryParseSVGContents = (contents) => {
const doc = domParser.parseFromString(contents, 'image/svg+xml')
const parserError = doc.querySelector('parsererror')
const svg = doc.querySelector('svg')
if (!parserError && svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`
}
return null
}
/**
* @summary SVG element that takes both filepaths and file contents
* @type {Object}
* @public
*/
class SVGIcon extends react.Component {
/**
* @summary Render the SVG
* @returns {react.Element}
*/
render () {
// __dirname behaves strangely inside a Webpack bundle,
// so we need to provide different base directories
// depending on whether __dirname is absolute or not,
// which helps detecting a Webpack bundle.
// We use global.__dirname inside a Webpack bundle since
// that's the only way to get the "real" __dirname.
const baseDirectory = path.isAbsolute(__dirname)
? path.join(__dirname, '..')
// eslint-disable-next-line no-underscore-dangle
: global.__dirname
let svgData = ''
_.find(this.props.contents, (content) => {
const attempt = tryParseSVGContents(content)
if (attempt) {
svgData = attempt
return true
}
return false
})
if (!svgData) {
_.find(this.props.paths, (relativePath) => {
// This means the path to the icon should be
// relative to *this directory*.
// TODO: There might be a way to compute the path
// relatively to the `index.html`.
const imagePath = path.join(baseDirectory, 'assets', relativePath)
const contents = _.attempt(() => {
return fs.readFileSync(imagePath, {
encoding: 'utf8'
})
})
if (_.isError(contents)) {
analytics.logException(contents)
return false
}
const parsed = _.attempt(tryParseSVGContents, contents)
if (parsed) {
svgData = parsed
return true
}
return false
})
}
const width = this.props.width || DEFAULT_SIZE
const height = this.props.height || DEFAULT_SIZE
return react.createElement('img', {
className: 'svg-icon',
style: {
width,
height
},
src: svgData,
disabled: this.props.disabled
})
}
/**
* @summary Cause a re-render due to changed element properties
* @param {Object} nextProps - the new properties
*/
componentWillReceiveProps (nextProps) {
// This will update the element if the properties change
this.setState(nextProps)
}
}
SVGIcon.propTypes = {
/**
* @summary Paths to SVG files to be tried in succession if any fails
*/
paths: propTypes.array,
/**
* @summary List of embedded SVG contents to be tried in succession if any fails
*/
contents: propTypes.array,
/**
* @summary SVG image width unit
*/
width: propTypes.string,
/**
* @summary SVG image height unit
*/
height: propTypes.string,
/**
* @summary Should the element visually appear grayed out and disabled?
*/
disabled: propTypes.bool
}
angularSVGIcon.component('svgIcon', react2angular(SVGIcon))
module.exports = MODULE_NAME

View File

@ -0,0 +1,9 @@
svg-icon {
display: inline-block;
img {
width: 100%;
height: 100%;
}
}

View File

@ -1,74 +0,0 @@
/*
* Copyright 2018 balena.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 React from 'react';
const domParser = new window.DOMParser();
const DEFAULT_SIZE = '40px';
/**
* @summary Try to parse SVG contents and return it data encoded
*
*/
function tryParseSVGContents(contents?: string): string | undefined {
if (contents === undefined) {
return;
}
const doc = domParser.parseFromString(contents, 'image/svg+xml');
const parserError = doc.querySelector('parsererror');
const svg = doc.querySelector('svg');
if (!parserError && svg) {
return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`;
}
}
interface SVGIconProps {
// Optional string representing the SVG contents to be tried
contents?: string;
// Fallback SVG element to show if `contents` is invalid/undefined
fallback: React.FunctionComponent<React.SVGProps<HTMLOrSVGElement>>;
// SVG image width unit
width?: string;
// SVG image height unit
height?: string;
// Should the element visually appear grayed out and disabled?
disabled?: boolean;
style?: React.CSSProperties;
}
/**
* @summary SVG element that takes file contents
*/
export class SVGIcon extends React.PureComponent<SVGIconProps> {
public render() {
const svgData = tryParseSVGContents(this.props.contents);
const { width, height, style = {} } = this.props;
style.width = width || DEFAULT_SIZE;
style.height = height || DEFAULT_SIZE;
if (svgData !== undefined) {
return (
<img
className={this.props.disabled ? 'disabled' : ''}
style={style}
src={svgData}
/>
);
}
const { fallback: FallbackSVG } = this.props;
return <FallbackSVG style={style} />;
}
}

View File

@ -1,157 +0,0 @@
/*
* Copyright 2019 balena.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 ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg';
import * as React from 'react';
import type { FlexProps } from 'rendition';
import { Flex, Txt } from 'rendition';
import type { DriveStatus } from '../../../../shared/drive-constraints';
import { getDriveImageCompatibilityStatuses } from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import prettyBytes from 'pretty-bytes';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import {
ChangeButton,
DetailsText,
StepButton,
StepNameButton,
} from '../../styled-components';
import { middleEllipsis } from '../../utils/middle-ellipsis';
import * as i18next from 'i18next';
interface TargetSelectorProps {
targets: any[];
disabled: boolean;
openDriveSelector: () => void;
reselectDrive: () => void;
flashing: boolean;
show: boolean;
tooltip: string;
}
function getDriveWarning(status: DriveStatus) {
switch (status.message) {
case compatibility.containsImage():
return warning.sourceDrive();
case compatibility.largeDrive():
return warning.largeDriveSize();
case compatibility.system():
return warning.systemDrive();
default:
return '';
}
}
const DriveCompatibilityWarning = ({
warnings,
...props
}: {
warnings: string[];
} & FlexProps) => {
const systemDrive = warnings.find(
(message) => message === warning.systemDrive(),
);
return (
<Flex tooltip={warnings.join(', ')} {...props}>
<ExclamationTriangleSvg
fill={systemDrive ? '#fca321' : '#8f9297'}
height="1em"
/>
</Flex>
);
};
export function TargetSelectorButton(props: TargetSelectorProps) {
const targets = getSelectedDrives();
if (targets.length === 1) {
const target = targets[0];
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{warnings.length > 0 && (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
)}
{middleEllipsis(target.description, 20)}
</StepNameButton>
{!props.flashing && (
<ChangeButton plain mb={14} onClick={props.reselectDrive}>
{i18next.t('target.change')}
</ChangeButton>
)}
{target.size != null && (
<DetailsText>{prettyBytes(target.size)}</DetailsText>
)}
</>
);
}
if (targets.length > 1) {
const targetsTemplate = [];
for (const target of targets) {
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
targetsTemplate.push(
<DetailsText
key={target.device}
tooltip={`${target.description} ${target.displayName} ${
target.size != null ? prettyBytes(target.size) : ''
}`}
px={21}
>
{warnings.length > 0 ? (
<DriveCompatibilityWarning warnings={warnings} mr={2} />
) : null}
<Txt mr={2}>{middleEllipsis(target.description, 14)}</Txt>
{target.size != null && <Txt>{prettyBytes(target.size)}</Txt>}
</DetailsText>,
);
}
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
{targets.length} {i18next.t('target.targets')}
</StepNameButton>
{!props.flashing && (
<ChangeButton plain onClick={props.reselectDrive} mb={14}>
{i18next.t('target.change')}
</ChangeButton>
)}
{targetsTemplate}
</>
);
}
return (
<StepButton
primary
tabIndex={targets.length > 0 ? -1 : 2}
disabled={props.disabled}
onClick={props.openDriveSelector}
>
{i18next.t('target.selectTarget')}
</StepButton>
);
}

View File

@ -1,195 +0,0 @@
/*
* Copyright 2016 balena.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 React from 'react';
import { Flex, Txt } from 'rendition';
import type { DriveSelectorProps } from '../drive-selector/drive-selector';
import { DriveSelector } from '../drive-selector/drive-selector';
import {
isDriveSelected,
getImage,
getSelectedDrives,
deselectDrive,
selectDrive,
deselectAllDrives,
} from '../../models/selection-state';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { TargetSelectorButton } from './target-selector-button';
import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages';
import type { DrivelistDrive } from '../../../../shared/drive-constraints';
import * as i18next from 'i18next';
export const getDriveListLabel = () => {
return getSelectedDrives()
.map((drive: any) => {
return `${drive.description} (${drive.displayName})`;
})
.join('\n');
};
const getDriveSelectionStateSlice = () => ({
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
});
export const TargetSelectorModal = (
props: Omit<
DriveSelectorProps,
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
>,
) => (
<DriveSelector
multipleSelection={true}
titleLabel={i18next.t('target.selectTarget')}
emptyListLabel={i18next.t('target.plugTarget')}
emptyListIcon={<TgtSvg width="40px" />}
showWarnings={true}
selectedList={getSelectedDrives()}
updateSelectedList={getSelectedDrives}
{...props}
/>
);
export const selectAllTargets = (modalTargets: DrivelistDrive[]) => {
const selectedDrivesFromState = getSelectedDrives();
const deselected = selectedDrivesFromState.filter(
(drive) =>
!modalTargets.find((modalTarget) => modalTarget.device === drive.device),
);
// deselect drives
deselected.forEach((drive) => {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: true,
});
deselectDrive(drive.device);
});
// select drives
modalTargets.forEach((drive) => {
// Don't send events for drives that were already selected
if (!isDriveSelected(drive.device)) {
analytics.logEvent('Toggle drive', {
drive,
previouslySelected: false,
});
}
selectDrive(drive.device);
});
};
interface TargetSelectorProps {
disabled: boolean;
hasDrive: boolean;
flashing: boolean;
hideAnalyticsAlert: () => void;
}
export const TargetSelector = ({
disabled,
hasDrive,
flashing,
hideAnalyticsAlert,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
getDriveSelectionStateSlice(),
);
const [showTargetSelectorModal, setShowTargetSelectorModal] =
React.useState(false);
React.useEffect(() => {
return observe(() => {
setStateSlice(getDriveSelectionStateSlice());
});
}, []);
const hasSystemDrives = targets.some((target) => target.isSystem);
return (
<Flex flexDirection="column" alignItems="center">
<DriveSvg
className={disabled ? 'disabled' : ''}
width="40px"
style={{
marginBottom: 30,
}}
/>
<TargetSelectorButton
disabled={disabled}
show={!hasDrive}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
hideAnalyticsAlert();
}}
reselectDrive={() => {
analytics.logEvent('Reselect drive');
setShowTargetSelectorModal(true);
}}
flashing={flashing}
targets={targets}
/>
{hasSystemDrives ? (
<Txt
color="#fca321"
style={{
position: 'absolute',
bottom: '25px',
}}
>
Warning: {warning.systemDrive()}
</Txt>
) : null}
{showTargetSelectorModal && (
<TargetSelectorModal
write={true}
cancel={(originalList) => {
if (originalList.length) {
selectAllTargets(originalList);
} else {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
done={(modalTargets) => {
if (modalTargets.length === 0) {
deselectAllDrives();
}
setShowTargetSelectorModal(false);
}}
onSelect={(drive) => {
if (
getSelectedDrives().find(
(selectedDrive) => selectedDrive.device === drive.device,
)
) {
return deselectDrive(drive.device);
}
selectDrive(drive.device);
}}
/>
)}
</Flex>
);
};

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016 balena.io
* 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.
@ -14,28 +14,25 @@
* limitations under the License.
*/
import { basename } from 'path';
'use strict'
export const SUPPORTED_EXTENSIONS = [
'bin',
'bz2',
'dmg',
'dsk',
'etch',
'gz',
'hddimg',
'img',
'iso',
'raw',
'rpi-sdimg',
'sdcard',
'vhd',
'wic',
'xz',
'zip',
];
module.exports = function ($uibModalInstance, tooltipData) {
/**
* @summary Tooltip data
* @type {Object}
* @public
*/
this.data = tooltipData
export function looksLikeWindowsImage(imagePath: string): boolean {
const regex = /windows|win7|win8|win10|winxp/i;
return regex.test(basename(imagePath));
/**
* @summary Close the modal
* @function
* @public
*
* @example
* TooltipModalController.closeModal();
*/
this.closeModal = () => {
$uibModalInstance.dismiss()
}
}

View File

@ -0,0 +1,49 @@
/*
* 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 _ = require('lodash')
module.exports = function (ModalService) {
/**
* @summary Open the tooltip modal
* @function
* @public
*
* @param {Object} options - tooltip options
* @param {String} options.title - tooltip title
* @param {String} options.message - tooltip message
* @returns {Promise}
*
* @example
* TooltipModalService.show({
* title: 'Important tooltip',
* message: 'Tooltip contents'
* });
*/
this.show = (options) => {
return ModalService.open({
name: 'tooltip',
template: require('../templates/tooltip-modal.tpl.html'),
controller: 'TooltipModalController as modal',
size: 'tooltip-modal',
resolve: {
tooltipData: _.constant(options)
}
}).result
}
}

View File

@ -0,0 +1,23 @@
/*
* 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.
*/
.modal-tooltip-modal .modal-body {
text-align: center;
margin: 15px;
color: $palette-theme-light-foreground;
background-color: darken($palette-theme-light-background, 5%);
word-wrap: break-word;
}

View File

@ -0,0 +1,6 @@
<div class="modal-header">
<h4 class="modal-title">{{ ::modal.data.title }}</h4>
<button class="close" ng-click="modal.closeModal()">&times;</button>
</div>
<div class="modal-body">{{ ::modal.data.message }}</div>

View File

@ -1,5 +1,5 @@
/*
* Copyright 2016 balena.io
* 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.
@ -14,20 +14,19 @@
* limitations under the License.
*/
import type { DrivelistDrive } from '../../../shared/drive-constraints';
import { Actions, store } from './store';
'use strict'
export function hasAvailableDrives() {
return getDrives().length > 0;
}
/**
* @module Etcher.Components.TooltipModal
*/
export function setDrives(drives: any[]) {
store.dispatch({
type: Actions.SET_AVAILABLE_TARGETS,
data: drives,
});
}
const angular = require('angular')
const MODULE_NAME = 'Etcher.Components.TooltipModal'
const TooltipModal = angular.module(MODULE_NAME, [
require('../modal/modal')
])
export function getDrives(): DrivelistDrive[] {
return store.getState().toJS().availableDrives;
}
TooltipModal.controller('TooltipModalController', require('./controllers/tooltip-modal'))
TooltipModal.service('TooltipModalService', require('./services/tooltip-modal'))
module.exports = MODULE_NAME

View File

@ -0,0 +1,151 @@
/*
* 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 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) => {
_.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(this.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 ${this.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
})
if (results.agreed) {
electron.shell.openExternal('https://etcher.io?ref=etcher_update')
}
})
}

View File

@ -0,0 +1,50 @@
/*
* 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.exports = function ($uibModalInstance, options) {
/**
* @summary Modal options
* @type {Object}
* @public
*/
this.options = options
/**
* @summary Reject the warning prompt
* @function
* @public
*
* @example
* WarningModalController.reject();
*/
this.reject = () => {
$uibModalInstance.close(false)
}
/**
* @summary Accept the warning prompt
* @function
* @public
*
* @example
* WarningModalController.accept();
*/
this.accept = () => {
$uibModalInstance.close(true)
}
}

Some files were not shown because too many files have changed in this diff Show More