mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 15:27:17 +00:00
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:
parent
377dfb8e22
commit
dda022df37
@ -167,7 +167,7 @@ export class DriveSelector extends React.Component<
|
||||
DriveSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
multipleSelection: boolean;
|
||||
multipleSelection: boolean = true;
|
||||
tableColumns: Array<TableColumn<Drive>>;
|
||||
|
||||
constructor(props: DriveSelectorProps) {
|
||||
@ -175,7 +175,11 @@ export class DriveSelector extends React.Component<
|
||||
|
||||
const defaultMissingDriversModalState: { drive?: DriverlessDrive } = {};
|
||||
const selectedList = getSelectedDrives();
|
||||
this.multipleSelection = !!this.props.multipleSelection;
|
||||
const multipleSelection = this.props.multipleSelection;
|
||||
this.multipleSelection =
|
||||
multipleSelection !== undefined
|
||||
? !!multipleSelection
|
||||
: this.multipleSelection;
|
||||
|
||||
this.state = {
|
||||
drives: getDrives(),
|
||||
@ -383,10 +387,7 @@ export class DriveSelector extends React.Component<
|
||||
<b>{this.props.emptyListLabel}</b>
|
||||
</Flex>
|
||||
) : (
|
||||
<ScrollableFlex
|
||||
flexDirection="column"
|
||||
width="100%"
|
||||
>
|
||||
<ScrollableFlex flexDirection="column" width="100%">
|
||||
<DrivesTable
|
||||
refFn={(t: Table<Drive>) => {
|
||||
if (t !== null) {
|
||||
|
@ -14,10 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import CopySvg from '@fortawesome/fontawesome-free/svgs/solid/copy.svg';
|
||||
import FileSvg from '@fortawesome/fontawesome-free/svgs/solid/file.svg';
|
||||
import LinkSvg from '@fortawesome/fontawesome-free/svgs/solid/link.svg';
|
||||
import ExclamationTriangleSvg from '@fortawesome/fontawesome-free/svgs/solid/exclamation-triangle.svg';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import { sourceDestination, scanner } from 'etcher-sdk';
|
||||
import { ipcRenderer, IpcRendererEvent } from 'electron';
|
||||
import * as _ from 'lodash';
|
||||
import { GPTPartition, MBRPartition } from 'partitioninfo';
|
||||
@ -57,6 +58,7 @@ import { middleEllipsis } from '../../utils/middle-ellipsis';
|
||||
import { SVGIcon } from '../svg-icon/svg-icon';
|
||||
|
||||
import ImageSvg from '../../../assets/image.svg';
|
||||
import { DriveSelector } from '../drive-selector/drive-selector';
|
||||
|
||||
const recentUrlImagesKey = 'recentUrlImages';
|
||||
|
||||
@ -92,6 +94,9 @@ function setRecentUrlImages(urls: URL[]) {
|
||||
localStorage.setItem(recentUrlImagesKey, JSON.stringify(normalized));
|
||||
}
|
||||
|
||||
const isURL = (imagePath: string) =>
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
|
||||
const Card = styled(BaseCard)`
|
||||
hr {
|
||||
margin: 5px 0;
|
||||
@ -117,6 +122,10 @@ function getState() {
|
||||
};
|
||||
}
|
||||
|
||||
function isString(value: any): value is string {
|
||||
return typeof value === 'string';
|
||||
}
|
||||
|
||||
const URLSelector = ({
|
||||
done,
|
||||
cancel,
|
||||
@ -203,7 +212,12 @@ interface Flow {
|
||||
const FlowSelector = styled(
|
||||
({ flow, ...props }: { flow: Flow; props?: ButtonProps }) => {
|
||||
return (
|
||||
<StepButton plain onClick={flow.onClick} icon={flow.icon} {...props}>
|
||||
<StepButton
|
||||
plain
|
||||
onClick={(evt) => flow.onClick(evt)}
|
||||
icon={flow.icon}
|
||||
{...props}
|
||||
>
|
||||
{flow.label}
|
||||
</StepButton>
|
||||
);
|
||||
@ -225,16 +239,20 @@ const FlowSelector = styled(
|
||||
|
||||
export type Source =
|
||||
| typeof sourceDestination.File
|
||||
| typeof sourceDestination.BlockDevice
|
||||
| typeof sourceDestination.Http;
|
||||
|
||||
export interface SourceOptions {
|
||||
imagePath: string;
|
||||
export interface SourceMetadata extends sourceDestination.Metadata {
|
||||
hasMBR: boolean;
|
||||
partitions: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
SourceType: Source;
|
||||
drive?: scanner.adapters.DrivelistDrive;
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
interface SourceSelectorProps {
|
||||
flashing: boolean;
|
||||
afterSelected: (options: SourceOptions) => void;
|
||||
}
|
||||
|
||||
interface SourceSelectorState {
|
||||
@ -244,6 +262,7 @@ interface SourceSelectorState {
|
||||
warning: { message: string; title: string | null } | null;
|
||||
showImageDetails: boolean;
|
||||
showURLSelector: boolean;
|
||||
showDriveSelector: boolean;
|
||||
}
|
||||
|
||||
export class SourceSelector extends React.Component<
|
||||
@ -251,7 +270,6 @@ export class SourceSelector extends React.Component<
|
||||
SourceSelectorState
|
||||
> {
|
||||
private unsubscribe: (() => void) | undefined;
|
||||
private afterSelected: SourceSelectorProps['afterSelected'];
|
||||
|
||||
constructor(props: SourceSelectorProps) {
|
||||
super(props);
|
||||
@ -260,15 +278,8 @@ export class SourceSelector extends React.Component<
|
||||
warning: null,
|
||||
showImageDetails: 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() {
|
||||
@ -285,15 +296,28 @@ export class SourceSelector extends React.Component<
|
||||
}
|
||||
|
||||
private async onSelectImage(_event: IpcRendererEvent, imagePath: string) {
|
||||
const isURL =
|
||||
imagePath.startsWith('https://') || imagePath.startsWith('http://');
|
||||
await this.selectImageByPath({
|
||||
await this.selectSource(
|
||||
imagePath,
|
||||
SourceType: isURL ? sourceDestination.Http : sourceDestination.File,
|
||||
}).promise;
|
||||
isURL(imagePath) ? sourceDestination.Http : sourceDestination.File,
|
||||
).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', {
|
||||
previousImage: selectionState.getImage(),
|
||||
});
|
||||
@ -301,144 +325,134 @@ export class SourceSelector extends React.Component<
|
||||
selectionState.deselectImage();
|
||||
}
|
||||
|
||||
private selectImage(
|
||||
image: sourceDestination.Metadata & {
|
||||
path: string;
|
||||
extension: string;
|
||||
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 } {
|
||||
private selectSource(
|
||||
selected: string | scanner.adapters.DrivelistDrive,
|
||||
SourceType: Source,
|
||||
): { promise: Promise<void>; cancel: () => void } {
|
||||
let cancelled = false;
|
||||
return {
|
||||
cancel: () => {
|
||||
cancelled = true;
|
||||
},
|
||||
promise: (async () => {
|
||||
try {
|
||||
imagePath = await replaceWindowsNetworkDriveLetter(imagePath);
|
||||
} catch (error) {
|
||||
analytics.logException(error);
|
||||
}
|
||||
if (cancelled) {
|
||||
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 {
|
||||
const innerSource = await source.getInnerSource();
|
||||
const sourcePath = isString(selected) ? selected : selected.device;
|
||||
let metadata: SourceMetadata | undefined;
|
||||
if (isString(selected)) {
|
||||
const source = await this.createSource(selected, SourceType);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const metadata = (await innerSource.getMetadata()) as sourceDestination.Metadata & {
|
||||
hasMBR: boolean;
|
||||
partitions: MBRPartition[] | GPTPartition[];
|
||||
path: string;
|
||||
extension: string;
|
||||
};
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const partitionTable = await innerSource.getPartitionTable();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (partitionTable) {
|
||||
metadata.hasMBR = true;
|
||||
metadata.partitions = partitionTable.partitions;
|
||||
} else {
|
||||
metadata.hasMBR = false;
|
||||
}
|
||||
metadata.path = imagePath;
|
||||
metadata.extension = path.extname(imagePath).slice(1);
|
||||
this.selectImage(metadata);
|
||||
this.afterSelected({
|
||||
imagePath,
|
||||
SourceType,
|
||||
});
|
||||
} catch (error) {
|
||||
const imageError = errors.createUserError({
|
||||
title: 'Error opening image',
|
||||
description: messages.error.openImage(
|
||||
path.basename(imagePath),
|
||||
error.message,
|
||||
),
|
||||
});
|
||||
osDialog.showError(imageError);
|
||||
analytics.logException(error);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
const innerSource = await source.getInnerSource();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
metadata = await this.getMetadata(innerSource);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (SourceType === sourceDestination.Http && !isURL(selected)) {
|
||||
this.handleError(
|
||||
'Unsupported protocol',
|
||||
selected,
|
||||
messages.error.unsupportedProtocol(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (supportedFormats.looksLikeWindowsImage(selected)) {
|
||||
analytics.logEvent('Possibly Windows image', { image: selected });
|
||||
this.setState({
|
||||
warning: {
|
||||
message: messages.warning.looksLikeWindowsImage(),
|
||||
title: 'Possible Windows image detected',
|
||||
},
|
||||
});
|
||||
}
|
||||
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) {
|
||||
// Noop
|
||||
this.handleError(
|
||||
'Error opening source',
|
||||
sourcePath,
|
||||
messages.error.openSource(sourcePath, error.message),
|
||||
error,
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
await source.close();
|
||||
} catch (error) {
|
||||
// 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() {
|
||||
analytics.logEvent('Open image selector');
|
||||
|
||||
@ -450,10 +464,7 @@ export class SourceSelector extends React.Component<
|
||||
analytics.logEvent('Image selector closed');
|
||||
return;
|
||||
}
|
||||
await this.selectImageByPath({
|
||||
imagePath,
|
||||
SourceType: sourceDestination.File,
|
||||
}).promise;
|
||||
await this.selectSource(imagePath, sourceDestination.File).promise;
|
||||
} catch (error) {
|
||||
exceptionReporter.report(error);
|
||||
}
|
||||
@ -462,10 +473,7 @@ export class SourceSelector extends React.Component<
|
||||
private async onDrop(event: React.DragEvent<HTMLDivElement>) {
|
||||
const [file] = event.dataTransfer.files;
|
||||
if (file) {
|
||||
await this.selectImageByPath({
|
||||
imagePath: file.path,
|
||||
SourceType: sourceDestination.File,
|
||||
}).promise;
|
||||
await this.selectSource(file.path, 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>) {
|
||||
// Needed to get onDrop events on div elements
|
||||
event.preventDefault();
|
||||
@ -500,27 +516,35 @@ export class SourceSelector extends React.Component<
|
||||
// TODO add a visual change when dragging a file over the selector
|
||||
public render() {
|
||||
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 = () => {
|
||||
// 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 (
|
||||
<>
|
||||
<Flex
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
onDrop={this.onDrop}
|
||||
onDragEnter={this.onDragEnter}
|
||||
onDragOver={this.onDragOver}
|
||||
onDrop={(evt: React.DragEvent<HTMLDivElement>) => this.onDrop(evt)}
|
||||
onDragEnter={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragEnter(evt)
|
||||
}
|
||||
onDragOver={(evt: React.DragEvent<HTMLDivElement>) =>
|
||||
this.onDragOver(evt)
|
||||
}
|
||||
>
|
||||
<SVGIcon
|
||||
contents={imageLogo}
|
||||
@ -530,17 +554,21 @@ export class SourceSelector extends React.Component<
|
||||
}}
|
||||
/>
|
||||
|
||||
{hasImage ? (
|
||||
{hasSource ? (
|
||||
<>
|
||||
<StepNameButton
|
||||
plain
|
||||
onClick={this.showSelectedImageDetails}
|
||||
onClick={() => this.showSelectedImageDetails()}
|
||||
tooltip={imageName || imageBasename}
|
||||
>
|
||||
{middleEllipsis(imageName || imageBasename, 20)}
|
||||
</StepNameButton>
|
||||
{!flashing && (
|
||||
<ChangeButton plain mb={14} onClick={this.reselectImage}>
|
||||
<ChangeButton
|
||||
plain
|
||||
mb={14}
|
||||
onClick={() => this.reselectSource()}
|
||||
>
|
||||
Remove
|
||||
</ChangeButton>
|
||||
)}
|
||||
@ -551,7 +579,7 @@ export class SourceSelector extends React.Component<
|
||||
<FlowSelector
|
||||
key="Flash from file"
|
||||
flow={{
|
||||
onClick: this.openImageSelector,
|
||||
onClick: () => this.openImageSelector(),
|
||||
label: 'Flash from file',
|
||||
icon: <FileSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
@ -559,11 +587,19 @@ export class SourceSelector extends React.Component<
|
||||
<FlowSelector
|
||||
key="Flash from URL"
|
||||
flow={{
|
||||
onClick: this.openURLSelector,
|
||||
onClick: () => this.openURLSelector(),
|
||||
label: 'Flash from URL',
|
||||
icon: <LinkSvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
/>
|
||||
<FlowSelector
|
||||
key="Clone drive"
|
||||
flow={{
|
||||
onClick: () => this.openDriveSelector(),
|
||||
label: 'Clone drive',
|
||||
icon: <CopySvg height="1em" fill="currentColor" />,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
@ -579,7 +615,7 @@ export class SourceSelector extends React.Component<
|
||||
action="Continue"
|
||||
cancel={() => {
|
||||
this.setState({ warning: null });
|
||||
this.reselectImage();
|
||||
this.reselectSource();
|
||||
}}
|
||||
done={() => {
|
||||
this.setState({ warning: null });
|
||||
@ -625,13 +661,10 @@ export class SourceSelector extends React.Component<
|
||||
analytics.logEvent('URL selector closed');
|
||||
} else {
|
||||
let promise;
|
||||
({
|
||||
promise,
|
||||
cancel: cancelURLSelection,
|
||||
} = this.selectImageByPath({
|
||||
imagePath: imageURL,
|
||||
SourceType: sourceDestination.Http,
|
||||
}));
|
||||
({ promise, cancel: cancelURLSelection } = this.selectSource(
|
||||
imageURL,
|
||||
sourceDestination.Http,
|
||||
));
|
||||
await promise;
|
||||
}
|
||||
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,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export function hasAvailableDrives() {
|
||||
|
||||
export function setDrives(drives: any[]) {
|
||||
store.dispatch({
|
||||
type: Actions.SET_AVAILABLE_DRIVES,
|
||||
type: Actions.SET_AVAILABLE_TARGETS,
|
||||
data: drives,
|
||||
});
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import { Actions, store } from './store';
|
||||
*/
|
||||
export function selectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_DRIVE,
|
||||
type: Actions.SELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
@ -40,10 +40,10 @@ export function toggleDrive(driveDevice: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function selectImage(image: any) {
|
||||
export function selectSource(source: any) {
|
||||
store.dispatch({
|
||||
type: Actions.SELECT_IMAGE,
|
||||
data: image,
|
||||
type: Actions.SELECT_SOURCE,
|
||||
data: source,
|
||||
});
|
||||
}
|
||||
|
||||
@ -122,14 +122,14 @@ export function hasImage(): boolean {
|
||||
*/
|
||||
export function deselectDrive(driveDevice: string) {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: driveDevice,
|
||||
});
|
||||
}
|
||||
|
||||
export function deselectImage() {
|
||||
store.dispatch({
|
||||
type: Actions.DESELECT_IMAGE,
|
||||
type: Actions.DESELECT_SOURCE,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
@ -80,15 +80,15 @@ export const DEFAULT_STATE = Immutable.fromJS({
|
||||
export enum Actions {
|
||||
SET_DEVICE_PATHS,
|
||||
SET_FAILED_DEVICE_PATHS,
|
||||
SET_AVAILABLE_DRIVES,
|
||||
SET_AVAILABLE_TARGETS,
|
||||
SET_FLASH_STATE,
|
||||
RESET_FLASH_STATE,
|
||||
SET_FLASHING_FLAG,
|
||||
UNSET_FLASHING_FLAG,
|
||||
SELECT_DRIVE,
|
||||
SELECT_IMAGE,
|
||||
DESELECT_DRIVE,
|
||||
DESELECT_IMAGE,
|
||||
SELECT_TARGET,
|
||||
SELECT_SOURCE,
|
||||
DESELECT_TARGET,
|
||||
DESELECT_SOURCE,
|
||||
SET_APPLICATION_SESSION_UUID,
|
||||
SET_FLASHING_WORKFLOW_UUID,
|
||||
}
|
||||
@ -116,7 +116,7 @@ function storeReducer(
|
||||
action: Action,
|
||||
): typeof DEFAULT_STATE {
|
||||
switch (action.type) {
|
||||
case Actions.SET_AVAILABLE_DRIVES: {
|
||||
case Actions.SET_AVAILABLE_TARGETS: {
|
||||
// Type: action.data : Array<DriveObject>
|
||||
|
||||
if (!action.data) {
|
||||
@ -158,7 +158,7 @@ function storeReducer(
|
||||
) {
|
||||
// Deselect this drive gone from availableDrives
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@ -206,14 +206,14 @@ function storeReducer(
|
||||
) {
|
||||
// Auto-select this drive
|
||||
return storeReducer(accState, {
|
||||
type: Actions.SELECT_DRIVE,
|
||||
type: Actions.SELECT_TARGET,
|
||||
data: drive.device,
|
||||
});
|
||||
}
|
||||
|
||||
// Deselect this drive in case it still is selected
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: drive.device,
|
||||
});
|
||||
},
|
||||
@ -341,7 +341,7 @@ function storeReducer(
|
||||
.set('flashState', DEFAULT_STATE.get('flashState'));
|
||||
}
|
||||
|
||||
case Actions.SELECT_DRIVE: {
|
||||
case Actions.SELECT_TARGET: {
|
||||
// Type: action.data : String
|
||||
|
||||
const device = action.data;
|
||||
@ -391,10 +391,12 @@ function storeReducer(
|
||||
// with image-stream / supported-formats, and have *one*
|
||||
// place where all the image extension / format handling
|
||||
// takes place, to avoid having to check 2+ locations with different logic
|
||||
case Actions.SELECT_IMAGE: {
|
||||
case Actions.SELECT_SOURCE: {
|
||||
// Type: action.data : ImageObject
|
||||
|
||||
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
|
||||
if (!action.data.drive) {
|
||||
verifyNoNilFields(action.data, selectImageNoNilFields, 'image');
|
||||
}
|
||||
|
||||
if (!_.isString(action.data.path)) {
|
||||
throw errors.createError({
|
||||
@ -456,7 +458,7 @@ function storeReducer(
|
||||
!constraints.isDriveSizeRecommended(drive, action.data)
|
||||
) {
|
||||
return storeReducer(accState, {
|
||||
type: Actions.DESELECT_DRIVE,
|
||||
type: Actions.DESELECT_TARGET,
|
||||
data: device,
|
||||
});
|
||||
}
|
||||
@ -467,7 +469,7 @@ function storeReducer(
|
||||
).setIn(['selection', 'image'], Immutable.fromJS(action.data));
|
||||
}
|
||||
|
||||
case Actions.DESELECT_DRIVE: {
|
||||
case Actions.DESELECT_TARGET: {
|
||||
// Type: action.data : String
|
||||
|
||||
if (!action.data) {
|
||||
@ -491,7 +493,7 @@ function storeReducer(
|
||||
);
|
||||
}
|
||||
|
||||
case Actions.DESELECT_IMAGE: {
|
||||
case Actions.DESELECT_SOURCE: {
|
||||
return state.deleteIn(['selection', 'image']);
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ import * as path from 'path';
|
||||
import * as packageJSON from '../../../../package.json';
|
||||
import * as errors from '../../../shared/errors';
|
||||
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 selectionState from '../models/selection-state';
|
||||
import * as settings from '../models/settings';
|
||||
@ -134,15 +134,11 @@ interface FlashResults {
|
||||
cancelled?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Perform write operation
|
||||
*/
|
||||
async function performWrite(
|
||||
image: string,
|
||||
export async function performWrite(
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
onProgress: sdk.multiWrite.OnProgressFunction,
|
||||
source: SourceOptions,
|
||||
): Promise<FlashResults> {
|
||||
): Promise<{ cancelled?: boolean }> {
|
||||
let cancelled = false;
|
||||
ipc.serve();
|
||||
const {
|
||||
@ -196,10 +192,9 @@ async function performWrite(
|
||||
|
||||
ipc.server.on('ready', (_data, socket) => {
|
||||
ipc.server.emit(socket, 'write', {
|
||||
imagePath: image,
|
||||
image,
|
||||
destinations: drives,
|
||||
source,
|
||||
SourceType: source.SourceType.name,
|
||||
SourceType: image.SourceType.name,
|
||||
validateWriteOnSuccess,
|
||||
autoBlockmapping,
|
||||
unmountOnSuccess,
|
||||
@ -258,9 +253,8 @@ async function performWrite(
|
||||
* @summary Flash an image to drives
|
||||
*/
|
||||
export async function flash(
|
||||
image: string,
|
||||
image: SourceMetadata,
|
||||
drives: DrivelistDrive[],
|
||||
source: SourceOptions,
|
||||
// This function is a parameter so it can be mocked in tests
|
||||
write = performWrite,
|
||||
): Promise<void> {
|
||||
@ -287,12 +281,7 @@ export async function flash(
|
||||
analytics.logEvent('Flash', analyticsData);
|
||||
|
||||
try {
|
||||
const result = await write(
|
||||
image,
|
||||
drives,
|
||||
flashState.setProgressState,
|
||||
source,
|
||||
);
|
||||
const result = await write(image, drives, flashState.setProgressState);
|
||||
flashState.unsetFlashingFlag(result);
|
||||
} catch (error) {
|
||||
flashState.unsetFlashingFlag({ cancelled: false, errorCode: error.code });
|
||||
|
@ -23,7 +23,6 @@ import { Flex, Modal, Txt } from 'rendition';
|
||||
import * as constraints from '../../../../shared/drive-constraints';
|
||||
import * as messages from '../../../../shared/messages';
|
||||
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 flashState from '../../models/flash-state';
|
||||
import * as selection from '../../models/selection-state';
|
||||
@ -79,7 +78,6 @@ const getErrorMessageFromCode = (errorCode: string) => {
|
||||
async function flashImageToDrive(
|
||||
isFlashing: boolean,
|
||||
goToSuccess: () => void,
|
||||
sourceOptions: SourceOptions,
|
||||
): Promise<string> {
|
||||
const devices = selection.getSelectedDevices();
|
||||
const image: any = selection.getImage();
|
||||
@ -98,7 +96,7 @@ async function flashImageToDrive(
|
||||
const iconPath = path.join('media', 'icon.png');
|
||||
const basename = path.basename(image.path);
|
||||
try {
|
||||
await imageWriter.flash(image.path, drives, sourceOptions);
|
||||
await imageWriter.flash(image, drives);
|
||||
if (!flashState.wasLastFlashCancelled()) {
|
||||
const flashResults: any = flashState.getFlashResults();
|
||||
notification.send(
|
||||
@ -146,7 +144,6 @@ const formatSeconds = (totalSeconds: number) => {
|
||||
interface FlashStepProps {
|
||||
shouldFlashStepBeDisabled: boolean;
|
||||
goToSuccess: () => void;
|
||||
source: SourceOptions;
|
||||
isFlashing: boolean;
|
||||
style?: React.CSSProperties;
|
||||
// TODO: factorize
|
||||
@ -187,7 +184,6 @@ export class FlashStep extends React.PureComponent<
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
@ -230,7 +226,6 @@ export class FlashStep extends React.PureComponent<
|
||||
errorMessage: await flashImageToDrive(
|
||||
this.props.isFlashing,
|
||||
this.props.goToSuccess,
|
||||
this.props.source,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
import CogSvg from '@fortawesome/fontawesome-free/svgs/solid/cog.svg';
|
||||
import QuestionCircleSvg from '@fortawesome/fontawesome-free/svgs/solid/question-circle.svg';
|
||||
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
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 { SafeWebview } from '../../components/safe-webview/safe-webview';
|
||||
import { SettingsModal } from '../../components/settings/settings';
|
||||
import {
|
||||
SourceOptions,
|
||||
SourceSelector,
|
||||
} from '../../components/source-selector/source-selector';
|
||||
import { SourceSelector } from '../../components/source-selector/source-selector';
|
||||
import * as flashState from '../../models/flash-state';
|
||||
import * as selectionState from '../../models/selection-state';
|
||||
import * as settings from '../../models/settings';
|
||||
@ -75,9 +71,12 @@ function getImageBasename() {
|
||||
return '';
|
||||
}
|
||||
|
||||
const selectionImageName = selectionState.getImageName();
|
||||
const imageBasename = path.basename(selectionState.getImagePath());
|
||||
return selectionImageName || imageBasename;
|
||||
const image = selectionState.getImage();
|
||||
if (image.drive) {
|
||||
return image.drive.description;
|
||||
}
|
||||
const imageBasename = path.basename(image.path);
|
||||
return image.name || imageBasename;
|
||||
}
|
||||
|
||||
const StepBorder = styled.div<{
|
||||
@ -115,7 +114,6 @@ interface MainPageState {
|
||||
current: 'main' | 'success';
|
||||
isWebviewShowing: boolean;
|
||||
hideSettings: boolean;
|
||||
source: SourceOptions;
|
||||
featuredProjectURL?: string;
|
||||
}
|
||||
|
||||
@ -129,10 +127,6 @@ export class MainPage extends React.Component<
|
||||
current: 'main',
|
||||
isWebviewShowing: false,
|
||||
hideSettings: true,
|
||||
source: {
|
||||
imagePath: '',
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
...this.stateHelper(),
|
||||
};
|
||||
}
|
||||
@ -246,12 +240,7 @@ export class MainPage extends React.Component<
|
||||
>
|
||||
{notFlashingOrSplitView && (
|
||||
<>
|
||||
<SourceSelector
|
||||
flashing={this.state.isFlashing}
|
||||
afterSelected={(source: SourceOptions) =>
|
||||
this.setState({ source })
|
||||
}
|
||||
/>
|
||||
<SourceSelector flashing={this.state.isFlashing} />
|
||||
<Flex>
|
||||
<StepBorder disabled={shouldDriveStepBeDisabled} left />
|
||||
</Flex>
|
||||
@ -316,7 +305,6 @@ export class MainPage extends React.Component<
|
||||
<FlashStep
|
||||
goToSuccess={() => this.setState({ current: 'success' })}
|
||||
shouldFlashStepBeDisabled={shouldFlashStepBeDisabled}
|
||||
source={this.state.source}
|
||||
isFlashing={this.state.isFlashing}
|
||||
step={state.type}
|
||||
percentage={state.percentage}
|
||||
|
@ -20,10 +20,11 @@ import { cleanupTmpFiles } from 'etcher-sdk/build/tmp';
|
||||
import * as ipc from 'node-ipc';
|
||||
import { totalmem } from 'os';
|
||||
|
||||
import { BlockDevice, File, Http } from 'etcher-sdk/build/source-destination';
|
||||
import { toJSON } from '../../shared/errors';
|
||||
import { GENERAL_ERROR, SUCCESS } from '../../shared/exit-codes';
|
||||
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.socketRoot = process.env.IPC_SOCKET_ROOT as string;
|
||||
@ -143,13 +144,12 @@ async function writeAndValidate({
|
||||
}
|
||||
|
||||
interface WriteOptions {
|
||||
imagePath: string;
|
||||
image: SourceMetadata;
|
||||
destinations: DrivelistDrive[];
|
||||
unmountOnSuccess: boolean;
|
||||
validateWriteOnSuccess: boolean;
|
||||
autoBlockmapping: boolean;
|
||||
decompressFirst: boolean;
|
||||
source: SourceOptions;
|
||||
SourceType: string;
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
};
|
||||
|
||||
const destinations = options.destinations.map((d) => d.device);
|
||||
log(`Image: ${options.imagePath}`);
|
||||
const imagePath = options.image.path;
|
||||
log(`Devices: ${destinations.join(', ')}`);
|
||||
log(`Umount on success: ${options.unmountOnSuccess}`);
|
||||
log(`Validate on success: ${options.validateWriteOnSuccess}`);
|
||||
@ -243,18 +243,23 @@ ipc.connectTo(IPC_SERVER_ID, () => {
|
||||
});
|
||||
});
|
||||
const { SourceType } = options;
|
||||
let source;
|
||||
if (SourceType === sdk.sourceDestination.File.name) {
|
||||
source = new sdk.sourceDestination.File({
|
||||
path: options.imagePath,
|
||||
});
|
||||
} else {
|
||||
source = new sdk.sourceDestination.Http({
|
||||
url: options.imagePath,
|
||||
avoidRandomAccess: true,
|
||||
});
|
||||
}
|
||||
try {
|
||||
let source;
|
||||
if (options.image.drive) {
|
||||
source = new BlockDevice({
|
||||
drive: options.image.drive,
|
||||
write: false,
|
||||
direct: !options.autoBlockmapping,
|
||||
});
|
||||
} else {
|
||||
if (SourceType === File.name) {
|
||||
source = new File({
|
||||
path: imagePath,
|
||||
});
|
||||
} else {
|
||||
source = new Http({ url: imagePath, avoidRandomAccess: true });
|
||||
}
|
||||
}
|
||||
const results = await writeAndValidate({
|
||||
source,
|
||||
destinations: dests,
|
||||
|
@ -20,6 +20,7 @@ import * as pathIsInside from 'path-is-inside';
|
||||
import * as prettyBytes from 'pretty-bytes';
|
||||
|
||||
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
|
||||
@ -44,13 +45,22 @@ export function isSystemDrive(drive: DrivelistDrive): boolean {
|
||||
}
|
||||
|
||||
export interface Image {
|
||||
path?: string;
|
||||
path: string;
|
||||
isSizeEstimated?: boolean;
|
||||
compressedSize?: number;
|
||||
recommendedDriveSize?: 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
|
||||
*
|
||||
@ -60,11 +70,16 @@ export interface Image {
|
||||
*/
|
||||
export function isSourceDrive(
|
||||
drive: DrivelistDrive,
|
||||
image: Image = {},
|
||||
selection?: SourceMetadata,
|
||||
): boolean {
|
||||
for (const mountpoint of drive.mountpoints || []) {
|
||||
if (image.path !== undefined && pathIsInside(image.path, mountpoint.path)) {
|
||||
return true;
|
||||
if (selection) {
|
||||
if (selection.drive) {
|
||||
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;
|
||||
@ -112,7 +127,7 @@ export function isDriveValid(drive: DrivelistDrive, image: Image): boolean {
|
||||
return (
|
||||
!isDriveLocked(drive) &&
|
||||
isDriveLargeEnough(drive, image) &&
|
||||
!isSourceDrive(drive, image) &&
|
||||
!isSourceDrive(drive, image as SourceMetadata) &&
|
||||
!isDriveDisabled(drive)
|
||||
);
|
||||
}
|
||||
@ -167,7 +182,7 @@ export const COMPATIBILITY_STATUS_TYPES = {
|
||||
*/
|
||||
export function getDriveImageCompatibilityStatuses(
|
||||
drive: DrivelistDrive,
|
||||
image: Image = {},
|
||||
image: Image,
|
||||
) {
|
||||
const statusList = [];
|
||||
|
||||
@ -191,7 +206,7 @@ export function getDriveImageCompatibilityStatuses(
|
||||
message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)),
|
||||
});
|
||||
} else {
|
||||
if (isSourceDrive(drive, image)) {
|
||||
if (isSourceDrive(drive, image as SourceMetadata)) {
|
||||
statusList.push({
|
||||
type: COMPATIBILITY_STATUS_TYPES.ERROR,
|
||||
message: messages.compatibility.containsImage(),
|
||||
|
@ -143,9 +143,9 @@ export const error = {
|
||||
].join(' ');
|
||||
},
|
||||
|
||||
openImage: (imageBasename: string, errorMessage: string) => {
|
||||
openSource: (sourceName: string, errorMessage: string) => {
|
||||
return [
|
||||
`Something went wrong while opening ${imageBasename}\n\n`,
|
||||
`Something went wrong while opening ${sourceName}\n\n`,
|
||||
`Error: ${errorMessage}`,
|
||||
].join('');
|
||||
},
|
||||
|
@ -157,7 +157,7 @@ describe('Model: availableDrives', function () {
|
||||
}
|
||||
|
||||
selectionState.clear();
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: this.imagePath,
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
|
@ -359,7 +359,7 @@ describe('Model: selectionState', function () {
|
||||
logo: '<svg><text fill="red">Raspbian</text></svg>',
|
||||
};
|
||||
|
||||
selectionState.selectImage(this.image);
|
||||
selectionState.selectSource(this.image);
|
||||
});
|
||||
|
||||
describe('.selectDrive()', function () {
|
||||
@ -445,7 +445,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
describe('.selectImage()', function () {
|
||||
it('should override the image', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'bar.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -476,7 +476,7 @@ describe('Model: selectionState', function () {
|
||||
afterEach(selectionState.clear);
|
||||
|
||||
it('should be able to set an image', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -490,7 +490,7 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
it('should be able to set an image with an archive extension', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.zip',
|
||||
extension: 'img',
|
||||
archiveExtension: 'zip',
|
||||
@ -503,7 +503,7 @@ describe('Model: selectionState', function () {
|
||||
});
|
||||
|
||||
it('should infer a compressed raw image if the penultimate extension is missing', function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.xz',
|
||||
extension: 'img',
|
||||
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 () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'something.linux-x86-64.gz',
|
||||
extension: 'img',
|
||||
archiveExtension: 'gz',
|
||||
@ -530,7 +530,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if no path', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
isSizeEstimated: false,
|
||||
@ -540,7 +540,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if path is not a string', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 123,
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -551,7 +551,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if the original size is not a number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -563,7 +563,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if the original size is a float number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -575,7 +575,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if the original size is negative', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -587,7 +587,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if the final size is not a number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: '999999999',
|
||||
@ -598,7 +598,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if the final size is a float number', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999.999,
|
||||
@ -609,7 +609,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it('should throw if the final size is negative', function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: -1,
|
||||
@ -620,7 +620,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it("should throw if url is defined but it's not a string", function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -632,7 +632,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it("should throw if name is defined but it's not a string", function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -644,7 +644,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
it("should throw if logo is defined but it's not a string", function () {
|
||||
expect(function () {
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -667,7 +667,7 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
expect(selectionState.hasDrive()).to.be.true;
|
||||
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 1234567890,
|
||||
@ -691,7 +691,7 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
expect(selectionState.hasDrive()).to.be.true;
|
||||
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -726,7 +726,7 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
expect(selectionState.hasDrive()).to.be.true;
|
||||
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: imagePath,
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -752,7 +752,7 @@ describe('Model: selectionState', function () {
|
||||
|
||||
selectionState.selectDrive('/dev/disk1');
|
||||
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
@ -850,7 +850,7 @@ describe('Model: selectionState', function () {
|
||||
selectionState.selectDrive('/dev/disk2');
|
||||
selectionState.selectDrive('/dev/disk3');
|
||||
|
||||
selectionState.selectImage({
|
||||
selectionState.selectSource({
|
||||
path: 'foo.img',
|
||||
extension: 'img',
|
||||
size: 999999999,
|
||||
|
@ -28,10 +28,12 @@ const fakeDrive: DrivelistDrive = {};
|
||||
|
||||
describe('Browser: imageWriter', () => {
|
||||
describe('.flash()', () => {
|
||||
const imagePath = 'foo.img';
|
||||
const sourceOptions = {
|
||||
imagePath,
|
||||
const image = {
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
path: 'foo.img',
|
||||
SourceType: sourceDestination.File,
|
||||
extension: 'img',
|
||||
};
|
||||
|
||||
describe('given a successful write', () => {
|
||||
@ -58,12 +60,7 @@ describe('Browser: imageWriter', () => {
|
||||
});
|
||||
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
@ -79,18 +76,8 @@ describe('Browser: imageWriter', () => {
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
),
|
||||
imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
),
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||
imageWriter.flash(image, [fakeDrive], performWriteStub),
|
||||
]);
|
||||
assert.fail('Writing twice should fail');
|
||||
} catch (error) {
|
||||
@ -117,12 +104,7 @@ describe('Browser: imageWriter', () => {
|
||||
|
||||
it('should set flashing to false when done', async () => {
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
@ -132,12 +114,7 @@ describe('Browser: imageWriter', () => {
|
||||
|
||||
it('should set the error code in the flash results', async () => {
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch {
|
||||
// noop
|
||||
} finally {
|
||||
@ -152,12 +129,7 @@ describe('Browser: imageWriter', () => {
|
||||
sourceChecksum: '1234',
|
||||
});
|
||||
try {
|
||||
await imageWriter.flash(
|
||||
imagePath,
|
||||
[fakeDrive],
|
||||
sourceOptions,
|
||||
performWriteStub,
|
||||
);
|
||||
await imageWriter.flash(image, [fakeDrive], performWriteStub);
|
||||
} catch (error) {
|
||||
expect(error).to.be.an.instanceof(Error);
|
||||
expect(error.message).to.equal('write error');
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { Drive as DrivelistDrive } from 'drivelist';
|
||||
import { sourceDestination } from 'etcher-sdk';
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
|
||||
@ -126,6 +127,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: '/Volumes/Untitled/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -160,6 +164,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: 'E:\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -182,6 +189,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: 'E:\\foo\\bar\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -204,6 +214,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: 'G:\\image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -222,6 +235,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: 'E:\\foo/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -252,6 +268,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: '/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -272,6 +291,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: '/Volumes/A/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -292,6 +314,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: '/Volumes/A/foo/bar/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -312,6 +337,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: '/Volumes/C/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
@ -329,6 +357,9 @@ describe('Shared: DriveConstraints', function () {
|
||||
} as DrivelistDrive,
|
||||
{
|
||||
path: '/Volumes/foo/image.img',
|
||||
hasMBR: false,
|
||||
partitions: [],
|
||||
SourceType: sourceDestination.File,
|
||||
},
|
||||
);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user