├── .dockerignore ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── actions │ ├── publish │ │ └── action.yml │ └── test │ │ └── action.yml └── workflows │ ├── flowzone.yml │ └── winget.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.js ├── .versionbot └── CHANGELOG.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── after-install.tpl ├── assets ├── dmg │ ├── background.png │ ├── background.tiff │ └── background@2x.png ├── icon.icns ├── icon.ico ├── icon.png └── iconset │ ├── 128x128.png │ ├── 16x16.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ └── 512x512.png ├── docs ├── ARCHITECTURE.md ├── COMMIT-GUIDELINES.md ├── CONTRIBUTING.md ├── FAQ.md ├── MAINTAINERS.md ├── MANUAL-TESTING.md ├── PUBLISHING.md ├── SUPPORT.md └── USER-DOCUMENTATION.md ├── entitlements.mac.plist ├── forge.config.ts ├── forge.sidecar.ts ├── lib ├── gui │ ├── app │ │ ├── app.ts │ │ ├── components │ │ │ ├── drive-selector │ │ │ │ └── drive-selector.tsx │ │ │ ├── drive-status-warning-modal │ │ │ │ └── drive-status-warning-modal.tsx │ │ │ ├── finish │ │ │ │ └── finish.tsx │ │ │ ├── flash-another │ │ │ │ └── flash-another.tsx │ │ │ ├── flash-results │ │ │ │ └── flash-results.tsx │ │ │ ├── progress-button │ │ │ │ └── progress-button.tsx │ │ │ ├── reduced-flashing-infos │ │ │ │ └── reduced-flashing-infos.tsx │ │ │ ├── safe-webview │ │ │ │ └── safe-webview.tsx │ │ │ ├── settings │ │ │ │ └── settings.tsx │ │ │ ├── source-selector │ │ │ │ └── source-selector.tsx │ │ │ ├── svg-icon │ │ │ │ └── svg-icon.tsx │ │ │ └── target-selector │ │ │ │ ├── target-selector-button.tsx │ │ │ │ └── target-selector.tsx │ │ ├── css │ │ │ ├── fonts │ │ │ │ ├── SourceSansPro-Regular.ttf │ │ │ │ └── SourceSansPro-SemiBold.ttf │ │ │ └── main.css │ │ ├── i18n.ts │ │ ├── i18n │ │ │ ├── README.md │ │ │ ├── en.ts │ │ │ ├── zh-CN.ts │ │ │ └── zh-TW.ts │ │ ├── index.html │ │ ├── models │ │ │ ├── available-drives.ts │ │ │ ├── flash-state.ts │ │ │ ├── leds.ts │ │ │ ├── selection-state.ts │ │ │ ├── settings.ts │ │ │ └── store.ts │ │ ├── modules │ │ │ ├── analytics.ts │ │ │ ├── api.ts │ │ │ ├── exception-reporter.ts │ │ │ ├── image-writer.ts │ │ │ └── progress-status.ts │ │ ├── os │ │ │ ├── dialog.ts │ │ │ ├── notification.ts │ │ │ ├── open-external │ │ │ │ └── services │ │ │ │ │ └── open-external.ts │ │ │ ├── window-progress.ts │ │ │ └── windows-network-drives.ts │ │ ├── pages │ │ │ └── main │ │ │ │ ├── Flash.tsx │ │ │ │ └── MainPage.tsx │ │ ├── preload.ts │ │ ├── renderer.ts │ │ ├── styled-components.tsx │ │ ├── theme.ts │ │ └── utils │ │ │ ├── etcher-pro-specific.ts │ │ │ └── middle-ellipsis.ts │ ├── assets │ │ ├── balena.svg │ │ ├── drive.svg │ │ ├── etcher.svg │ │ ├── flash.svg │ │ ├── image.svg │ │ ├── love.svg │ │ ├── raspberrypi.svg │ │ ├── src.svg │ │ └── tgt.svg │ ├── etcher.ts │ ├── menu.ts │ └── webapi.ts ├── shared │ ├── drive-constraints.ts │ ├── errors.ts │ ├── exit-codes.ts │ ├── messages.ts │ ├── permissions.ts │ ├── sudo │ │ ├── darwin.ts │ │ ├── linux.ts │ │ ├── sudo-askpass.osascript-en.js │ │ ├── sudo-askpass.osascript-zh.js │ │ └── windows.ts │ ├── supported-formats.ts │ ├── typings │ │ └── source-selector.ts │ ├── units.ts │ └── utils.ts └── util │ ├── api.ts │ ├── child-writer.ts │ ├── drive-scanner.ts │ ├── scanner.ts │ ├── source-metadata.ts │ └── types │ └── types.d.ts ├── npm-shrinkwrap.json ├── package.json ├── pkg-sidecar.json ├── repo.yml ├── tests ├── .eslintrc.yml ├── data │ └── wmic-output.txt ├── gui │ ├── allow-renderer-process-reuse.ts │ ├── models │ │ ├── available-drives.spec.ts │ │ ├── flash-state.spec.ts │ │ ├── selection-state.spec.ts │ │ └── settings.spec.ts │ ├── modules │ │ ├── image-writer.spec.ts │ │ └── progress-status.spec.ts │ ├── os │ │ ├── window-progress.spec.ts │ │ └── windows-network-drives.spec.ts │ ├── utils │ │ └── middle-ellipsis.spec.ts │ └── window-config.json ├── shared │ ├── drive-constraints.spec.ts │ ├── errors.spec.ts │ ├── messages.spec.ts │ ├── permissions.spec.ts │ ├── supported-formats.spec.ts │ ├── units.spec.ts │ └── utils.spec.ts └── test.e2e.ts ├── tsconfig.json ├── tsconfig.sidecar.json ├── typings ├── omit-deep-lodash │ └── index.d.ts ├── path-is-inside │ └── index.d.ts ├── sudo-prompt │ └── index.d.ts └── svg │ └── index.d.ts ├── wdio.conf.ts └── webpack.config.ts /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !requirements.txt 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [*.ts] 18 | indent_style = tab 19 | 20 | [*.tsx] 21 | indent_style = tab 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["./node_modules/@balena/lint/config/.eslintrc.js"], 3 | root: true, 4 | ignorePatterns: ["node_modules/"], 5 | rules: { 6 | "@typescript-eslint/no-floating-promises": "off", 7 | "@typescript-eslint/no-var-requires": "off", 8 | "@typescript-eslint/ban-ts-comment": "off", 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # default 2 | * text 3 | 4 | # Javascript files must retain LF line-endings (to keep eslint happy) 5 | *.js text eol=lf 6 | *.jsx text eol=lf 7 | *.ts text eol=lf 8 | *.tsx text eol=lf 9 | # CSS and SCSS files must retain LF line-endings (to keep ensure-staged-sass.sh happy) 10 | *.css text eol=lf 11 | *.scss text eol=lf 12 | 13 | # Text files 14 | Dockerfile* text 15 | .dockerignore text 16 | .editorconfig text 17 | etcher text 18 | .git* text 19 | *.html text 20 | *.json text eol=lf 21 | *.cpp text 22 | *.h text 23 | *.gyp text 24 | LICENSE text 25 | Makefile text 26 | *.md text 27 | *.sh text 28 | *.bat text 29 | *.svg text 30 | *.yml text 31 | *.patch text 32 | *.txt text 33 | *.tpl text 34 | CODEOWNERS text 35 | *.plist text 36 | 37 | # Binary files (no line-ending conversions) 38 | *.bz2 binary diff=hex 39 | *.gz binary diff=hex 40 | *.icns binary diff=hex 41 | *.ico binary diff=hex 42 | *.tiff binary diff=hex 43 | *.img binary diff=hex 44 | *.iso binary diff=hex 45 | *.png binary diff=hex 46 | *.bin binary diff=hex 47 | *.elf binary diff=hex 48 | *.xz binary diff=hex 49 | *.zip binary diff=hex 50 | *.dtb binary diff=hex 51 | *.dtbo binary diff=hex 52 | *.dat binary diff=hex 53 | *.bin binary diff=hex 54 | *.dmg binary diff=hex 55 | *.rpi-sdcard binary diff=hex 56 | *.wic binary diff=hex 57 | *.foo binary diff=hex 58 | *.eot binary diff=hex 59 | *.otf binary diff=hex 60 | *.woff binary diff=hex 61 | *.woff2 binary diff=hex 62 | *.ttf binary diff=hex 63 | xz-without-extension binary diff=hex 64 | wmic-output.txt binary diff=hex 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - **Etcher version:** 2 | - **Operating system and architecture:** 3 | - **Image flashed:** 4 | - **What do you think should have happened:** 5 | - **What happened:** 6 | - **Do you see any meaningful error information in the DevTools?** 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/actions/test/action.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test release 3 | # https://github.com/product-os/flowzone/tree/master/.github/actions 4 | inputs: 5 | json: 6 | description: 'JSON stringified object containing all the inputs from the calling workflow' 7 | required: true 8 | secrets: 9 | description: 'JSON stringified object containing all the secrets from the calling workflow' 10 | required: true 11 | 12 | # --- custom environment 13 | NODE_VERSION: 14 | type: string 15 | default: '20.19' 16 | VERBOSE: 17 | type: string 18 | default: 'true' 19 | 20 | runs: 21 | # https://docs.github.com/en/actions/creating-actions/creating-a-composite-action 22 | using: 'composite' 23 | steps: 24 | # https://github.com/actions/setup-node#caching-global-packages-data 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v3 27 | with: 28 | node-version: ${{ inputs.NODE_VERSION }} 29 | cache: npm 30 | 31 | - name: Install host dependencies 32 | if: runner.os == 'Linux' 33 | shell: bash 34 | run: | 35 | sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb libudev-dev 36 | cat < package.json | jq -r '.hostDependencies[][]' - | \ 37 | xargs -L1 echo | sed 's/|//g' | xargs -L1 \ 38 | sudo apt-get --ignore-missing install || true 39 | 40 | - name: Install host dependencies 41 | if: runner.os == 'macOS' 42 | # FIXME: Python 3.12 dropped distutils that node-gyp depends upon. 43 | # This is a temporary workaround to make the job use Python 3.11 until 44 | # we update to npm 10+. 45 | uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4 46 | with: 47 | python-version: '3.11' 48 | 49 | - name: Test release 50 | shell: bash 51 | run: | 52 | ## FIXME: causes issues with `xxhash` which tries to load a debug build which doens't exist and cannot be compiled 53 | # if [[ '${{ inputs.VERBOSE }}' =~ on|On|Yes|yes|true|True ]]; then 54 | # export DEBUG='electron-forge:*,sidecar' 55 | # fi 56 | 57 | npm ci 58 | 59 | # as the shrinkwrap might have been done on mac/linux, this is ensure the package is there for windows 60 | if [[ "$RUNNER_OS" == "Windows" ]]; then 61 | npm i -D winusb-driver-generator 62 | fi 63 | 64 | npm run lint 65 | npm run package 66 | npm run wdio # test stage, note that it requires the package to be done first 67 | 68 | env: 69 | # https://www.electronjs.org/docs/latest/api/environment-variables 70 | ELECTRON_NO_ATTACH_CONSOLE: 'true' 71 | 72 | - name: Compress custom source 73 | if: runner.os != 'Windows' 74 | shell: bash 75 | run: tar -acf ${{ runner.temp }}/custom.tgz . 76 | 77 | - name: Compress custom source 78 | if: runner.os == 'Windows' 79 | shell: pwsh 80 | run: C:\"Program Files"\Git\usr\bin\tar.exe --force-local -acf ${{ runner.temp }}\custom.tgz . 81 | 82 | - name: Upload custom artifact 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: custom-${{ github.event.pull_request.head.sha || github.event.head_commit.id }}-${{ runner.os }}-${{ runner.arch }} 86 | path: ${{ runner.temp }}/custom.tgz 87 | retention-days: 1 88 | -------------------------------------------------------------------------------- /.github/workflows/flowzone.yml: -------------------------------------------------------------------------------- 1 | name: Flowzone 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, closed] 5 | branches: [main, master] 6 | # allow external contributions to use secrets within trusted code 7 | pull_request_target: 8 | types: [opened, synchronize, closed] 9 | branches: [main, master] 10 | jobs: 11 | flowzone: 12 | name: Flowzone 13 | uses: product-os/flowzone/.github/workflows/flowzone.yml@master 14 | # prevent duplicate workflows and only allow one `pull_request` or `pull_request_target` for 15 | # internal or external contributions respectively 16 | if: | 17 | (github.event.pull_request.head.repo.full_name == github.repository && github.event_name == 'pull_request') || 18 | (github.event.pull_request.head.repo.full_name != github.repository && github.event_name == 'pull_request_target') 19 | secrets: inherit 20 | with: 21 | custom_test_matrix: > 22 | { 23 | "os": [ 24 | ["ubuntu-22.04"], 25 | ["windows-2019"], 26 | ["macos-13"], 27 | ["macos-latest-xlarge"] 28 | ] 29 | } 30 | custom_publish_matrix: > 31 | { 32 | "os": [ 33 | ["ubuntu-22.04"], 34 | ["windows-2019"], 35 | ["macos-13"], 36 | ["macos-latest-xlarge"] 37 | ] 38 | } 39 | restrict_custom_actions: false 40 | github_prerelease: true 41 | cloudflare_website: "etcher" 42 | -------------------------------------------------------------------------------- /.github/workflows/winget.yml: -------------------------------------------------------------------------------- 1 | name: Publish to WinGet 2 | on: 3 | release: 4 | types: [released] 5 | jobs: 6 | publish: 7 | runs-on: windows-latest # action can only be run on windows 8 | steps: 9 | - uses: vedantmgoyal2009/winget-releaser@v2 10 | with: 11 | identifier: Balena.Etcher 12 | # matches something like "balenaEtcher-1.19.0.Setup.exe" 13 | installers-regex: 'balenaEtcher-[\d.-]+\.Setup.exe$' 14 | token: ${{ secrets.WINGET_PAT }} 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # -- ADD NEW ENTRIES AT THE END OF THE FILE --- 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | .DS_Store 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # TypeScript cache 46 | *.tsbuildinfo 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | .env.test 66 | 67 | # parcel-bundler cache (https://parceljs.org/) 68 | .cache 69 | 70 | # next.js build output 71 | .next 72 | 73 | # nuxt.js build output 74 | .nuxt 75 | 76 | # vuepress build output 77 | .vuepress/dist 78 | 79 | # Serverless directories 80 | .serverless/ 81 | 82 | # FuseBox cache 83 | .fusebox/ 84 | 85 | # DynamoDB Local files 86 | .dynamodb/ 87 | 88 | # Webpack 89 | .webpack/ 90 | 91 | # Vite 92 | .vite/ 93 | 94 | # Electron-Forge 95 | out/ 96 | 97 | # ---- Do not modify entries above this line ---- 98 | 99 | # Build artifacts 100 | dist/ 101 | 102 | # Certificates 103 | *.spc 104 | *.pvk 105 | *.p12 106 | *.cer 107 | *.crt 108 | *.pem 109 | 110 | # Secrets 111 | .gitsecret/keys/random_seed 112 | !*.secret 113 | secrets/APPLE_SIGNING_PASSWORD.txt 114 | secrets/WINDOWS_SIGNING_PASSWORD.txt 115 | secrets/XCODE_APP_LOADER_PASSWORD.txt 116 | secrets/WINDOWS_SIGNING.pfx 117 | 118 | # Image stream output directory 119 | /tests/image-stream/output 120 | 121 | #local development 122 | .yalc 123 | yalc.lock -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | 4 | module.exports = JSON.parse( 5 | fs.readFileSync(path.join(__dirname, "node_modules", "@balena", "lint", "config", ".prettierrc"), "utf8"), 6 | ); 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Etcher 2 | 3 | > Flash OS images to SD cards & USB drives, safely and easily. 4 | 5 | Etcher is a powerful OS image flasher built with web technologies to ensure 6 | flashing an SDCard or USB drive is a pleasant and safe experience. It protects 7 | you from accidentally writing to your hard-drives, ensures every byte of data 8 | 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). 9 | 10 | [![Current Release](https://img.shields.io/github/release/balena-io/etcher.svg?style=flat-square)](https://balena.io/etcher) 11 | [![License](https://img.shields.io/github/license/balena-io/etcher.svg?style=flat-square)](https://github.com/balena-io/etcher/blob/master/LICENSE) 12 | [![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) 13 | 14 | --- 15 | 16 | [**Download**][etcher] | [**Support**][support] | [**Documentation**][user-documentation] | [**Contributing**][contributing] | [**Roadmap**][milestones] 17 | 18 | ## Supported Operating Systems 19 | 20 | - Linux; most distros; Intel 64-bit. 21 | - Windows 10 and later; Intel 64-bit. 22 | - macOS 10.13 (High Sierra) and later; both Intel and Apple Silicon. 23 | 24 | ## Installers 25 | 26 | Refer to the [downloads page][etcher] for the latest pre-made 27 | installers for all supported operating systems. 28 | 29 | ## Packages 30 | 31 | #### Debian and Ubuntu based Package Repository (GNU/Linux x86/x64) 32 | 33 | Package for Debian and Ubuntu can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/) 34 | 35 | ##### Install .deb file using apt 36 | 37 | ```sh 38 | sudo apt install ./balena-etcher_******_amd64.deb 39 | ``` 40 | 41 | ##### Uninstall 42 | 43 | ```sh 44 | sudo apt remove balena-etcher 45 | ``` 46 | 47 | #### Redhat (RHEL) and Fedora-based Package Repository (GNU/Linux x86/x64) 48 | 49 | ##### Yum 50 | 51 | Package for Fedora-based and Redhat can be downloaded from the [Github release page](https://github.com/balena-io/etcher/releases/) 52 | 53 | 1. Install using yum 54 | 55 | ```sh 56 | sudo yum localinstall balena-etcher-***.x86_64.rpm 57 | ``` 58 | 59 | #### Arch/Manjaro Linux (GNU/Linux x64) 60 | 61 | 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: 62 | 63 | ```sh 64 | yay -S balena-etcher 65 | ``` 66 | 67 | ##### Uninstall 68 | 69 | ```sh 70 | yay -R balena-etcher 71 | ``` 72 | 73 | #### WinGet (Windows) 74 | 75 | This package is updated by [gh-action](https://github.com/vedantmgoyal2009/winget-releaser), and is kept up to date automatically. 76 | 77 | ```sh 78 | winget install balenaEtcher #or Balena.Etcher 79 | ``` 80 | 81 | ##### Uninstall 82 | 83 | ```sh 84 | winget uninstall balenaEtcher 85 | ``` 86 | 87 | #### Chocolatey (Windows) 88 | 89 | This package is maintained by [@majkinetor](https://github.com/majkinetor), and 90 | is kept up to date automatically. 91 | 92 | ```sh 93 | choco install etcher 94 | ``` 95 | 96 | ##### Uninstall 97 | 98 | ```sh 99 | choco uninstall etcher 100 | ``` 101 | 102 | ## Support 103 | 104 | If you're having any problem, please [raise an issue][newissue] on GitHub, and 105 | the balena.io team will be happy to help. 106 | 107 | ## License 108 | 109 | Etcher is free software and may be redistributed under the terms specified in 110 | the [license]. 111 | 112 | [etcher]: https://balena.io/etcher 113 | [electron]: https://electronjs.org/ 114 | [electron-supported-platforms]: https://electronjs.org/docs/tutorial/support#supported-platforms 115 | [support]: https://github.com/balena-io/etcher/blob/master/docs/SUPPORT.md 116 | [contributing]: https://github.com/balena-io/etcher/blob/master/docs/CONTRIBUTING.md 117 | [user-documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md 118 | [milestones]: https://github.com/balena-io/etcher/milestones 119 | [newissue]: https://github.com/balena-io/etcher/issues/new 120 | [license]: https://github.com/balena-io/etcher/blob/master/LICENSE 121 | -------------------------------------------------------------------------------- /after-install.tpl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Link to the binary 4 | # Must hardcode balenaEtcher directory; no variable available 5 | ln -sf '/opt/balenaEtcher/${executable}' '/usr/bin/${executable}' 6 | 7 | # SUID chrome-sandbox for Electron 5+ 8 | chmod 4755 '/opt/balenaEtcher/chrome-sandbox' || true 9 | 10 | update-mime-database /usr/share/mime || true 11 | update-desktop-database /usr/share/applications || true 12 | -------------------------------------------------------------------------------- /assets/dmg/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/dmg/background.png -------------------------------------------------------------------------------- /assets/dmg/background.tiff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/dmg/background.tiff -------------------------------------------------------------------------------- /assets/dmg/background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/dmg/background@2x.png -------------------------------------------------------------------------------- /assets/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/icon.icns -------------------------------------------------------------------------------- /assets/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/icon.ico -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/icon.png -------------------------------------------------------------------------------- /assets/iconset/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/iconset/128x128.png -------------------------------------------------------------------------------- /assets/iconset/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/iconset/16x16.png -------------------------------------------------------------------------------- /assets/iconset/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/iconset/256x256.png -------------------------------------------------------------------------------- /assets/iconset/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/iconset/32x32.png -------------------------------------------------------------------------------- /assets/iconset/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/iconset/48x48.png -------------------------------------------------------------------------------- /assets/iconset/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/assets/iconset/512x512.png -------------------------------------------------------------------------------- /docs/ARCHITECTURE.md: -------------------------------------------------------------------------------- 1 | Etcher Architecture 2 | =================== 3 | 4 | This document aims to serve as a high-level overview of how Etcher works, 5 | specially oriented for contributors who want to understand the big picture. 6 | 7 | Technologies 8 | ------------ 9 | 10 | This is a non exhaustive list of the major frameworks, libraries, and other 11 | technologies used in Etcher that you should become familiar with: 12 | 13 | - [Electron][electron] 14 | - [NodeJS][nodejs] 15 | - [Redux][redux] 16 | - [ImmutableJS][immutablejs] 17 | - [Sass][sass] 18 | - [Mocha][mocha] 19 | - [JSDoc][jsdoc] 20 | 21 | Module architecture 22 | ------------------- 23 | 24 | Instead of embedding all the functionality required to create a full-featured 25 | image writer as a monolithic project, we try to hard to follow the ["lego block 26 | approach"][lego-blocks]. 27 | 28 | This has the advantage of allowing other applications to re-use logic we 29 | implemented for Etcher in their own project, even for things we didn't expect, 30 | which leads to users benefitting from what we've built, and we benefitting from 31 | user's bug reports, suggestions, etc, as an indirect way to make Etcher better. 32 | 33 | The fact that low-level details are scattered around many different modules can 34 | make it challenging for a new contributor to wrap their heads around the 35 | project as a whole, and get a clear high level view of how things work or where 36 | to submit their work or bug reports. 37 | 38 | These are the main Etcher components, in a nutshell: 39 | 40 | - [Drivelist](https://github.com/balena-io-modules/drivelist) 41 | 42 | As the name implies, this module's duty is to detect the connected drives 43 | uniformly in all major operating systems, along with valuable metadata, like if 44 | a drive is removable or not, to prevent users from trying to write an image to 45 | a system drive. 46 | 47 | - [Etcher](https://github.com/balena-io/etcher) 48 | 49 | This is the *"main repository"*, from which you're reading this from, which is 50 | basically the front-end and glue for all previously listed projects. 51 | 52 | Summary 53 | ------- 54 | 55 | We always welcome contributions to Etcher as well as our documentation. If you 56 | want to give back, but feel that your knowledge on how Etcher works is not 57 | enough to tackle a bug report or feature request, use that as your advantage, 58 | since fresh eyes could help unveil things that we take for granted, but should 59 | be documented instead! 60 | 61 | [lego-blocks]: https://github.com/sindresorhus/ama/issues/10#issuecomment-117766328 62 | [exit-codes]: https://github.com/balena-io/etcher/blob/master/lib/shared/exit-codes.js 63 | [gui-dir]: https://github.com/balena-io/etcher/tree/master/lib/gui 64 | [electron]: http://electron.atom.io 65 | [nodejs]: https://nodejs.org 66 | [redux]: http://redux.js.org 67 | [immutablejs]: http://facebook.github.io/immutable-js/ 68 | [sass]: http://sass-lang.com 69 | [mocha]: http://mochajs.org 70 | [jsdoc]: http://usejsdoc.org 71 | -------------------------------------------------------------------------------- /docs/COMMIT-GUIDELINES.md: -------------------------------------------------------------------------------- 1 | Commit Guidelines 2 | ================= 3 | 4 | We enforce certain rules on commits with the following goals in mind: 5 | 6 | - Be able to reliably auto-generate the `CHANGELOG.md` *without* any human 7 | intervention. 8 | - Be able to automatically and correctly increment the semver version number 9 | based on what was done since the last release. 10 | - Be able to get a quick overview of what happened to the project by glancing 11 | over the commit history. 12 | - Be able to automatically reference relevant changes from a dependency 13 | upgrade. 14 | 15 | 16 | Commit structure 17 | ---------------- 18 | 19 | Each commit message needs to specify the semver-type. Which can be `patch|minor|major`. 20 | See the [Semantic Versioning][semver] specification for a more detailed explanation of the meaning of these types. 21 | See balena commit guidelines for more info about the whole commit structure. 22 | 23 | ``` 24 | : 25 | ``` 26 | or 27 | ``` 28 | 29 | 30 |
31 | 32 | Change-Type: 33 | ``` 34 | 35 | The subject should not contain more than 70 characters, including the type and 36 | scope, and the body should be wrapped at 72 characters. 37 | 38 | Tags 39 | ---- 40 | 41 | ### `See: `/`Link: ` 42 | 43 | This tag can be used to reference a resource that is relevant to the commit, 44 | and can be repeated multiple times in the same commit. 45 | 46 | Resource examples include: 47 | 48 | - A link to pull requests. 49 | - A link to a GitHub issue. 50 | - A link to a website providing useful information. 51 | - A commit hash. 52 | 53 | Its recommended that you avoid relative URLs, and that you include the whole 54 | commit hash to avoid any potential ambiguity issues in the future. 55 | 56 | If the commit type equals `upgrade`, this tag should be present, and should 57 | link to the CHANGELOG section of the dependency describing the changes 58 | introduced from the previously used version. 59 | 60 | Examples: 61 | 62 | ``` 63 | See: https://github.com/xxx/yyy/ 64 | See: 49d89b4acebd80838303b011d30517cd6229fdbe 65 | Link: https://github.com/xxx/yyy/issues/zzz 66 | ``` 67 | 68 | ### `Closes: `/`Fixes: ` 69 | 70 | This tag is used to make GitHub close the referenced issue automatically when 71 | the commit is merged. 72 | 73 | Its recommended that you provide the absolute URL to the GitHub issue rather 74 | than simply writing the ID prefixed by a hash tag for convenience when browsing 75 | the commit history outside the GitHub web interface. 76 | 77 | A commit can include multiple instances of this tag. 78 | 79 | Examples: 80 | 81 | ``` 82 | Closes: https://github.com/balena-io/etcher/issues/XXX 83 | Fixes: https://github.com/balena-io/etcher/issues/XXX 84 | ``` 85 | 86 | [semver]: http://semver.org 87 | -------------------------------------------------------------------------------- /docs/FAQ.md: -------------------------------------------------------------------------------- 1 | ## Why is my drive not bootable? 2 | 3 | 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). 4 | 5 | ## How can I configure persistent storage? 6 | 7 | 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/). 8 | 9 | ## How do I flash Ubuntu ISOs 10 | 11 | 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. 12 | 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: 13 | 14 | > /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. 15 | 16 | > Warning: The driver descriptor says the physical block size is 2048 bytes, but Linux says it is 512 bytes. 17 | 18 | All these warnings are safe to ignore, and your drive should be able to boot without any problems. 19 | 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. 20 | 21 | ## How do I run Etcher on Wayland? 22 | 23 | The XWayland Server provides backwards compatibility to run any X client on Wayland, including Etcher. 24 | 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): 25 | 26 | ``` 27 | [core] 28 | modules=xwayland.so 29 | ``` 30 | 31 | ## What are the runtime GNU/LINUX dependencies? 32 | 33 | [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. 34 | 35 | ## How can I recover the broken drive? 36 | 37 | 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. 38 | 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. 39 | 40 | ## I receive "No polkit authentication agent found" error in GNU/Linux 41 | 42 | 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. 43 | 44 | ## May I run Etcher in older macOS versions? 45 | 46 | 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). 47 | 48 | ## Can I use the Flash With Etcher button on my site? 49 | 50 | 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: 51 | 52 | `` -------------------------------------------------------------------------------- /docs/MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Maintaining Etcher 2 | 3 | This document is meant to serve as a guide for maintainers to perform common tasks. 4 | 5 | ## Releasing 6 | 7 | ### Release Types 8 | 9 | - **draft**: A continues snapshot of current master, made by the CI services 10 | - **pre-release** (default): A continues snapshot of current master, made by the CI services 11 | - **release**: Full releases 12 | 13 | Draft release is created from each PR, tagged with the branch name. 14 | All merged PR will generate a new tag/version as a _pre-release_. 15 | Mark the pre-release as final when it is necessary, then distribute the packages in alternative channels as necessary. 16 | 17 | #### Preparation 18 | 19 | - [Prepare the new version](#preparing-a-new-version) 20 | - [Generate build artifacts](#generating-binaries) (binaries, archives, etc.) 21 | - [Draft a release on GitHub](https://github.com/balena-io/etcher/releases) 22 | - Upload build artifacts to GitHub release draft 23 | 24 | #### Testing 25 | 26 | - Test the prepared release and build artifacts properly on **all supported operating systems** to prevent regressions that went uncaught by the CI tests (see [MANUAL-TESTING.md](MANUAL-TESTING.md)) 27 | - If regressions or other issues arise, create issues on the repository for each one, and decide whether to fix them in this release (meaning repeating the process up until this point), or to follow up with a patch release 28 | 29 | #### Publishing 30 | 31 | - [Publish release draft on GitHub](https://github.com/balena-io/etcher/releases) 32 | - [Post release note to forums](https://forums.balena.io/c/etcher) 33 | - [Submit Windows binaries to Symantec for whitelisting](#submitting-binaries-to-symantec) 34 | - [Update the website](https://github.com/balena-io/etcher-homepage) 35 | - Wait 2-3 hours for analytics (Sentry) to trickle in and check for elevated error rates, or regressions 36 | - If regressions arise; pull the release, and release a patched version, else: 37 | - [Upload deb & rpm packages to Cloudfront](#uploading-packages-to-cloudfront) 38 | - Post changelog with `#release-notes` tag on internal chat 39 | - If this release packs noteworthy major changes: 40 | - Write a blog post about it, and / or 41 | - Write about it to the Etcher mailing list 42 | 43 | ### Generating binaries 44 | 45 | **Environment** 46 | 47 | Make sure to set the analytics tokens when generating production release binaries: 48 | 49 | ```bash 50 | export ANALYTICS_SENTRY_TOKEN="xxxxxx" 51 | ``` 52 | 53 | #### Linux 54 | 55 | ##### Clean dist folder 56 | 57 | Delete `.webpack` and `out/`. 58 | 59 | ##### Generating artifacts 60 | 61 | The artifacts are generated by the CI and published as draft-release or pre-release. 62 | Etcher is built with electron-forge. Run: 63 | 64 | ``` 65 | npm run make 66 | ``` 67 | 68 | Our CI will appropriately sign artifacts for macOS and some Windows targets. 69 | 70 | ### Uploading packages to Cloudfront 71 | 72 | Log in to cloudfront and upload the `rpm` and `deb` files. 73 | 74 | ### Dealing with a Problematic Release 75 | 76 | There can be times where a release is accidentally plagued with bugs. If you 77 | released a new version and notice the error rates are higher than normal, then 78 | revert the problematic release as soon as possible, until the bugs are fixed. 79 | 80 | You can revert a version by deleting its builds from the S3 bucket and Bintray. 81 | Refer to the `Makefile` for the up to date information about the S3 bucket 82 | where we push builds to, and get in touch with the balena.io operations team to 83 | get write access to it. 84 | 85 | The Etcher update notifier dialog and the website only show the a certain 86 | version if all the expected files have been uploaded to it, so deleting a 87 | single package or two is enough to bring down the whole version. 88 | 89 | Use the following command to delete files from S3: 90 | 91 | ```bash 92 | aws s3api delete-object --bucket --key 93 | ``` 94 | 95 | The Bintray dashboard provides an easy way to delete a version's files. 96 | 97 | ### Submitting binaries to Symantec 98 | 99 | - [Report a Suspected Erroneous Detection](https://submit.symantec.com/false_positive/standard/) 100 | - Fill out form: 101 | - **Select Submission Type:** "Provide a direct download URL" 102 | - **Name of the software being detected:** Etcher 103 | - **Name of detection given by Symantec product:** WS.Reputation.1 104 | - **Contact name:** Balena.io Ltd 105 | - **E-mail address:** hello@etcher.io 106 | - **Are you the creator or distributor of the software in question?** Yes 107 | -------------------------------------------------------------------------------- /docs/MANUAL-TESTING.md: -------------------------------------------------------------------------------- 1 | # Manual Testing 2 | 3 | This document describes a high-level script of manual tests to check for. We 4 | should aim to replace items on this list with automated Spectron test cases. 5 | 6 | ## Image Selection 7 | 8 | - [ ] Cancel image selection dialog 9 | - [ ] Select an unbootable image (without a partition table), and expect a 10 | sensible warning 11 | - [ ] Attempt to select a ZIP archive with more than one image 12 | - [ ] Attempt to select a tar archive (with any compression method) 13 | - [ ] Change image selection 14 | - [ ] Select a Windows image, and expect a sensible warning 15 | 16 | ## Drive Selection 17 | 18 | - [ ] Open the drive selection modal 19 | - [ ] Switch drive selection 20 | - [ ] Insert a single drive, and expect auto-selection 21 | - [ ] Insert more than one drive, and don't expect auto-selection 22 | - [ ] Insert a locked SD Card and expect a warning 23 | - [ ] Insert a too small drive and expect a warning 24 | - [ ] Put an image into a drive and attempt to flash the image to the drive 25 | that contains it 26 | - [ ] Attempt to flash a compressed image (for which we can get the 27 | uncompressed size) into a drive that is big enough to hold the compressed 28 | image, but not big enough to hold the uncompressed version 29 | - [ ] Enable "Unsafe Mode" and attempt to select a system drive 30 | - [ ] Enable "Unsafe Mode", and if there is only one system drive (and no 31 | removable ones), don't expect autoselection 32 | 33 | ## Image Support 34 | 35 | Run the following tests with and without validation enabled: 36 | 37 | - [ ] Flash an uncompressed image 38 | - [ ] Flash a Bzip2 image 39 | - [ ] Flash a XZ image 40 | - [ ] Flash a ZIP image 41 | - [ ] Flash a GZ image 42 | - [ ] Flash a DMG image 43 | - [ ] Flash an image whose size is not a multiple of 512 bytes 44 | - [ ] Flash a compressed image whose size is not a multiple of 512 bytes 45 | - [ ] Flash an archive whose image size is not a multiple of 512 bytes 46 | - [ ] Flash an archive image containing a logo 47 | - [ ] Flash an archive image containing a blockmap file 48 | - [ ] Flash an archive image containing a manifest metadata file 49 | 50 | ## Flashing Process 51 | 52 | - [ ] Unplug the drive during flash or validation 53 | - [ ] Click "Flash", cancel elevation dialog, and click "Flash" again 54 | - [ ] Start flashing an image, try to close Etcher, cancel the application 55 | close warning dialog, and check that Etcher continues to flash the image 56 | 57 | ### Child Writer 58 | 59 | - [ ] Kill the child writer process (i.e. with `SIGINT` or `SIGKILL`), and 60 | check that the UI reacts appropriately 61 | - [ ] Close the application while flashing using the window manager close icon 62 | - [ ] Close the application while flashing using the OS keyboard shortcut 63 | - [ ] Close the application from the terminal using Ctrl-C while flashing 64 | - [ ] Force kill the application (using a process monitor tool, etc) 65 | 66 | In all these cases, the child writer process should not remain alive. Note that 67 | in some systems you need to open your process monitor tool of choice with extra 68 | permissions to see the elevated child writer process. 69 | 70 | ## GUI 71 | 72 | - [ ] Close application from the terminal using Ctrl-C while the application is 73 | idle 74 | - [ ] Click footer links that take you to an external website 75 | - [ ] Attempt to change image or drive selection while flashing 76 | - [ ] Go to the settings page while flashing and come back 77 | - [ ] Flash consecutive images without closing the application 78 | - [ ] Remove the selected drive right before clicking "Flash" 79 | - [ ] Minimize the application 80 | - [ ] Start the application given no internet connection 81 | 82 | ## Success Banner 83 | 84 | - [ ] Click an external link on the success banner (with and without internet 85 | connection) 86 | 87 | ## Elevation Prompt 88 | 89 | - [ ] Flash an image as `root`/administrator 90 | - [ ] Reject elevation prompt 91 | - [ ] Put incorrect elevation prompt password 92 | - [ ] Unplug the drive during elevation 93 | 94 | ## Unmounting 95 | 96 | - [ ] Disable unmounting and flash an image 97 | - [ ] Flash an image with a file system that is readable by the host OS, and 98 | check that is unmounted correctly 99 | -------------------------------------------------------------------------------- /docs/PUBLISHING.md: -------------------------------------------------------------------------------- 1 | Publishing Etcher 2 | ================= 3 | 4 | This is a small guide to package and publish Etcher to all supported operating 5 | systems. 6 | 7 | Release Types 8 | ------------- 9 | 10 | Etcher supports **pre-release** and **final** release types as does Github. Each is 11 | published to Github releases. 12 | The release version is generated automatically from the commit messasges. 13 | 14 | Signing 15 | ------- 16 | 17 | ### OS X 18 | 19 | 1. Get our Apple Developer ID certificate for signing applications distributed 20 | outside the Mac App Store from the balena.io Apple account. 21 | 22 | 2. Install the Developer ID certificate to your Mac's Keychain by double 23 | clicking on the certificate file. 24 | 25 | The application will be signed automatically using this certificate when 26 | packaging for OS X. 27 | 28 | ### Windows 29 | 30 | 1. Get access to our code signing certificate and decryption key as a balena.io 31 | employee by asking for it from the relevant people. 32 | 33 | 2. Place the certificate in the root of the Etcher repository naming it 34 | `certificate.p12`. 35 | 36 | Packaging 37 | --------- 38 | 39 | Run the following command on each platform: 40 | 41 | ```sh 42 | npm run make 43 | ``` 44 | 45 | This will produce all targets (eg. zip, dmg) specified in forge.config.ts for the 46 | host platform and architecture. 47 | 48 | The resulting artifacts can be found in `out/make`. 49 | 50 | 51 | Publishing to Cloudfront 52 | --------------------- 53 | 54 | We publish GNU/Linux Debian packages to [Cloudfront][cloudfront]. 55 | 56 | Log in to cloudfront and upload the `rpm` and `deb` files. 57 | 58 | Publishing to Homebrew Cask 59 | --------------------------- 60 | 61 | 1. Update [`Casks/etcher.rb`][etcher-cask-file] with the new version and 62 | `sha256` 63 | 64 | 2. Send a PR with the changes above to 65 | [`caskroom/homebrew-cask`][homebrew-cask] 66 | 67 | Announcing 68 | ---------- 69 | 70 | Post messages to the [Etcher forum][balena-forum-etcher] announcing the new version 71 | of Etcher, and including the relevant section of the Changelog. 72 | 73 | [aws-cli]: https://aws.amazon.com/cli 74 | [cloudfront]: https://cloudfront.com 75 | [etcher-cask-file]: https://github.com/caskroom/homebrew-cask/blob/master/Casks/balenaetcher.rb 76 | [homebrew-cask]: https://github.com/caskroom/homebrew-cask 77 | [balena-forum-etcher]: https://forums.balena.io/c/etcher 78 | [github-releases]: https://github.com/balena-io/etcher/releases 79 | 80 | Updating EFP / Success-Banner 81 | ----------------------------- 82 | 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) 83 | -------------------------------------------------------------------------------- /docs/SUPPORT.md: -------------------------------------------------------------------------------- 1 | Getting help with BalenaEtcher 2 | =============================== 3 | 4 | There are various ways to get support for Etcher if you experience an issue or 5 | have an idea you'd like to share with us. 6 | 7 | Documentation 8 | ------ 9 | 10 | We have answers to a variety of frequently asked questions in the [user 11 | documentation][documentation] and also in the [FAQs][faq] on the Etcher website. 12 | 13 | 14 | Forums 15 | ------ 16 | 17 | We have a [Discourse forum][discourse] which is open to everyone, so please 18 | come join us :). Drop us a line there and the balena.io staff and community 19 | users will be happy to assist. Your question might already be answered, so take 20 | a look at the existing threads before opening a new one! 21 | 22 | Make sure to mention the following information to help us provide better 23 | support: 24 | 25 | - The BalenaEtcher version you're running. 26 | 27 | - The operating system you're running Etcher in. 28 | 29 | - Relevant logging output, if any, from DevTools, which you can open by 30 | pressing `Ctrl+Shift+I` or `Cmd+Alt+I` depending on your platform. 31 | 32 | GitHub 33 | ------ 34 | 35 | If you encounter an issue or have a suggestion, head on over to BalenaEtcher's [issue 36 | tracker][issues] and if there isn't a ticket covering it, [create 37 | one][new-issue]. 38 | 39 | [discourse]: https://forums.balena.io/c/etcher 40 | [issues]: https://github.com/balena-io/etcher/issues 41 | [new-issue]: https://github.com/balena-io/etcher/issues/new 42 | [documentation]: https://github.com/balena-io/etcher/blob/master/docs/USER-DOCUMENTATION.md 43 | [faq]: https://etcher.io 44 | -------------------------------------------------------------------------------- /entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | com.apple.security.device.usb 12 | 13 | com.apple.security.files.user-selected.read-only 14 | 15 | com.apple.security.network.client 16 | 17 | com.apple.security.cs.disable-library-validation 18 | 19 | com.apple.security.get-task-allow 20 | 21 | com.apple.security.cs.disable-executable-page-protection 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /forge.config.ts: -------------------------------------------------------------------------------- 1 | import type { ForgeConfig } from '@electron-forge/shared-types'; 2 | import { MakerSquirrel } from '@electron-forge/maker-squirrel'; 3 | import { MakerZIP } from '@electron-forge/maker-zip'; 4 | import { MakerDeb } from '@electron-forge/maker-deb'; 5 | import { MakerRpm } from '@electron-forge/maker-rpm'; 6 | import { MakerDMG } from '@electron-forge/maker-dmg'; 7 | import { MakerAppImage } from '@reforged/maker-appimage'; 8 | import { AutoUnpackNativesPlugin } from '@electron-forge/plugin-auto-unpack-natives'; 9 | import { WebpackPlugin } from '@electron-forge/plugin-webpack'; 10 | import { exec } from 'child_process'; 11 | 12 | import { mainConfig, rendererConfig } from './webpack.config'; 13 | import * as sidecar from './forge.sidecar'; 14 | 15 | import { hostDependencies, productDescription } from './package.json'; 16 | 17 | const osxSigningConfig: any = {}; 18 | let winSigningConfig: any = {}; 19 | 20 | if (process.env.NODE_ENV === 'production') { 21 | osxSigningConfig.osxNotarize = { 22 | tool: 'notarytool', 23 | appleId: process.env.XCODE_APP_LOADER_EMAIL, 24 | appleIdPassword: process.env.XCODE_APP_LOADER_PASSWORD, 25 | teamId: process.env.XCODE_APP_LOADER_TEAM_ID, 26 | }; 27 | 28 | winSigningConfig = { 29 | signWithParams: `-sha1 ${process.env.SM_CODE_SIGNING_CERT_SHA1_HASH} -tr ${process.env.TIMESTAMP_SERVER} -td sha256 -fd sha256 -d balena-etcher`, 30 | }; 31 | } 32 | 33 | const config: ForgeConfig = { 34 | packagerConfig: { 35 | asar: true, 36 | icon: './assets/icon', 37 | executableName: 38 | process.platform === 'linux' ? 'balena-etcher' : 'balenaEtcher', 39 | appBundleId: 'io.balena.etcher', 40 | appCategoryType: 'public.app-category.developer-tools', 41 | appCopyright: 'Copyright 2016-2023 Balena Ltd', 42 | darwinDarkModeSupport: true, 43 | protocols: [{ name: 'etcher', schemes: ['etcher'] }], 44 | extraResource: [ 45 | 'lib/shared/sudo/sudo-askpass.osascript-zh.js', 46 | 'lib/shared/sudo/sudo-askpass.osascript-en.js', 47 | ], 48 | osxSign: { 49 | optionsForFile: () => ({ 50 | entitlements: './entitlements.mac.plist', 51 | hardenedRuntime: true, 52 | }), 53 | }, 54 | ...osxSigningConfig, 55 | }, 56 | rebuildConfig: { 57 | onlyModules: [], // prevent rebuilding *any* native modules as they won't be used by electron but by the sidecar 58 | }, 59 | makers: [ 60 | new MakerZIP(), 61 | new MakerSquirrel({ 62 | setupIcon: 'assets/icon.ico', 63 | loadingGif: 'assets/icon.png', 64 | ...winSigningConfig, 65 | }), 66 | new MakerDMG({ 67 | background: './assets/dmg/background.tiff', 68 | icon: './assets/icon.icns', 69 | iconSize: 110, 70 | contents: ((opts: { appPath: string }) => { 71 | return [ 72 | { x: 140, y: 250, type: 'file', path: opts.appPath }, 73 | { x: 415, y: 250, type: 'link', path: '/Applications' }, 74 | ]; 75 | }) as any, // type of MakerDMGConfig omits `appPath` 76 | additionalDMGOptions: { 77 | window: { 78 | size: { 79 | width: 540, 80 | height: 425, 81 | }, 82 | position: { 83 | x: 400, 84 | y: 500, 85 | }, 86 | }, 87 | }, 88 | }), 89 | new MakerAppImage({ 90 | options: { 91 | icon: './assets/icon.png', 92 | categories: ['Utility'], 93 | }, 94 | }), 95 | new MakerRpm({ 96 | options: { 97 | icon: './assets/icon.png', 98 | categories: ['Utility'], 99 | productDescription, 100 | requires: ['util-linux'], 101 | }, 102 | }), 103 | new MakerDeb({ 104 | options: { 105 | icon: './assets/icon.png', 106 | categories: ['Utility'], 107 | section: 'utils', 108 | priority: 'optional', 109 | productDescription, 110 | scripts: { 111 | postinst: './after-install.tpl', 112 | }, 113 | depends: hostDependencies['debian'], 114 | }, 115 | }), 116 | ], 117 | plugins: [ 118 | new AutoUnpackNativesPlugin({}), 119 | new WebpackPlugin({ 120 | mainConfig, 121 | renderer: { 122 | config: rendererConfig, 123 | nodeIntegration: true, 124 | entryPoints: [ 125 | { 126 | html: './lib/gui/app/index.html', 127 | js: './lib/gui/app/renderer.ts', 128 | name: 'main_window', 129 | preload: { 130 | js: './lib/gui/app/preload.ts', 131 | }, 132 | }, 133 | ], 134 | }, 135 | }), 136 | new sidecar.SidecarPlugin(), 137 | ], 138 | hooks: { 139 | postPackage: async (_forgeConfig, options) => { 140 | if (options.platform === 'linux') { 141 | // symlink the etcher binary from balena-etcher to balenaEtcher to ensure compatibility with the wdio suite and the old name 142 | await new Promise((resolve, reject) => { 143 | exec( 144 | `ln -s "${options.outputPaths}/balena-etcher" "${options.outputPaths}/balenaEtcher"`, 145 | (err) => { 146 | if (err) { 147 | reject(err); 148 | } else { 149 | resolve(); 150 | } 151 | }, 152 | ); 153 | }); 154 | } 155 | }, 156 | }, 157 | }; 158 | 159 | export default config; 160 | -------------------------------------------------------------------------------- /lib/gui/app/components/drive-status-warning-modal/drive-status-warning-modal.tsx: -------------------------------------------------------------------------------- 1 | import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; 2 | import * as React from 'react'; 3 | import type { ModalProps } from 'rendition'; 4 | import { Badge, Flex, Txt } from 'rendition'; 5 | import { Modal, ScrollableFlex } from '../../styled-components'; 6 | import { middleEllipsis } from '../../utils/middle-ellipsis'; 7 | 8 | import prettyBytes from 'pretty-bytes'; 9 | import type { DriveWithWarnings } from '../../pages/main/Flash'; 10 | import * as i18next from 'i18next'; 11 | 12 | const DriveStatusWarningModal = ({ 13 | done, 14 | cancel, 15 | isSystem, 16 | drivesWithWarnings, 17 | }: ModalProps & { 18 | isSystem: boolean; 19 | drivesWithWarnings: DriveWithWarnings[]; 20 | }) => { 21 | let warningSubtitle = i18next.t('drives.largeDriveWarning'); 22 | let warningCta = i18next.t('drives.largeDriveWarningMsg'); 23 | 24 | if (isSystem) { 25 | warningSubtitle = i18next.t('drives.systemDriveWarning'); 26 | warningCta = i18next.t('drives.systemDriveWarningMsg'); 27 | } 28 | return ( 29 | 45 | 51 | 52 | 53 | 54 | {i18next.t('warning')} 55 | 56 | 57 | {warningSubtitle} 58 | 66 | {drivesWithWarnings.map((drive, i, array) => ( 67 | <> 68 | 69 | {middleEllipsis(drive.description, 28)}{' '} 70 | {drive.size && prettyBytes(drive.size) + ' '} 71 | {drive.statuses[0].message} 72 | 73 | {i !== array.length - 1 ?
: null} 74 | 75 | ))} 76 |
77 | {warningCta} 78 |
79 |
80 | ); 81 | }; 82 | 83 | export default DriveStatusWarningModal; 84 | -------------------------------------------------------------------------------- /lib/gui/app/components/finish/finish.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import { Flex } from 'rendition'; 19 | import { v4 as uuidV4 } from 'uuid'; 20 | 21 | import * as flashState from '../../models/flash-state'; 22 | import * as selectionState from '../../models/selection-state'; 23 | import * as settings from '../../models/settings'; 24 | import { Actions, store } from '../../models/store'; 25 | import { FlashAnother } from '../flash-another/flash-another'; 26 | import type { FlashError } from '../flash-results/flash-results'; 27 | import { FlashResults } from '../flash-results/flash-results'; 28 | import { SafeWebview } from '../safe-webview/safe-webview'; 29 | 30 | function restart(goToMain: () => void) { 31 | selectionState.deselectAllDrives(); 32 | 33 | // Reset the flashing workflow uuid 34 | store.dispatch({ 35 | type: Actions.SET_FLASHING_WORKFLOW_UUID, 36 | data: uuidV4(), 37 | }); 38 | 39 | goToMain(); 40 | } 41 | 42 | async function getSuccessBannerURL() { 43 | return ( 44 | (await settings.get('successBannerURL')) ?? 45 | 'https://efp.balena.io/success-banner?borderTop=false&darkBackground=true' 46 | ); 47 | } 48 | 49 | function FinishPage({ goToMain }: { goToMain: () => void }) { 50 | const [webviewShowing, setWebviewShowing] = React.useState(false); 51 | const [successBannerURL, setSuccessBannerURL] = React.useState(''); 52 | (async () => { 53 | setSuccessBannerURL(await getSuccessBannerURL()); 54 | })(); 55 | const flashResults = flashState.getFlashResults(); 56 | const errors: FlashError[] = ( 57 | store.getState().toJS().failedDeviceErrors || [] 58 | ).map(([, error]: [string, FlashError]) => ({ 59 | ...error, 60 | })); 61 | const { averageSpeed, blockmappedSize, bytesWritten, failed, size } = 62 | flashState.getFlashState(); 63 | const { 64 | skip, 65 | results = { 66 | bytesWritten, 67 | sourceMetadata: { 68 | size, 69 | blockmappedSize, 70 | }, 71 | averageFlashingSpeed: averageSpeed, 72 | devices: { failed, successful: 0 }, 73 | }, 74 | } = flashResults; 75 | return ( 76 | 77 | 90 | 98 | 99 | { 101 | restart(goToMain); 102 | }} 103 | /> 104 | 105 | {successBannerURL.length && ( 106 | 118 | )} 119 | 120 | ); 121 | } 122 | 123 | export default FinishPage; 124 | -------------------------------------------------------------------------------- /lib/gui/app/components/flash-another/flash-another.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | 19 | import { BaseButton } from '../../styled-components'; 20 | import * as i18next from 'i18next'; 21 | 22 | export interface FlashAnotherProps { 23 | onClick: () => void; 24 | } 25 | 26 | export const FlashAnother = (props: FlashAnotherProps) => { 27 | return ( 28 | 29 | {i18next.t('flash.another')} 30 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /lib/gui/app/components/progress-button/progress-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import { Flex, Button, ProgressBar, Txt } from 'rendition'; 19 | import { default as styled } from 'styled-components'; 20 | 21 | import { fromFlashState } from '../../modules/progress-status'; 22 | import { StepButton } from '../../styled-components'; 23 | import * as i18next from 'i18next'; 24 | 25 | const FlashProgressBar = styled(ProgressBar)` 26 | > div { 27 | width: 100%; 28 | height: 12px; 29 | color: white !important; 30 | text-shadow: none !important; 31 | transition-duration: 0s; 32 | 33 | > div { 34 | transition-duration: 0s; 35 | } 36 | } 37 | 38 | width: 100%; 39 | height: 12px; 40 | margin-bottom: 6px; 41 | border-radius: 14px; 42 | font-size: 16px; 43 | line-height: 48px; 44 | 45 | background: #2f3033; 46 | `; 47 | 48 | interface ProgressButtonProps { 49 | type: 'decompressing' | 'flashing' | 'verifying'; 50 | active: boolean; 51 | percentage: number; 52 | position: number; 53 | disabled: boolean; 54 | cancel: (type: string) => void; 55 | callback: () => void; 56 | warning?: boolean; 57 | } 58 | 59 | const colors = { 60 | decompressing: '#00aeef', 61 | flashing: '#da60ff', 62 | verifying: '#1ac135', 63 | } as const; 64 | 65 | const CancelButton = styled(({ type, onClick, ...props }) => { 66 | const status = type === 'verifying' ? i18next.t('skip') : i18next.t('cancel'); 67 | return ( 68 | 71 | ); 72 | })` 73 | font-weight: 600; 74 | 75 | &&& { 76 | width: auto; 77 | height: auto; 78 | font-size: 14px; 79 | } 80 | `; 81 | 82 | export class ProgressButton extends React.PureComponent { 83 | public render() { 84 | const percentage = this.props.percentage; 85 | const warning = this.props.warning; 86 | const { status, position } = fromFlashState({ 87 | type: this.props.type, 88 | percentage, 89 | position: this.props.position, 90 | }); 91 | const type = this.props.type || 'default'; 92 | if (this.props.active) { 93 | return ( 94 | <> 95 | 106 | 107 | {status}  108 | {position} 109 | 110 | {type && ( 111 | 116 | )} 117 | 118 | 119 | 120 | ); 121 | } 122 | return ( 123 | 132 | {i18next.t('flash.flashNow')} 133 | 134 | ); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /lib/gui/app/components/reduced-flashing-infos/reduced-flashing-infos.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | import { Flex, Txt } from 'rendition'; 19 | 20 | import DriveSvg from '../../../assets/drive.svg'; 21 | import ImageSvg from '../../../assets/image.svg'; 22 | import { SVGIcon } from '../svg-icon/svg-icon'; 23 | import { middleEllipsis } from '../../utils/middle-ellipsis'; 24 | 25 | interface ReducedFlashingInfosProps { 26 | imageLogo?: string; 27 | imageName?: string; 28 | imageSize: string; 29 | driveTitle: string; 30 | driveLabel: string; 31 | style?: React.CSSProperties; 32 | } 33 | 34 | export class ReducedFlashingInfos extends React.Component { 35 | constructor(props: ReducedFlashingInfosProps) { 36 | super(props); 37 | this.state = {}; 38 | } 39 | 40 | public render() { 41 | const { imageName = '' } = this.props; 42 | return ( 43 | 47 | 48 | 56 | 60 | {middleEllipsis(imageName, 16)} 61 | 62 | {this.props.imageSize} 63 | 64 | 65 | 66 | 67 | 68 | {middleEllipsis(this.props.driveTitle, 16)} 69 | 70 | 71 | 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/gui/app/components/settings/settings.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import GithubSvg from '@fortawesome/fontawesome-free/svgs/brands/github.svg'; 18 | import * as _ from 'lodash'; 19 | import * as React from 'react'; 20 | import { Box, Checkbox, Flex, Txt } from 'rendition'; 21 | 22 | import { version, packageType } from '../../../../../package.json'; 23 | import * as settings from '../../models/settings'; 24 | import { open as openExternal } from '../../os/open-external/services/open-external'; 25 | import { Modal } from '../../styled-components'; 26 | import * as i18next from 'i18next'; 27 | import { etcherProInfo } from '../../utils/etcher-pro-specific'; 28 | 29 | interface Setting { 30 | name: string; 31 | label: string | JSX.Element; 32 | } 33 | 34 | async function getSettingsList(): Promise { 35 | const list: Setting[] = [ 36 | { 37 | name: 'errorReporting', 38 | label: i18next.t('settings.errorReporting'), 39 | }, 40 | { 41 | name: 'autoBlockmapping', 42 | label: i18next.t('settings.trimExtPartitions'), 43 | }, 44 | ]; 45 | if (['appimage', 'nsis', 'dmg'].includes(packageType)) { 46 | list.push({ 47 | name: 'updatesEnabled', 48 | label: i18next.t('settings.autoUpdate'), 49 | }); 50 | } 51 | return list; 52 | } 53 | 54 | interface SettingsModalProps { 55 | toggleModal: (value: boolean) => void; 56 | } 57 | 58 | const EPInfo = etcherProInfo(); 59 | 60 | const InfoBox = (props: any) => ( 61 | 62 | {props.label} 63 | 64 | {props.value}{' '} 65 | 66 | 67 | ); 68 | 69 | export function SettingsModal({ toggleModal }: SettingsModalProps) { 70 | const [settingsList, setCurrentSettingsList] = React.useState([]); 71 | React.useEffect(() => { 72 | (async () => { 73 | if (settingsList.length === 0) { 74 | setCurrentSettingsList(await getSettingsList()); 75 | } 76 | })(); 77 | }); 78 | const [currentSettings, setCurrentSettings] = React.useState< 79 | _.Dictionary 80 | >({}); 81 | React.useEffect(() => { 82 | (async () => { 83 | if (_.isEmpty(currentSettings)) { 84 | setCurrentSettings(await settings.getAll()); 85 | } 86 | })(); 87 | }); 88 | 89 | const toggleSetting = async (setting: string) => { 90 | const value = currentSettings[setting]; 91 | await settings.set(setting, !value); 92 | setCurrentSettings({ 93 | ...currentSettings, 94 | [setting]: !value, 95 | }); 96 | }; 97 | 98 | return ( 99 | 102 | {i18next.t('settings.settings')} 103 | 104 | } 105 | done={() => toggleModal(false)} 106 | > 107 | 108 | {settingsList.map((setting: Setting, i: number) => { 109 | return ( 110 | 111 | toggleSetting(setting.name)} 117 | /> 118 | 119 | ); 120 | })} 121 | {EPInfo !== undefined && ( 122 | 123 | {i18next.t('settings.systemInformation')} 124 | {EPInfo.get_serial() === undefined ? ( 125 | 126 | ) : ( 127 | 128 | )} 129 | 130 | )} 131 | 141 | openExternal( 142 | 'https://github.com/balena-io/etcher/blob/master/CHANGELOG.md', 143 | ) 144 | } 145 | > 146 | 151 | {version} 152 | 153 | 154 | 155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /lib/gui/app/components/svg-icon/svg-icon.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as React from 'react'; 18 | 19 | const domParser = new window.DOMParser(); 20 | 21 | const DEFAULT_SIZE = '40px'; 22 | 23 | /** 24 | * @summary Try to parse SVG contents and return it data encoded 25 | * 26 | */ 27 | function tryParseSVGContents(contents?: string): string | undefined { 28 | if (contents === undefined) { 29 | return; 30 | } 31 | const doc = domParser.parseFromString(contents, 'image/svg+xml'); 32 | const parserError = doc.querySelector('parsererror'); 33 | const svg = doc.querySelector('svg'); 34 | if (!parserError && svg) { 35 | return `data:image/svg+xml,${encodeURIComponent(svg.outerHTML)}`; 36 | } 37 | } 38 | 39 | interface SVGIconProps { 40 | // Optional string representing the SVG contents to be tried 41 | contents?: string; 42 | // Fallback SVG element to show if `contents` is invalid/undefined 43 | fallback: React.FunctionComponent>; 44 | // SVG image width unit 45 | width?: string; 46 | // SVG image height unit 47 | height?: string; 48 | // Should the element visually appear grayed out and disabled? 49 | disabled?: boolean; 50 | style?: React.CSSProperties; 51 | } 52 | 53 | /** 54 | * @summary SVG element that takes file contents 55 | */ 56 | export class SVGIcon extends React.PureComponent { 57 | public render() { 58 | const svgData = tryParseSVGContents(this.props.contents); 59 | const { width, height, style = {} } = this.props; 60 | style.width = width || DEFAULT_SIZE; 61 | style.height = height || DEFAULT_SIZE; 62 | if (svgData !== undefined) { 63 | return ( 64 | 69 | ); 70 | } 71 | const { fallback: FallbackSVG } = this.props; 72 | return ; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/gui/app/components/target-selector/target-selector-button.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/triangle-exclamation.svg'; 18 | import * as React from 'react'; 19 | import type { FlexProps } from 'rendition'; 20 | import { Flex, Txt } from 'rendition'; 21 | 22 | import type { DriveStatus } from '../../../../shared/drive-constraints'; 23 | import { getDriveImageCompatibilityStatuses } from '../../../../shared/drive-constraints'; 24 | import { compatibility, warning } from '../../../../shared/messages'; 25 | import prettyBytes from 'pretty-bytes'; 26 | import { getImage, getSelectedDrives } from '../../models/selection-state'; 27 | import { 28 | ChangeButton, 29 | DetailsText, 30 | StepButton, 31 | StepNameButton, 32 | } from '../../styled-components'; 33 | import { middleEllipsis } from '../../utils/middle-ellipsis'; 34 | import * as i18next from 'i18next'; 35 | 36 | interface TargetSelectorProps { 37 | targets: any[]; 38 | disabled: boolean; 39 | openDriveSelector: () => void; 40 | reselectDrive: () => void; 41 | flashing: boolean; 42 | show: boolean; 43 | tooltip: string; 44 | } 45 | 46 | function getDriveWarning(status: DriveStatus) { 47 | switch (status.message) { 48 | case compatibility.containsImage(): 49 | return warning.sourceDrive(); 50 | case compatibility.largeDrive(): 51 | return warning.largeDriveSize(); 52 | case compatibility.system(): 53 | return warning.systemDrive(); 54 | default: 55 | return ''; 56 | } 57 | } 58 | 59 | const DriveCompatibilityWarning = ({ 60 | warnings, 61 | ...props 62 | }: { 63 | warnings: string[]; 64 | } & FlexProps) => { 65 | const systemDrive = warnings.find( 66 | (message) => message === warning.systemDrive(), 67 | ); 68 | return ( 69 | 70 | 74 | 75 | ); 76 | }; 77 | 78 | export function TargetSelectorButton(props: TargetSelectorProps) { 79 | const targets = getSelectedDrives(); 80 | 81 | if (targets.length === 1) { 82 | const target = targets[0]; 83 | const warnings = getDriveImageCompatibilityStatuses( 84 | target, 85 | getImage(), 86 | true, 87 | ).map(getDriveWarning); 88 | return ( 89 | <> 90 | 91 | {warnings.length > 0 && ( 92 | 93 | )} 94 | {middleEllipsis(target.description, 20)} 95 | 96 | {!props.flashing && ( 97 | 98 | {i18next.t('target.change')} 99 | 100 | )} 101 | {target.size != null && ( 102 | {prettyBytes(target.size)} 103 | )} 104 | 105 | ); 106 | } 107 | 108 | if (targets.length > 1) { 109 | const targetsTemplate = []; 110 | for (const target of targets) { 111 | const warnings = getDriveImageCompatibilityStatuses( 112 | target, 113 | getImage(), 114 | true, 115 | ).map(getDriveWarning); 116 | targetsTemplate.push( 117 | 124 | {warnings.length > 0 ? ( 125 | 126 | ) : null} 127 | {middleEllipsis(target.description, 14)} 128 | {target.size != null && {prettyBytes(target.size)}} 129 | , 130 | ); 131 | } 132 | return ( 133 | <> 134 | 135 | {targets.length} {i18next.t('target.targets')} 136 | 137 | {!props.flashing && ( 138 | 139 | {i18next.t('target.change')} 140 | 141 | )} 142 | {targetsTemplate} 143 | 144 | ); 145 | } 146 | 147 | return ( 148 | 0 ? -1 : 2} 151 | disabled={props.disabled} 152 | onClick={props.openDriveSelector} 153 | > 154 | {i18next.t('target.selectTarget')} 155 | 156 | ); 157 | } 158 | -------------------------------------------------------------------------------- /lib/gui/app/css/fonts/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/lib/gui/app/css/fonts/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /lib/gui/app/css/fonts/SourceSansPro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/lib/gui/app/css/fonts/SourceSansPro-SemiBold.ttf -------------------------------------------------------------------------------- /lib/gui/app/css/main.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | @font-face { 18 | font-family: 'SourceSansPro'; 19 | src: url('./fonts/SourceSansPro-Regular.ttf') format('truetype'); 20 | font-weight: 500; 21 | font-style: normal; 22 | } 23 | 24 | @font-face { 25 | font-family: 'SourceSansPro'; 26 | src: url('./fonts/SourceSansPro-SemiBold.ttf') format('truetype'); 27 | font-weight: 600; 28 | font-style: normal; 29 | } 30 | 31 | html, 32 | body { 33 | margin: 0; 34 | overflow: hidden; 35 | 36 | /* Prevent white flash when running application */ 37 | background-color: #4d5057; 38 | 39 | /* Prevent WebView bounce effect in OS X */ 40 | height: 100%; 41 | width: 100%; 42 | } 43 | 44 | /* Prevent text selection */ 45 | body { 46 | -webkit-user-select: none; 47 | -webkit-overflow-scrolling: touch; 48 | } 49 | 50 | /* Prevent blue outline */ 51 | a:focus, 52 | input:focus, 53 | button:focus, 54 | [tabindex]:focus, 55 | input[type='checkbox'] + div { 56 | outline: none !important; 57 | box-shadow: none !important; 58 | } 59 | 60 | .disabled { 61 | opacity: 0.4; 62 | } 63 | 64 | #rendition-tooltip-root > div { 65 | font-family: 'SourceSansPro', sans-serif; 66 | } 67 | -------------------------------------------------------------------------------- /lib/gui/app/i18n.ts: -------------------------------------------------------------------------------- 1 | import * as i18next from 'i18next'; 2 | import { initReactI18next } from 'react-i18next'; 3 | import zh_CN_translation from './i18n/zh-CN'; 4 | import zh_TW_translation from './i18n/zh-TW'; 5 | import en_translation from './i18n/en'; 6 | 7 | export function langParser() { 8 | if (process.env.LANG !== undefined) { 9 | // Bypass mocha, where lang-detect don't works 10 | return 'en'; 11 | } 12 | 13 | const lang = Intl.DateTimeFormat().resolvedOptions().locale; 14 | 15 | switch (lang.substr(0, 2)) { 16 | case 'zh': 17 | if (lang === 'zh-CN' || lang === 'zh-SG') { 18 | return 'zh-CN'; 19 | } // Simplified Chinese 20 | else { 21 | return 'zh-TW'; 22 | } // Traditional Chinese 23 | default: 24 | return lang.substr(0, 2); 25 | } 26 | } 27 | 28 | i18next.use(initReactI18next).init({ 29 | lng: langParser(), 30 | fallbackLng: 'en', 31 | nonExplicitSupportedLngs: true, 32 | interpolation: { 33 | escapeValue: false, 34 | }, 35 | resources: { 36 | 'zh-CN': zh_CN_translation, 37 | 'zh-TW': zh_TW_translation, 38 | en: en_translation, 39 | }, 40 | }); 41 | 42 | export const supportedLocales = ['en', 'zh']; 43 | 44 | export default i18next; 45 | -------------------------------------------------------------------------------- /lib/gui/app/i18n/README.md: -------------------------------------------------------------------------------- 1 | # i18n 2 | 3 | ## How it was done 4 | 5 | Using the open-source lib [i18next](https://www.i18next.com/). 6 | 7 | ## How to add your own language 8 | 9 | 1. Go to `lib/gui/app/i18n` and add a file named `xx.ts` (use the codes mentioned 10 | in [the link](https://www.science.co.il/language/Locale-codes.php), and we support styles as `fr`, `de`, `es-ES` 11 | and `pt-BR`) 12 | . 13 | 2. Copy the content from an existing translation and start to translate. 14 | 3. Once done, go to `lib/gui/app/i18n.ts` and add a line of `import xx_translation from './i18n/xx'` after the 15 | already-added imports and add `xx: xx_translation` in the `resources` section of `i18next.init()` function. 16 | 4. Now go to `lib/shared/catalina-sudo/` and copy the `sudo-askpass.osascript-en.js`, change it to 17 | be `sudo-askpass.osascript-xx.js` and edit 18 | the `'balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.'` line and 19 | those `Ok`s and `Cancel`s to your own language. 20 | 5. If, your language has several variations when they are used in several countries/regions, such as `zh-CN` and `zh-TW` 21 | , or `pt-BR` and `pt-PT`, edit 22 | the `langParser()` in the `lib/gui/app/i18n.ts` file to meet your need. 23 | 6. Make a commit, and then a pull request on GitHub. -------------------------------------------------------------------------------- /lib/gui/app/i18n/zh-CN.ts: -------------------------------------------------------------------------------- 1 | const translation = { 2 | translation: { 3 | ok: '好', 4 | cancel: '取消', 5 | continue: '继续', 6 | skip: '跳过', 7 | sure: '我确定', 8 | warning: '请注意!', 9 | attention: '请注意', 10 | failed: '失败', 11 | completed: '完毕', 12 | yesExit: '是的,可以退出', 13 | reallyExit: '真的要现在退出 Etcher 吗?', 14 | yesContinue: '是的,继续', 15 | progress: { 16 | starting: '正在启动……', 17 | decompressing: '正在解压……', 18 | flashing: '正在烧录……', 19 | finishing: '正在结束……', 20 | verifying: '正在验证……', 21 | failing: '失败……', 22 | }, 23 | message: { 24 | sizeNotRecommended: '大小不推荐', 25 | tooSmall: '空间太小', 26 | locked: '被锁定', 27 | system: '系统盘', 28 | containsImage: '存放源镜像', 29 | largeDrive: '很大的磁盘', 30 | sourceLarger: '所选的镜像比目标盘大了 {{byte}} 比特。', 31 | flashSucceed_one: '烧录成功', 32 | flashSucceed_other: '烧录成功', 33 | flashFail_one: '烧录失败', 34 | flashFail_other: '烧录失败', 35 | toDrive: '到 {{description}} ({{name}})', 36 | toTarget_one: '到 {{num}} 个目标', 37 | toTarget_other: '到 {{num}} 个目标', 38 | andFailTarget_one: '并烧录失败了 {{num}} 个目标', 39 | andFailTarget_other: '并烧录失败了 {{num}} 个目标', 40 | succeedTo: '{{name}} 被成功烧录 {{target}}', 41 | exitWhileFlashing: 42 | '您当前正在刷机。 关闭 Etcher 可能会导致您的磁盘无法使用。', 43 | looksLikeWindowsImage: 44 | '看起来您正在尝试刻录 Windows 镜像。\n\n与其他镜像不同,Windows 镜像需要特殊处理才能使其可启动。 我们建议您使用专门为此目的设计的工具,例如 Rufus (Windows)、WoeUSB (Linux) 或 Boot Camp 助理 (macOS)。', 45 | image: '镜像', 46 | drive: '磁盘', 47 | missingPartitionTable: 48 | '看起来这不是一个可启动的{{type}}。\n\n这个{{type}}似乎不包含分区表,因此您的设备可能无法识别或无法正确启动。', 49 | largeDriveSize: '这是个很大的磁盘!请检查并确认它不包含对您很重要的信息', 50 | systemDrive: '选择系统盘很危险,因为这将会删除你的系统', 51 | sourceDrive: '源镜像位于这个分区中', 52 | noSpace: '磁盘空间不足。 请插入另一个较大的磁盘并重试。', 53 | genericFlashError: 54 | '出了点问题。如果源镜像曾被压缩过,请检查它是否已损坏。\n{{error}}', 55 | validation: 56 | '写入已成功完成,但 Etcher 在从磁盘读取镜像时检测到潜在的损坏问题。 \n\n请考虑将镜像写入其他磁盘。', 57 | openError: '打开 {{source}} 时出错。\n\n错误信息: {{error}}', 58 | flashError: '烧录 {{image}} {{targets}} 失败。', 59 | unplug: 60 | '看起来 Etcher 失去了对磁盘的连接。 它是不是被意外拔掉了?\n\n有时这个错误是因为读卡器出了故障。', 61 | cannotWrite: 62 | '看起来 Etcher 无法写入磁盘的这个位置。 此错误通常是由故障的磁盘、读取器或端口引起的。 \n\n请使用其他磁盘、读卡器或端口重试。', 63 | childWriterDied: 64 | '写入进程意外崩溃。请再试一次,如果问题仍然存在,请联系 Etcher 团队。', 65 | badProtocol: '仅支持 http:// 和 https:// 开头的网址。', 66 | }, 67 | target: { 68 | selectTarget: '选择目标磁盘', 69 | plugTarget: '请插入目标磁盘', 70 | targets: '个目标', 71 | change: '更改', 72 | }, 73 | menu: { 74 | edit: '编辑', 75 | view: '视图', 76 | devTool: '打开开发者工具', 77 | window: '窗口', 78 | help: '帮助', 79 | pro: 'Etcher 专业版', 80 | website: 'Etcher 的官网', 81 | issue: '提交一个 issue', 82 | about: '关于 Etcher', 83 | hide: '隐藏 Etcher', 84 | hideOthers: '隐藏其它窗口', 85 | unhide: '取消隐藏', 86 | quit: '退出 Etcher', 87 | }, 88 | source: { 89 | useSourceURL: '使用镜像网络地址', 90 | auth: '验证', 91 | username: '输入用户名', 92 | password: '输入密码', 93 | unsupportedProtocol: '不支持的协议', 94 | windowsImage: '这可能是 Windows 系统镜像', 95 | partitionTable: '找不到分区表', 96 | errorOpen: '打开源镜像时出错', 97 | fromFile: '从文件烧录', 98 | fromURL: '从在线地址烧录', 99 | clone: '克隆磁盘', 100 | image: '镜像信息', 101 | name: '名称:', 102 | path: '路径:', 103 | selectSource: '选择源', 104 | plugSource: '请插入源磁盘', 105 | osImages: '系统镜像格式', 106 | allFiles: '任何文件格式', 107 | enterValidURL: '请输入一个正确的地址', 108 | }, 109 | drives: { 110 | name: '名称', 111 | size: '大小', 112 | location: '位置', 113 | find: '找到 {{length}} 个', 114 | select: '选定 {{select}}', 115 | showHidden: '显示 {{num}} 个隐藏的磁盘', 116 | systemDriveDanger: '选择系统盘很危险,因为这将会删除你的系统!', 117 | openInBrowser: 'Etcher 会在浏览器中打开 {{link}}', 118 | changeTarget: '改变目标', 119 | largeDriveWarning: '您即将擦除一个非常大的磁盘', 120 | largeDriveWarningMsg: '您确定所选磁盘不是存储磁盘吗?', 121 | systemDriveWarning: '您将要擦除系统盘', 122 | systemDriveWarningMsg: '您确定要烧录到系统盘吗?', 123 | }, 124 | flash: { 125 | another: '烧录另一目标', 126 | target: '目标', 127 | location: '位置', 128 | error: '错误', 129 | flash: '烧录', 130 | flashNow: '现在烧录!', 131 | skip: '跳过了验证', 132 | moreInfo: '更多信息', 133 | speedTip: 134 | '通过将镜像大小除以烧录时间来计算速度。\n由于我们能够跳过未使用的部分,因此具有EXT分区的磁盘镜像烧录速度更快。', 135 | speed: '速度:{{speed}} MB/秒', 136 | speedShort: '{{speed}} MB/秒', 137 | eta: '预计还需要:{{eta}}', 138 | failedTarget: '失败的烧录目标', 139 | failedRetry: '重试烧录失败目标', 140 | flashFailed: '烧录失败。', 141 | flashCompleted: '烧录成功!', 142 | }, 143 | settings: { 144 | errorReporting: '匿名地向 balena.io 报告运行错误和使用统计', 145 | autoUpdate: '自动更新', 146 | settings: '软件设置', 147 | systemInformation: '系统信息', 148 | }, 149 | }, 150 | }; 151 | 152 | export default translation; 153 | -------------------------------------------------------------------------------- /lib/gui/app/i18n/zh-TW.ts: -------------------------------------------------------------------------------- 1 | const translation = { 2 | translation: { 3 | continue: '繼續', 4 | ok: '好', 5 | cancel: '取消', 6 | skip: '跳過', 7 | sure: '我確定', 8 | warning: '請注意!', 9 | attention: '請注意', 10 | failed: '失敗', 11 | completed: '完成', 12 | yesContinue: '是的,繼續', 13 | reallyExit: '真的要現在結束 Etcher 嗎?', 14 | yesExit: '是的,可以結束', 15 | progress: { 16 | starting: '正在啟動……', 17 | decompressing: '正在解壓縮……', 18 | flashing: '正在燒錄……', 19 | finishing: '正在結束……', 20 | verifying: '正在驗證……', 21 | failing: '失敗……', 22 | }, 23 | message: { 24 | sizeNotRecommended: '大小不建議', 25 | tooSmall: '空間太小', 26 | locked: '被鎖定', 27 | system: '系統', 28 | containsImage: '存放來源映像檔', 29 | largeDrive: '很大的磁碟', 30 | sourceLarger: '所選的映像檔比目標磁碟大了 {{byte}} 位元組。', 31 | flashSucceed_one: '燒錄成功', 32 | flashSucceed_other: '燒錄成功', 33 | flashFail_one: '燒錄失敗', 34 | flashFail_other: '燒錄失敗', 35 | toDrive: '到 {{description}} ({{name}})', 36 | toTarget_one: '到 {{num}} 個目標', 37 | toTarget_other: '到 {{num}} 個目標', 38 | andFailTarget_one: '並燒錄失敗了 {{num}} 個目標', 39 | andFailTarget_other: '並燒錄失敗了 {{num}} 個目標', 40 | succeedTo: '{{name}} 被成功燒錄 {{target}}', 41 | exitWhileFlashing: 42 | '您目前正在刷寫。關閉 Etcher 可能會導致您的磁碟無法使用。', 43 | looksLikeWindowsImage: 44 | '看起來您正在嘗試燒錄 Windows 映像檔。\n\n與其他映像檔不同,Windows 映像檔需要特殊處理才能使其可啟動。我們建議您使用專門為此目的設計的工具,例如 Rufus (Windows)、WoeUSB (Linux) 或 Boot Camp 助理 (macOS)。', 45 | image: '映像檔', 46 | drive: '磁碟', 47 | missingPartitionTable: 48 | '看起來這不是一個可啟動的{{type}}。\n\n這個{{type}}似乎不包含分割表,因此您的設備可能無法識別或無法正確啟動。', 49 | largeDriveSize: 50 | '這是個很大容量的磁碟!請檢查並確認它不包含對您來說存放很重要的資料', 51 | systemDrive: '選擇系統分割區很危險,因為這將會刪除你的系統', 52 | sourceDrive: '來源映像檔位於這個分割區中', 53 | noSpace: '磁碟空間不足。請插入另一個較大的磁碟並重試。', 54 | genericFlashError: 55 | '出了點問題。如果來源映像檔曾被壓縮過,請檢查它是否已損壞。\n{{error}}', 56 | validation: 57 | '寫入已成功完成,但 Etcher 在從磁碟讀取映像檔時檢測到潛在的損壞問題。\n\n請考慮將映像檔寫入其他磁碟。', 58 | openError: '打開 {{source}} 時發生錯誤。\n\n錯誤訊息: {{error}}', 59 | flashError: '燒錄 {{image}} {{targets}} 失敗。', 60 | unplug: 61 | '看起來 Etcher 失去了對磁碟的連接。是不是被意外拔掉了?\n\n有時這個錯誤是因為讀卡器出了故障。', 62 | cannotWrite: 63 | '看起來 Etcher 無法寫入磁碟的這個位置。此錯誤通常是由故障的磁碟、讀取器或連接埠引起的。\n\n請使用其他磁碟、讀卡器或連接埠重試。', 64 | childWriterDied: 65 | '寫入處理程序意外崩潰。請再試一次,如果問題仍然存在,請聯絡 Etcher 團隊。', 66 | badProtocol: '僅支援 http:// 和 https:// 開頭的網址。', 67 | }, 68 | target: { 69 | selectTarget: '選擇目標磁碟', 70 | plugTarget: '請插入目標磁碟', 71 | targets: '個目標', 72 | change: '更改', 73 | }, 74 | source: { 75 | useSourceURL: '使用映像檔網址', 76 | auth: '驗證', 77 | username: '輸入使用者名稱', 78 | password: '輸入密碼', 79 | unsupportedProtocol: '不支持的通訊協定', 80 | windowsImage: '這可能是 Windows 系統映像檔', 81 | partitionTable: '找不到分割表', 82 | errorOpen: '打開來源映像檔時出錯', 83 | fromFile: '從檔案燒錄', 84 | fromURL: '從網址燒錄', 85 | clone: '再製磁碟', 86 | image: '映像檔訊息', 87 | name: '名稱:', 88 | path: '路徑:', 89 | selectSource: '選擇來源', 90 | plugSource: '請插入來源磁碟', 91 | osImages: '系統映像檔格式', 92 | allFiles: '任何檔案格式', 93 | enterValidURL: '請輸入正確的網址', 94 | }, 95 | drives: { 96 | name: '名稱', 97 | size: '大小', 98 | location: '位置', 99 | find: '找到 {{length}} 個', 100 | select: '選取 {{select}}', 101 | showHidden: '顯示 {{num}} 個隱藏的磁碟', 102 | systemDriveDanger: '選擇系統分割區很危險,因為這將會刪除你的系統!', 103 | openInBrowser: 'Etcher 會在瀏覽器中打開 {{link}}', 104 | changeTarget: '更改目標', 105 | largeDriveWarning: '您即將格式化一個非常大的磁碟', 106 | largeDriveWarningMsg: '您確定所選磁碟不是儲存資料的磁碟嗎?', 107 | systemDriveWarning: '您將要格式化系統分割區', 108 | systemDriveWarningMsg: '您確定要燒錄到系統分割區嗎?', 109 | }, 110 | flash: { 111 | another: '燒錄另一目標', 112 | target: '目標', 113 | location: '位置', 114 | error: '錯誤', 115 | flash: '燒錄', 116 | flashNow: '現在燒錄!', 117 | skip: '跳過了驗證', 118 | moreInfo: '更多資訊', 119 | speedTip: 120 | '透過將映像檔大小除以燒錄時間來計算速度。\n由於我們能夠跳過未使用的部分,因此具有 ext 分割區的磁碟映像檔燒錄速度更快。', 121 | speed: '速度:{{speed}} MB/秒', 122 | speedShort: '{{speed}} MB/秒', 123 | eta: '預計還需要:{{eta}}', 124 | failedTarget: '目標燒錄失敗', 125 | failedRetry: '重試燒錄失敗的目標', 126 | flashFailed: '燒錄失敗。', 127 | flashCompleted: '燒錄成功!', 128 | }, 129 | settings: { 130 | errorReporting: '匿名向 balena.io 回報程式錯誤和使用統計資料', 131 | autoUpdate: '自動更新', 132 | settings: '軟體設定', 133 | systemInformation: '系統資訊', 134 | trimExtPartitions: '修改原始映像檔上未分配的空間(在 ext 類型分割區中)', 135 | }, 136 | menu: { 137 | edit: '編輯', 138 | view: '預覽', 139 | devTool: '打開開發者工具', 140 | window: '視窗', 141 | help: '協助', 142 | pro: 'Etcher 專業版', 143 | website: 'Etcher 的官網', 144 | issue: '提交 issue', 145 | about: '關於 Etcher', 146 | hide: '隱藏 Etcher', 147 | hideOthers: '隱藏其它視窗', 148 | unhide: '取消隱藏', 149 | quit: '結束 Etcher', 150 | }, 151 | }, 152 | }; 153 | 154 | export default translation; 155 | -------------------------------------------------------------------------------- /lib/gui/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | balenaEtcher 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /lib/gui/app/models/available-drives.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import type { DrivelistDrive } from '../../../shared/drive-constraints'; 18 | import { Actions, store } from './store'; 19 | 20 | export function hasAvailableDrives() { 21 | return getDrives().length > 0; 22 | } 23 | 24 | export function setDrives(drives: any[]) { 25 | store.dispatch({ 26 | type: Actions.SET_AVAILABLE_TARGETS, 27 | data: drives, 28 | }); 29 | } 30 | 31 | export function getDrives(): DrivelistDrive[] { 32 | return store.getState().toJS().availableDrives; 33 | } 34 | -------------------------------------------------------------------------------- /lib/gui/app/models/flash-state.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import type * as sdk from 'etcher-sdk'; 19 | import * as _ from 'lodash'; 20 | import type { DrivelistDrive } from '../../../shared/drive-constraints'; 21 | import { bytesToMegabytes } from '../../../shared/units'; 22 | import { Actions, store } from './store'; 23 | 24 | /** 25 | * @summary Reset flash state 26 | */ 27 | export function resetState() { 28 | store.dispatch({ 29 | type: Actions.RESET_FLASH_STATE, 30 | data: {}, 31 | }); 32 | } 33 | 34 | /** 35 | * @summary Check if currently flashing 36 | */ 37 | export function isFlashing(): boolean { 38 | return store.getState().toJS().isFlashing; 39 | } 40 | 41 | /** 42 | * @summary Set the flashing flag 43 | * 44 | * @description 45 | * The flag is used to signify that we're going to 46 | * start a flash process. 47 | */ 48 | export function setFlashingFlag() { 49 | // see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods 50 | electron.ipcRenderer.send('disable-screensaver'); 51 | store.dispatch({ 52 | type: Actions.SET_FLASHING_FLAG, 53 | data: {}, 54 | }); 55 | } 56 | 57 | /** 58 | * @summary Unset the flashing flag 59 | * 60 | * @description 61 | * The flag is used to signify that the write process ended. 62 | */ 63 | export function unsetFlashingFlag(results: { 64 | cancelled?: boolean; 65 | sourceChecksum?: string; 66 | errorCode?: string | number; 67 | }) { 68 | store.dispatch({ 69 | type: Actions.UNSET_FLASHING_FLAG, 70 | data: results, 71 | }); 72 | // see https://github.com/balenablocks/balena-electron-env/blob/4fce9c461f294d4a768db8f247eea6f75d7b08b0/README.md#remote-methods 73 | 74 | electron.ipcRenderer.send('enable-screensaver'); 75 | } 76 | 77 | export function setDevicePaths(devicePaths: string[]) { 78 | store.dispatch({ 79 | type: Actions.SET_DEVICE_PATHS, 80 | data: devicePaths, 81 | }); 82 | } 83 | 84 | export function addFailedDeviceError({ 85 | device, 86 | error, 87 | }: { 88 | device: DrivelistDrive; 89 | error: Error; 90 | }) { 91 | const failedDeviceErrorsMap = new Map( 92 | store.getState().toJS().failedDeviceErrors, 93 | ); 94 | if (failedDeviceErrorsMap.has(device.device)) { 95 | // Only store the first error 96 | return; 97 | } 98 | failedDeviceErrorsMap.set(device.device, { 99 | description: device.description, 100 | device: device.device, 101 | devicePath: device.devicePath, 102 | ...error, 103 | }); 104 | store.dispatch({ 105 | type: Actions.SET_FAILED_DEVICE_ERRORS, 106 | data: Array.from(failedDeviceErrorsMap), 107 | }); 108 | } 109 | 110 | /** 111 | * @summary Set the flashing state 112 | */ 113 | export function setProgressState( 114 | state: sdk.multiWrite.MultiDestinationProgress, 115 | ) { 116 | // Preserve only one decimal place 117 | const PRECISION = 1; 118 | const data = { 119 | ...state, 120 | percentage: 121 | state.percentage !== undefined && _.isFinite(state.percentage) 122 | ? Math.floor(state.percentage) 123 | : undefined, 124 | 125 | speed: _.attempt(() => { 126 | if (_.isFinite(state.speed)) { 127 | return _.round(bytesToMegabytes(state.speed), PRECISION); 128 | } 129 | 130 | return null; 131 | }), 132 | }; 133 | 134 | store.dispatch({ 135 | type: Actions.SET_FLASH_STATE, 136 | data, 137 | }); 138 | } 139 | 140 | export function getFlashResults() { 141 | return store.getState().toJS().flashResults; 142 | } 143 | 144 | export function getFlashState() { 145 | return store.getState().get('flashState').toJS(); 146 | } 147 | 148 | export function wasLastFlashCancelled() { 149 | return _.get(getFlashResults(), ['cancelled'], false); 150 | } 151 | 152 | export function getLastFlashSourceChecksum(): string { 153 | return getFlashResults().sourceChecksum; 154 | } 155 | 156 | export function getLastFlashErrorCode() { 157 | return getFlashResults().errorCode; 158 | } 159 | 160 | export function getFlashUuid() { 161 | return store.getState().toJS().flashUuid; 162 | } 163 | -------------------------------------------------------------------------------- /lib/gui/app/models/selection-state.ts: -------------------------------------------------------------------------------- 1 | import type { DrivelistDrive } from '../../../shared/drive-constraints'; 2 | /* 3 | * Copyright 2016 balena.io 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | import type { SourceMetadata } from '../../../shared/typings/source-selector'; 19 | 20 | import * as availableDrives from './available-drives'; 21 | import { Actions, store } from './store'; 22 | 23 | /** 24 | * @summary Select a drive by its device path 25 | */ 26 | export function selectDrive(driveDevice: string) { 27 | store.dispatch({ 28 | type: Actions.SELECT_TARGET, 29 | data: driveDevice, 30 | }); 31 | } 32 | 33 | /** 34 | * @summary Toggle drive selection 35 | */ 36 | export function toggleDrive(driveDevice: string) { 37 | if (isDriveSelected(driveDevice)) { 38 | deselectDrive(driveDevice); 39 | } else { 40 | selectDrive(driveDevice); 41 | } 42 | } 43 | 44 | export function selectSource(source: SourceMetadata) { 45 | store.dispatch({ 46 | type: Actions.SELECT_SOURCE, 47 | data: source, 48 | }); 49 | } 50 | 51 | /** 52 | * @summary Get all selected drives' devices 53 | */ 54 | export function getSelectedDevices(): string[] { 55 | return store.getState().getIn(['selection', 'devices']).toJS(); 56 | } 57 | 58 | /** 59 | * @summary Get all selected drive objects 60 | */ 61 | export function getSelectedDrives(): DrivelistDrive[] { 62 | const selectedDevices = getSelectedDevices(); 63 | return availableDrives 64 | .getDrives() 65 | .filter((drive) => selectedDevices.includes(drive.device)); 66 | } 67 | 68 | /** 69 | * @summary Get the selected image 70 | */ 71 | export function getImage(): SourceMetadata | undefined { 72 | return store.getState().toJS().selection.image; 73 | } 74 | 75 | /** 76 | * @summary Check if there is a selected drive 77 | */ 78 | export function hasDrive(): boolean { 79 | return Boolean(getSelectedDevices().length); 80 | } 81 | 82 | /** 83 | * @summary Check if there is a selected image 84 | */ 85 | export function hasImage(): boolean { 86 | return getImage() !== undefined; 87 | } 88 | 89 | /** 90 | * @summary Remove drive from selection 91 | */ 92 | export function deselectDrive(driveDevice: string) { 93 | store.dispatch({ 94 | type: Actions.DESELECT_TARGET, 95 | data: driveDevice, 96 | }); 97 | } 98 | 99 | export function deselectImage() { 100 | store.dispatch({ 101 | type: Actions.DESELECT_SOURCE, 102 | data: {}, 103 | }); 104 | } 105 | 106 | export function deselectAllDrives() { 107 | getSelectedDevices().forEach(deselectDrive); 108 | } 109 | 110 | /** 111 | * @summary Clear selections 112 | */ 113 | export function clear() { 114 | deselectImage(); 115 | deselectAllDrives(); 116 | } 117 | 118 | /** 119 | * @summary Check whether a given device is selected. 120 | */ 121 | export function isDriveSelected(driveDevice: string) { 122 | if (!driveDevice) { 123 | return false; 124 | } 125 | 126 | const selectedDriveDevices = getSelectedDevices(); 127 | return selectedDriveDevices.includes(driveDevice); 128 | } 129 | -------------------------------------------------------------------------------- /lib/gui/app/models/settings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _debug from 'debug'; 18 | import * as electron from 'electron'; 19 | import * as _ from 'lodash'; 20 | import { promises as fs } from 'fs'; 21 | import { join } from 'path'; 22 | 23 | import * as packageJSON from '../../../../package.json'; 24 | 25 | const debug = _debug('etcher:models:settings'); 26 | 27 | const JSON_INDENT = 2; 28 | 29 | export const DEFAULT_WIDTH = 800; 30 | export const DEFAULT_HEIGHT = 480; 31 | 32 | /** 33 | * @summary Userdata directory path 34 | * @description 35 | * Defaults to the following: 36 | * - `%APPDATA%/etcher` on Windows 37 | * - `$XDG_CONFIG_HOME/etcher` or `~/.config/etcher` on Linux 38 | * - `~/Library/Application Support/etcher` on macOS 39 | * See https://electronjs.org/docs/api/app#appgetpathname 40 | * 41 | * NOTE: We use the remote property when this module 42 | * is loaded in the Electron's renderer process 43 | */ 44 | function getConfigPath() { 45 | const app = electron.app || require('@electron/remote').app; 46 | return join(app.getPath('userData'), 'config.json'); 47 | } 48 | 49 | async function readConfigFile(filename: string): Promise<_.Dictionary> { 50 | let contents = '{}'; 51 | try { 52 | contents = await fs.readFile(filename, { encoding: 'utf8' }); 53 | } catch (error: any) { 54 | // noop 55 | } 56 | try { 57 | return JSON.parse(contents); 58 | } catch (parseError) { 59 | console.error(parseError); 60 | return {}; 61 | } 62 | } 63 | 64 | // exported for tests 65 | export async function readAll() { 66 | return await readConfigFile(getConfigPath()); 67 | } 68 | 69 | // exported for tests 70 | export async function writeConfigFile( 71 | filename: string, 72 | data: _.Dictionary, 73 | ): Promise { 74 | await fs.writeFile(filename, JSON.stringify(data, null, JSON_INDENT)); 75 | } 76 | 77 | const DEFAULT_SETTINGS: _.Dictionary = { 78 | errorReporting: true, 79 | updatesEnabled: ['appimage', 'nsis', 'dmg'].includes(packageJSON.packageType), 80 | desktopNotifications: true, 81 | autoBlockmapping: true, 82 | decompressFirst: true, 83 | }; 84 | 85 | const settings = _.cloneDeep(DEFAULT_SETTINGS); 86 | 87 | async function load(): Promise { 88 | debug('load'); 89 | const loadedSettings = await readAll(); 90 | _.assign(settings, loadedSettings); 91 | } 92 | 93 | const loaded = load(); 94 | 95 | export async function set( 96 | key: string, 97 | value: any, 98 | writeConfigFileFn = writeConfigFile, 99 | ): Promise { 100 | debug('set', key, value); 101 | await loaded; 102 | const previousValue = settings[key]; 103 | settings[key] = value; 104 | try { 105 | await writeConfigFileFn(getConfigPath(), settings); 106 | } catch (error: any) { 107 | // Revert to previous value if persisting settings failed 108 | settings[key] = previousValue; 109 | throw error; 110 | } 111 | } 112 | 113 | export async function get(key: string): Promise { 114 | await loaded; 115 | return getSync(key); 116 | } 117 | 118 | export function getSync(key: string): any { 119 | return _.cloneDeep(settings[key]); 120 | } 121 | 122 | export async function getAll() { 123 | debug('getAll'); 124 | await loaded; 125 | return _.cloneDeep(settings); 126 | } 127 | -------------------------------------------------------------------------------- /lib/gui/app/modules/analytics.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { findLastIndex, once } from 'lodash'; 18 | import * as SentryRenderer from '@sentry/electron/renderer'; 19 | import * as settings from '../models/settings'; 20 | 21 | type AnalyticsPayload = _.Dictionary; 22 | 23 | const clearUserPath = (filename: string): string => { 24 | const generatedFile = filename.split('generated').reverse()[0]; 25 | return generatedFile !== filename ? `generated${generatedFile}` : filename; 26 | }; 27 | 28 | export const anonymizeSentryData = ( 29 | event: SentryRenderer.Event, 30 | ): SentryRenderer.Event => { 31 | event.exception?.values?.forEach((exception) => { 32 | exception.stacktrace?.frames?.forEach((frame) => { 33 | if (frame.filename) { 34 | frame.filename = clearUserPath(frame.filename); 35 | } 36 | }); 37 | }); 38 | 39 | event.breadcrumbs?.forEach((breadcrumb) => { 40 | if (breadcrumb.data?.url) { 41 | breadcrumb.data.url = clearUserPath(breadcrumb.data.url); 42 | } 43 | }); 44 | 45 | if (event.request?.url) { 46 | event.request.url = clearUserPath(event.request.url); 47 | } 48 | 49 | return event; 50 | }; 51 | 52 | const extractPathRegex = /(.*)(^|\s)(file:\/\/)?(\w:)?([\\/].+)/; 53 | const etcherSegmentMarkers = ['app.asar', 'Resources']; 54 | 55 | export const anonymizePath = (input: string) => { 56 | // First, extract a part of the value that matches a path pattern. 57 | const match = extractPathRegex.exec(input); 58 | if (match === null) { 59 | return input; 60 | } 61 | const mainPart = match[5]; 62 | const space = match[2]; 63 | const beginning = match[1]; 64 | const uriPrefix = match[3] || ''; 65 | 66 | // We have to deal with both Windows and POSIX here. 67 | // The path starts with its separator (we work with absolute paths). 68 | const sep = mainPart[0]; 69 | const segments = mainPart.split(sep); 70 | 71 | // Moving from the end, find the first marker and cut the path from there. 72 | const startCutIndex = findLastIndex(segments, (segment) => 73 | etcherSegmentMarkers.includes(segment), 74 | ); 75 | return ( 76 | beginning + 77 | space + 78 | uriPrefix + 79 | '[PERSONAL PATH]' + 80 | sep + 81 | segments.splice(startCutIndex).join(sep) 82 | ); 83 | }; 84 | 85 | const safeAnonymizePath = (input: string) => { 86 | try { 87 | return anonymizePath(input); 88 | } catch (e) { 89 | return '[ANONYMIZE PATH FAILED]'; 90 | } 91 | }; 92 | 93 | const sensitiveEtcherProperties = [ 94 | 'error.description', 95 | 'error.message', 96 | 'error.stack', 97 | 'image', 98 | 'image.path', 99 | 'path', 100 | ]; 101 | 102 | export const anonymizeAnalyticsPayload = ( 103 | data: AnalyticsPayload, 104 | ): AnalyticsPayload => { 105 | for (const prop of sensitiveEtcherProperties) { 106 | const value = data[prop]; 107 | if (value != null) { 108 | data[prop] = safeAnonymizePath(value.toString()); 109 | } 110 | } 111 | return data; 112 | }; 113 | 114 | /** 115 | * @summary Init analytics configurations 116 | */ 117 | export const initAnalytics = once(() => { 118 | const dsn = 119 | settings.getSync('analyticsSentryToken') || process.env.SENTRY_TOKEN; 120 | SentryRenderer.init({ 121 | dsn, 122 | beforeSend: anonymizeSentryData, 123 | debug: process.env.ETCHER_SENTRY_DEBUG === 'true', 124 | }); 125 | }); 126 | 127 | /** 128 | * @summary Log an exception 129 | * 130 | * @description 131 | * This function logs an exception to error reporting services. 132 | */ 133 | export function logException(error: any) { 134 | const shouldReportErrors = settings.getSync('errorReporting'); 135 | console.error(error); 136 | if (shouldReportErrors) { 137 | initAnalytics(); 138 | SentryRenderer.captureException(error); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /lib/gui/app/modules/exception-reporter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { logException } from '../modules/analytics'; 18 | import { showError } from '../os/dialog'; 19 | 20 | /** 21 | * @summary Report an exception 22 | */ 23 | export function report(exception?: Error) { 24 | if (exception === undefined) { 25 | return; 26 | } 27 | showError(exception); 28 | logException(exception); 29 | } 30 | -------------------------------------------------------------------------------- /lib/gui/app/modules/progress-status.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License") 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import prettyBytes from 'pretty-bytes'; 18 | import * as i18next from 'i18next'; 19 | 20 | export interface FlashState { 21 | active: number; 22 | failed: number; 23 | percentage?: number; 24 | speed: number; 25 | position: number; 26 | type?: 'decompressing' | 'flashing' | 'verifying'; 27 | } 28 | 29 | export function fromFlashState({ 30 | type, 31 | percentage, 32 | position, 33 | }: Pick): { 34 | status: string; 35 | position?: string; 36 | } { 37 | console.log(i18next.t('progress.starting')); 38 | 39 | if (type === undefined) { 40 | return { status: i18next.t('progress.starting') }; 41 | } else if (type === 'decompressing') { 42 | if (percentage == null) { 43 | return { status: i18next.t('progress.decompressing') }; 44 | } else { 45 | return { 46 | position: `${percentage}%`, 47 | status: i18next.t('progress.decompressing'), 48 | }; 49 | } 50 | } else if (type === 'flashing') { 51 | if (percentage != null) { 52 | if (percentage < 100) { 53 | return { 54 | position: `${percentage}%`, 55 | status: i18next.t('progress.flashing'), 56 | }; 57 | } else { 58 | return { status: i18next.t('progress.finishing') }; 59 | } 60 | } else { 61 | return { 62 | status: i18next.t('progress.flashing'), 63 | position: `${position ? prettyBytes(position) : ''}`, 64 | }; 65 | } 66 | } else if (type === 'verifying') { 67 | if (percentage == null) { 68 | return { status: i18next.t('progress.verifying') }; 69 | } else if (percentage < 100) { 70 | return { 71 | position: `${percentage}%`, 72 | status: i18next.t('progress.verifying'), 73 | }; 74 | } else { 75 | return { status: i18next.t('progress.finishing') }; 76 | } 77 | } 78 | return { status: i18next.t('progress.failing') }; 79 | } 80 | 81 | export function titleFromFlashState( 82 | state: Pick, 83 | ): string { 84 | const { status, position } = fromFlashState(state); 85 | if (position !== undefined) { 86 | return `${position} ${status}`; 87 | } 88 | return status; 89 | } 90 | -------------------------------------------------------------------------------- /lib/gui/app/os/dialog.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import * as remote from '@electron/remote'; 19 | import * as _ from 'lodash'; 20 | 21 | import * as errors from '../../../shared/errors'; 22 | import * as settings from '../../../gui/app/models/settings'; 23 | import { SUPPORTED_EXTENSIONS } from '../../../shared/supported-formats'; 24 | import * as i18next from 'i18next'; 25 | 26 | async function mountSourceDrive() { 27 | // sourceDrivePath is the name of the link in /dev/disk/by-path 28 | const sourceDrivePath = await settings.get('automountOnFileSelect'); 29 | if (sourceDrivePath) { 30 | try { 31 | await electron.ipcRenderer.invoke('mount-drive', sourceDrivePath); 32 | } catch (error: any) { 33 | // noop 34 | } 35 | } 36 | } 37 | 38 | /** 39 | * @summary Open an image selection dialog 40 | * 41 | * @description 42 | * Notice that by image, we mean *.img/*.iso/*.zip/etc files. 43 | */ 44 | export async function selectImage(): Promise { 45 | await mountSourceDrive(); 46 | const options: electron.OpenDialogOptions = { 47 | // This variable is set when running in GNU/Linux from 48 | // inside an AppImage, and represents the working directory 49 | // from where the AppImage was run (which might not be the 50 | // place where the AppImage is located). `OWD` stands for 51 | // "Original Working Directory". 52 | // 53 | // See: https://github.com/probonopd/AppImageKit/commit/1569d6f8540aa6c2c618dbdb5d6fcbf0003952b7 54 | defaultPath: process.env.OWD, 55 | properties: ['openFile', 'treatPackageAsDirectory'], 56 | filters: [ 57 | { 58 | name: i18next.t('source.osImages'), 59 | extensions: SUPPORTED_EXTENSIONS, 60 | }, 61 | { 62 | name: i18next.t('source.allFiles'), 63 | extensions: ['*'], 64 | }, 65 | ], 66 | }; 67 | const currentWindow = remote.getCurrentWindow(); 68 | const [file] = (await remote.dialog.showOpenDialog(currentWindow, options)) 69 | .filePaths; 70 | return file; 71 | } 72 | 73 | /** 74 | * @summary Open a warning dialog 75 | */ 76 | export async function showWarning(options: { 77 | confirmationLabel: string; 78 | rejectionLabel: string; 79 | title: string; 80 | description: string; 81 | }): Promise { 82 | _.defaults(options, { 83 | confirmationLabel: i18next.t('ok'), 84 | rejectionLabel: i18next.t('cancel'), 85 | }); 86 | 87 | const BUTTONS = [options.confirmationLabel, options.rejectionLabel]; 88 | 89 | const BUTTON_CONFIRMATION_INDEX = _.indexOf( 90 | BUTTONS, 91 | options.confirmationLabel, 92 | ); 93 | const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel); 94 | 95 | const { response } = await remote.dialog.showMessageBox( 96 | remote.getCurrentWindow(), 97 | { 98 | type: 'warning', 99 | buttons: BUTTONS, 100 | defaultId: BUTTON_REJECTION_INDEX, 101 | cancelId: BUTTON_REJECTION_INDEX, 102 | title: i18next.t('attention'), 103 | message: options.title, 104 | detail: options.description, 105 | }, 106 | ); 107 | return response === BUTTON_CONFIRMATION_INDEX; 108 | } 109 | 110 | /** 111 | * @summary Show error dialog for an Error instance 112 | */ 113 | export function showError(error: Error) { 114 | const title = errors.getTitle(error); 115 | const message = errors.getDescription(error); 116 | remote.dialog.showErrorBox(title, message); 117 | } 118 | -------------------------------------------------------------------------------- /lib/gui/app/os/notification.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as remote from '@electron/remote'; 18 | 19 | import * as settings from '../models/settings'; 20 | 21 | /** 22 | * @summary Send a notification 23 | */ 24 | export async function send(title: string, body: string, icon: string) { 25 | // Bail out if desktop notifications are disabled 26 | if (!(await settings.get('desktopNotifications'))) { 27 | return; 28 | } 29 | 30 | // `app.dock` is only defined in OS X 31 | if (remote.app.dock) { 32 | remote.app.dock.bounce(); 33 | } 34 | 35 | return new window.Notification(title, { body, icon }); 36 | } 37 | -------------------------------------------------------------------------------- /lib/gui/app/os/open-external/services/open-external.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import * as settings from '../../../models/settings'; 19 | 20 | /** 21 | * @summary Open an external resource 22 | */ 23 | export async function open(url: string) { 24 | // Don't open links if they're disabled by the env var 25 | if (await settings.get('disableExternalLinks')) { 26 | return; 27 | } 28 | 29 | if (url) { 30 | electron.shell.openExternal(url); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/gui/app/os/window-progress.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as remote from '@electron/remote'; 18 | 19 | import { percentageToFloat } from '../../../shared/utils'; 20 | import type { FlashState } from '../modules/progress-status'; 21 | import { titleFromFlashState } from '../modules/progress-status'; 22 | 23 | /** 24 | * @summary The title of the main window upon program launch 25 | */ 26 | const INITIAL_TITLE = document.title; 27 | 28 | /** 29 | * @summary Make the full window status title 30 | */ 31 | function getWindowTitle(state?: FlashState) { 32 | if (state) { 33 | return `${INITIAL_TITLE} – ${titleFromFlashState(state)}`; 34 | } 35 | return INITIAL_TITLE; 36 | } 37 | 38 | /** 39 | * @summary A reference to the current renderer Electron window 40 | * 41 | * @description 42 | * We expose this property to `this` for testability purposes. 43 | */ 44 | export const currentWindow = remote.getCurrentWindow(); 45 | 46 | /** 47 | * @summary Set operating system window progress 48 | * 49 | * @description 50 | * Show progress inline in operating system task bar 51 | */ 52 | export function set(state: FlashState) { 53 | if (state.percentage != null) { 54 | currentWindow.setProgressBar(percentageToFloat(state.percentage)); 55 | } 56 | currentWindow.setTitle(getWindowTitle(state)); 57 | } 58 | 59 | /** 60 | * @summary Clear the window progress bar 61 | */ 62 | export function clear() { 63 | // Passing 0 or null/undefined doesn't work. 64 | currentWindow.setProgressBar(-1); 65 | currentWindow.setTitle(getWindowTitle(undefined)); 66 | } 67 | -------------------------------------------------------------------------------- /lib/gui/app/os/windows-network-drives.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { exec } from 'child_process'; 18 | import { withTmpFile } from 'etcher-sdk/build/tmp'; 19 | import { readFile } from 'fs'; 20 | import { chain, trim } from 'lodash'; 21 | import { platform } from 'os'; 22 | import { join } from 'path'; 23 | import { env } from 'process'; 24 | import { promisify } from 'util'; 25 | 26 | const readFileAsync = promisify(readFile); 27 | 28 | const execAsync = promisify(exec); 29 | 30 | /** 31 | * @summary Returns wmic's output for network drives 32 | */ 33 | async function getWmicNetworkDrivesOutput(): Promise { 34 | // When trying to read wmic's stdout directly from node, it is encoded with the current 35 | // console codepage (depending on the computer). 36 | // Decoding this would require getting this codepage somehow and using iconv as node 37 | // doesn't know how to read cp850 directly for example. 38 | // We could also use wmic's "/output:" switch but it doesn't work when the filename 39 | // contains a space and the os temp dir may contain spaces ("D:\Windows Temp Files" for example). 40 | // So we just redirect to a file and read it afterwards as we know it will be ucs2 encoded. 41 | const options = { 42 | // Close the file once it's created 43 | keepOpen: false, 44 | // Wmic fails with "Invalid global switch" when the "/output:" switch filename contains a dash ("-") 45 | prefix: 'tmp', 46 | }; 47 | return withTmpFile(options, async ({ path }) => { 48 | const command = [ 49 | join(env.SystemRoot as string, 'System32', 'Wbem', 'wmic'), 50 | 'path', 51 | 'Win32_LogicalDisk', 52 | 'Where', 53 | 'DriveType="4"', 54 | 'get', 55 | 'DeviceID,ProviderName', 56 | '>', 57 | `"${path}"`, 58 | ]; 59 | await execAsync(command.join(' '), { windowsHide: true }); 60 | return readFileAsync(path, 'ucs2'); 61 | }); 62 | } 63 | 64 | /** 65 | * @summary returns a Map of drive letter -> network locations on Windows: 'Z:' -> '\\\\192.168.0.1\\Public' 66 | */ 67 | async function getWindowsNetworkDrives( 68 | getWmicOutput: () => Promise, 69 | ): Promise> { 70 | const result = await getWmicOutput(); 71 | const couples: Array<[string, string]> = chain(result) 72 | .split('\n') 73 | // Remove header line 74 | .slice(1) 75 | // Remove extra spaces / tabs / carriage returns 76 | .invokeMap(String.prototype.trim) 77 | // Filter out empty lines 78 | .compact() 79 | .map((str: string): [string, string] => { 80 | const colonPosition = str.indexOf(':'); 81 | if (colonPosition === -1) { 82 | throw new Error(`Can't parse wmic output: ${result}`); 83 | } 84 | return [ 85 | str.slice(0, colonPosition + 1), 86 | trim(str.slice(colonPosition + 1)), 87 | ]; 88 | }) 89 | .filter((couple) => couple[1].length > 0) 90 | .value(); 91 | return new Map(couples); 92 | } 93 | 94 | /** 95 | * @summary Replaces network drive letter with network drive location in the provided filePath on Windows 96 | */ 97 | export async function replaceWindowsNetworkDriveLetter( 98 | filePath: string, 99 | // getWmicOutput is a parameter so it can be replaced in tests 100 | getWmicOutput = getWmicNetworkDrivesOutput, 101 | ): Promise { 102 | let result = filePath; 103 | if (platform() === 'win32') { 104 | const matches = /^([A-Z]+:)\\(.*)$/.exec(filePath); 105 | if (matches !== null) { 106 | const [, drive, relativePath] = matches; 107 | const drives = await getWindowsNetworkDrives(getWmicOutput); 108 | const location = drives.get(drive); 109 | if (location !== undefined) { 110 | result = `${location}\\${relativePath}`; 111 | } 112 | } 113 | } 114 | return result; 115 | } 116 | -------------------------------------------------------------------------------- /lib/gui/app/preload.ts: -------------------------------------------------------------------------------- 1 | // See the Electron documentation for details on how to use preload scripts: 2 | // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts 3 | 4 | import * as webapi from '../webapi'; 5 | 6 | declare global { 7 | interface Window { 8 | etcher: typeof webapi; 9 | } 10 | } 11 | 12 | window['etcher'] = webapi; 13 | -------------------------------------------------------------------------------- /lib/gui/app/renderer.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { main } from './app'; 3 | import './i18n'; 4 | import { langParser } from './i18n'; 5 | import { ipcRenderer } from 'electron'; 6 | 7 | ipcRenderer.send('change-lng', langParser()); 8 | 9 | main(); 10 | -------------------------------------------------------------------------------- /lib/gui/app/theme.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"), 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as _ from 'lodash'; 18 | import { Theme } from 'rendition'; 19 | 20 | export const colors = { 21 | dark: { 22 | foreground: '#fff', 23 | background: '#4d5057', 24 | soft: { 25 | foreground: '#ddd', 26 | background: '#64686a', 27 | }, 28 | disabled: { 29 | foreground: '#787c7f', 30 | background: '#3a3c41', 31 | }, 32 | }, 33 | light: { 34 | foreground: '#666', 35 | background: '#fff', 36 | soft: { 37 | foreground: '#b3b3b3', 38 | }, 39 | disabled: { 40 | foreground: '#787c7f', 41 | background: '#d5d5d5', 42 | }, 43 | }, 44 | default: { 45 | foreground: '#b3b3b3', 46 | background: '#ececec', 47 | }, 48 | primary: { 49 | foreground: '#fff', 50 | background: '#00aeef', 51 | }, 52 | secondary: { 53 | foreground: '#000', 54 | background: '#ddd', 55 | main: '#fff', 56 | }, 57 | warning: { 58 | foreground: '#fff', 59 | background: '#fca321', 60 | }, 61 | danger: { 62 | foreground: '#fff', 63 | background: '#d9534f', 64 | }, 65 | success: { 66 | foreground: '#fff', 67 | background: '#5fb835', 68 | }, 69 | }; 70 | 71 | const font = 'SourceSansPro'; 72 | 73 | export const theme = _.merge({}, Theme, { 74 | colors, 75 | font, 76 | header: { 77 | height: '40px', 78 | }, 79 | global: { 80 | font: { 81 | family: font, 82 | size: 16, 83 | }, 84 | text: { 85 | medium: { 86 | size: 16, 87 | }, 88 | }, 89 | }, 90 | button: { 91 | border: { 92 | width: '0', 93 | radius: '24px', 94 | }, 95 | disabled: { 96 | opacity: 1, 97 | }, 98 | extend: () => ` 99 | width: 200px; 100 | font-size: 16px; 101 | 102 | && { 103 | width: 200px; 104 | height: 48px; 105 | } 106 | 107 | :disabled { 108 | background-color: ${colors.dark.disabled.background}; 109 | color: ${colors.dark.disabled.foreground}; 110 | opacity: 1; 111 | 112 | :hover { 113 | background-color: ${colors.dark.disabled.background}; 114 | color: ${colors.dark.disabled.foreground}; 115 | } 116 | } 117 | `, 118 | }, 119 | layer: { 120 | extend: () => ` 121 | > div:first-child { 122 | background-color: transparent; 123 | } 124 | `, 125 | }, 126 | }); 127 | -------------------------------------------------------------------------------- /lib/gui/app/utils/etcher-pro-specific.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import type { Dictionary } from 'lodash'; 18 | 19 | type BalenaTag = { 20 | id: number; 21 | name: string; 22 | value: string; 23 | }; 24 | 25 | export class EtcherPro { 26 | private supervisorAddr: string; 27 | private supervisorKey: string; 28 | private tags: Dictionary | undefined; 29 | public uuid: string; 30 | 31 | constructor(supervisorAddr: string, supervisorKey: string) { 32 | this.supervisorAddr = supervisorAddr; 33 | this.supervisorKey = supervisorKey; 34 | this.uuid = (process.env.BALENA_DEVICE_UUID ?? 'NO-UUID').substring(0, 7); 35 | this.tags = undefined; 36 | this.get_tags().then((tags) => (this.tags = tags)); 37 | } 38 | 39 | async get_tags(): Promise> { 40 | const result = await fetch( 41 | this.supervisorAddr + '/v2/device/tags?apikey=' + this.supervisorKey, 42 | ); 43 | const parsed = await result.json(); 44 | if (parsed['status'] === 'success') { 45 | return Object.assign( 46 | {}, 47 | ...parsed['tags'].map((tag: BalenaTag) => { 48 | return { [tag.name]: tag.value }; 49 | }), 50 | ); 51 | } else { 52 | return {}; 53 | } 54 | } 55 | 56 | public get_serial(): string | undefined { 57 | if (this.tags) { 58 | return this.tags['Serial']; 59 | } else { 60 | return undefined; 61 | } 62 | } 63 | } 64 | 65 | export function etcherProInfo(): EtcherPro | undefined { 66 | const BALENA_SUPERVISOR_ADDRESS = process.env.BALENA_SUPERVISOR_ADDRESS; 67 | const BALENA_SUPERVISOR_API_KEY = process.env.BALENA_SUPERVISOR_API_KEY; 68 | 69 | if (BALENA_SUPERVISOR_ADDRESS && BALENA_SUPERVISOR_API_KEY) { 70 | return new EtcherPro(BALENA_SUPERVISOR_ADDRESS, BALENA_SUPERVISOR_API_KEY); 71 | } 72 | return undefined; 73 | } 74 | -------------------------------------------------------------------------------- /lib/gui/app/utils/middle-ellipsis.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 Juan Cruz Viotti. https://github.com/jviotti 3 | * Copyright 2018 balena.io 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * @summary Truncate text from the middle with an ellipsis 20 | */ 21 | export function middleEllipsis(input: string, limit: number): string { 22 | // We can't provide a 100% expected result if the limit is less than 3. For example: 23 | // 24 | // If the limit == 2: 25 | // Should we display the first at last character without an ellipses in the middle? 26 | // Should we display just one character and an ellipses before or after? 27 | // Should we display nothing at all? 28 | // 29 | // If the limit == 1: 30 | // Should we display just one character? 31 | // Should we display just an ellipses? 32 | // Should we display nothing at all? 33 | // 34 | // Etc. 35 | if (limit < 3) { 36 | throw new Error('middleEllipsis: Limit should be at least 3'); 37 | } 38 | 39 | // Do nothing, the string doesn't need truncation. 40 | if (input.length <= limit) { 41 | return input; 42 | } 43 | 44 | const lengthOfTheSidesAfterTruncation = Math.floor((limit - 1) / 2); 45 | const finalLeftPart = input.slice(0, lengthOfTheSidesAfterTruncation); 46 | const finalRightPart = input.slice( 47 | input.length - lengthOfTheSidesAfterTruncation, 48 | ); 49 | 50 | return finalLeftPart + '…' + finalRightPart; 51 | } 52 | -------------------------------------------------------------------------------- /lib/gui/assets/balena.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/drive.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/etcher.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/flash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/image.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/love.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/raspberrypi.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/assets/src.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lib/gui/assets/tgt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/gui/menu.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as electron from 'electron'; 18 | import { displayName } from '../../package.json'; 19 | 20 | import * as i18next from 'i18next'; 21 | 22 | /** 23 | * @summary Builds a native application menu for a given window 24 | */ 25 | export function buildWindowMenu(window: electron.BrowserWindow) { 26 | /** 27 | * @summary Toggle the main window's devtools 28 | */ 29 | function toggleDevTools() { 30 | if (!window) { 31 | return; 32 | } 33 | // NOTE: We can't use `webContents.toggleDevTools()` here, 34 | // as we need to force detached mode 35 | if (window.webContents.isDevToolsOpened()) { 36 | window.webContents.closeDevTools(); 37 | } else { 38 | window.webContents.openDevTools({ 39 | mode: 'detach', 40 | }); 41 | } 42 | } 43 | 44 | const menuTemplate: electron.MenuItemConstructorOptions[] = [ 45 | { 46 | role: 'editMenu', 47 | label: i18next.t('menu.edit'), 48 | }, 49 | { 50 | label: i18next.t('menu.view'), 51 | submenu: [ 52 | { 53 | label: i18next.t('menu.devTool'), 54 | accelerator: 55 | process.platform === 'darwin' ? 'Command+Alt+I' : 'Control+Shift+I', 56 | click: toggleDevTools, 57 | }, 58 | ], 59 | }, 60 | { 61 | role: 'windowMenu', 62 | label: i18next.t('menu.window'), 63 | }, 64 | { 65 | role: 'help', 66 | label: i18next.t('menu.help'), 67 | submenu: [ 68 | { 69 | label: i18next.t('menu.pro'), 70 | click() { 71 | electron.shell.openExternal( 72 | 'https://etcher.io/pro?utm_source=etcher_menu&ref=etcher_menu', 73 | ); 74 | }, 75 | }, 76 | { 77 | label: i18next.t('menu.website'), 78 | click() { 79 | electron.shell.openExternal('https://etcher.io?ref=etcher_menu'); 80 | }, 81 | }, 82 | { 83 | label: i18next.t('menu.issue'), 84 | click() { 85 | electron.shell.openExternal( 86 | 'https://github.com/balena-io/etcher/issues', 87 | ); 88 | }, 89 | }, 90 | ], 91 | }, 92 | ]; 93 | 94 | if (process.platform === 'darwin') { 95 | menuTemplate.unshift({ 96 | label: displayName, 97 | submenu: [ 98 | { 99 | role: 'about' as const, 100 | label: i18next.t('menu.about'), 101 | }, 102 | { 103 | type: 'separator' as const, 104 | }, 105 | { 106 | role: 'hide' as const, 107 | label: i18next.t('menu.hide'), 108 | }, 109 | { 110 | role: 'hideOthers' as const, 111 | label: i18next.t('menu.hideOthers'), 112 | }, 113 | { 114 | role: 'unhide' as const, 115 | label: i18next.t('menu.unhide'), 116 | }, 117 | { 118 | type: 'separator' as const, 119 | }, 120 | { 121 | role: 'quit' as const, 122 | label: i18next.t('menu.quit'), 123 | }, 124 | ], 125 | }); 126 | } else { 127 | menuTemplate.unshift({ 128 | label: displayName, 129 | submenu: [ 130 | { 131 | role: 'quit', 132 | }, 133 | ], 134 | }); 135 | } 136 | 137 | const menu = electron.Menu.buildFromTemplate(menuTemplate); 138 | 139 | electron.Menu.setApplicationMenu(menu); 140 | } 141 | -------------------------------------------------------------------------------- /lib/gui/webapi.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Anything exported from this module will become available to the 3 | // renderer process via preload. They're accessible as `window.etcher.foo()`. 4 | // 5 | 6 | import { ipcRenderer } from 'electron'; 7 | 8 | // FIXME: this is a workaround for the renderer to be able to find the etcher-util 9 | // binary. We should instead export a function that asks the main process to launch 10 | // the binary itself. 11 | export async function getEtcherUtilPath(): Promise { 12 | const utilPath = await ipcRenderer.invoke('get-util-path'); 13 | console.log(utilPath); 14 | return utilPath; 15 | } 16 | -------------------------------------------------------------------------------- /lib/shared/exit-codes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export const SUCCESS = 0; 18 | export const GENERAL_ERROR = 1; 19 | export const VALIDATION_ERROR = 2; 20 | export const CANCELLED = 3; 21 | -------------------------------------------------------------------------------- /lib/shared/sudo/darwin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { spawn } from 'child_process'; 18 | import { join } from 'path'; 19 | import { env } from 'process'; 20 | // import { promisify } from "util"; 21 | 22 | import { supportedLocales } from '../../gui/app/i18n'; 23 | 24 | // const execFileAsync = promisify(execFile); 25 | 26 | const SUCCESSFUL_AUTH_MARKER = 'AUTHENTICATION SUCCEEDED'; 27 | const EXPECTED_SUCCESSFUL_AUTH_MARKER = `${SUCCESSFUL_AUTH_MARKER}\n`; 28 | 29 | function getAskPassScriptPath(lang: string): string { 30 | if (process.env.NODE_ENV === 'development') { 31 | // Force webpack's hand to bundle the script. 32 | return require.resolve(`./sudo-askpass.osascript-${lang}.js`); 33 | } 34 | // Otherwise resolve the script relative to resources path. 35 | return join(process.resourcesPath, `sudo-askpass.osascript-${lang}.js`); 36 | } 37 | 38 | export async function sudo( 39 | command: string, 40 | ): Promise<{ cancelled: boolean; stdout?: string; stderr?: string }> { 41 | try { 42 | let lang = Intl.DateTimeFormat().resolvedOptions().locale; 43 | lang = lang.substr(0, 2); 44 | if (supportedLocales.indexOf(lang) > -1) { 45 | // language should be present 46 | } else { 47 | // fallback to eng 48 | lang = 'en'; 49 | } 50 | 51 | const elevateProcess = spawn( 52 | 'sudo', 53 | ['--askpass', 'sh', '-c', `echo ${SUCCESSFUL_AUTH_MARKER} && ${command}`], 54 | { 55 | // encoding: "utf8", 56 | env: { 57 | PATH: env.PATH, 58 | SUDO_ASKPASS: getAskPassScriptPath(lang), 59 | }, 60 | }, 61 | ); 62 | 63 | let elevated = 'pending'; 64 | 65 | elevateProcess.stdout.on('data', (data) => { 66 | if (data.toString().includes(SUCCESSFUL_AUTH_MARKER)) { 67 | // if the first data comming out of the sudo command is the expected marker we resolve the promise 68 | elevated = 'granted'; 69 | } else { 70 | // if the first data comming out of the sudo command is not the expected marker we reject the promise 71 | elevated = 'rejected'; 72 | } 73 | }); 74 | 75 | // we don't spawn or read stdout in the promise otherwise resolving stop the process 76 | return new Promise((resolve, reject) => { 77 | const checkElevation = setInterval(() => { 78 | if (elevated === 'granted') { 79 | clearInterval(checkElevation); 80 | resolve({ cancelled: false }); 81 | } else if (elevated === 'rejected') { 82 | clearInterval(checkElevation); 83 | resolve({ cancelled: true }); 84 | } 85 | }, 300); 86 | 87 | // if the elevation didn't occured in 30 seconds we reject the promise 88 | setTimeout(() => { 89 | clearInterval(checkElevation); 90 | reject(new Error('Elevation timeout')); 91 | }, 30000); 92 | }); 93 | } catch (error: any) { 94 | if (error.code === 1) { 95 | if (!error.stdout.startsWith(EXPECTED_SUCCESSFUL_AUTH_MARKER)) { 96 | return { cancelled: true }; 97 | } 98 | error.stdout = error.stdout.slice(EXPECTED_SUCCESSFUL_AUTH_MARKER.length); 99 | } 100 | throw error; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /lib/shared/sudo/sudo-askpass.osascript-en.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | 3 | ObjC.import('stdlib') 4 | 5 | const app = Application.currentApplication() 6 | app.includeStandardAdditions = true 7 | 8 | const result = app.displayDialog('balenaEtcher needs privileged access in order to flash disks.\n\nType your password to allow this.', { 9 | defaultAnswer: '', 10 | withIcon: 'caution', 11 | buttons: ['Cancel', 'Ok'], 12 | defaultButton: 'Ok', 13 | hiddenAnswer: true, 14 | }) 15 | 16 | if (result.buttonReturned === 'Ok') { 17 | result.textReturned 18 | } else { 19 | $.exit(255) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /lib/shared/sudo/sudo-askpass.osascript-zh.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env osascript -l JavaScript 2 | 3 | ObjC.import('stdlib') 4 | 5 | const app = Application.currentApplication() 6 | app.includeStandardAdditions = true 7 | 8 | const result = app.displayDialog('balenaEtcher 需要来自管理员的权限才能烧录镜像到磁盘。\n\n输入您的密码以允许此操作。', { 9 | defaultAnswer: '', 10 | withIcon: 'caution', 11 | buttons: ['取消', '好'], 12 | defaultButton: '好', 13 | hiddenAnswer: true, 14 | }) 15 | 16 | if (result.buttonReturned === '好') { 17 | result.textReturned 18 | } else { 19 | $.exit(255) 20 | } 21 | 22 | -------------------------------------------------------------------------------- /lib/shared/supported-formats.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { basename } from 'path'; 18 | 19 | export const SUPPORTED_EXTENSIONS = [ 20 | 'bin', 21 | 'bz2', 22 | 'dmg', 23 | 'dsk', 24 | 'etch', 25 | 'gz', 26 | 'hddimg', 27 | 'img', 28 | 'iso', 29 | 'raw', 30 | 'rpi-sdimg', 31 | 'sdcard', 32 | 'vhd', 33 | 'wic', 34 | 'xz', 35 | 'zip', 36 | ]; 37 | 38 | export function looksLikeWindowsImage(imagePath: string): boolean { 39 | const regex = /windows|win7|win8|win10|winxp/i; 40 | return regex.test(basename(imagePath)); 41 | } 42 | -------------------------------------------------------------------------------- /lib/shared/typings/source-selector.ts: -------------------------------------------------------------------------------- 1 | import type { GPTPartition, MBRPartition } from 'partitioninfo'; 2 | import type { sourceDestination } from 'etcher-sdk'; 3 | import type { DrivelistDrive } from '../drive-constraints'; 4 | 5 | export type Source = 'File' | 'BlockDevice' | 'Http'; 6 | 7 | export interface SourceMetadata extends sourceDestination.Metadata { 8 | hasMBR?: boolean; 9 | partitions?: MBRPartition[] | GPTPartition[]; 10 | path: string; 11 | displayName: string; 12 | description: string; 13 | SourceType: Source; 14 | drive?: DrivelistDrive; 15 | extension?: string; 16 | archiveExtension?: string; 17 | auth?: Authentication; 18 | } 19 | 20 | export interface Authentication { 21 | username: string; 22 | password: string; 23 | } 24 | -------------------------------------------------------------------------------- /lib/shared/units.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const MEGABYTE_TO_BYTE_RATIO = 1000000; 18 | 19 | export function bytesToMegabytes(bytes: number): number { 20 | return bytes / MEGABYTE_TO_BYTE_RATIO; 21 | } 22 | -------------------------------------------------------------------------------- /lib/shared/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as errors from './errors'; 18 | 19 | export function isValidPercentage(percentage: any): boolean { 20 | return typeof percentage === 'number' && percentage >= 0 && percentage <= 100; 21 | } 22 | 23 | export function percentageToFloat(percentage: any) { 24 | if (!isValidPercentage(percentage)) { 25 | throw errors.createError({ 26 | title: `Invalid percentage: ${percentage}`, 27 | }); 28 | } 29 | return percentage / 100; 30 | } 31 | 32 | export async function delay(duration: number): Promise { 33 | await new Promise((resolve) => { 34 | setTimeout(resolve, duration); 35 | }); 36 | } 37 | 38 | export function isJson(jsonString: string) { 39 | try { 40 | JSON.parse(jsonString); 41 | } catch (e) { 42 | return false; 43 | } 44 | return true; 45 | } 46 | -------------------------------------------------------------------------------- /lib/util/drive-scanner.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import * as sdk from 'etcher-sdk'; 18 | import type { Adapter } from 'etcher-sdk/build/scanner/adapters'; 19 | import { 20 | BlockDeviceAdapter, 21 | UsbbootDeviceAdapter, 22 | } from 'etcher-sdk/build/scanner/adapters'; 23 | import { geteuid, platform } from 'process'; 24 | 25 | const adapters: Adapter[] = [ 26 | new BlockDeviceAdapter({ 27 | includeSystemDrives: () => true, 28 | }), 29 | ]; 30 | 31 | // Can't use permissions.isElevated() here as it returns a promise and we need to set 32 | // module.exports = scanner right now. 33 | if (platform !== 'linux' || (geteuid && geteuid() === 0)) { 34 | adapters.push(new UsbbootDeviceAdapter()); 35 | } 36 | 37 | if (platform === 'win32') { 38 | const { 39 | DriverlessDeviceAdapter: driverless, 40 | } = require('etcher-sdk/build/scanner/adapters/driverless'); 41 | adapters.push(new driverless()); 42 | } 43 | 44 | export const scanner = new sdk.scanner.Scanner(adapters); 45 | -------------------------------------------------------------------------------- /lib/util/source-metadata.ts: -------------------------------------------------------------------------------- 1 | /** Get metadata for a source */ 2 | 3 | import { sourceDestination } from 'etcher-sdk'; 4 | import { replaceWindowsNetworkDriveLetter } from '../gui/app/os/windows-network-drives'; 5 | import type { AxiosRequestConfig } from 'axios'; 6 | import axios from 'axios'; 7 | import { isJson } from '../shared/utils'; 8 | import * as path from 'path'; 9 | import type { 10 | SourceMetadata, 11 | Authentication, 12 | Source, 13 | } from '../shared/typings/source-selector'; 14 | import type { DrivelistDrive } from '../shared/drive-constraints'; 15 | import { omit } from 'lodash'; 16 | 17 | function isString(value: any): value is string { 18 | return typeof value === 'string'; 19 | } 20 | 21 | async function createSource( 22 | selected: string, 23 | SourceType: Source, 24 | auth?: Authentication, 25 | ) { 26 | try { 27 | selected = await replaceWindowsNetworkDriveLetter(selected); 28 | } catch (error: any) { 29 | // TODO: analytics.logException(error); 30 | } 31 | 32 | if (isJson(decodeURIComponent(selected))) { 33 | const config: AxiosRequestConfig = JSON.parse(decodeURIComponent(selected)); 34 | return new sourceDestination.Http({ 35 | url: config.url!, 36 | axiosInstance: axios.create(omit(config, ['url'])), 37 | }); 38 | } 39 | 40 | if (SourceType === 'File') { 41 | return new sourceDestination.File({ 42 | path: selected, 43 | }); 44 | } 45 | 46 | return new sourceDestination.Http({ url: selected, auth }); 47 | } 48 | 49 | async function getMetadata( 50 | source: sourceDestination.SourceDestination, 51 | selected: string | DrivelistDrive, 52 | ) { 53 | const metadata = (await source.getMetadata()) as SourceMetadata; 54 | const partitionTable = await source.getPartitionTable(); 55 | if (partitionTable) { 56 | metadata.hasMBR = true; 57 | metadata.partitions = partitionTable.partitions; 58 | } else { 59 | metadata.hasMBR = false; 60 | } 61 | if (isString(selected)) { 62 | metadata.extension = path.extname(selected).slice(1); 63 | metadata.path = selected; 64 | } 65 | return metadata; 66 | } 67 | 68 | async function getSourceMetadata( 69 | selected: string | DrivelistDrive, 70 | SourceType: Source, 71 | auth?: Authentication, 72 | ): Promise> { 73 | // `Record` means an empty object 74 | if (isString(selected)) { 75 | const source = await createSource(selected, SourceType, auth); 76 | 77 | try { 78 | const innerSource = await source.getInnerSource(); 79 | 80 | const metadata = await getMetadata(innerSource, selected); 81 | 82 | return metadata; 83 | } catch (error: any) { 84 | // TODO: handle error 85 | return {}; 86 | } finally { 87 | await source.close(); 88 | } 89 | } else { 90 | return {}; 91 | } 92 | } 93 | 94 | export { getSourceMetadata }; 95 | -------------------------------------------------------------------------------- /lib/util/types/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'etcher-sdk/build/source-destination'; 2 | import type { SourceMetadata } from '../../shared/typings/source-selector'; 3 | import type { Drive as DrivelistDrive } from 'drivelist'; 4 | 5 | export interface WriteResult { 6 | bytesWritten?: number; 7 | devices?: { 8 | failed: number; 9 | successful: number; 10 | }; 11 | errors: FlashError[]; 12 | sourceMetadata?: Metadata; 13 | } 14 | 15 | export interface FlashError extends Error { 16 | description: string; 17 | device: string; 18 | code: string; 19 | } 20 | 21 | export interface FlashResults extends WriteResult { 22 | skip?: boolean; 23 | cancelled?: boolean; 24 | } 25 | 26 | interface WriteOptions { 27 | image: SourceMetadata; 28 | destinations: DrivelistDrive[]; 29 | autoBlockmapping: boolean; 30 | decompressFirst: boolean; 31 | SourceType: string; 32 | httpRequest?: any; 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "balena-etcher", 3 | "private": true, 4 | "displayName": "balenaEtcher", 5 | "productName": "balenaEtcher", 6 | "version": "2.1.3", 7 | "packageType": "local", 8 | "main": ".webpack/main", 9 | "description": "Flash OS images to SD cards and USB drives, safely and easily.", 10 | "productDescription": "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.", 11 | "homepage": "https://github.com/balena-io/etcher", 12 | "repository": { 13 | "type": "git", 14 | "url": "git@github.com:balena-io/etcher.git" 15 | }, 16 | "scripts": { 17 | "prettify": "prettier --write lib/**/*.css && balena-lint --fix --typescript typings lib tests forge.config.ts forge.sidecar.ts webpack.config.ts", 18 | "lint": "npm run prettify && catch-uncommitted", 19 | "test": "echo 'Only use custom tests; if you want to test locally, use `npm run wdio`' && exit 0", 20 | "package": "electron-forge package", 21 | "start": "electron-forge start", 22 | "make": "electron-forge make", 23 | "wdio": "xvfb-maybe wdio run ./wdio.conf.ts" 24 | }, 25 | "husky": { 26 | "hooks": { 27 | "pre-commit": "npm run prettify" 28 | } 29 | }, 30 | "author": "Balena Ltd. ", 31 | "license": "Apache-2.0", 32 | "dependencies": { 33 | "@electron/remote": "^2.1.2", 34 | "@fortawesome/fontawesome-free": "^6.5.2", 35 | "@ronomon/direct-io": "^3.0.1", 36 | "@sentry/electron": "^4.24.0", 37 | "axios": "^1.6.8", 38 | "debug": "4.3.4", 39 | "drivelist": "^12.0.2", 40 | "electron-squirrel-startup": "^1.0.0", 41 | "electron-updater": "6.1.8", 42 | "etcher-sdk": "9.1.2", 43 | "i18next": "23.11.2", 44 | "immutable": "3.8.2", 45 | "lodash": "4.17.21", 46 | "outdent": "0.8.0", 47 | "path-is-inside": "1.0.2", 48 | "pretty-bytes": "6.1.1", 49 | "react": "17.0.2", 50 | "react-dom": "17.0.2", 51 | "react-i18next": "13.5.0", 52 | "redux": "4.2.1", 53 | "rendition": "35.2.0", 54 | "semver": "7.6.0", 55 | "styled-components": "5.3.6", 56 | "sys-class-rgb-led": "3.0.1", 57 | "uuid": "9.0.1", 58 | "ws": "^8.16.0" 59 | }, 60 | "devDependencies": { 61 | "@balena/lint": "8.0.2", 62 | "@electron-forge/cli": "7.4.0", 63 | "@electron-forge/maker-deb": "7.4.0", 64 | "@electron-forge/maker-dmg": "7.4.0", 65 | "@electron-forge/maker-rpm": "7.4.0", 66 | "@electron-forge/maker-squirrel": "7.4.0", 67 | "@electron-forge/maker-zip": "7.4.0", 68 | "@electron-forge/plugin-auto-unpack-natives": "7.4.0", 69 | "@electron-forge/plugin-webpack": "7.4.0", 70 | "@reforged/maker-appimage": "3.3.2", 71 | "@svgr/webpack": "8.1.0", 72 | "@types/chai": "4.3.14", 73 | "@types/debug": "^4.1.12", 74 | "@types/mime-types": "2.1.4", 75 | "@types/node": "^20.11.6", 76 | "@types/react": "17.0.2", 77 | "@types/react-dom": "17.0.2", 78 | "@types/semver": "7.5.8", 79 | "@types/sinon": "17.0.3", 80 | "@types/tmp": "0.2.6", 81 | "@vercel/webpack-asset-relocator-loader": "1.7.3", 82 | "@wdio/cli": "^8.36.1", 83 | "@wdio/local-runner": "^8.36.1", 84 | "@wdio/mocha-framework": "^8.36.1", 85 | "@wdio/spec-reporter": "^8.36.1", 86 | "@yao-pkg/pkg": "^5.11.5", 87 | "catch-uncommitted": "^2.0.0", 88 | "chai": "4.3.10", 89 | "css-loader": "5.2.7", 90 | "electron": "30.0.1", 91 | "file-loader": "6.2.0", 92 | "husky": "8.0.3", 93 | "native-addon-loader": "2.0.1", 94 | "node-loader": "^2.0.0", 95 | "sinon": "^17.0.1", 96 | "string-replace-loader": "3.1.0", 97 | "style-loader": "3.3.3", 98 | "ts-loader": "^9.5.1", 99 | "ts-node": "^10.9.2", 100 | "tslib": "2.6.2", 101 | "typescript": "^5.3.3", 102 | "url-loader": "4.1.1", 103 | "wdio-electron-service": "^6.4.1", 104 | "xvfb-maybe": "^0.2.1" 105 | }, 106 | "hostDependencies": { 107 | "debian": [ 108 | "libasound2", 109 | "libatk1.0-0", 110 | "libc6", 111 | "libcairo2", 112 | "libcups2", 113 | "libdbus-1-3", 114 | "libexpat1", 115 | "libfontconfig1", 116 | "libfreetype6", 117 | "libgbm1", 118 | "libgcc1", 119 | "libgdk-pixbuf2.0-0", 120 | "libglib2.0-0", 121 | "libgtk-3-0", 122 | "liblzma5", 123 | "libnotify4", 124 | "libnspr4", 125 | "libnss3", 126 | "libpango1.0-0 | libpango-1.0-0", 127 | "libstdc++6", 128 | "libx11-6", 129 | "libxcomposite1", 130 | "libxcursor1", 131 | "libxdamage1", 132 | "libxext6", 133 | "libxfixes3", 134 | "libxi6", 135 | "libxrandr2", 136 | "libxrender1", 137 | "libxss1", 138 | "libxtst6", 139 | "polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1" 140 | ] 141 | }, 142 | "engines": { 143 | "node": ">=20 <21" 144 | }, 145 | "versionist": { 146 | "publishedAt": "2025-05-15T18:09:56.320Z" 147 | }, 148 | "optionalDependencies": { 149 | "bufferutil": "^4.0.8", 150 | "utf-8-validate": "^5.0.10", 151 | "winusb-driver-generator": "2.1.2" 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /pkg-sidecar.json: -------------------------------------------------------------------------------- 1 | { 2 | "assets": [ 3 | "node_modules/usb/**", 4 | "node_modules/lzma-native/**", 5 | "node_modules/drivelist/**", 6 | "node_modules/mountutils/**", 7 | "node_modules/winusb-driver-generator/**", 8 | "node_modules/node-raspberrypi-usbboot/**", 9 | "node_modules/xxhash-addon/**", 10 | "node_modules/axios/**" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /repo.yml: -------------------------------------------------------------------------------- 1 | --- 2 | type: electron 3 | release: github 4 | publishMetadata: true 5 | sentry: 6 | org: balenaetcher 7 | team: resinio 8 | type: electron 9 | triggerNotification: 10 | version: 1.7.9 11 | stagingPercentage: 100 12 | upstream: 13 | - repo: etcher-sdk 14 | url: https://github.com/balena-io-modules/etcher-sdk 15 | module: etcher-sdk 16 | - repo: sys-class-rgb-led 17 | url: https://github.com/balena-io-modules/sys-class-rgb-led 18 | module: sys-class-rgb-led 19 | - repo: rendition 20 | url: https://github.com/balena-io-modules/rendition 21 | module: rendition 22 | -------------------------------------------------------------------------------- /tests/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | require-jsdoc: 3 | - off 4 | no-undefined: 5 | - off 6 | init-declarations: 7 | - off 8 | no-unused-expressions: 9 | - off 10 | prefer-arrow-callback: 11 | - off 12 | no-magic-numbers: 13 | - off 14 | id-length: 15 | - error 16 | - min: 2 17 | exceptions: 18 | - "_" 19 | - "m" 20 | -------------------------------------------------------------------------------- /tests/data/wmic-output.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/balena-io/etcher/391164bf15810aeab53a1f0d3c959c9944c944fa/tests/data/wmic-output.txt -------------------------------------------------------------------------------- /tests/gui/allow-renderer-process-reuse.ts: -------------------------------------------------------------------------------- 1 | const { app } = require('electron'); 2 | 3 | if (app !== undefined) { 4 | const remoteMain = require('@electron/remote/main'); 5 | 6 | remoteMain.initialize(); 7 | 8 | app.on('browser-window-created', (_event, window) => 9 | remoteMain.enable(window.webContents), 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /tests/gui/models/settings.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { stub } from 'sinon'; 19 | 20 | import * as settings from '../../../lib/gui/app/models/settings'; 21 | 22 | async function checkError(promise: Promise, fn: (err: Error) => any) { 23 | try { 24 | await promise; 25 | } catch (error: any) { 26 | await fn(error); 27 | return; 28 | } 29 | throw new Error('Expected error was not thrown'); 30 | } 31 | 32 | describe('Browser: settings', () => { 33 | it('should be able to set and read values', async () => { 34 | expect(await settings.get('foo')).to.be.undefined; 35 | await settings.set('foo', true); 36 | expect(await settings.get('foo')).to.be.true; 37 | await settings.set('foo', false); 38 | expect(await settings.get('foo')).to.be.false; 39 | }); 40 | 41 | describe('.set()', () => { 42 | it('should not change the application state if storing to the local machine results in an error', async () => { 43 | await settings.set('foo', 'bar'); 44 | expect(await settings.get('foo')).to.equal('bar'); 45 | 46 | const writeConfigFileStub = stub(); 47 | writeConfigFileStub.returns(Promise.reject(new Error('settings error'))); 48 | 49 | const promise = settings.set('foo', 'baz', writeConfigFileStub); 50 | await checkError(promise, async (error) => { 51 | expect(error).to.be.an.instanceof(Error); 52 | expect(error.message).to.equal('settings error'); 53 | expect(await settings.get('foo')).to.equal('bar'); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('.set()', () => { 59 | it('should set an unknown key', async () => { 60 | expect(await settings.get('foobar')).to.be.undefined; 61 | await settings.set('foobar', true); 62 | expect(await settings.get('foobar')).to.be.true; 63 | }); 64 | 65 | it('should set the key to undefined if no value', async () => { 66 | await settings.set('foo', 'bar'); 67 | expect(await settings.get('foo')).to.equal('bar'); 68 | await settings.set('foo', undefined); 69 | expect(await settings.get('foo')).to.be.undefined; 70 | }); 71 | 72 | it('should store the setting to the local machine', async () => { 73 | const data = await settings.readAll(); 74 | expect(data.foo).to.be.undefined; 75 | await settings.set('foo', 'bar'); 76 | const data1 = await settings.readAll(); 77 | expect(data1.foo).to.equal('bar'); 78 | }); 79 | 80 | it('should not change the application state if storing to the local machine results in an error', async () => { 81 | await settings.set('foo', 'bar'); 82 | expect(await settings.get('foo')).to.equal('bar'); 83 | 84 | const writeConfigFileStub = stub(); 85 | writeConfigFileStub.returns(Promise.reject(new Error('settings error'))); 86 | 87 | await checkError( 88 | settings.set('foo', 'baz', writeConfigFileStub), 89 | async (error) => { 90 | expect(error).to.be.an.instanceof(Error); 91 | expect(error.message).to.equal('settings error'); 92 | expect(await settings.get('foo')).to.equal('bar'); 93 | }, 94 | ); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /tests/gui/modules/image-writer.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * TODO: 4 | * This test should be replaced by an E2E test. 5 | * 6 | */ 7 | 8 | /* 9 | * Copyright 2020 balena.io 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | */ 23 | 24 | import { expect } from 'chai'; 25 | import type { Drive as DrivelistDrive } from 'drivelist'; 26 | import type { SinonStub } from 'sinon'; 27 | import { assert, stub } from 'sinon'; 28 | 29 | import type { SourceMetadata } from '../../../lib/shared/typings/source-selector'; 30 | import * as flashState from '../../../lib/gui/app/models/flash-state'; 31 | import * as imageWriter from '../../../lib/gui/app/modules/image-writer'; 32 | 33 | // @ts-ignore 34 | const fakeDrive: DrivelistDrive = {}; 35 | 36 | describe('Browser: imageWriter', () => { 37 | describe('.flash()', () => { 38 | const image: SourceMetadata = { 39 | hasMBR: false, 40 | partitions: [], 41 | description: 'foo.img', 42 | displayName: 'foo.img', 43 | path: 'foo.img', 44 | SourceType: 'File', 45 | extension: 'img', 46 | }; 47 | 48 | describe('given a successful write', () => { 49 | let performWriteStub: SinonStub; 50 | 51 | beforeEach(() => { 52 | performWriteStub = stub(); 53 | performWriteStub.returns( 54 | Promise.resolve({ 55 | cancelled: false, 56 | sourceChecksum: '1234', 57 | }), 58 | ); 59 | }); 60 | 61 | afterEach(() => { 62 | performWriteStub.reset(); 63 | }); 64 | 65 | it('should set flashing to false when done', async () => { 66 | flashState.unsetFlashingFlag({ 67 | cancelled: false, 68 | sourceChecksum: '1234', 69 | }); 70 | 71 | try { 72 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 73 | } catch { 74 | // noop 75 | } finally { 76 | expect(flashState.isFlashing()).to.be.false; 77 | } 78 | }); 79 | 80 | it('should prevent writing more than once', async () => { 81 | flashState.unsetFlashingFlag({ 82 | cancelled: false, 83 | sourceChecksum: '1234', 84 | }); 85 | 86 | try { 87 | await Promise.all([ 88 | imageWriter.flash(image, [fakeDrive], performWriteStub), 89 | imageWriter.flash(image, [fakeDrive], performWriteStub), 90 | ]); 91 | assert.fail('Writing twice should fail'); 92 | } catch (error: any) { 93 | expect(error.message).to.equal( 94 | 'There is already a flash in progress', 95 | ); 96 | } 97 | }); 98 | }); 99 | 100 | describe('given an unsuccessful write', () => { 101 | let performWriteStub: SinonStub; 102 | 103 | beforeEach(() => { 104 | performWriteStub = stub(); 105 | const error: Error & { code?: string } = new Error('write error'); 106 | error.code = 'FOO'; 107 | performWriteStub.returns(Promise.reject(error)); 108 | }); 109 | 110 | afterEach(() => { 111 | performWriteStub.reset(); 112 | }); 113 | 114 | it('should set flashing to false when done', async () => { 115 | try { 116 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 117 | } catch { 118 | // noop 119 | } finally { 120 | expect(flashState.isFlashing()).to.be.false; 121 | } 122 | }); 123 | 124 | it('should set the error code in the flash results', async () => { 125 | try { 126 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 127 | } catch { 128 | // noop 129 | } finally { 130 | const flashResults = flashState.getFlashResults(); 131 | expect(flashResults.errorCode).to.equal('FOO'); 132 | } 133 | }); 134 | 135 | it('should be rejected with the error', async () => { 136 | flashState.unsetFlashingFlag({ 137 | cancelled: false, 138 | sourceChecksum: '1234', 139 | }); 140 | try { 141 | await imageWriter.flash(image, [fakeDrive], performWriteStub); 142 | } catch (error: any) { 143 | expect(error).to.be.an.instanceof(Error); 144 | expect(error.message).to.equal('write error'); 145 | } 146 | }); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /tests/gui/modules/progress-status.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as i18next from 'i18next'; 19 | import en_translation from '../../../lib/gui/app/i18n/en'; 20 | 21 | import * as progressStatus from '../../../lib/gui/app/modules/progress-status'; 22 | 23 | describe('Browser: progressStatus', function () { 24 | describe('.titleFromFlashState()', function () { 25 | beforeEach(async function () { 26 | this.state = { 27 | active: 1, 28 | type: 'flashing', 29 | failed: 0, 30 | percentage: 0, 31 | eta: 15, 32 | speed: 100000000000000, 33 | }; 34 | 35 | await i18next.init({ 36 | lng: 'en', // Set the default language 37 | resources: { 38 | en: en_translation, 39 | }, 40 | }); 41 | }); 42 | 43 | it('should report 0% if percentage == 0 but speed != 0', function () { 44 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 45 | '0% Flashing...', 46 | ); 47 | }); 48 | 49 | it('should handle percentage == 0, flashing', function () { 50 | this.state.speed = 0; 51 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 52 | '0% Flashing...', 53 | ); 54 | }); 55 | 56 | it('should handle percentage == 0, verifying', function () { 57 | this.state.speed = 0; 58 | this.state.type = 'verifying'; 59 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 60 | '0% Validating...', 61 | ); 62 | }); 63 | 64 | it('should handle percentage == 50, flashing', function () { 65 | this.state.percentage = 50; 66 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 67 | '50% Flashing...', 68 | ); 69 | }); 70 | 71 | it('should handle percentage == 50, verifying', function () { 72 | this.state.percentage = 50; 73 | this.state.type = 'verifying'; 74 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 75 | '50% Validating...', 76 | ); 77 | }); 78 | 79 | it('should handle percentage == 100, flashing', function () { 80 | this.state.percentage = 100; 81 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 82 | 'Finishing...', 83 | ); 84 | }); 85 | 86 | it('should handle percentage == 100, verifying', function () { 87 | this.state.percentage = 100; 88 | this.state.type = 'verifying'; 89 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 90 | 'Finishing...', 91 | ); 92 | }); 93 | 94 | it('should handle percentage == 100, validating', function () { 95 | this.state.percentage = 100; 96 | expect(progressStatus.titleFromFlashState(this.state)).to.equal( 97 | 'Finishing...', 98 | ); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/gui/os/window-progress.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * TODO: 4 | * This test should be replaced by an E2E test. 5 | * 6 | */ 7 | 8 | /* 9 | * Copyright 2016 balena.io 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | */ 23 | 24 | import { expect } from 'chai'; 25 | import { assert, spy } from 'sinon'; 26 | 27 | import * as windowProgress from '../../../lib/gui/app/os/window-progress'; 28 | 29 | describe('Browser: WindowProgress', function () { 30 | describe('windowProgress', function () { 31 | describe('given a stubbed current window', function () { 32 | beforeEach(function () { 33 | this.setProgressBarSpy = spy(); 34 | this.setTitleSpy = spy(); 35 | 36 | windowProgress.currentWindow.setProgressBar = this.setProgressBarSpy; 37 | windowProgress.currentWindow.setTitle = this.setTitleSpy; 38 | 39 | this.state = { 40 | active: 1, 41 | type: 'flashing', 42 | failed: 0, 43 | percentage: 85, 44 | speed: 100, 45 | }; 46 | }); 47 | 48 | describe('.set()', function () { 49 | it('should translate 0-100 percentages to 0-1 ranges', function () { 50 | windowProgress.set(this.state); 51 | assert.calledWith(this.setProgressBarSpy, 0.85); 52 | }); 53 | 54 | it('should set 0 given 0', function () { 55 | this.state.percentage = 0; 56 | windowProgress.set(this.state); 57 | assert.calledWith(this.setProgressBarSpy, 0); 58 | }); 59 | 60 | it('should set 1 given 100', function () { 61 | this.state.percentage = 100; 62 | windowProgress.set(this.state); 63 | assert.calledWith(this.setProgressBarSpy, 1); 64 | }); 65 | 66 | it('should throw if given a percentage higher than 100', function () { 67 | this.state.percentage = 101; 68 | const state = this.state; 69 | expect(function () { 70 | windowProgress.set(state); 71 | }).to.throw('Invalid percentage: 101'); 72 | }); 73 | 74 | it('should throw if given a percentage less than 0', function () { 75 | this.state.percentage = -1; 76 | const state = this.state; 77 | expect(function () { 78 | windowProgress.set(state); 79 | }).to.throw('Invalid percentage: -1'); 80 | }); 81 | 82 | it('should set the flashing title', function () { 83 | windowProgress.set(this.state); 84 | assert.calledWith(this.setTitleSpy, ' – 85% Flashing...'); 85 | }); 86 | 87 | it('should set the verifying title', function () { 88 | this.state.type = 'verifying'; 89 | windowProgress.set(this.state); 90 | assert.calledWith(this.setTitleSpy, ' – 85% Validating...'); 91 | }); 92 | 93 | it('should set the starting title', function () { 94 | this.state.percentage = 0; 95 | this.state.speed = 0; 96 | windowProgress.set(this.state); 97 | assert.calledWith(this.setTitleSpy, ' – 0% Flashing...'); 98 | }); 99 | 100 | it('should set the finishing title', function () { 101 | this.state.percentage = 100; 102 | windowProgress.set(this.state); 103 | assert.calledWith(this.setTitleSpy, ' – Finishing...'); 104 | }); 105 | }); 106 | 107 | describe('.clear()', function () { 108 | it('should set -1', function () { 109 | windowProgress.clear(); 110 | assert.calledWith(this.setProgressBarSpy, -1); 111 | }); 112 | 113 | it('should clear the window title', function () { 114 | windowProgress.clear(); 115 | assert.calledWith(this.setTitleSpy, ''); 116 | }); 117 | }); 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/gui/os/windows-network-drives.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { promises as fs } from 'fs'; 19 | import * as os from 'os'; 20 | import type { SinonStub } from 'sinon'; 21 | import { stub } from 'sinon'; 22 | 23 | import * as wnd from '../../../lib/gui/app/os/windows-network-drives'; 24 | 25 | function mockGetWmicOutput() { 26 | return fs.readFile('tests/data/wmic-output.txt', { 27 | encoding: 'ucs2', 28 | }); 29 | } 30 | 31 | describe('Network drives on Windows', () => { 32 | let osPlatformStub: SinonStub; 33 | 34 | before(async () => { 35 | osPlatformStub = stub(os, 'platform'); 36 | osPlatformStub.returns('win32'); 37 | }); 38 | 39 | it('should parse network drive mapping on Windows', async () => { 40 | expect( 41 | await wnd.replaceWindowsNetworkDriveLetter( 42 | 'Z:\\some-folder\\some-file', 43 | mockGetWmicOutput, 44 | ), 45 | ).to.equal('\\\\192.168.1.1\\Publicé\\some-folder\\some-file'); 46 | }); 47 | 48 | after(() => { 49 | osPlatformStub.restore(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/gui/utils/middle-ellipsis.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | 19 | import { middleEllipsis } from '../../../lib/gui/app/utils/middle-ellipsis'; 20 | 21 | describe('Browser: MiddleEllipsis', function () { 22 | describe('.middleEllipsis()', function () { 23 | it('should throw error if limit < 3', function () { 24 | expect(() => { 25 | middleEllipsis('No', 2); 26 | }).to.throw('middleEllipsis: Limit should be at least 3'); 27 | }); 28 | 29 | describe('given the input length is greater than the limit', function () { 30 | it('should always truncate input to an odd length', function () { 31 | const alphabet = 'abcdefghijklmnopqrstuvwxyz'; 32 | expect(middleEllipsis(alphabet, 3)).to.have.lengthOf(3); 33 | expect(middleEllipsis(alphabet, 4)).to.have.lengthOf(3); 34 | expect(middleEllipsis(alphabet, 5)).to.have.lengthOf(5); 35 | expect(middleEllipsis(alphabet, 6)).to.have.lengthOf(5); 36 | }); 37 | }); 38 | 39 | it('should return the input if it is within the bounds of limit', function () { 40 | expect(middleEllipsis('Hello', 10)).to.equal('Hello'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /tests/gui/window-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "webPreferences": { 3 | "enableRemoteModule": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/shared/messages.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as _ from 'lodash'; 19 | 20 | import * as messages from '../../lib/shared/messages'; 21 | 22 | describe('Shared: Messages', function () { 23 | beforeEach(function () { 24 | this.drives = [ 25 | { 26 | description: 'My Drive', 27 | displayName: '/dev/disk1', 28 | }, 29 | { 30 | description: 'Other Drive', 31 | displayName: '/dev/disk2', 32 | }, 33 | ]; 34 | }); 35 | 36 | it('should contain object properties', function () { 37 | expect(_.every(_.map(messages, _.isPlainObject))).to.be.true; 38 | }); 39 | 40 | it('should contain function properties in each category', function () { 41 | _.each(messages, (category) => { 42 | expect(_.every(_.map(category, _.isFunction))).to.be.true; 43 | }); 44 | }); 45 | 46 | describe('.info', function () { 47 | describe('.flashComplete()', function () { 48 | it('should use singular when there are single results', function () { 49 | const msg = messages.info.flashComplete('image.img', this.drives, { 50 | failed: 1, 51 | successful: 1, 52 | }); 53 | 54 | expect(msg).to.equal( 55 | 'image.img was successfully flashed to 1 target and failed to be flashed to 1 target', 56 | ); 57 | }); 58 | 59 | it('should use plural when there are multiple results', function () { 60 | const msg = messages.info.flashComplete('image.img', this.drives, { 61 | failed: 2, 62 | successful: 2, 63 | }); 64 | 65 | expect(msg).to.equal( 66 | 'image.img was successfully flashed to 2 targets and failed to be flashed to 2 targets', 67 | ); 68 | }); 69 | 70 | it('should not contain failed target part when there are none', function () { 71 | const msg = messages.info.flashComplete('image.img', this.drives, { 72 | failed: 0, 73 | successful: 2, 74 | }); 75 | 76 | expect(msg).to.equal('image.img was successfully flashed to 2 targets'); 77 | }); 78 | 79 | it('should show drive name and description when only target', function () { 80 | const msg = messages.info.flashComplete('image.img', this.drives, { 81 | failed: 0, 82 | successful: 1, 83 | }); 84 | 85 | expect(msg).to.equal( 86 | 'image.img was successfully flashed to My Drive (/dev/disk1)', 87 | ); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('.error', function () { 93 | describe('.flashFailure()', function () { 94 | it('should use plural when there are multiple drives', function () { 95 | const msg = messages.error.flashFailure('image.img', this.drives); 96 | 97 | expect(msg).to.equal( 98 | 'Something went wrong while writing image.img to 2 targets.', 99 | ); 100 | }); 101 | 102 | it('should use singular when there is one drive', function () { 103 | const msg = messages.error.flashFailure('image.img', [this.drives[0]]); 104 | 105 | expect(msg).to.equal( 106 | 'Something went wrong while writing image.img to My Drive (/dev/disk1).', 107 | ); 108 | }); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /tests/shared/permissions.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as os from 'os'; 19 | import { stub } from 'sinon'; 20 | 21 | import * as permissions from '../../lib/shared/permissions'; 22 | 23 | describe('Shared: permissions', function () { 24 | describe('.createLaunchScript()', function () { 25 | describe('given windows', function () { 26 | beforeEach(function () { 27 | this.osPlatformStub = stub(os, 'platform'); 28 | this.osPlatformStub.returns('win32'); 29 | }); 30 | 31 | afterEach(function () { 32 | this.osPlatformStub.restore(); 33 | }); 34 | 35 | it('should escape environment variables and arguments', function () { 36 | expect( 37 | permissions.createLaunchScript( 38 | 'C:\\Users\\Alice & Bob\'s Laptop\\"what"\\balenaEtcher', 39 | ['"a Laser"', 'arg1', "'&/ ^ \\", '" $ % *'], 40 | { 41 | key: 'value', 42 | key2: ' " \' ^ & = + $ % / \\', 43 | key3: '8', 44 | }, 45 | ), 46 | ).to.equal( 47 | `chcp 65001${os.EOL}` + 48 | `set "key=value"${os.EOL}` + 49 | `set "key2= " ' ^ & = + $ % / \\"${os.EOL}` + 50 | `set "key3=8"${os.EOL}` + 51 | `"C:\\Users\\Alice & Bob's Laptop\\\\"what\\"\\balenaEtcher" "\\"a Laser\\"" "arg1" "'&/ ^ \\" "\\" $ % *"`, 52 | ); 53 | }); 54 | }); 55 | 56 | for (const platform of ['linux', 'darwin']) { 57 | describe(`given ${platform}`, function () { 58 | beforeEach(function () { 59 | this.osPlatformStub = stub(os, 'platform'); 60 | this.osPlatformStub.returns(platform); 61 | }); 62 | 63 | afterEach(function () { 64 | this.osPlatformStub.restore(); 65 | }); 66 | 67 | it('should escape environment variables and arguments', function () { 68 | expect( 69 | permissions.createLaunchScript( 70 | '/home/Alice & Bob\'s Laptop/"what"/balenaEtcher', 71 | ['arg1', "'&/ ^ \\", '" $ % *'], 72 | { 73 | key: 'value', 74 | key2: ' " \' ^ & = + $ % / \\', 75 | key3: '8', 76 | }, 77 | ), 78 | ).to.equal( 79 | `export key='value'${os.EOL}` + 80 | `export key2=' " '\\'' ^ & = + $ % / \\'${os.EOL}` + 81 | `export key3='8'${os.EOL}` + 82 | `'/home/Alice & Bob'\\''s Laptop/"what"/balenaEtcher' 'arg1' ''\\''&/ ^ \\' '" $ % *'`, 83 | ); 84 | }); 85 | }); 86 | } 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /tests/shared/supported-formats.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import * as _ from 'lodash'; 19 | 20 | import * as supportedFormats from '../../lib/shared/supported-formats'; 21 | 22 | describe('Shared: SupportedFormats', function () { 23 | describe('.looksLikeWindowsImage()', function () { 24 | _.each( 25 | [ 26 | 'C:\\path\\to\\en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', 27 | '/path/to/en_windows_10_multiple_editions_version_1607_updated_jan_2017_x64_dvd_9714399.iso', 28 | '/path/to/Win10_1607_SingleLang_English_x32.iso', 29 | '/path/to/en_winxp_pro_x86_build2600_iso.img', 30 | ], 31 | (imagePath) => { 32 | it(`should return true if filename is ${imagePath}`, function () { 33 | const looksLikeWindowsImage = 34 | supportedFormats.looksLikeWindowsImage(imagePath); 35 | expect(looksLikeWindowsImage).to.be.true; 36 | }); 37 | }, 38 | ); 39 | 40 | _.each( 41 | [ 42 | 'C:\\path\\to\\2017-01-11-raspbian-jessie.img', 43 | '/path/to/2017-01-11-raspbian-jessie.img', 44 | ], 45 | (imagePath) => { 46 | it(`should return false if filename is ${imagePath}`, function () { 47 | const looksLikeWindowsImage = 48 | supportedFormats.looksLikeWindowsImage(imagePath); 49 | expect(looksLikeWindowsImage).to.be.false; 50 | }); 51 | }, 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tests/shared/units.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2016 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | import { bytesToMegabytes } from '../../lib/shared/units'; 19 | 20 | describe('Shared: Units', function () { 21 | describe('.bytesToMegabytes()', function () { 22 | it('should convert bytes to megabytes', function () { 23 | expect(bytesToMegabytes(1.2e7)).to.equal(12); 24 | expect(bytesToMegabytes(332000)).to.equal(0.332); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /tests/shared/utils.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { expect } from 'chai'; 18 | 19 | import * as utils from '../../lib/shared/utils'; 20 | 21 | describe('Shared: Utils', function () { 22 | describe('.isValidPercentage()', function () { 23 | it('should return false if percentage is not a number', function () { 24 | expect(utils.isValidPercentage('50')).to.be.false; 25 | }); 26 | 27 | it('should return false if percentage is null', function () { 28 | expect(utils.isValidPercentage(null)).to.be.false; 29 | }); 30 | 31 | it('should return false if percentage is undefined', function () { 32 | expect(utils.isValidPercentage(undefined)).to.be.false; 33 | }); 34 | 35 | it('should return false if percentage is an integer less than 0', function () { 36 | expect(utils.isValidPercentage(-1)).to.be.false; 37 | }); 38 | 39 | it('should return false if percentage is a float less than 0', function () { 40 | expect(utils.isValidPercentage(-0.1)).to.be.false; 41 | }); 42 | 43 | it('should return true if percentage is 0', function () { 44 | expect(utils.isValidPercentage(0)).to.be.true; 45 | }); 46 | 47 | it('should return true if percentage is an integer greater than 0, but less than 100', function () { 48 | expect(utils.isValidPercentage(50)).to.be.true; 49 | }); 50 | 51 | it('should return true if percentage is a float greater than 0, but less than 100', function () { 52 | expect(utils.isValidPercentage(49.55)).to.be.true; 53 | }); 54 | 55 | it('should return true if percentage is 100', function () { 56 | expect(utils.isValidPercentage(100)).to.be.true; 57 | }); 58 | 59 | it('should return false if percentage is an integer greater than 100', function () { 60 | expect(utils.isValidPercentage(101)).to.be.false; 61 | }); 62 | 63 | it('should return false if percentage is a float greater than 100', function () { 64 | expect(utils.isValidPercentage(100.001)).to.be.false; 65 | }); 66 | }); 67 | 68 | describe('.percentageToFloat()', function () { 69 | it('should throw an error if given a string percentage', function () { 70 | expect(function () { 71 | utils.percentageToFloat('50'); 72 | }).to.throw('Invalid percentage: 50'); 73 | }); 74 | 75 | it('should throw an error if given a null percentage', function () { 76 | expect(function () { 77 | utils.percentageToFloat(null); 78 | }).to.throw('Invalid percentage: null'); 79 | }); 80 | 81 | it('should throw an error if given an undefined percentage', function () { 82 | expect(function () { 83 | utils.percentageToFloat(undefined); 84 | }).to.throw('Invalid percentage: undefined'); 85 | }); 86 | 87 | it('should throw an error if given an integer percentage < 0', function () { 88 | expect(function () { 89 | utils.percentageToFloat(-1); 90 | }).to.throw('Invalid percentage: -1'); 91 | }); 92 | 93 | it('should throw an error if given a float percentage < 0', function () { 94 | expect(function () { 95 | utils.percentageToFloat(-0.1); 96 | }).to.throw('Invalid percentage: -0.1'); 97 | }); 98 | 99 | it('should covert a 0 percentage to 0', function () { 100 | expect(utils.percentageToFloat(0)).to.equal(0); 101 | }); 102 | 103 | it('should covert an integer percentage to a float', function () { 104 | expect(utils.percentageToFloat(50)).to.equal(0.5); 105 | }); 106 | 107 | it('should covert an float percentage to a float', function () { 108 | expect(utils.percentageToFloat(46.54)).to.equal(0.4654); 109 | }); 110 | 111 | it('should covert a 100 percentage to 1', function () { 112 | expect(utils.percentageToFloat(100)).to.equal(1); 113 | }); 114 | 115 | it('should throw an error if given an integer percentage > 100', function () { 116 | expect(function () { 117 | utils.percentageToFloat(101); 118 | }).to.throw('Invalid percentage: 101'); 119 | }); 120 | 121 | it('should throw an error if given a float percentage > 100', function () { 122 | expect(function () { 123 | utils.percentageToFloat(100.01); 124 | }).to.throw('Invalid percentage: 100.01'); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /tests/test.e2e.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '@wdio/globals'; 2 | 3 | describe('Electron Testing', () => { 4 | it('should print application title', async () => { 5 | console.log('Hello', await browser.getTitle(), 'application!'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "target": "es2019", 5 | "typeRoots": ["./node_modules/@types", "./typings"], 6 | "module": "commonjs", 7 | "lib": ["dom", "esnext"], 8 | "declaration": true, 9 | "declarationMap": true, 10 | "jsx": "react", 11 | "pretty": true, 12 | "sourceMap": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "moduleResolution": "node", 18 | "allowSyntheticDefaultImports": true, 19 | "resolveJsonModule": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.sidecar.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "allowJs": false, 5 | "skipLibCheck": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "typeRoots": ["./node_modules/@types", "./typings"], 11 | "module": "commonjs", 12 | "moduleResolution": "Node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true 15 | }, 16 | "include": ["lib/util"] 17 | } 18 | -------------------------------------------------------------------------------- /typings/omit-deep-lodash/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'omit-deep-lodash'; 2 | -------------------------------------------------------------------------------- /typings/path-is-inside/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'path-is-inside'; 2 | -------------------------------------------------------------------------------- /typings/sudo-prompt/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@balena/sudo-prompt'; 2 | -------------------------------------------------------------------------------- /typings/svg/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import { FunctionComponent, SVGProps } from 'react'; 3 | const _: FunctionComponent>; 4 | export = _; 5 | } 6 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License") 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import type { Configuration, ModuleOptions } from 'webpack'; 18 | import { resolve } from 'path'; 19 | 20 | import { BannerPlugin, IgnorePlugin, DefinePlugin } from 'webpack'; 21 | 22 | const rules: Required['rules'] = [ 23 | // Add support for native node modules 24 | { 25 | // We're specifying native_modules in the test because the asset relocator loader generates a 26 | // "fake" .node file which is really a cjs file. 27 | test: /native_modules[/\\].+\.node$/, 28 | use: 'node-loader', 29 | }, 30 | { 31 | test: /[/\\]node_modules[/\\].+\.(m?js|node)$/, 32 | parser: { amd: false }, 33 | use: { 34 | loader: '@vercel/webpack-asset-relocator-loader', 35 | options: { 36 | outputAssetBase: 'native_modules', 37 | }, 38 | }, 39 | }, 40 | { 41 | test: /\.tsx?$/, 42 | exclude: /(node_modules|\.webpack)/, 43 | use: { 44 | loader: 'ts-loader', 45 | options: { 46 | transpileOnly: true, 47 | }, 48 | }, 49 | }, 50 | { 51 | test: /\.css$/, 52 | use: ['style-loader', 'css-loader'], 53 | }, 54 | { 55 | test: /\.(woff|woff2|eot|ttf|otf)$/, 56 | loader: 'file-loader', 57 | }, 58 | { 59 | test: /\.svg$/, 60 | use: '@svgr/webpack', 61 | }, 62 | ]; 63 | 64 | const injectAnalyticsToken = new DefinePlugin({ 65 | 'process.env.SENTRY_TOKEN': JSON.stringify(process.env.SENTRY_TOKEN || ''), 66 | }); 67 | 68 | export const rendererConfig: Configuration = { 69 | module: { 70 | rules, 71 | }, 72 | plugins: [ 73 | // Ignore `aws-crt` which is a dependency of (ultimately) `aws4-axios` which is used 74 | // by etcher-sdk and does a runtime check to its availability. We’re not currently 75 | // using the “assume role” functionality (AFAIU) of aws4-axios and we don’t care that 76 | // it’s not found, so force webpack to ignore the import. 77 | // See https://github.com/aws/aws-sdk-js-v3/issues/3025 78 | new IgnorePlugin({ 79 | resourceRegExp: /^aws-crt$/, 80 | }), 81 | // Remove "Download the React DevTools for a better development experience" message 82 | new BannerPlugin({ 83 | banner: '__REACT_DEVTOOLS_GLOBAL_HOOK__ = { isDisabled: true };', 84 | raw: true, 85 | }), 86 | injectAnalyticsToken, 87 | ], 88 | 89 | resolve: { 90 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'], 91 | alias: { 92 | // need to alias ws to the wrapper to avoid the browser fake version to be used 93 | ws: resolve(__dirname, 'node_modules/ws/wrapper.mjs'), 94 | }, 95 | }, 96 | }; 97 | 98 | export const mainConfig: Configuration = { 99 | entry: { 100 | etcher: './lib/gui/etcher.ts', 101 | }, 102 | module: { 103 | rules, 104 | }, 105 | resolve: { 106 | extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.json'], 107 | }, 108 | plugins: [injectAnalyticsToken], 109 | }; 110 | --------------------------------------------------------------------------------