diff --git a/build/css/main.css b/build/css/main.css
index cf217d32..5e202182 100644
--- a/build/css/main.css
+++ b/build/css/main.css
@@ -6252,6 +6252,25 @@ button.btn:focus, button.progress-button:focus {
.alert-ribbon--open {
top: 0; }
+/*
+ * 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.
+ */
+.list-group-item[disabled] {
+ text-decoration: line-through;
+ cursor: not-allowed; }
+
/*
* Copyright 2016 Resin.io
*
diff --git a/lib/gui/app.js b/lib/gui/app.js
index a63babe7..7d87a5e4 100644
--- a/lib/gui/app.js
+++ b/lib/gui/app.js
@@ -160,10 +160,14 @@ app.controller('AppController', function(
// `angular.equals` is used instead of `_.isEqual` to
// cope with `$$hashKey`.
if (!angular.equals(self.selection.getDrive(), drive)) {
- AnalyticsService.logEvent('Auto-select drive', {
- device: drive.device
- });
- self.selectDrive(drive);
+
+ if (self.selection.isDriveLargeEnough(drive)) {
+ self.selectDrive(drive);
+
+ AnalyticsService.logEvent('Auto-select drive', {
+ device: drive.device
+ });
+ }
}
}
@@ -181,9 +185,7 @@ app.controller('AppController', function(
this.selectImage = function(image) {
self.selection.setImage(image);
- AnalyticsService.logEvent('Select image', {
- image: image
- });
+ AnalyticsService.logEvent('Select image', image);
};
this.openImageSelector = function() {
diff --git a/lib/gui/components/drive-selector/templates/drive-selector-modal.tpl.html b/lib/gui/components/drive-selector/templates/drive-selector-modal.tpl.html
index e0b49966..311a693e 100644
--- a/lib/gui/components/drive-selector/templates/drive-selector-modal.tpl.html
+++ b/lib/gui/components/drive-selector/templates/drive-selector-modal.tpl.html
@@ -6,12 +6,15 @@
-
+ ng-disabled="!modal.state.isDriveLargeEnough(drive)"
+ ng-click="modal.state.isDriveLargeEnough(drive) && modal.state.toggleSetDrive(drive)">
{{ drive.description }} - {{ drive.size | gigabyte | number:1 }} GB
{{ drive.name }}
-
+
diff --git a/lib/gui/models/selection-state.js b/lib/gui/models/selection-state.js
index d4b439fe..6ef0b8d7 100644
--- a/lib/gui/models/selection-state.js
+++ b/lib/gui/models/selection-state.js
@@ -42,15 +42,88 @@ SelectionStateModel.service('SelectionStateModel', function() {
*
* @param {Object} drive - drive
*
+ * @throws Will throw if drive lacks `.device`.
+ * @throws Will throw if `drive.device` is not a string.
+ * @throws Will throw if drive lacks `.name`.
+ * @throws Will throw if `drive.name` is not a string.
+ * @throws Will throw if drive lacks `.size`.
+ * @throws Will throw if `drive.size` is not a number.
+ * @throws Will throw if there is an image and the drive is not large enough.
+ *
* @example
* SelectionStateModel.setDrive({
- * device: '/dev/disk2'
+ * device: '/dev/disk2',
+ * name: 'USB drive',
+ * size: 999999999
* });
*/
this.setDrive = function(drive) {
+
+ if (!drive.device) {
+ throw new Error('Missing drive device');
+ }
+
+ if (!_.isString(drive.device)) {
+ throw new Error(`Invalid drive device: ${drive.device}`);
+ }
+
+ if (!drive.name) {
+ throw new Error('Missing drive name');
+ }
+
+ if (!_.isString(drive.name)) {
+ throw new Error(`Invalid drive name: ${drive.name}`);
+ }
+
+ if (!drive.size) {
+ throw new Error('Missing drive size');
+ }
+
+ if (!_.isNumber(drive.size)) {
+ throw new Error(`Invalid drive size: ${drive.size}`);
+ }
+
+ if (!self.isDriveLargeEnough(drive)) {
+ throw new Error('The drive is not large enough');
+ }
+
selection.drive = drive;
};
+ /**
+ * @summary Check if a drive is large enough for the selected image
+ * @function
+ * @public
+ *
+ * @description
+ * For convenience, if there is no image selected, this function
+ * returns true.
+ *
+ * Notice that if you select the drive before the image, the check
+ * will not take place and it'll be the client's responsibility
+ * to do so.
+ *
+ * @param {Object} drive - drive
+ * @returns {Boolean} whether the drive is large enough
+ *
+ * @example
+ * SelectionStateModel.setImage({
+ * path: 'rpi.img',
+ * size: 100000000
+ * });
+ *
+ * if (SelectionStateModel.isDriveLargeEnough({
+ * device: '/dev/disk2',
+ * name: 'My Drive',
+ * size: 123456789
+ * })) {
+ * console.log('We can flash the image to this drive!');
+ * }
+ */
+ this.isDriveLargeEnough = function(drive) {
+ return (self.getImageSize() || 0) <= drive.size;
+ };
+
/**
* @summary Toggle set drive
* @function
@@ -76,12 +149,36 @@ SelectionStateModel.service('SelectionStateModel', function() {
* @function
* @public
*
- * @param {String} image - image
+ * @param {Object} image - image
+ *
+ * @throws Will throw if image lacks `.path`.
+ * @throws Will throw if `image.path` is not a string.
+ * @throws Will throw if image lacks `.size`.
+ * @throws Will throw if `image.size` is not a number.
*
* @example
- * SelectionStateModel.setImage('foo.img');
+ * SelectionStateModel.setImage({
+ * path: 'foo.img'
+ * });
*/
this.setImage = function(image) {
+
+ if (!image.path) {
+ throw new Error('Missing image path');
+ }
+
+ if (!_.isString(image.path)) {
+ throw new Error(`Invalid image path: ${image.path}`);
+ }
+
+ if (!image.size) {
+ throw new Error('Missing image size');
+ }
+
+ if (!_.isNumber(image.size)) {
+ throw new Error(`Invalid image size: ${image.size}`);
+ }
+
selection.image = image;
};
@@ -104,17 +201,31 @@ SelectionStateModel.service('SelectionStateModel', function() {
};
/**
- * @summary Get image
+ * @summary Get image path
* @function
* @public
*
- * @returns {String} image
+ * @returns {String} image path
*
* @example
- * const image = SelectionStateModel.getImage();
+ * const imagePath = SelectionStateModel.getImagePath();
*/
- this.getImage = function() {
- return selection.image;
+ this.getImagePath = function() {
+ return _.get(selection.image, 'path');
+ };
+
+ /**
+ * @summary Get image size
+ * @function
+ * @public
+ *
+ * @returns {Number} image size
+ *
+ * @example
+ * const imageSize = SelectionStateModel.getImageSize();
+ */
+ this.getImageSize = function() {
+ return _.get(selection.image, 'size');
};
/**
@@ -146,7 +257,7 @@ SelectionStateModel.service('SelectionStateModel', function() {
* }
*/
this.hasImage = function() {
- return Boolean(self.getImage());
+ return Boolean(self.getImagePath() && self.getImageSize());
};
/**
@@ -157,7 +268,7 @@ SelectionStateModel.service('SelectionStateModel', function() {
* @example
* SelectionStateModel.removeDrive();
*/
- this.removeDrive = _.partial(self.setDrive, undefined);
+ this.removeDrive = _.partial(_.unset, selection, 'drive');
/**
* @summary Remove image
@@ -167,7 +278,7 @@ SelectionStateModel.service('SelectionStateModel', function() {
* @example
* SelectionStateModel.removeImage();
*/
- this.removeImage = _.partial(self.setImage, undefined);
+ this.removeImage = _.partial(_.unset, selection, 'image');
/**
* @summary Clear selections
diff --git a/lib/gui/os/dialog/services/dialog.js b/lib/gui/os/dialog/services/dialog.js
index 2b808976..8da032c2 100644
--- a/lib/gui/os/dialog/services/dialog.js
+++ b/lib/gui/os/dialog/services/dialog.js
@@ -17,6 +17,7 @@
'use strict';
const _ = require('lodash');
+const fs = require('fs');
const electron = require('electron');
const imageStream = require('etcher-image-stream');
@@ -30,16 +31,16 @@ module.exports = function($q) {
* @description
* Notice that by image, we mean *.img/*.iso/*.zip/etc files.
*
- * @fulfil {String} - selected image
+ * @fulfil {Object} - selected image
* @returns {Promise};
*
* @example
* OSDialogService.selectImage().then(function(image) {
- * console.log('The selected image is', image);
+ * console.log('The selected image is', image.path);
* });
*/
this.selectImage = function() {
- return $q(function(resolve) {
+ return $q(function(resolve, reject) {
electron.remote.dialog.showOpenDialog({
properties: [
'openFile'
@@ -55,8 +56,23 @@ module.exports = function($q) {
// `_.first` is smart enough to not throw and return `undefined`
// if we pass it an `undefined` value (e.g: when the selection
// dialog was cancelled).
- return resolve(_.first(files));
+ const imagePath = _.first(files);
+ if (!imagePath) {
+ return resolve();
+ }
+
+ fs.stat(imagePath, function(error, stats) {
+
+ if (error) {
+ return reject(error);
+ }
+
+ return resolve({
+ path: imagePath,
+ size: stats.size
+ });
+ });
});
});
};
diff --git a/lib/gui/partials/main.html b/lib/gui/partials/main.html
index ed35e614..4886d614 100644
--- a/lib/gui/partials/main.html
+++ b/lib/gui/partials/main.html
@@ -11,7 +11,7 @@