Merge pull request #3345 from balena-io/111

111
This commit is contained in:
bulldozer-balena[bot] 2020-11-23 17:52:38 +00:00 committed by GitHub
commit a5ceba8435
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1401 additions and 602 deletions

View File

@ -23,17 +23,13 @@ import * as ReactDOM from 'react-dom';
import { v4 as uuidV4 } from 'uuid';
import * as packageJSON from '../../../package.json';
import {
DrivelistDrive,
isDriveValid,
isSourceDrive,
} from '../../shared/drive-constraints';
import { DrivelistDrive, isSourceDrive } from '../../shared/drive-constraints';
import * as EXIT_CODES from '../../shared/exit-codes';
import * as messages from '../../shared/messages';
import * as availableDrives from './models/available-drives';
import * as flashState from './models/flash-state';
import { init as ledsInit } from './models/leds';
import { deselectImage, getImage, selectDrive } from './models/selection-state';
import { deselectImage, getImage } from './models/selection-state';
import * as settings from './models/settings';
import { Actions, observe, store } from './models/store';
import * as analytics from './modules/analytics';
@ -251,14 +247,6 @@ async function addDrive(drive: Drive) {
const drives = getDrives();
drives[preparedDrive.device] = preparedDrive;
setDrives(drives);
if (
(await settings.get('autoSelectAllDrives')) &&
drive instanceof sdk.sourceDestination.BlockDevice &&
// @ts-ignore BlockDevice.drive is private
isDriveValid(drive.drive, getImage())
) {
selectDrive(drive.device);
}
}
function removeDrive(drive: Drive) {

View File

@ -42,7 +42,6 @@ import {
Table,
} from '../../styled-components';
import DriveSVGIcon from '../../../assets/tgt.svg';
import { SourceMetadata } from '../source-selector/source-selector';
interface UsbbootDrive extends sourceDestination.UsbbootDrive {
@ -138,12 +137,14 @@ const InitProgress = styled(
export interface DriveSelectorProps
extends Omit<ModalProps, 'done' | 'cancel'> {
write: boolean;
multipleSelection: boolean;
showWarnings?: boolean;
cancel: () => void;
done: (drives: DrivelistDrive[]) => void;
titleLabel: string;
emptyListLabel: string;
emptyListIcon: JSX.Element;
selectedList?: DrivelistDrive[];
updateSelectedList?: () => DrivelistDrive[];
}
@ -258,7 +259,8 @@ export class DriveSelector extends React.Component<
return (
isUsbbootDrive(drive) ||
isDriverlessDrive(drive) ||
!isDriveValid(drive, image)
!isDriveValid(drive, image) ||
(this.props.write && drive.isReadOnly)
);
}
@ -311,6 +313,7 @@ export class DriveSelector extends React.Component<
const statuses: DriveStatus[] = getDriveImageCompatibilityStatuses(
drive,
this.state.image,
this.props.write,
).slice(0, 2);
return (
// the column render fn expects a single Element
@ -422,7 +425,7 @@ export class DriveSelector extends React.Component<
alignItems="center"
width="100%"
>
<DriveSVGIcon width="40px" height="90px" />
{this.props.emptyListIcon}
<b>{this.props.emptyListLabel}</b>
</Flex>
) : (

View File

@ -20,6 +20,7 @@ import { v4 as uuidV4 } from 'uuid';
import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings';
import { Actions, store } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { FlashAnother } from '../flash-another/flash-another';
@ -39,8 +40,19 @@ function restart(goToMain: () => void) {
goToMain();
}
async function getSuccessBannerURL() {
return (
(await settings.get('successBannerURL')) ??
'https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true'
);
}
function FinishPage({ goToMain }: { goToMain: () => void }) {
const [webviewShowing, setWebviewShowing] = React.useState(false);
const [successBannerURL, setSuccessBannerURL] = React.useState('');
(async () => {
setSuccessBannerURL(await getSuccessBannerURL());
})();
const flashResults = flashState.getFlashResults();
const errors: FlashError[] = (
store.getState().toJS().failedDeviceErrors || []
@ -96,18 +108,20 @@ function FinishPage({ goToMain }: { goToMain: () => void }) {
}}
/>
</Flex>
<SafeWebview
src="https://www.balena.io/etcher/success-banner?borderTop=false&darkBackground=true"
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
{successBannerURL.length && (
<SafeWebview
src={successBannerURL}
onWebviewShow={setWebviewShowing}
style={{
display: webviewShowing ? 'flex' : 'none',
position: 'absolute',
right: 0,
bottom: 0,
width: '63.8vw',
height: '100vh',
}}
/>
)}
</Flex>
);
}

View File

@ -54,10 +54,6 @@ async function getSettingsList(): Promise<Setting[]> {
*/
label: `${platform === 'win32' ? 'Eject' : 'Auto-unmount'} on success`,
},
{
name: 'validateWriteOnSuccess',
label: 'Validate write on success',
},
{
name: 'updatesEnabled',
label: 'Auto-updates enabled',

View File

@ -58,6 +58,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
import { SVGIcon } from '../svg-icon/svg-icon';
import ImageSvg from '../../../assets/image.svg';
import SrcSvg from '../../../assets/src.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
import { DrivelistDrive } from '../../../../shared/drive-constraints';
@ -277,6 +278,7 @@ interface SourceSelectorState {
showURLSelector: boolean;
showDriveSelector: boolean;
defaultFlowActive: boolean;
imageSelectorOpen: boolean;
}
export class SourceSelector extends React.Component<
@ -294,6 +296,7 @@ export class SourceSelector extends React.Component<
showURLSelector: false,
showDriveSelector: false,
defaultFlowActive: true,
imageSelectorOpen: false,
};
// Bind `this` since it's used in an event's callback
@ -416,6 +419,15 @@ export class SourceSelector extends React.Component<
}
}
} else {
if (selected.partitionTableType === null) {
analytics.logEvent('Missing partition table', { selected });
this.setState({
warning: {
message: messages.warning.driveMissingPartitionTable(),
title: 'Missing partition table',
},
});
}
metadata = {
path: selected.device,
displayName: selected.displayName,
@ -481,6 +493,7 @@ export class SourceSelector extends React.Component<
private async openImageSelector() {
analytics.logEvent('Open image selector');
this.setState({ imageSelectorOpen: true });
try {
const imagePath = await osDialog.selectImage();
@ -493,6 +506,8 @@ export class SourceSelector extends React.Component<
await this.selectSource(imagePath, sourceDestination.File).promise;
} catch (error) {
exceptionReporter.report(error);
} finally {
this.setState({ imageSelectorOpen: false });
}
}
@ -609,6 +624,7 @@ export class SourceSelector extends React.Component<
) : (
<>
<FlowSelector
disabled={this.state.imageSelectorOpen}
primary={this.state.defaultFlowActive}
key="Flash from file"
flow={{
@ -715,9 +731,11 @@ export class SourceSelector extends React.Component<
{showDriveSelector && (
<DriveSelector
write={false}
multipleSelection={false}
titleLabel="Select source"
emptyListLabel="Plug a source"
emptyListLabel="Plug a source drive"
emptyListIcon={<SrcSvg width="40px" />}
cancel={() => {
this.setState({
showDriveSelector: false,

View File

@ -24,7 +24,7 @@ import {
} from '../../../../shared/drive-constraints';
import { compatibility, warning } from '../../../../shared/messages';
import * as prettyBytes from 'pretty-bytes';
import { getSelectedDrives } from '../../models/selection-state';
import { getImage, getSelectedDrives } from '../../models/selection-state';
import {
ChangeButton,
DetailsText,
@ -80,9 +80,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
if (targets.length === 1) {
const target = targets[0];
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
return (
<>
<StepNameButton plain tooltip={props.tooltip}>
@ -106,9 +108,11 @@ export function TargetSelectorButton(props: TargetSelectorProps) {
if (targets.length > 1) {
const targetsTemplate = [];
for (const target of targets) {
const warnings = getDriveImageCompatibilityStatuses(target).map(
getDriveWarning,
);
const warnings = getDriveImageCompatibilityStatuses(
target,
getImage(),
true,
).map(getDriveWarning);
targetsTemplate.push(
<DetailsText
key={target.device}

View File

@ -29,11 +29,11 @@ import {
deselectDrive,
selectDrive,
} from '../../models/selection-state';
import * as settings from '../../models/settings';
import { observe } from '../../models/store';
import * as analytics from '../../modules/analytics';
import { TargetSelectorButton } from './target-selector-button';
import TgtSvg from '../../../assets/tgt.svg';
import DriveSvg from '../../../assets/drive.svg';
import { warning } from '../../../../shared/messages';
@ -45,12 +45,7 @@ export const getDriveListLabel = () => {
.join('\n');
};
const shouldShowDrivesButton = () => {
return !settings.getSync('disableExplicitDriveSelection');
};
const getDriveSelectionStateSlice = () => ({
showDrivesButton: shouldShowDrivesButton(),
driveListLabel: getDriveListLabel(),
targets: getSelectedDrives(),
image: getImage(),
@ -59,13 +54,14 @@ const getDriveSelectionStateSlice = () => ({
export const TargetSelectorModal = (
props: Omit<
DriveSelectorProps,
'titleLabel' | 'emptyListLabel' | 'multipleSelection'
'titleLabel' | 'emptyListLabel' | 'multipleSelection' | 'emptyListIcon'
>,
) => (
<DriveSelector
multipleSelection={true}
titleLabel="Select target"
emptyListLabel="Plug a target drive"
emptyListIcon={<TgtSvg width="40px" />}
showWarnings={true}
selectedList={getSelectedDrives()}
updateSelectedList={getSelectedDrives}
@ -114,10 +110,9 @@ export const TargetSelector = ({
flashing,
}: TargetSelectorProps) => {
// TODO: inject these from redux-connector
const [
{ showDrivesButton, driveListLabel, targets },
setStateSlice,
] = React.useState(getDriveSelectionStateSlice());
const [{ driveListLabel, targets }, setStateSlice] = React.useState(
getDriveSelectionStateSlice(),
);
const [showTargetSelectorModal, setShowTargetSelectorModal] = React.useState(
false,
);
@ -141,7 +136,7 @@ export const TargetSelector = ({
<TargetSelectorButton
disabled={disabled}
show={!hasDrive && showDrivesButton}
show={!hasDrive}
tooltip={driveListLabel}
openDriveSelector={() => {
setShowTargetSelectorModal(true);
@ -168,6 +163,7 @@ export const TargetSelector = ({
{showTargetSelectorModal && (
<TargetSelectorModal
write={true}
cancel={() => setShowTargetSelectorModal(false)}
done={(modalTargets) => {
selectAllTargets(modalTargets);

View File

@ -78,7 +78,6 @@ export async function writeConfigFile(
const DEFAULT_SETTINGS: _.Dictionary<any> = {
errorReporting: true,
unmountOnSuccess: true,
validateWriteOnSuccess: true,
updatesEnabled: !_.includes(['rpm', 'deb'], packageJSON.packageType),
desktopNotifications: true,
autoBlockmapping: true,

View File

@ -16,6 +16,7 @@
import * as Immutable from 'immutable';
import * as _ from 'lodash';
import { basename } from 'path';
import * as redux from 'redux';
import { v4 as uuidV4 } from 'uuid';
@ -133,11 +134,16 @@ function storeReducer(
});
}
// Drives order is a list of devicePaths
const drivesOrder = settings.getSync('drivesOrder') ?? [];
drives = _.sortBy(drives, [
// System drives last
(d) => !!d.isSystem,
// Devices with no devicePath first (usbboot)
(d) => !!d.devicePath,
// Sort as defined in the drivesOrder setting if there is one (only for Linux with udev)
(d) => drivesOrder.indexOf(basename(d.devicePath || '')),
// Then sort by devicePath (only available on Linux with udev) or device
(d) => d.devicePath || d.device,
]);
@ -169,7 +175,7 @@ function storeReducer(
);
const shouldAutoselectAll = Boolean(
settings.getSync('disableExplicitDriveSelection'),
settings.getSync('autoSelectAllDrives'),
);
const AUTOSELECT_DRIVE_COUNT = 1;
const nonStaleSelectedDevices = nonStaleNewState
@ -191,18 +197,13 @@ function storeReducer(
drives,
(accState, drive) => {
if (
_.every([
constraints.isDriveValid(drive, image),
constraints.isDriveSizeRecommended(drive, image),
// We don't want to auto-select large drives
!constraints.isDriveSizeLarge(drive),
// We don't want to auto-select system drives,
// even when "unsafe mode" is enabled
!constraints.isSystemDrive(drive),
]) ||
(shouldAutoselectAll && constraints.isDriveValid(drive, image))
constraints.isDriveValid(drive, image) &&
!drive.isReadOnly &&
constraints.isDriveSizeRecommended(drive, image) &&
// We don't want to auto-select large drives execpt is autoSelectAllDrives is true
(!constraints.isDriveSizeLarge(drive) || shouldAutoselectAll) &&
// We don't want to auto-select system drives
!constraints.isSystemDrive(drive)
) {
// Auto-select this drive
return storeReducer(accState, {

View File

@ -145,7 +145,6 @@ async function performWrite(
ipc.serve();
const {
unmountOnSuccess,
validateWriteOnSuccess,
autoBlockmapping,
decompressFirst,
} = await settings.getAll();
@ -168,7 +167,6 @@ async function performWrite(
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess,
validateWriteOnSuccess,
};
ipc.server.on('fail', ({ device, error }) => {
@ -202,7 +200,6 @@ async function performWrite(
image,
destinations: drives,
SourceType: image.SourceType.name,
validateWriteOnSuccess,
autoBlockmapping,
unmountOnSuccess,
decompressFirst,
@ -284,7 +281,6 @@ export async function flash(
status: 'started',
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
};
analytics.logEvent('Flash', analyticsData);
@ -340,7 +336,6 @@ export async function cancel(type: string) {
uuid: flashState.getFlashUuid(),
flashInstanceUuid: flashState.getFlashUuid(),
unmountOnSuccess: await settings.get('unmountOnSuccess'),
validateWriteOnSuccess: await settings.get('validateWriteOnSuccess'),
status,
};
analytics.logEvent('Cancel', analyticsData);

View File

@ -199,7 +199,11 @@ export class FlashStep extends React.PureComponent<
const drives = selection.getSelectedDrives().map((drive) => {
return {
...drive,
statuses: constraints.getDriveImageCompatibilityStatuses(drive),
statuses: constraints.getDriveImageCompatibilityStatuses(
drive,
undefined,
true,
),
};
});
if (drives.length === 0 || this.props.isFlashing) {
@ -308,6 +312,7 @@ export class FlashStep extends React.PureComponent<
)}
{this.state.showDriveSelectorModal && (
<TargetSelectorModal
write={true}
cancel={() => this.setState({ showDriveSelectorModal: false })}
done={(modalTargets) => {
selectAllTargets(modalTargets);

18
lib/gui/assets/src.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 39 90" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd">
<g transform="translate(-380 -166)">
<g transform="translate(380 166)">
<path d="m30.88 39.87h-23.363v23.209c0 0.6909 0.56062 1.251 1.251 1.251h20.861c0.69114 0 1.251-0.55986 1.251-1.251v-23.209zm-22.363 0.9999h21.363l4e-4 22.209c0 0.13886-0.11214 0.251-0.251 0.251h-20.861l-0.057452-0.0066403c-0.11075-0.026055-0.19355-0.12572-0.19355-0.24436l-4e-4 -22.209z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m16.558 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m25.97 48.924h-3.967c-0.58314 0-1.055 0.47186-1.055 1.055v2.732c0 0.58235 0.47206 1.055 1.055 1.055h3.967c0.58223 0 1.054-0.47295 1.054-1.055v-2.732c0-0.58285-0.47156-1.055-1.054-1.055zm-3.967 1h3.967c0.029872 0 0.054 0.024158 0.054 0.055v2.732c0 0.030327-0.024612 0.055-0.054 0.055h-3.967c-0.030373 0-0.055-0.024658-0.055-0.055v-2.732c0-0.030858 0.024142-0.055 0.055-0.055z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m37.398 5.418v30.534c0 2.43-1.988 4.418-4.418 4.418h-27.562c-2.43 0-4.418-1.988-4.418-4.418v-30.534c0-2.43 1.988-4.418 4.418-4.418h27.562c2.43 0 4.418 1.988 4.418 4.418" fill="#2A506F"/>
<path d="m32.98-5.6843e-14h-27.562c-2.9823 0-5.418 2.4357-5.418 5.418v30.534c0 2.9823 2.4357 5.418 5.418 5.418h27.562c2.9823 0 5.418-2.4357 5.418-5.418v-30.534c0-2.9823-2.4357-5.418-5.418-5.418zm-27.562 2h27.562c1.8777 0 3.418 1.5403 3.418 3.418v30.534c0 1.8777-1.5403 3.418-3.418 3.418h-27.562c-1.8777 0-3.418-1.5403-3.418-3.418v-30.534c0-1.8777 1.5403-3.418 3.418-3.418z" fill="#2A506F" fill-rule="nonzero"/>
<path d="m19.147 73.551c0.24546 0 0.44961 0.17688 0.49194 0.41012l0.0080557 0.089876v14.882c0 0.27614-0.22386 0.5-0.5 0.5-0.24546 0-0.44961-0.17688-0.49194-0.41012l-0.0080557-0.089876v-14.882c0-0.27614 0.22386-0.5 0.5-0.5z" fill="#2A506F" fill-rule="nonzero"/>
<line x1="19.147" x2="14.532" y1="88.933" y2="84.214" stroke="#2A506F" stroke-linecap="round"/>
<line x1="19.147" x2="23.866" y1="88.933" y2="84.318" stroke="#2A506F" stroke-linecap="round"/>
<path d="m14.007 26.177c0.51076 0 0.96749-0.071211 1.3702-0.21363s0.74649-0.33887 1.0313-0.58934 0.50339-0.54268 0.65564-0.87664 0.22837-0.69247 0.22837-1.0755c0-0.3536-0.051567-0.66546-0.1547-0.93557s-0.2431-0.50585-0.4199-0.7072-0.38798-0.37816-0.63354-0.5304-0.50585-0.2873-0.78087-0.40517l-1.3702-0.58934c-0.19645-0.078578-0.38798-0.16452-0.5746-0.25783s-0.35851-0.20136-0.51567-0.32413-0.28239-0.2652-0.3757-0.42727-0.13997-0.36097-0.13997-0.5967c0-0.442 0.16452-0.78824 0.49357-1.0387s0.76368-0.3757 1.3039-0.3757c0.45182 0 0.85699 0.081034 1.2155 0.2431s0.6851 0.38552 0.97977 0.67037l0.663-0.7956c-0.34378-0.3536-0.76123-0.6409-1.2523-0.8619s-1.0264-0.3315-1.6059-0.3315c-0.442 0-0.84717 0.063845-1.2155 0.19153s-0.68756 0.30695-0.95767 0.53777-0.48129 0.50339-0.63354 0.8177-0.22837 0.65318-0.22837 1.0166c0 0.3536 0.058934 0.66546 0.1768 0.93557s0.27011 0.50339 0.45674 0.69984 0.3978 0.36342 0.63354 0.50094 0.46656 0.25538 0.69247 0.3536l1.3849 0.60407c0.22591 0.10804 0.43709 0.21118 0.63354 0.3094s0.36588 0.20872 0.5083 0.3315 0.25538 0.27011 0.33887 0.442 0.12523 0.38061 0.12523 0.62617c0 0.47147-0.1768 0.85208-0.5304 1.1418s-0.84963 0.43464-1.4881 0.43464c-0.50094 0-0.98468-0.1105-1.4512-0.3315s-0.87173-0.51321-1.2155-0.87664l-0.73667 0.85454c0.42236 0.442 0.92329 0.79069 1.5028 1.0461s1.2081 0.38307 1.8859 0.38307zm6.2664-0.1768v-4.5968c0.24556-0.60898 0.53286-1.0362 0.8619-1.2818s0.64581-0.36834 0.9503-0.36834c0.14733 0 0.27011 0.0098223 0.36834 0.029467s0.20627 0.049111 0.32413 0.0884l0.23573-1.0608c-0.22591-0.098223-0.48129-0.14733-0.76614-0.14733-0.41254 0-0.79315 0.1326-1.1418 0.3978s-0.64581 0.62371-0.89137 1.0755h-0.0442l-0.10313-1.2965h-1.0019v7.1604h1.2081zm6.5758 0.1768c0.43218 0 0.84471-0.081034 1.2376-0.2431s0.7514-0.38552 1.0755-0.67037l-0.5304-0.81034c-0.22591 0.19645-0.47884 0.36588-0.75877 0.5083s-0.58688 0.21363-0.92084 0.21363c-0.32413 0-0.62371-0.0663-0.89874-0.1989s-0.5083-0.31922-0.69984-0.55987-0.34132-0.52795-0.44937-0.8619-0.16207-0.7072-0.16207-1.1197 0.056478-0.78824 0.16943-1.1271 0.26766-0.63108 0.4641-0.87664 0.43218-0.43464 0.7072-0.56724 0.5746-0.1989 0.89874-0.1989c0.28485 0 0.54268 0.058934 0.7735 0.1768s0.44937 0.27011 0.65564 0.45674l0.6188-0.7956c-0.25538-0.22591-0.55005-0.42236-0.884-0.58934s-0.73667-0.25047-1.2081-0.25047c-0.46165 0-0.90119 0.083489-1.3186 0.25047s-0.78333 0.41254-1.0976 0.73667-0.56478 0.71948-0.7514 1.186-0.27993 0.99942-0.27993 1.5986c0 0.58934 0.085945 1.1173 0.25783 1.5838s0.40762 0.85945 0.7072 1.1787 0.65564 0.56232 1.0682 0.7293 0.85454 0.25047 1.326 0.25047z" fill="#fff" fill-rule="nonzero"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -161,7 +161,6 @@ interface WriteOptions {
image: SourceMetadata;
destinations: DrivelistDrive[];
unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean;
autoBlockmapping: boolean;
decompressFirst: boolean;
SourceType: string;
@ -255,7 +254,6 @@ ipc.connectTo(IPC_SERVER_ID, () => {
log(`Image: ${imagePath}`);
log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`);
log(`Auto blockmapping: ${options.autoBlockmapping}`);
log(`Decompress first: ${options.decompressFirst}`);
const dests = options.destinations.map((destination) => {
@ -286,7 +284,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
const results = await writeAndValidate({
source,
destinations: dests,
verify: options.validateWriteOnSuccess,
verify: true,
autoBlockmapping: options.autoBlockmapping,
decompressFirst: options.decompressFirst,
onProgress,

View File

@ -34,16 +34,6 @@ export type DrivelistDrive = Drive & {
displayName: string;
};
/**
* @summary Check if a drive is locked
*
* @description
* This usually points out a locked SD Card.
*/
export function isDriveLocked(drive: DrivelistDrive): boolean {
return Boolean(drive.isReadOnly);
}
/**
* @summary Check if a drive is a system drive
*/
@ -122,14 +112,13 @@ export function isDriveDisabled(drive: DrivelistDrive): boolean {
}
/**
* @summary Check if a drive is valid, i.e. not locked and large enough for an image
* @summary Check if a drive is valid, i.e. large enough for an image
*/
export function isDriveValid(
drive: DrivelistDrive,
image?: SourceMetadata,
): boolean {
return (
!isDriveLocked(drive) &&
isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image as SourceMetadata) &&
!isDriveDisabled(drive)
@ -213,17 +202,19 @@ export const statuses = {
*/
export function getDriveImageCompatibilityStatuses(
drive: DrivelistDrive,
image?: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
const statusList = [];
// Mind the order of the if-statements if you modify.
if (isDriveLocked(drive)) {
if (drive.isReadOnly && write) {
statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.locked(),
});
} else if (
}
if (
!_.isNil(drive) &&
!_.isNil(drive.size) &&
!isDriveLargeEnough(drive, image)
@ -262,10 +253,11 @@ export function getDriveImageCompatibilityStatuses(
*/
export function getListDriveImageCompatibilityStatuses(
drives: DrivelistDrive[],
image: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
return drives.flatMap((drive) => {
return getDriveImageCompatibilityStatuses(drive, image);
return getDriveImageCompatibilityStatuses(drive, image, write);
});
}
@ -277,9 +269,12 @@ export function getListDriveImageCompatibilityStatuses(
*/
export function hasDriveImageCompatibilityStatus(
drive: DrivelistDrive,
image: SourceMetadata,
image: SourceMetadata | undefined,
write: boolean,
) {
return Boolean(getDriveImageCompatibilityStatuses(drive, image).length);
return Boolean(
getDriveImageCompatibilityStatuses(drive, image, write).length,
);
}
export interface DriveStatus {

View File

@ -117,6 +117,14 @@ export const warning = {
].join(' ');
},
driveMissingPartitionTable: () => {
return outdent({ newline: ' ' })`
It looks like this is not a bootable drive.
The drive does not appear to contain a partition table,
and might not be recognized or bootable by your device.
`;
},
largeDriveSize: () => {
return 'This is a large drive! Make sure it doesn\'t contain files that you want to keep.';
},

1630
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -71,13 +71,13 @@
"css-loader": "^4.2.1",
"d3": "^4.13.0",
"debug": "^4.2.0",
"electron": "9.2.1",
"electron-builder": "^22.7.0",
"electron-mocha": "^9.1.0",
"electron": "9.3.3",
"electron-builder": "^22.9.1",
"electron-mocha": "^9.3.2",
"electron-notarize": "^1.0.0",
"electron-rebuild": "^1.11.0",
"electron-updater": "^4.3.2",
"etcher-sdk": "^4.1.30",
"electron-rebuild": "^2.3.2",
"electron-updater": "^4.3.5",
"etcher-sdk": "^5.1.5",
"file-loader": "^6.0.0",
"husky": "^4.2.5",
"immutable": "^3.8.1",
@ -108,7 +108,7 @@
"ts-loader": "^8.0.0",
"ts-node": "^9.0.0",
"tslib": "^2.0.0",
"typescript": "^4.0.2",
"typescript": "^4.1.2",
"uuid": "^8.1.0",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.9"

View File

@ -32,7 +32,6 @@ describe('Browser: progressStatus', function () {
};
settings.set('unmountOnSuccess', true);
settings.set('validateWriteOnSuccess', true);
});
it('should report 0% if percentage == 0 but speed != 0', function () {
@ -105,25 +104,16 @@ describe('Browser: progressStatus', function () {
);
});
it('should handle percentage == 100, flashing, unmountOnSuccess, validateWriteOnSuccess', function () {
it('should handle percentage == 100, flashing, unmountOnSuccess', function () {
this.state.percentage = 100;
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);
});
it('should handle percentage == 100, flashing, unmountOnSuccess, !validateWriteOnSuccess', function () {
this.state.percentage = 100;
settings.set('validateWriteOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);
});
it('should handle percentage == 100, flashing, !unmountOnSuccess, !validateWriteOnSuccess', function () {
it('should handle percentage == 100, flashing, !unmountOnSuccess', function () {
this.state.percentage = 100;
settings.set('unmountOnSuccess', false);
settings.set('validateWriteOnSuccess', false);
expect(progressStatus.titleFromFlashState(this.state)).to.equal(
'Finishing...',
);

View File

@ -23,37 +23,6 @@ import * as constraints from '../../lib/shared/drive-constraints';
import * as messages from '../../lib/shared/messages';
describe('Shared: DriveConstraints', function () {
describe('.isDriveLocked()', function () {
it('should return true if the drive is read-only', function () {
const result = constraints.isDriveLocked({
device: '/dev/disk2',
size: 999999999,
isReadOnly: true,
} as constraints.DrivelistDrive);
expect(result).to.be.true;
});
it('should return false if the drive is not read-only', function () {
const result = constraints.isDriveLocked({
device: '/dev/disk2',
size: 999999999,
isReadOnly: false,
} as constraints.DrivelistDrive);
expect(result).to.be.false;
});
it("should return false if we don't know if the drive is read-only", function () {
const result = constraints.isDriveLocked({
device: '/dev/disk2',
size: 999999999,
} as constraints.DrivelistDrive);
expect(result).to.be.false;
});
});
describe('.isSystemDrive()', function () {
it('should return true if the drive is a system drive', function () {
const result = constraints.isSystemDrive({
@ -745,7 +714,7 @@ describe('Shared: DriveConstraints', function () {
this.drive.disabled = false;
});
it('should return false if the drive is not large enough and is a source drive', function () {
it('should return false if the drive is not large enough and is the source drive', function () {
expect(
constraints.isDriveValid(this.drive, {
...image,
@ -755,7 +724,7 @@ describe('Shared: DriveConstraints', function () {
).to.be.false;
});
it('should return false if the drive is not large enough and is not a source drive', function () {
it('should return false if the drive is not large enough and is not the source drive', function () {
expect(
constraints.isDriveValid(this.drive, {
...image,
@ -765,17 +734,17 @@ describe('Shared: DriveConstraints', function () {
).to.be.false;
});
it('should return false if the drive is large enough and is a source drive', function () {
expect(constraints.isDriveValid(this.drive, image)).to.be.false;
it('should return true if the drive is large enough and is the source drive', function () {
expect(constraints.isDriveValid(this.drive, image)).to.be.true;
});
it('should return false if the drive is large enough and is not a source drive', function () {
it('should return true if the drive is large enough and is not the source drive', function () {
expect(
constraints.isDriveValid(this.drive, {
...image,
path: path.resolve(this.mountpoint, '../bar/rpi.img'),
}),
).to.be.false;
).to.be.true;
});
});
});
@ -983,6 +952,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
expect(result).to.deep.equal([]);
@ -995,6 +965,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expectedTuples: Array<['WARNING' | 'ERROR', string]> = [];
@ -1009,6 +980,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'containsImage']];
@ -1025,6 +997,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expectedTuples = [['WARNING', 'system']];
@ -1040,6 +1013,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expected = [
{
@ -1060,6 +1034,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [];
@ -1076,6 +1051,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'locked']];
@ -1092,6 +1068,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['WARNING', 'sizeNotRecommended']];
@ -1108,6 +1085,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expectedTuples = [['WARNING', 'largeDrive']];
@ -1128,9 +1106,13 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'locked']];
const expectedTuples = [
['ERROR', 'locked'],
['ERROR', 'containsImage'],
];
// @ts-ignore
expectStatusTypesAndMessagesToBe(result, expectedTuples);
@ -1144,6 +1126,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [['ERROR', 'locked']];
@ -1161,6 +1144,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
const expected = [
{
@ -1181,6 +1165,7 @@ describe('Shared: DriveConstraints', function () {
const result = constraints.getDriveImageCompatibilityStatuses(
this.drive,
this.image,
true,
);
// @ts-ignore
const expectedTuples = [
@ -1287,7 +1272,7 @@ describe('Shared: DriveConstraints', function () {
describe('given no drives', function () {
it('should return no statuses', function () {
expect(
constraints.getListDriveImageCompatibilityStatuses([], image),
constraints.getListDriveImageCompatibilityStatuses([], image, true),
).to.deep.equal([]);
});
});
@ -1298,6 +1283,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[0]],
image,
true,
),
).to.deep.equal([
{
@ -1312,6 +1298,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[1]],
image,
true,
),
).to.deep.equal([
{
@ -1326,6 +1313,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[2]],
image,
true,
),
).to.deep.equal([
{
@ -1340,6 +1328,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[3]],
image,
true,
),
).to.deep.equal([
{
@ -1354,6 +1343,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[4]],
image,
true,
),
).to.deep.equal([
{
@ -1368,6 +1358,7 @@ describe('Shared: DriveConstraints', function () {
constraints.getListDriveImageCompatibilityStatuses(
[drives[5]],
image,
true,
),
).to.deep.equal([
{
@ -1381,7 +1372,11 @@ describe('Shared: DriveConstraints', function () {
describe('given multiple drives with all warnings/errors', function () {
it('should return all statuses', function () {
expect(
constraints.getListDriveImageCompatibilityStatuses(drives, image),
constraints.getListDriveImageCompatibilityStatuses(
drives,
image,
true,
),
).to.deep.equal([
{
message: 'Source drive',

View File

@ -108,6 +108,23 @@ function replace(test: RegExp, ...replacements: ReplacementRule[]) {
};
}
function fetchWasm(...where: string[]) {
const whereStr = where.map((x) => `'${x}'`).join(', ');
return outdent`
const Path = require('path');
let electron;
try {
// This doesn't exist in the child-writer
electron = require('electron');
} catch {
}
function appPath() {
return Path.isAbsolute(__dirname) ? __dirname : Path.join(electron.remote.app.getAppPath(), 'generated');
}
scriptDirectory = Path.join(appPath(), 'modules', ${whereStr}, '/');
`;
}
const commonConfig = {
mode: 'production',
optimization: {
@ -193,11 +210,6 @@ const commonConfig = {
search: 'require(binding_path)',
replace: "require('./build/Release/usb_bindings.node')",
}),
// remove bindings magic from ext2fs
replace(/node_modules\/ext2fs\/lib\/(ext2fs|binding)\.js$/, {
search: "require('bindings')('bindings')",
replace: "require('../build/Release/bindings.node')",
}),
// remove bindings magic from mountutils
replace(/node_modules\/mountutils\/index\.js$/, {
search: outdent`
@ -232,6 +244,18 @@ const commonConfig = {
return await readFile(Path.join((app || remote.app).getAppPath(), 'generated', __dirname.replace('node_modules', 'modules'), '..', 'blobs', filename));
`,
}),
// Use the libext2fs.wasm file in the generated folder
// The way to find the app directory depends on whether we run in the renderer or in the child-writer
// We use __dirname in the child-writer and electron.remote.app.getAppPath() in the renderer
replace(/node_modules\/ext2fs\/lib\/libext2fs\.js$/, {
search: 'scriptDirectory=__dirname+"/"',
replace: fetchWasm('ext2fs', 'lib'),
}),
// Same for node-crc-utils
replace(/node_modules\/@balena\/node-crc-utils\/crc32\.js$/, {
search: 'scriptDirectory=__dirname+"/"',
replace: fetchWasm('@balena', 'node-crc-utils'),
}),
// Copy native modules to generated folder
{
test: /\.node$/,
@ -281,6 +305,14 @@ const guiConfigCopyPatterns = [
from: 'node_modules/node-raspberrypi-usbboot/blobs',
to: 'modules/node-raspberrypi-usbboot/blobs',
},
{
from: 'node_modules/ext2fs/lib/libext2fs.wasm',
to: 'modules/ext2fs/lib/libext2fs.wasm',
},
{
from: 'node_modules/@balena/node-crc-utils/crc32.wasm',
to: 'modules/@balena/node-crc-utils/crc32.wasm',
},
];
if (os.platform() === 'win32') {