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 @@ 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 @@
-
+