mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-21 05:47:18 +00:00

Instead of storing the whole selected drive object, we barely store a reference to the corresponding drive in the available drives array (the reference being the drive device). This greatly simplifies the application state in the following ways: - The drive metadata (size, description, etc) is not duplicated in the state, enforcing a single source of truth. - If the selected drive stops being available (e.g: is unplugged), the reference doesn't hold anymore, making this functionality very natural to implement. - Makes `SelectionStateModel.isCurrentDrive()` much more inuitive, since we don't have to document that changes in the metadata of the drive object, or extra keys such as `$$hashKey` don't change the result of this function. - Ensures the state never goes into a problematic state where we try to to write to an unavailable drive. Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
338 lines
8.7 KiB
JavaScript
338 lines
8.7 KiB
JavaScript
/*
|
|
* Copyright 2016 Resin.io
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
/**
|
|
* @module Etcher
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/* eslint-disable no-var */
|
|
|
|
var angular = require('angular');
|
|
|
|
/* eslint-enable no-var */
|
|
|
|
const _ = require('lodash');
|
|
const Store = require('./models/store');
|
|
|
|
const app = angular.module('Etcher', [
|
|
require('angular-ui-router'),
|
|
require('angular-ui-bootstrap'),
|
|
require('angular-moment'),
|
|
require('angular-middle-ellipses'),
|
|
require('angular-if-state'),
|
|
require('angular-seconds-to-date'),
|
|
|
|
// Etcher modules
|
|
require('./modules/drive-scanner'),
|
|
require('./modules/image-writer'),
|
|
require('./modules/analytics'),
|
|
|
|
// Models
|
|
require('./models/selection-state'),
|
|
require('./models/settings'),
|
|
require('./models/supported-formats'),
|
|
require('./models/drives'),
|
|
|
|
// Components
|
|
require('./components/progress-button/progress-button'),
|
|
require('./components/drive-selector/drive-selector'),
|
|
require('./components/svg-icon/svg-icon'),
|
|
require('./components/update-notifier/update-notifier'),
|
|
require('./components/tooltip-modal/tooltip-modal'),
|
|
|
|
// Pages
|
|
require('./pages/finish/finish'),
|
|
require('./pages/settings/settings'),
|
|
|
|
// OS
|
|
require('./os/notification/notification'),
|
|
require('./os/window-progress/window-progress'),
|
|
require('./os/open-external/open-external'),
|
|
require('./os/dropzone/dropzone'),
|
|
require('./os/dialog/dialog'),
|
|
|
|
// Utils
|
|
require('./utils/path/path'),
|
|
require('./utils/manifest-bind/manifest-bind'),
|
|
require('./utils/byte-size/byte-size')
|
|
]);
|
|
|
|
app.run((AnalyticsService, UpdateNotifierService, SelectionStateModel) => {
|
|
AnalyticsService.logEvent('Application start');
|
|
|
|
if (UpdateNotifierService.shouldCheckForUpdates()) {
|
|
AnalyticsService.logEvent('Checking for updates');
|
|
|
|
UpdateNotifierService.isLatestVersion().then((isLatestVersion) => {
|
|
|
|
// In case the internet connection is not good and checking the
|
|
// latest published version takes too long, only show notify
|
|
// the user about the new version if he didn't start the flash
|
|
// process (e.g: selected an image), otherwise such interruption
|
|
// might be annoying.
|
|
if (!isLatestVersion && !SelectionStateModel.hasImage()) {
|
|
|
|
AnalyticsService.logEvent('Notifying update');
|
|
UpdateNotifierService.notify();
|
|
}
|
|
});
|
|
}
|
|
|
|
});
|
|
|
|
app.run((AnalyticsService, OSWindowProgressService, ImageWriterService) => {
|
|
Store.subscribe(() => {
|
|
const state = Store.getState().toJS();
|
|
|
|
// There is usually a short time period between the `isFlashing()`
|
|
// property being set, and the flashing actually starting, which
|
|
// might cause some non-sense flashing state logs including
|
|
// `undefined` values.
|
|
//
|
|
// We use the presence of `.eta` to determine that the actual
|
|
// writing started.
|
|
if (!ImageWriterService.isFlashing() || !state.flashState.eta) {
|
|
return;
|
|
}
|
|
|
|
AnalyticsService.log([
|
|
`Progress (${state.flashState.type}):`,
|
|
`${state.flashState.percentage}% at ${state.flashState.speed} MB/s`,
|
|
`(eta ${state.flashState.eta}s)`
|
|
].join(' '));
|
|
|
|
OSWindowProgressService.set(state.flashState.percentage);
|
|
});
|
|
});
|
|
|
|
app.config(($stateProvider, $urlRouterProvider) => {
|
|
$urlRouterProvider.otherwise('/main');
|
|
|
|
$stateProvider
|
|
.state('main', {
|
|
url: '/main',
|
|
controller: 'AppController as app',
|
|
templateUrl: './partials/main.html'
|
|
});
|
|
});
|
|
|
|
app.controller('AppController', function(
|
|
$state,
|
|
$scope,
|
|
DriveScannerService,
|
|
SelectionStateModel,
|
|
SettingsModel,
|
|
SupportedFormatsModel,
|
|
DrivesModel,
|
|
ImageWriterService,
|
|
AnalyticsService,
|
|
DriveSelectorService,
|
|
UpdateNotifierService,
|
|
TooltipModalService,
|
|
OSWindowProgressService,
|
|
OSNotificationService,
|
|
OSDialogService,
|
|
OSOpenExternalService
|
|
) {
|
|
this.formats = SupportedFormatsModel;
|
|
this.selection = SelectionStateModel;
|
|
this.drives = DrivesModel;
|
|
this.writer = ImageWriterService;
|
|
this.settings = SettingsModel;
|
|
this.tooltipModal = TooltipModalService;
|
|
|
|
this.handleError = (error) => {
|
|
|
|
// This particular error is handled by the alert ribbon
|
|
// on the main application page.
|
|
if (error.code === 'ENOSPC') {
|
|
AnalyticsService.logEvent('Drive ran out of space');
|
|
return;
|
|
}
|
|
|
|
OSDialogService.showError(error);
|
|
|
|
// Also throw it so it gets displayed in DevTools
|
|
// and its reported by TrackJS.
|
|
throw error;
|
|
};
|
|
|
|
// This catches the case where the user enters
|
|
// the settings screen when a flash finished
|
|
// and goes back to the main screen with the back button.
|
|
if (!this.writer.isFlashing()) {
|
|
|
|
this.selection.clear({
|
|
|
|
// Preserve image, in case there is one, otherwise
|
|
// we revert the behaviour of "Use same image".
|
|
preserveImage: true
|
|
|
|
});
|
|
}
|
|
|
|
DriveScannerService.start();
|
|
|
|
DriveScannerService.on('error', this.handleError);
|
|
|
|
DriveScannerService.on('drives', (drives) => {
|
|
this.drives.setDrives(drives);
|
|
|
|
if (_.isEmpty(drives)) {
|
|
DriveSelectorService.close();
|
|
}
|
|
});
|
|
|
|
this.selectImage = (image) => {
|
|
if (!SupportedFormatsModel.isSupportedImage(image.path)) {
|
|
OSDialogService.showError('Invalid image', `${image.path} is not a supported image type.`);
|
|
AnalyticsService.logEvent('Invalid image', image);
|
|
return;
|
|
}
|
|
|
|
this.selection.setImage(image);
|
|
AnalyticsService.logEvent('Select image', _.omit(image, 'logo'));
|
|
};
|
|
|
|
this.openImageUrl = () => {
|
|
const imageUrl = this.selection.getImageUrl();
|
|
|
|
if (imageUrl) {
|
|
OSOpenExternalService.open(imageUrl);
|
|
}
|
|
};
|
|
|
|
this.openImageSelector = () => {
|
|
return OSDialogService.selectImage().then((image) => {
|
|
|
|
// Avoid analytics and selection state changes
|
|
// if no file was resolved from the dialog.
|
|
if (!image) {
|
|
return;
|
|
}
|
|
|
|
this.selectImage(image);
|
|
}).catch(this.handleError);
|
|
};
|
|
|
|
this.selectDrive = (drive) => {
|
|
if (!drive) {
|
|
return;
|
|
}
|
|
|
|
this.selection.setDrive(drive.device);
|
|
|
|
AnalyticsService.logEvent('Select drive', {
|
|
device: drive.device
|
|
});
|
|
};
|
|
|
|
this.openDriveSelector = () => {
|
|
DriveSelectorService.open()
|
|
.then(this.selectDrive)
|
|
.catch(this.handleError);
|
|
};
|
|
|
|
this.reselectImage = () => {
|
|
if (this.writer.isFlashing()) {
|
|
return;
|
|
}
|
|
|
|
// Reselecting an image automatically
|
|
// de-selects the current drive, if any.
|
|
// This is made so the user effectively
|
|
// "returns" to the first step.
|
|
this.selection.clear();
|
|
|
|
this.openImageSelector();
|
|
AnalyticsService.logEvent('Reselect image');
|
|
};
|
|
|
|
this.reselectDrive = () => {
|
|
if (this.writer.isFlashing()) {
|
|
return;
|
|
}
|
|
|
|
this.openDriveSelector();
|
|
AnalyticsService.logEvent('Reselect drive');
|
|
};
|
|
|
|
this.restartAfterFailure = () => {
|
|
this.selection.clear({
|
|
preserveImage: true
|
|
});
|
|
|
|
this.writer.resetState();
|
|
AnalyticsService.logEvent('Restart after failure');
|
|
};
|
|
|
|
this.wasLastFlashSuccessful = () => {
|
|
const flashResults = this.writer.getFlashResults();
|
|
|
|
if (_.get(flashResults, 'cancelled', false)) {
|
|
return true;
|
|
}
|
|
|
|
return _.get(flashResults, 'passedValidation', true);
|
|
};
|
|
|
|
this.flash = (image, drive) => {
|
|
|
|
if (this.writer.isFlashing()) {
|
|
return;
|
|
}
|
|
|
|
// Stop scanning drives when flashing
|
|
// otherwise Windows throws EPERM
|
|
DriveScannerService.stop();
|
|
|
|
AnalyticsService.logEvent('Flash', {
|
|
image: image,
|
|
device: drive.device
|
|
});
|
|
|
|
return this.writer.flash(image, drive).then(() => {
|
|
const results = ImageWriterService.getFlashResults();
|
|
|
|
if (results.cancelled) {
|
|
return;
|
|
}
|
|
|
|
if (results.passedValidation) {
|
|
OSNotificationService.send('Success!', 'Your flash is complete');
|
|
AnalyticsService.logEvent('Done');
|
|
$state.go('success');
|
|
} else {
|
|
OSNotificationService.send('Oops!', 'Looks like your flash has failed');
|
|
AnalyticsService.logEvent('Validation error');
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
|
|
if (error.type === 'check') {
|
|
AnalyticsService.logEvent('Validation error');
|
|
} else {
|
|
AnalyticsService.logEvent('Flash error');
|
|
}
|
|
|
|
this.handleError(error);
|
|
})
|
|
.finally(OSWindowProgressService.clear);
|
|
};
|
|
});
|