Add clone-drive workflow

Change-type: patch
Changelog-entry: Add clone-drive workflow
Signed-off-by: Lorenzo Alberto Maria Ambrosi <lorenzothunder.ambrosi@gmail.com>
This commit is contained in:
Lorenzo Alberto Maria Ambrosi 2020-06-22 17:16:41 +02:00
parent 377dfb8e22
commit dda022df37
15 changed files with 389 additions and 332 deletions

View File

@ -167,7 +167,7 @@ export class DriveSelector extends React.Component<
DriveSelectorState DriveSelectorState
> { > {
private unsubscribe: (() => void) | undefined; private unsubscribe: (() => void) | undefined;
multipleSelection: boolean; multipleSelection: boolean = true;
tableColumns: Array<TableColumn<Drive>>; tableColumns: Array<TableColumn<Drive>>;
constructor(props: DriveSelectorProps) { constructor(props: DriveSelectorProps) {
@ -175,7 +175,11 @@ export class DriveSelector extends React.Component<
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {}; const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
const selectedList = getSelectedDrives(); const selectedList = getSelectedDrives();
this.multipleSelection = !!this.props.multipleSelection; const multipleSelection = this.props.multipleSelection;
this.multipleSelection =
multipleSelection !== undefined
? !!multipleSelection
: this.multipleSelection;
this.state = { this.state = {
drives: getDrives(), drives: getDrives(),
@ -383,10 +387,7 @@ export class DriveSelector extends React.Component<
<b>{this.props.emptyListLabel}</b> <b>{this.props.emptyListLabel}</b>
</Flex> </Flex>
) : ( ) : (
<ScrollableFlex <ScrollableFlex flexDirection="column" width="100%">
flexDirection="column"
width="100%"
>
<DrivesTable <DrivesTable
refFn={(t: Table<Drive>) => { refFn={(t: Table<Drive>) => {
if (t !== null) { if (t !== null) {

View File

@ -14,10 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg'; import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg'; import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg'; import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
import { sourceDestination } from 'etcher-sdk'; import { sourceDestination, scanner } from 'etcher-sdk';
import { ipcRenderer, IpcRendererEvent } from 'electron'; import { ipcRenderer, IpcRendererEvent } from 'electron';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { GPTPartition, MBRPartition } from 'partitioninfo'; import { GPTPartition, MBRPartition } from 'partitioninfo';
@ -57,6 +58,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
import { SVGIcon } from '../svg-icon/svg-icon'; import { SVGIcon } from '../svg-icon/svg-icon';
import ImageSvg from '../../../assets/image.svg'; import ImageSvg from '../../../assets/image.svg';
import { DriveSelector } from '../drive-selector/drive-selector';
const recentUrlImagesKey = 'recentUrlImages'; const recentUrlImagesKey = 'recentUrlImages';
@ -92,6 +94,9 @@ function setRecentUrlImages(urls: URL[]) {
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized)); localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
} }
const isURL = (imagePath: string) =>
imagePath.startsWith('https://') || imagePath.startsWith('http://');
const Card = styled(BaseCard)` const Card = styled(BaseCard)`
hr { hr {
margin: 5px 0; margin: 5px 0;
@ -117,6 +122,10 @@ function getState() {
}; };
} }
function isString(value: any): value is string {
return typeof value === 'string';
}
const URLSelector = ({ const URLSelector = ({
done, done,
cancel, cancel,
@ -203,7 +212,12 @@ interface Flow {
const FlowSelector = styled( const FlowSelector = styled(
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => { ({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => {
return ( return (
<StepButton plain onClick={flow.onClick} icon={flow.icon} {...props}> <StepButton
plain
onClick={(evt) => flow.onClick(evt)}
icon={flow.icon}
{...props}
>
{flow.label} {flow.label}
</StepButton> </StepButton>
); );
@ -225,16 +239,20 @@ const FlowSelector = styled(
export type Source = export type Source =
| typeof sourceDestination.File | typeof sourceDestination.File
| typeof sourceDestination.BlockDevice
| typeof sourceDestination.Http; | typeof sourceDestination.Http;
export interface SourceOptions { export interface SourceMetadata extends sourceDestination.Metadata {
imagePath: string; hasMBR: boolean;
partitions: MBRPartition[] | GPTPartition[];
path: string;
SourceType: Source; SourceType: Source;
drive?: scanner.adapters.DrivelistDrive;
extension?: string;
} }
interface SourceSelectorProps { interface SourceSelectorProps {
flashing: boolean; flashing: boolean;
afterSelected: (options: SourceOptions) => void;
} }
interface SourceSelectorState { interface SourceSelectorState {
@ -244,6 +262,7 @@ interface SourceSelectorState {
warning: { message: string; title: string | null } | null; warning: { message: string; title: string | null } | null;
showImageDetails: boolean; showImageDetails: boolean;
showURLSelector: boolean; showURLSelector: boolean;
showDriveSelector: boolean;
} }
export class SourceSelector extends React.Component< export class SourceSelector extends React.Component<
@ -251,7 +270,6 @@ export class SourceSelector extends React.Component<
SourceSelectorState SourceSelectorState
> { > {
private unsubscribe: (() => void) | undefined; private unsubscribe: (() => void) | undefined;
private afterSelected: SourceSelectorProps['afterSelected'];
constructor(props: SourceSelectorProps) { constructor(props: SourceSelectorProps) {
super(props); super(props);
@ -260,15 +278,8 @@ export class SourceSelector extends React.Component<
warning: null, warning: null,
showImageDetails: false, showImageDetails: false,
showURLSelector: false, showURLSelector: false,
showDriveSelector: false,
}; };
this.openImageSelector = this.openImageSelector.bind(this);
this.openURLSelector = this.openURLSelector.bind(this);
this.reselectImage = this.reselectImage.bind(this);
this.onSelectImage = this.onSelectImage.bind(this);
this.onDrop = this.onDrop.bind(this);
this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this);
this.afterSelected = props.afterSelected.bind(this);
} }
public componentDidMount() { public componentDidMount() {
@ -285,15 +296,28 @@ export class SourceSelector extends React.Component<
} }
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) { private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
const isURL = await this.selectSource(
imagePath.startsWith('https://') || imagePath.startsWith('http://');
await this.selectImageByPath({
imagePath, imagePath,
SourceType: isURL ? sourceDestination.Http : sourceDestination.File, isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
}).promise; ).promise;
} }
private reselectImage() { private async createSource(selected: string, SourceType: Source) {
try {
selected = await replaceWindowsNetworkDriveLetter(selected);
} catch (error) {
analytics.logException(error);
}
if (SourceType === sourceDestination.File) {
return new sourceDestination.File({
path: selected,
});
}
return new sourceDestination.Http({ url: selected });
}
private reselectSource() {
analytics.logEvent('Reselect image', { analytics.logEvent('Reselect image', {
previousImage: selectionState.getImage(), previousImage: selectionState.getImage(),
}); });
@ -301,133 +325,68 @@ export class SourceSelector extends React.Component<
selectionState.deselectImage(); selectionState.deselectImage();
} }
private selectImage( private selectSource(
image: sourceDestination.Metadata & { selected: string | scanner.adapters.DrivelistDrive,
path: string; SourceType: Source,
extension: string; ): { promise: Promise<void>; cancel: () => void } {
hasMBR: boolean;
},
) {
try {
let message = null;
let title = null;
if (supportedFormats.looksLikeWindowsImage(image.path)) {
analytics.logEvent('Possibly Windows image', { image });
message = messages.warning.looksLikeWindowsImage();
title = 'Possible Windows image detected';
} else if (!image.hasMBR) {
analytics.logEvent('Missing partition table', { image });
title = 'Missing partition table';
message = messages.warning.missingPartitionTable();
}
if (message) {
this.setState({
warning: {
message,
title,
},
});
}
selectionState.selectImage(image);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
// certain features without printing pages of text to DevTools.
image: {
...image,
logo: Boolean(image.logo),
blockMap: Boolean(image.blockMap),
},
});
} catch (error) {
exceptionReporter.report(error);
}
}
private selectImageByPath({
imagePath,
SourceType,
}: SourceOptions): { promise: Promise<void>; cancel: () => void } {
let cancelled = false; let cancelled = false;
return { return {
cancel: () => { cancel: () => {
cancelled = true; cancelled = true;
}, },
promise: (async () => { promise: (async () => {
try { const sourcePath = isString(selected) ? selected : selected.device;
imagePath = await replaceWindowsNetworkDriveLetter(imagePath); let metadata: SourceMetadata | undefined;
} catch (error) { if (isString(selected)) {
analytics.logException(error); const source = await this.createSource(selected, SourceType);
}
if (cancelled) { if (cancelled) {
return; return;
} }
let source;
if (SourceType === sourceDestination.File) {
source = new sourceDestination.File({
path: imagePath,
});
} else {
if (
!imagePath.startsWith('https://') &&
!imagePath.startsWith('http://')
) {
const invalidImageError = errors.createUserError({
title: 'Unsupported protocol',
description: messages.error.unsupportedProtocol(),
});
osDialog.showError(invalidImageError);
analytics.logEvent('Unsupported protocol', { path: imagePath });
return;
}
source = new sourceDestination.Http({ url: imagePath });
}
try { try {
const innerSource = await source.getInnerSource(); const innerSource = await source.getInnerSource();
if (cancelled) { if (cancelled) {
return; return;
} }
const metadata = (await innerSource.getMetadata()) as sourceDestination.Metadata & { metadata = await this.getMetadata(innerSource);
hasMBR: boolean;
partitions: MBRPartition[] | GPTPartition[];
path: string;
extension: string;
};
if (cancelled) { if (cancelled) {
return; return;
} }
const partitionTable = await innerSource.getPartitionTable(); if (SourceType === sourceDestination.Http && !isURL(selected)) {
if (cancelled) { this.handleError(
'Unsupported protocol',
selected,
messages.error.unsupportedProtocol(),
);
return; return;
} }
if (partitionTable) { if (supportedFormats.looksLikeWindowsImage(selected)) {
metadata.hasMBR = true; analytics.logEvent('Possibly Windows image', { image: selected });
metadata.partitions = partitionTable.partitions; this.setState({
} else { warning: {
metadata.hasMBR = false; message: messages.warning.looksLikeWindowsImage(),
} title: 'Possible Windows image detected',
metadata.path = imagePath; },
metadata.extension = path.extname(imagePath).slice(1);
this.selectImage(metadata);
this.afterSelected({
imagePath,
SourceType,
}); });
}
metadata.extension = path.extname(selected).slice(1);
metadata.path = selected;
if (!metadata.hasMBR) {
analytics.logEvent('Missing partition table', { metadata });
this.setState({
warning: {
message: messages.warning.missingPartitionTable(),
title: 'Missing partition table',
},
});
}
} catch (error) { } catch (error) {
const imageError = errors.createUserError({ this.handleError(
title: 'Error opening image', 'Error opening source',
description: messages.error.openImage( sourcePath,
path.basename(imagePath), messages.error.openSource(sourcePath, error.message),
error.message, error,
), );
});
osDialog.showError(imageError);
analytics.logException(error);
} finally { } finally {
try { try {
await source.close(); await source.close();
@ -435,10 +394,65 @@ export class SourceSelector extends React.Component<
// Noop // Noop
} }
} }
} else {
metadata = {
path: selected.device,
size: selected.size as SourceMetadata['size'],
hasMBR: false,
partitions: [],
SourceType: sourceDestination.BlockDevice,
drive: selected,
};
}
if (metadata !== undefined) {
selectionState.selectSource(metadata);
analytics.logEvent('Select image', {
// An easy way so we can quickly identify if we're making use of
// certain features without printing pages of text to DevTools.
image: {
...metadata,
logo: Boolean(metadata.logo),
blockMap: Boolean(metadata.blockMap),
},
});
}
})(), })(),
}; };
} }
private handleError(
title: string,
sourcePath: string,
description: string,
error?: any,
) {
const imageError = errors.createUserError({
title,
description,
});
osDialog.showError(imageError);
if (error) {
analytics.logException(error);
return;
}
analytics.logEvent(title, { path: sourcePath });
}
private async getMetadata(
source: sourceDestination.SourceDestination | sourceDestination.BlockDevice,
) {
const metadata = (await source.getMetadata()) as SourceMetadata;
const partitionTable = await source.getPartitionTable();
if (partitionTable) {
metadata.hasMBR = true;
metadata.partitions = partitionTable.partitions;
} else {
metadata.hasMBR = false;
}
return metadata;
}
private async openImageSelector() { private async openImageSelector() {
analytics.logEvent('Open image selector'); analytics.logEvent('Open image selector');
@ -450,10 +464,7 @@ export class SourceSelector extends React.Component<
analytics.logEvent('Image selector closed'); analytics.logEvent('Image selector closed');
return; return;
} }
await this.selectImageByPath({ await this.selectSource(imagePath, sourceDestination.File).promise;
imagePath,
SourceType: sourceDestination.File,
}).promise;
} catch (error) { } catch (error) {
exceptionReporter.report(error); exceptionReporter.report(error);
} }
@ -462,10 +473,7 @@ export class SourceSelector extends React.Component<
private async onDrop(event: React.DragEvent<HTMLDivElement>) { private async onDrop(event: React.DragEvent<HTMLDivElement>) {
const [file] = event.dataTransfer.files; const [file] = event.dataTransfer.files;
if (file) { if (file) {
await this.selectImageByPath({ await this.selectSource(file.path, sourceDestination.File).promise;
imagePath: file.path,
SourceType: sourceDestination.File,
}).promise;
} }
} }
@ -477,6 +485,14 @@ export class SourceSelector extends React.Component<
}); });
} }
private openDriveSelector() {
analytics.logEvent('Open drive selector');
this.setState({
showDriveSelector: true,
});
}
private onDragOver(event: React.DragEvent<HTMLDivElement>) { private onDragOver(event: React.DragEvent<HTMLDivElement>) {
// Needed to get onDrop events on div elements // Needed to get onDrop events on div elements
event.preventDefault(); event.preventDefault();
@ -500,27 +516,35 @@ export class SourceSelector extends React.Component<
// TODO add a visual change when dragging a file over the selector // TODO add a visual change when dragging a file over the selector
public render() { public render() {
const { flashing } = this.props; const { flashing } = this.props;
const { showImageDetails, showURLSelector } = this.state; const { showImageDetails, showURLSelector, showDriveSelector } = this.state;
const hasImage = selectionState.hasImage(); const hasSource = selectionState.hasImage();
let image = hasSource ? selectionState.getImage() : {};
image = image.drive ? image.drive : image;
const imagePath = selectionState.getImagePath();
const imageBasename = hasImage ? path.basename(imagePath) : '';
const imageName = selectionState.getImageName();
const imageSize = selectionState.getImageSize();
const imageLogo = selectionState.getImageLogo();
let cancelURLSelection = () => { let cancelURLSelection = () => {
// noop // noop
}; };
image.name = image.description || image.name;
const imagePath = image.path || '';
const imageBasename = path.basename(image.path || '');
const imageName = image.name || '';
const imageSize = image.size || '';
const imageLogo = image.logo || '';
return ( return (
<> <>
<Flex <Flex
flexDirection="column" flexDirection="column"
alignItems="center" alignItems="center"
onDrop={this.onDrop} onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
onDragEnter={this.onDragEnter} onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
onDragOver={this.onDragOver} this.onDragEnter(evt)
}
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
this.onDragOver(evt)
}
> >
<SVGIcon <SVGIcon
contents={imageLogo} contents={imageLogo}
@ -530,17 +554,21 @@ export class SourceSelector extends React.Component<
}} }}
/> />
{hasImage ? ( {hasSource ? (
<> <>
<StepNameButton <StepNameButton
plain plain
onClick={this.showSelectedImageDetails} onClick={() => this.showSelectedImageDetails()}
tooltip={imageName || imageBasename} tooltip={imageName || imageBasename}
> >
{middleEllipsis(imageName || imageBasename, 20)} {middleEllipsis(imageName || imageBasename, 20)}
</StepNameButton> </StepNameButton>
{!flashing && ( {!flashing && (
<ChangeButton plain mb={14} onClick={this.reselectImage}> <ChangeButton
plain
mb={14}
onClick={() => this.reselectSource()}
>
Remove Remove
</ChangeButton> </ChangeButton>
)} )}
@ -551,7 +579,7 @@ export class SourceSelector extends React.Component<
<FlowSelector <FlowSelector
key="Flash from file" key="Flash from file"
flow={{ flow={{
onClick: this.openImageSelector, onClick: () => this.openImageSelector(),
label: 'Flash from file', label: 'Flash from file',
icon: <FileSvg height="1em" fill="currentColor" />, icon: <FileSvg height="1em" fill="currentColor" />,
}} }}
@ -559,11 +587,19 @@ export class SourceSelector extends React.Component<
<FlowSelector <FlowSelector
key="Flash from URL" key="Flash from URL"
flow={{ flow={{
onClick: this.openURLSelector, onClick: () => this.openURLSelector(),
label: 'Flash from URL', label: 'Flash from URL',
icon: <LinkSvg height="1em" fill="currentColor" />, icon: <LinkSvg height="1em" fill="currentColor" />,
}} }}
/> />
<FlowSelector
key="Clone drive"
flow={{
onClick: () => this.openDriveSelector(),
label: 'Clone drive',
icon: <CopySvg height="1em" fill="currentColor" />,
}}
/>
</> </>
)} )}
</Flex> </Flex>
@ -579,7 +615,7 @@ export class SourceSelector extends React.Component<
action="Continue" action="Continue"
cancel={() => { cancel={() => {
this.setState({ warning: null }); this.setState({ warning: null });
this.reselectImage(); this.reselectSource();
}} }}
done={() => { done={() => {
this.setState({ warning: null }); this.setState({ warning: null });
@ -625,13 +661,10 @@ export class SourceSelector extends React.Component<
analytics.logEvent('URL selector closed'); analytics.logEvent('URL selector closed');
} else { } else {
let promise; let promise;
({ ({ promise, cancel: cancelURLSelection } = this.selectSource(
promise, imageURL,
cancel: cancelURLSelection, sourceDestination.Http,
} = this.selectImageByPath({ ));
imagePath: imageURL,
SourceType: sourceDestination.Http,
}));
await promise; await promise;
} }
this.setState({ this.setState({
@ -640,6 +673,32 @@ export class SourceSelector extends React.Component<
}} }}
/> />
)} )}
{showDriveSelector && (
<DriveSelector
multipleSelection={false}
titleLabel="Select source"
emptyListLabel="Plug a source"
cancel={() => {
this.setState({
showDriveSelector: false,
});
}}
done={async (drives: scanner.adapters.DrivelistDrive[]) => {
if (!drives.length) {
analytics.logEvent('Drive selector closed');
this.setState({
showDriveSelector: false,
});
return;
}
await this.selectSource(drives[0], sourceDestination.BlockDevice);
this.setState({
showDriveSelector: false,
});
}}
/>
)}
</> </>
); );
} }

View File

@ -24,7 +24,7 @@ export function hasAvailableDrives() {
export function setDrives(drives: any[]) { export function setDrives(drives: any[]) {
store.dispatch({ store.dispatch({
type: Actions.SET_AVAILABLE_DRIVES, type: Actions.SET_AVAILABLE_TARGETS,
data: drives, data: drives,
}); });
} }

View File

@ -24,7 +24,7 @@ import { Actions, store } from './store';
*/ */
export function selectDrive(driveDevice: string) { export function selectDrive(driveDevice: string) {
store.dispatch({ store.dispatch({
type: Actions.SELECT_DRIVE, type: Actions.SELECT_TARGET,
data: driveDevice, data: driveDevice,
}); });
} }
@ -40,10 +40,10 @@ export function toggleDrive(driveDevice: string) {
} }
} }
export function selectImage(image: any) { export function selectSource(source: any) {
store.dispatch({ store.dispatch({
type: Actions.SELECT_IMAGE, type: Actions.SELECT_SOURCE,
data: image, data: source,
}); });
} }
@ -122,14 +122,14 @@ export function hasImage(): boolean {
*/ */
export function deselectDrive(driveDevice: string) { export function deselectDrive(driveDevice: string) {
store.dispatch({ store.dispatch({
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: driveDevice, data: driveDevice,
}); });
} }
export function deselectImage() { export function deselectImage() {
store.dispatch({ store.dispatch({
type: Actions.DESELECT_IMAGE, type: Actions.DESELECT_SOURCE,
data: {}, data: {},
}); });
} }

View File

@ -80,15 +80,15 @@ export const DEFAULT_STATE = Immutable.fromJS({
export enum Actions { export enum Actions {
SET_DEVICE_PATHS, SET_DEVICE_PATHS,
SET_FAILED_DEVICE_PATHS, SET_FAILED_DEVICE_PATHS,
SET_AVAILABLE_DRIVES, SET_AVAILABLE_TARGETS,
SET_FLASH_STATE, SET_FLASH_STATE,
RESET_FLASH_STATE, RESET_FLASH_STATE,
SET_FLASHING_FLAG, SET_FLASHING_FLAG,
UNSET_FLASHING_FLAG, UNSET_FLASHING_FLAG,
SELECT_DRIVE, SELECT_TARGET,
SELECT_IMAGE, SELECT_SOURCE,
DESELECT_DRIVE, DESELECT_TARGET,
DESELECT_IMAGE, DESELECT_SOURCE,
SET_APPLICATION_SESSION_UUID, SET_APPLICATION_SESSION_UUID,
SET_FLASHING_WORKFLOW_UUID, SET_FLASHING_WORKFLOW_UUID,
} }
@ -116,7 +116,7 @@ function storeReducer(
action: Action, action: Action,
): typeof DEFAULT_STATE { ): typeof DEFAULT_STATE {
switch (action.type) { switch (action.type) {
case Actions.SET_AVAILABLE_DRIVES: { case Actions.SET_AVAILABLE_TARGETS: {
// Type: action.data : Array<DriveObject> // Type: action.data : Array<DriveObject>
if (!action.data) { if (!action.data) {
@ -158,7 +158,7 @@ function storeReducer(
) { ) {
// Deselect this drive gone from availableDrives // Deselect this drive gone from availableDrives
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: device, data: device,
}); });
} }
@ -206,14 +206,14 @@ function storeReducer(
) { ) {
// Auto-select this drive // Auto-select this drive
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.SELECT_DRIVE, type: Actions.SELECT_TARGET,
data: drive.device, data: drive.device,
}); });
} }
// Deselect this drive in case it still is selected // Deselect this drive in case it still is selected
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: drive.device, data: drive.device,
}); });
}, },
@ -341,7 +341,7 @@ function storeReducer(
.set('flashState', DEFAULT_STATE.get('flashState')); .set('flashState', DEFAULT_STATE.get('flashState'));
} }
case Actions.SELECT_DRIVE: { case Actions.SELECT_TARGET: {
// Type: action.data : String // Type: action.data : String
const device = action.data; const device = action.data;
@ -391,10 +391,12 @@ function storeReducer(
// with image-stream / supported-formats, and have *one* // with image-stream / supported-formats, and have *one*
// place where all the image extension / format handling // place where all the image extension / format handling
// takes place, to avoid having to check 2+ locations with different logic // takes place, to avoid having to check 2+ locations with different logic
case Actions.SELECT_IMAGE: { case Actions.SELECT_SOURCE: {
// Type: action.data : ImageObject // Type: action.data : ImageObject
if (!action.data.drive) {
verifyNoNilFields(action.data, selectImageNoNilFields, 'image'); verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
}
if (!_.isString(action.data.path)) { if (!_.isString(action.data.path)) {
throw errors.createError({ throw errors.createError({
@ -456,7 +458,7 @@ function storeReducer(
!constraints.isDriveSizeRecommended(drive, action.data) !constraints.isDriveSizeRecommended(drive, action.data)
) { ) {
return storeReducer(accState, { return storeReducer(accState, {
type: Actions.DESELECT_DRIVE, type: Actions.DESELECT_TARGET,
data: device, data: device,
}); });
} }
@ -467,7 +469,7 @@ function storeReducer(
).setIn(['selection', 'image'], Immutable.fromJS(action.data)); ).setIn(['selection', 'image'], Immutable.fromJS(action.data));
} }
case Actions.DESELECT_DRIVE: { case Actions.DESELECT_TARGET: {
// Type: action.data : String // Type: action.data : String
if (!action.data) { if (!action.data) {
@ -491,7 +493,7 @@ function storeReducer(
); );
} }
case Actions.DESELECT_IMAGE: { case Actions.DESELECT_SOURCE: {
return state.deleteIn(['selection', 'image']); return state.deleteIn(['selection', 'image']);
} }

View File

@ -25,7 +25,7 @@ import * as path from 'path';
import * as packageJSON from '../../../../package.json'; import * as packageJSON from '../../../../package.json';
import * as errors from '../../../shared/errors'; import * as errors from '../../../shared/errors';
import * as permissions from '../../../shared/permissions'; import * as permissions from '../../../shared/permissions';
import { SourceOptions } from '../components/source-selector/source-selector'; import { SourceMetadata } from '../components/source-selector/source-selector';
import * as flashState from '../models/flash-state'; import * as flashState from '../models/flash-state';
import * as selectionState from '../models/selection-state'; import * as selectionState from '../models/selection-state';
import * as settings from '../models/settings'; import * as settings from '../models/settings';
@ -134,15 +134,11 @@ interface FlashResults {
cancelled?: boolean; cancelled?: boolean;
} }
/** export async function performWrite(
* @summary Perform write operation image: SourceMetadata,
*/
async function performWrite(
image: string,
drives: DrivelistDrive[], drives: DrivelistDrive[],
onProgress: sdk.multiWrite.OnProgressFunction, onProgress: sdk.multiWrite.OnProgressFunction,
source: SourceOptions, ): Promise<{ cancelled?: boolean }> {
): Promise<FlashResults> {
let cancelled = false; let cancelled = false;
ipc.serve(); ipc.serve();
const { const {
@ -196,10 +192,9 @@ async function performWrite(
ipc.server.on('ready', (_data, socket) => { ipc.server.on('ready', (_data, socket) => {
ipc.server.emit(socket, 'write', { ipc.server.emit(socket, 'write', {
imagePath: image, image,
destinations: drives, destinations: drives,
source, SourceType: image.SourceType.name,
SourceType: source.SourceType.name,
validateWriteOnSuccess, validateWriteOnSuccess,
autoBlockmapping, autoBlockmapping,
unmountOnSuccess, unmountOnSuccess,
@ -258,9 +253,8 @@ async function performWrite(
* @summary Flash an image to drives * @summary Flash an image to drives
*/ */
export async function flash( export async function flash(
image: string, image: SourceMetadata,
drives: DrivelistDrive[], drives: DrivelistDrive[],
source: SourceOptions,
// This function is a parameter so it can be mocked in tests // This function is a parameter so it can be mocked in tests
write = performWrite, write = performWrite,
): Promise<void> { ): Promise<void> {
@ -287,12 +281,7 @@ export async function flash(
analytics.logEvent('Flash', analyticsData); analytics.logEvent('Flash', analyticsData);
try { try {
const result = await write( const result = await write(image, drives, flashState.setProgressState);
image,
drives,
flashState.setProgressState,
source,
);
flashState.unsetFlashingFlag(result); flashState.unsetFlashingFlag(result);
} catch (error) { } catch (error) {
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code }); flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });

View File

@ -23,7 +23,6 @@ import { Flex, Modal, Txt } from 'rendition';
import * as constraints from '../../../../shared/drive-constraints'; import * as constraints from '../../../../shared/drive-constraints';
import * as messages from '../../../../shared/messages'; import * as messages from '../../../../shared/messages';
import { ProgressButton } from '../../components/progress-button/progress-button'; import { ProgressButton } from '../../components/progress-button/progress-button';
import { SourceOptions } from '../../components/source-selector/source-selector';
import * as availableDrives from '../../models/available-drives'; import * as availableDrives from '../../models/available-drives';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selection from '../../models/selection-state'; import * as selection from '../../models/selection-state';
@ -79,7 +78,6 @@ const getErrorMessageFromCode = (errorCode: string) => {
async function flashImageToDrive( async function flashImageToDrive(
isFlashing: boolean, isFlashing: boolean,
goToSuccess: () => void, goToSuccess: () => void,
sourceOptions: SourceOptions,
): Promise<string> { ): Promise<string> {
const devices = selection.getSelectedDevices(); const devices = selection.getSelectedDevices();
const image: any = selection.getImage(); const image: any = selection.getImage();
@ -98,7 +96,7 @@ async function flashImageToDrive(
const iconPath = path.join('media', 'icon.png'); const iconPath = path.join('media', 'icon.png');
const basename = path.basename(image.path); const basename = path.basename(image.path);
try { try {
await imageWriter.flash(image.path, drives, sourceOptions); await imageWriter.flash(image, drives);
if (!flashState.wasLastFlashCancelled()) { if (!flashState.wasLastFlashCancelled()) {
const flashResults: any = flashState.getFlashResults(); const flashResults: any = flashState.getFlashResults();
notification.send( notification.send(
@ -146,7 +144,6 @@ const formatSeconds = (totalSeconds: number) => {
interface FlashStepProps { interface FlashStepProps {
shouldFlashStepBeDisabled: boolean; shouldFlashStepBeDisabled: boolean;
goToSuccess: () => void; goToSuccess: () => void;
source: SourceOptions;
isFlashing: boolean; isFlashing: boolean;
style?: React.CSSProperties; style?: React.CSSProperties;
// TODO: factorize // TODO: factorize
@ -187,7 +184,6 @@ export class FlashStep extends React.PureComponent<
errorMessage: await flashImageToDrive( errorMessage: await flashImageToDrive(
this.props.isFlashing, this.props.isFlashing,
this.props.goToSuccess, this.props.goToSuccess,
this.props.source,
), ),
}); });
} }
@ -230,7 +226,6 @@ export class FlashStep extends React.PureComponent<
errorMessage: await flashImageToDrive( errorMessage: await flashImageToDrive(
this.props.isFlashing, this.props.isFlashing,
this.props.goToSuccess, this.props.goToSuccess,
this.props.source,
), ),
}); });
} }

View File

@ -17,7 +17,6 @@
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg'; import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg'; import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
import { sourceDestination } from 'etcher-sdk';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
import * as React from 'react'; import * as React from 'react';
@ -28,10 +27,7 @@ import FinishPage from '../../components/finish/finish';
import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos'; import { ReducedFlashingInfos } from '../../components/reduced-flashing-infos/reduced-flashing-infos';
import { SafeWebview } from '../../components/safe-webview/safe-webview'; import { SafeWebview } from '../../components/safe-webview/safe-webview';
import { SettingsModal } from '../../components/settings/settings'; import { SettingsModal } from '../../components/settings/settings';
import { import { SourceSelector } from '../../components/source-selector/source-selector';
SourceOptions,
SourceSelector,
} from '../../components/source-selector/source-selector';
import * as flashState from '../../models/flash-state'; import * as flashState from '../../models/flash-state';
import * as selectionState from '../../models/selection-state'; import * as selectionState from '../../models/selection-state';
import * as settings from '../../models/settings'; import * as settings from '../../models/settings';
@ -75,9 +71,12 @@ function getImageBasename() {
return ''; return '';
} }
const selectionImageName = selectionState.getImageName(); const image = selectionState.getImage();
const imageBasename = path.basename(selectionState.getImagePath()); if (image.drive) {
return selectionImageName || imageBasename; return image.drive.description;
}
const imageBasename = path.basename(image.path);
return image.name || imageBasename;
} }
const StepBorder = styled.div<{ const StepBorder = styled.div<{
@ -115,7 +114,6 @@ interface MainPageState {
current: 'main' | 'success'; current: 'main' | 'success';
isWebviewShowing: boolean; isWebviewShowing: boolean;
hideSettings: boolean; hideSettings: boolean;
source: SourceOptions;
featuredProjectURL?: string; featuredProjectURL?: string;
} }
@ -129,10 +127,6 @@ export class MainPage extends React.Component<
current: 'main', current: 'main',
isWebviewShowing: false, isWebviewShowing: false,
hideSettings: true, hideSettings: true,
source: {
imagePath: '',
SourceType: sourceDestination.File,
},
...this.stateHelper(), ...this.stateHelper(),
}; };
} }
@ -246,12 +240,7 @@ export class MainPage extends React.Component<
> >
{notFlashingOrSplitView && ( {notFlashingOrSplitView && (
<> <>
<SourceSelector <SourceSelector flashing={this.state.isFlashing} />
flashing={this.state.isFlashing}
afterSelected={(source: SourceOptions) =>
this.setState({ source })
}
/>
<Flex> <Flex>
<StepBorder disabled={shouldDriveStepBeDisabled} left /> <StepBorder disabled={shouldDriveStepBeDisabled} left />
</Flex> </Flex>
@ -316,7 +305,6 @@ export class MainPage extends React.Component<
<FlashStep <FlashStep
goToSuccess={() => this.setState({ current: 'success' })} goToSuccess={() => this.setState({ current: 'success' })}
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled} shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
source={this.state.source}
isFlashing={this.state.isFlashing} isFlashing={this.state.isFlashing}
step={state.type} step={state.type}
percentage={state.percentage} percentage={state.percentage}

View File

@ -20,10 +20,11 @@ import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
import * as ipc from 'node-ipc'; import * as ipc from 'node-ipc';
import { totalmem } from 'os'; import { totalmem } from 'os';
import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
import { toJSON } from '../../shared/errors'; import { toJSON } from '../../shared/errors';
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes'; import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
import { delay } from '../../shared/utils'; import { delay } from '../../shared/utils';
import { SourceOptions } from '../app/components/source-selector/source-selector'; import { SourceMetadata } from '../app/components/source-selector/source-selector';
ipc.config.id = process.env.IPC_CLIENT_ID as string; ipc.config.id = process.env.IPC_CLIENT_ID as string;
ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string; ipc.config.socketRoot = process.env.IPC_SOCKET_ROOT as string;
@ -143,13 +144,12 @@ async function writeAndValidate({
} }
interface WriteOptions { interface WriteOptions {
imagePath: string; image: SourceMetadata;
destinations: DrivelistDrive[]; destinations: DrivelistDrive[];
unmountOnSuccess: boolean; unmountOnSuccess: boolean;
validateWriteOnSuccess: boolean; validateWriteOnSuccess: boolean;
autoBlockmapping: boolean; autoBlockmapping: boolean;
decompressFirst: boolean; decompressFirst: boolean;
source: SourceOptions;
SourceType: string; SourceType: string;
} }
@ -228,7 +228,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
}; };
const destinations = options.destinations.map((d) => d.device); const destinations = options.destinations.map((d) => d.device);
log(`Image: ${options.imagePath}`); const imagePath = options.image.path;
log(`Devices: ${destinations.join(', ')}`); log(`Devices: ${destinations.join(', ')}`);
log(`Umount on success: ${options.unmountOnSuccess}`); log(`Umount on success: ${options.unmountOnSuccess}`);
log(`Validate on success: ${options.validateWriteOnSuccess}`); log(`Validate on success: ${options.validateWriteOnSuccess}`);
@ -243,18 +243,23 @@ ipc.connectTo(IPC_SERVER_ID, () => {
}); });
}); });
const { SourceType } = options; const { SourceType } = options;
try {
let source; let source;
if (SourceType === sdk.sourceDestination.File.name) { if (options.image.drive) {
source = new sdk.sourceDestination.File({ source = new BlockDevice({
path: options.imagePath, drive: options.image.drive,
write: false,
direct: !options.autoBlockmapping,
}); });
} else { } else {
source = new sdk.sourceDestination.Http({ if (SourceType === File.name) {
url: options.imagePath, source = new File({
avoidRandomAccess: true, path: imagePath,
}); });
} else {
source = new Http({ url: imagePath, avoidRandomAccess: true });
}
} }
try {
const results = await writeAndValidate({ const results = await writeAndValidate({
source, source,
destinations: dests, destinations: dests,

View File

@ -20,6 +20,7 @@ import * as pathIsInside from 'path-is-inside';
import * as prettyBytes from 'pretty-bytes'; import * as prettyBytes from 'pretty-bytes';
import * as messages from './messages'; import * as messages from './messages';
import { SourceMetadata } from '../gui/app/components/source-selector/source-selector';
/** /**
* @summary The default unknown size for things such as images and drives * @summary The default unknown size for things such as images and drives
@ -44,13 +45,22 @@ export function isSystemDrive(drive: DrivelistDrive): boolean {
} }
export interface Image { export interface Image {
path?: string; path: string;
isSizeEstimated?: boolean; isSizeEstimated?: boolean;
compressedSize?: number; compressedSize?: number;
recommendedDriveSize?: number; recommendedDriveSize?: number;
size?: number; size?: number;
} }
function sourceIsInsideDrive(source: string, drive: DrivelistDrive) {
for (const mountpoint of drive.mountpoints || []) {
if (pathIsInside(source, mountpoint.path)) {
return true;
}
}
return false;
}
/** /**
* @summary Check if a drive is source drive * @summary Check if a drive is source drive
* *
@ -60,11 +70,16 @@ export interface Image {
*/ */
export function isSourceDrive( export function isSourceDrive(
drive: DrivelistDrive, drive: DrivelistDrive,
image: Image = {}, selection?: SourceMetadata,
): boolean { ): boolean {
for (const mountpoint of drive.mountpoints || []) { if (selection) {
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) { if (selection.drive) {
return true; const sourcePath = selection.drive.devicePath || selection.drive.device;
const drivePath = drive.devicePath || drive.device;
return pathIsInside(sourcePath, drivePath);
}
if (selection.path) {
return sourceIsInsideDrive(selection.path, drive);
} }
} }
return false; return false;
@ -112,7 +127,7 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
return ( return (
!isDriveLocked(drive) && !isDriveLocked(drive) &&
isDriveLargeEnough(drive, image) && isDriveLargeEnough(drive, image) &&
!isSourceDrive(drive, image) && !isSourceDrive(drive, image as SourceMetadata) &&
!isDriveDisabled(drive) !isDriveDisabled(drive)
); );
} }
@ -167,7 +182,7 @@ export const COMPATIBILITY_STATUS_TYPES = {
*/ */
export function getDriveImageCompatibilityStatuses( export function getDriveImageCompatibilityStatuses(
drive: DrivelistDrive, drive: DrivelistDrive,
image: Image = {}, image: Image,
) { ) {
const statusList = []; const statusList = [];
@ -191,7 +206,7 @@ export function getDriveImageCompatibilityStatuses(
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)), message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
}); });
} else { } else {
if (isSourceDrive(drive, image)) { if (isSourceDrive(drive, image as SourceMetadata)) {
statusList.push({ statusList.push({
type: COMPATIBILITY_STATUS_TYPES.ERROR, type: COMPATIBILITY_STATUS_TYPES.ERROR,
message: messages.compatibility.containsImage(), message: messages.compatibility.containsImage(),

View File

@ -143,9 +143,9 @@ export const error = {
].join(' '); ].join(' ');
}, },
openImage: (imageBasename: string, errorMessage: string) => { openSource: (sourceName: string, errorMessage: string) => {
return [ return [
`Something went wrong while opening ${imageBasename}\n\n`, `Something went wrong while opening ${sourceName}\n\n`,
`Error: ${errorMessage}`, `Error: ${errorMessage}`,
].join(''); ].join('');
}, },

View File

@ -157,7 +157,7 @@ describe('Model: availableDrives', function () {
} }
selectionState.clear(); selectionState.clear();
selectionState.selectImage({ selectionState.selectSource({
path: this.imagePath, path: this.imagePath,
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,

View File

@ -359,7 +359,7 @@ describe('Model: selectionState', function () {
logo: '<svg><text fill="red">Raspbian</text></svg>', logo: '<svg><text fill="red">Raspbian</text></svg>',
}; };
selectionState.selectImage(this.image); selectionState.selectSource(this.image);
}); });
describe('.selectDrive()', function () { describe('.selectDrive()', function () {
@ -445,7 +445,7 @@ describe('Model: selectionState', function () {
describe('.selectImage()', function () { describe('.selectImage()', function () {
it('should override the image', function () { it('should override the image', function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'bar.img', path: 'bar.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -476,7 +476,7 @@ describe('Model: selectionState', function () {
afterEach(selectionState.clear); afterEach(selectionState.clear);
it('should be able to set an image', function () { it('should be able to set an image', function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -490,7 +490,7 @@ describe('Model: selectionState', function () {
}); });
it('should be able to set an image with an archive extension', function () { it('should be able to set an image with an archive extension', function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.zip', path: 'foo.zip',
extension: 'img', extension: 'img',
archiveExtension: 'zip', archiveExtension: 'zip',
@ -503,7 +503,7 @@ describe('Model: selectionState', function () {
}); });
it('should infer a compressed raw image if the penultimate extension is missing', function () { it('should infer a compressed raw image if the penultimate extension is missing', function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.xz', path: 'foo.xz',
extension: 'img', extension: 'img',
archiveExtension: 'xz', archiveExtension: 'xz',
@ -516,7 +516,7 @@ describe('Model: selectionState', function () {
}); });
it('should infer a compressed raw image if the penultimate extension is not a file extension', function () { it('should infer a compressed raw image if the penultimate extension is not a file extension', function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'something.linux-x86-64.gz', path: 'something.linux-x86-64.gz',
extension: 'img', extension: 'img',
archiveExtension: 'gz', archiveExtension: 'gz',
@ -530,7 +530,7 @@ describe('Model: selectionState', function () {
it('should throw if no path', function () { it('should throw if no path', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
isSizeEstimated: false, isSizeEstimated: false,
@ -540,7 +540,7 @@ describe('Model: selectionState', function () {
it('should throw if path is not a string', function () { it('should throw if path is not a string', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 123, path: 123,
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -551,7 +551,7 @@ describe('Model: selectionState', function () {
it('should throw if the original size is not a number', function () { it('should throw if the original size is not a number', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -563,7 +563,7 @@ describe('Model: selectionState', function () {
it('should throw if the original size is a float number', function () { it('should throw if the original size is a float number', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -575,7 +575,7 @@ describe('Model: selectionState', function () {
it('should throw if the original size is negative', function () { it('should throw if the original size is negative', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -587,7 +587,7 @@ describe('Model: selectionState', function () {
it('should throw if the final size is not a number', function () { it('should throw if the final size is not a number', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: '999999999', size: '999999999',
@ -598,7 +598,7 @@ describe('Model: selectionState', function () {
it('should throw if the final size is a float number', function () { it('should throw if the final size is a float number', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999.999, size: 999999999.999,
@ -609,7 +609,7 @@ describe('Model: selectionState', function () {
it('should throw if the final size is negative', function () { it('should throw if the final size is negative', function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: -1, size: -1,
@ -620,7 +620,7 @@ describe('Model: selectionState', function () {
it("should throw if url is defined but it's not a string", function () { it("should throw if url is defined but it's not a string", function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -632,7 +632,7 @@ describe('Model: selectionState', function () {
it("should throw if name is defined but it's not a string", function () { it("should throw if name is defined but it's not a string", function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -644,7 +644,7 @@ describe('Model: selectionState', function () {
it("should throw if logo is defined but it's not a string", function () { it("should throw if logo is defined but it's not a string", function () {
expect(function () { expect(function () {
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -667,7 +667,7 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk1'); selectionState.selectDrive('/dev/disk1');
expect(selectionState.hasDrive()).to.be.true; expect(selectionState.hasDrive()).to.be.true;
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 1234567890, size: 1234567890,
@ -691,7 +691,7 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk1'); selectionState.selectDrive('/dev/disk1');
expect(selectionState.hasDrive()).to.be.true; expect(selectionState.hasDrive()).to.be.true;
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -726,7 +726,7 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk1'); selectionState.selectDrive('/dev/disk1');
expect(selectionState.hasDrive()).to.be.true; expect(selectionState.hasDrive()).to.be.true;
selectionState.selectImage({ selectionState.selectSource({
path: imagePath, path: imagePath,
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -752,7 +752,7 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk1'); selectionState.selectDrive('/dev/disk1');
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,
@ -850,7 +850,7 @@ describe('Model: selectionState', function () {
selectionState.selectDrive('/dev/disk2'); selectionState.selectDrive('/dev/disk2');
selectionState.selectDrive('/dev/disk3'); selectionState.selectDrive('/dev/disk3');
selectionState.selectImage({ selectionState.selectSource({
path: 'foo.img', path: 'foo.img',
extension: 'img', extension: 'img',
size: 999999999, size: 999999999,

View File

@ -28,10 +28,12 @@ const fakeDrive: DrivelistDrive = {};
describe('Browser: imageWriter', () => { describe('Browser: imageWriter', () => {
describe('.flash()', () => { describe('.flash()', () => {
const imagePath = 'foo.img'; const image = {
const sourceOptions = { hasMBR: false,
imagePath, partitions: [],
path: 'foo.img',
SourceType: sourceDestination.File, SourceType: sourceDestination.File,
extension: 'img',
}; };
describe('given a successful write', () => { describe('given a successful write', () => {
@ -58,12 +60,7 @@ describe('Browser: imageWriter', () => {
}); });
try { try {
await imageWriter.flash( imageWriter.flash(image, [fakeDrive], performWriteStub);
imagePath,
[fakeDrive],
sourceOptions,
performWriteStub,
);
} catch { } catch {
// noop // noop
} finally { } finally {
@ -79,18 +76,8 @@ describe('Browser: imageWriter', () => {
try { try {
await Promise.all([ await Promise.all([
imageWriter.flash( imageWriter.flash(image, [fakeDrive], performWriteStub),
imagePath, imageWriter.flash(image, [fakeDrive], performWriteStub),
[fakeDrive],
sourceOptions,
performWriteStub,
),
imageWriter.flash(
imagePath,
[fakeDrive],
sourceOptions,
performWriteStub,
),
]); ]);
assert.fail('Writing twice should fail'); assert.fail('Writing twice should fail');
} catch (error) { } catch (error) {
@ -117,12 +104,7 @@ describe('Browser: imageWriter', () => {
it('should set flashing to false when done', async () => { it('should set flashing to false when done', async () => {
try { try {
await imageWriter.flash( await imageWriter.flash(image, [fakeDrive], performWriteStub);
imagePath,
[fakeDrive],
sourceOptions,
performWriteStub,
);
} catch { } catch {
// noop // noop
} finally { } finally {
@ -132,12 +114,7 @@ describe('Browser: imageWriter', () => {
it('should set the error code in the flash results', async () => { it('should set the error code in the flash results', async () => {
try { try {
await imageWriter.flash( await imageWriter.flash(image, [fakeDrive], performWriteStub);
imagePath,
[fakeDrive],
sourceOptions,
performWriteStub,
);
} catch { } catch {
// noop // noop
} finally { } finally {
@ -152,12 +129,7 @@ describe('Browser: imageWriter', () => {
sourceChecksum: '1234', sourceChecksum: '1234',
}); });
try { try {
await imageWriter.flash( await imageWriter.flash(image, [fakeDrive], performWriteStub);
imagePath,
[fakeDrive],
sourceOptions,
performWriteStub,
);
} catch (error) { } catch (error) {
expect(error).to.be.an.instanceof(Error); expect(error).to.be.an.instanceof(Error);
expect(error.message).to.equal('write error'); expect(error.message).to.equal('write error');

View File

@ -16,6 +16,7 @@
import { expect } from 'chai'; import { expect } from 'chai';
import { Drive as DrivelistDrive } from 'drivelist'; import { Drive as DrivelistDrive } from 'drivelist';
import { sourceDestination } from 'etcher-sdk';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as path from 'path'; import * as path from 'path';
@ -126,6 +127,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: '/Volumes/Untitled/image.img', path: '/Volumes/Untitled/image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -160,6 +164,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: 'E:\\image.img', path: 'E:\\image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -182,6 +189,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: 'E:\\foo\\bar\\image.img', path: 'E:\\foo\\bar\\image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -204,6 +214,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: 'G:\\image.img', path: 'G:\\image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -222,6 +235,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: 'E:\\foo/image.img', path: 'E:\\foo/image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -252,6 +268,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: '/image.img', path: '/image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -272,6 +291,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: '/Volumes/A/image.img', path: '/Volumes/A/image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -292,6 +314,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: '/Volumes/A/foo/bar/image.img', path: '/Volumes/A/foo/bar/image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -312,6 +337,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: '/Volumes/C/image.img', path: '/Volumes/C/image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );
@ -329,6 +357,9 @@ describe('Shared: DriveConstraints', function () {
} as DrivelistDrive, } as DrivelistDrive,
{ {
path: '/Volumes/foo/image.img', path: '/Volumes/foo/image.img',
hasMBR: false,
partitions: [],
SourceType: sourceDestination.File,
}, },
); );