mirror of
https://github.com/balena-io/etcher.git
synced 2025-07-27 21:26:38 +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
|
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) {
|
||||||
|
@ -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,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 });
|
||||||
|
@ -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,
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -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,
|
||||||
|
@ -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(),
|
||||||
|
@ -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('');
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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');
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user