Show a "Locked" label if the drive is write-protected (#475)

* Implement SelectionStateModel.isDriveLocked()

Notice we also increase the maximum number of lines JSCS check, since
the `SelectionStateModel` test suite already met that limit.

Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>

* Show a "Locked" label if the drive is write-protected

Fixes: https://github.com/resin-io/etcher/issues/458
Signed-off-by: Juan Cruz Viotti <jviottidc@gmail.com>
This commit is contained in:
Juan Cruz Viotti 2016-06-11 15:45:46 -04:00 committed by GitHub
parent 51b6de4634
commit fbe6fe1142
5 changed files with 260 additions and 31 deletions

View File

@ -131,7 +131,7 @@
"!=="
],
"maximumLineLength": 130,
"maximumNumberOfLines": 500,
"maximumNumberOfLines": 1000,
"requireAlignedMultilineParams": true,
"requireAlignedObjectValues": false,
"requireAnonymousFunctions": true,

View File

@ -6,20 +6,32 @@
<div class="component-drive-selector-body modal-body">
<ul class="list-group">
<li class="list-group-item" ng-repeat="drive in modal.drives.getDrives()"
ng-disabled="!modal.state.isDriveLargeEnough(drive)"
ng-click="modal.state.isDriveLargeEnough(drive) && modal.state.toggleSetDrive(drive)">
ng-disabled="!modal.state.isDriveValid(drive)"
ng-click="modal.state.isDriveValid(drive) && modal.state.toggleSetDrive(drive)">
<div>
<header class="list-group-item-header">
<!-- There can be a case where the device it not large enough, and it's also locked. -->
<!-- Since in this case both labels will be displayed, we chose to only show the -->
<!-- "not large enough label", since from the point of view of the user, the locked -->
<!-- state is irrelevent if the drive is not large enough anyway. -->
<span class="label label-danger"
ng-hide="modal.state.isDriveLargeEnough(drive)">
<i class="glyphicon glyphicon-resize-small"></i>
TOO SMALL FOR IMAGE</span>
<span class="label label-danger"
ng-show="modal.state.isDriveLocked(drive) && modal.state.isDriveLargeEnough(drive)">
<i class="glyphicon glyphicon-lock"></i>
LOCKED</span>
</header>
<h4 class="list-group-item-heading">{{ drive.description }} - {{ drive.size | gigabyte | number:1 }} GB</h4>
<p class="list-group-item-text">{{ drive.name }}</p>
</div>
<span class="tick tick--success"
ng-show="modal.state.isDriveLargeEnough(drive)"
ng-show="modal.state.isDriveValid(drive)"
ng-disabled="!modal.state.isCurrentDrive(drive)"></span>
</li>
</ul>

View File

@ -83,6 +83,10 @@ SelectionStateModel.service('SelectionStateModel', function() {
throw new Error(`Invalid drive size: ${drive.size}`);
}
if (!_.isBoolean(drive.protected)) {
throw new Error(`Invalid drive protected state: ${drive.protected}`);
}
if (!self.isDriveLargeEnough(drive)) {
throw new Error('The drive is not large enough');
}
@ -124,6 +128,62 @@ SelectionStateModel.service('SelectionStateModel', function() {
return (self.getImageSize() || 0) <= drive.size;
};
/**
* @summary Check if a drive is locked
* @function
* @public
*
* @description
* This usually points out a locked SD Card.
*
* @param {Object} drive - drive
* @returns {Boolean} whether the drive is locked
*
* @example
* if (SelectionStateModel.isDriveLocked({
* device: '/dev/disk2',
* name: 'My Drive',
* size: 123456789,
* protected: true
* })) {
* console.log('This drive is locked (e.g: write-protected)');
* }
*/
this.isDriveLocked = function(drive) {
return _.get(drive, 'protected', false);
};
/**
* @summary Check if a drive is valid
* @function
* @public
*
* @description
* This function is a facade to:
*
* - `SelectionStateModel.isDriveLargeEnough()`
* - `SelectionStateModel.isDriveLocked()`
*
* @param {Object} drive - drive
* @returns {Boolean} whether the drive is valid
*
* @example
* if (SelectionStateModel.isDriveValid({
* device: '/dev/disk2',
* name: 'My Drive',
* size: 123456789,
* protected: true
* })) {
* console.log('This drive is valid!');
* }
*/
this.isDriveValid = function(drive) {
return _.every([
self.isDriveLargeEnough(drive),
!self.isDriveLocked(drive)
]);
};
/**
* @summary Toggle set drive
* @function

View File

@ -57,7 +57,7 @@
"bluebird": "^3.0.5",
"bootstrap-sass": "^3.3.5",
"chalk": "^1.1.3",
"drivelist": "^3.1.2",
"drivelist": "^3.2.0",
"electron-is-running-in-asar": "^1.0.0",
"etcher-image-stream": "^2.0.0",
"etcher-image-write": "^5.0.0",

View File

@ -49,13 +49,76 @@ describe('Browser: SelectionState', function() {
});
describe('.isDriveLocked()', function() {
it('should return true if the drive is protected', function() {
const result = SelectionStateModel.isDriveLocked({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999,
protected: true
});
m.chai.expect(result).to.be.true;
});
it('should return false if the drive is not protected', function() {
const result = SelectionStateModel.isDriveLocked({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999,
protected: false
});
m.chai.expect(result).to.be.false;
});
it('should return false if we don\'t know if the drive is protected', function() {
const result = SelectionStateModel.isDriveLocked({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999
});
m.chai.expect(result).to.be.false;
});
});
describe('.isDriveValid()', function() {
it('should return true if the drive is not locked', function() {
const result = SelectionStateModel.isDriveValid({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999,
protected: false
});
m.chai.expect(result).to.be.true;
});
it('should return false if the drive is locked', function() {
const result = SelectionStateModel.isDriveValid({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999,
protected: true
});
m.chai.expect(result).to.be.false;
});
});
describe('given a drive', function() {
beforeEach(function() {
SelectionStateModel.setDrive({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
});
@ -66,7 +129,8 @@ describe('Browser: SelectionState', function() {
m.chai.expect(drive).to.deep.equal({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
});
@ -87,14 +151,16 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.setDrive({
device: '/dev/disk5',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
const drive = SelectionStateModel.getDrive();
m.chai.expect(drive).to.deep.equal({
device: '/dev/disk5',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
});
@ -120,14 +186,16 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.setDrive({
device: '/dev/disk5',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
const drive = SelectionStateModel.getDrive();
m.chai.expect(drive).to.deep.equal({
device: '/dev/disk5',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
});
@ -135,7 +203,8 @@ describe('Browser: SelectionState', function() {
m.chai.expect(function() {
SelectionStateModel.setDrive({
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
}).to.throw('Missing drive device');
});
@ -145,7 +214,8 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.setDrive({
device: 123,
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
}).to.throw('Invalid drive device: 123');
});
@ -154,7 +224,8 @@ describe('Browser: SelectionState', function() {
m.chai.expect(function() {
SelectionStateModel.setDrive({
device: '/dev/disk2',
size: 999999999
size: 999999999,
protected: false
});
}).to.throw('Missing drive name');
});
@ -164,7 +235,8 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.setDrive({
device: '/dev/disk2',
name: 123,
size: 999999999
size: 999999999,
protected: false
});
}).to.throw('Invalid drive name: 123');
});
@ -173,7 +245,8 @@ describe('Browser: SelectionState', function() {
m.chai.expect(function() {
SelectionStateModel.setDrive({
device: '/dev/disk2',
name: 'USB Drive'
name: 'USB Drive',
protected: false
});
}).to.throw('Missing drive size');
});
@ -183,11 +256,33 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.setDrive({
device: '/dev/disk2',
name: 'USB Drive',
size: '999999999'
size: '999999999',
protected: false
});
}).to.throw('Invalid drive size: 999999999');
});
it('should throw if no protected property', function() {
m.chai.expect(function() {
SelectionStateModel.setDrive({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999
});
}).to.throw('Invalid drive protected state: undefined');
});
it('should throw if the protected is not boolean', function() {
m.chai.expect(function() {
SelectionStateModel.setDrive({
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999,
protected: 'foo'
});
}).to.throw('Invalid drive protected state: foo');
});
});
});
@ -207,7 +302,8 @@ describe('Browser: SelectionState', function() {
const result = SelectionStateModel.isDriveLargeEnough({
device: '/dev/disk1',
name: 'USB Drive',
size: 99999999999999
size: 99999999999999,
protected: false
});
m.chai.expect(result).to.be.true;
@ -217,7 +313,8 @@ describe('Browser: SelectionState', function() {
const result = SelectionStateModel.isDriveLargeEnough({
device: '/dev/disk1',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
m.chai.expect(result).to.be.true;
@ -227,7 +324,56 @@ describe('Browser: SelectionState', function() {
const result = SelectionStateModel.isDriveLargeEnough({
device: '/dev/disk1',
name: 'USB Drive',
size: 999999998
size: 999999998,
protected: false
});
m.chai.expect(result).to.be.false;
});
});
describe('.isDriveValid()', function() {
it('should return true if the drive is large enough and it is not locked', function() {
const result = SelectionStateModel.isDriveValid({
device: '/dev/disk1',
name: 'USB Drive',
size: 99999999999999,
protected: false
});
m.chai.expect(result).to.be.true;
});
it('should return false if the drive is large enough but it is locked', function() {
const result = SelectionStateModel.isDriveValid({
device: '/dev/disk1',
name: 'USB Drive',
size: 99999999999999,
protected: true
});
m.chai.expect(result).to.be.false;
});
it('should return false if the drive is not large enough and it is not locked', function() {
const result = SelectionStateModel.isDriveValid({
device: '/dev/disk1',
name: 'USB Drive',
size: 1,
protected: false
});
m.chai.expect(result).to.be.false;
});
it('should return false if the drive is not large enough and it is locked', function() {
const result = SelectionStateModel.isDriveValid({
device: '/dev/disk1',
name: 'USB Drive',
size: 1,
protected: true
});
m.chai.expect(result).to.be.false;
@ -242,7 +388,8 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.setDrive({
device: '/dev/disk1',
name: 'USB Drive',
size: 999999998
size: 999999998,
protected: false
});
}).to.throw('The drive is not large enough');
});
@ -381,7 +528,8 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.setDrive({
device: '/dev/disk1',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
});
SelectionStateModel.setImage({
@ -452,7 +600,8 @@ describe('Browser: SelectionState', function() {
size: 999999999,
mountpoint: '/media/UNTITLED',
name: '/dev/sdb',
system: false
system: false,
protected: false
});
});
@ -471,7 +620,8 @@ describe('Browser: SelectionState', function() {
size: 999999999,
mountpoint: '/media/UNTITLED',
name: '/dev/sdb',
system: false
system: false,
protected: false
})).to.be.true;
});
@ -483,7 +633,8 @@ describe('Browser: SelectionState', function() {
mountpoint: '/media/UNTITLED',
name: '/dev/sdb',
system: false,
$$hashKey: 1234
$$hashKey: 1234,
protected: false
})).to.be.true;
});
@ -494,7 +645,8 @@ describe('Browser: SelectionState', function() {
size: 999999999,
mountpoint: '/media/UNTITLED',
name: '/dev/sdb',
system: false
system: false,
protected: false
})).to.be.false;
});
@ -505,7 +657,8 @@ describe('Browser: SelectionState', function() {
size: 999999999,
mountpoint: '/media/UNTITLED',
name: '/dev/sdb',
system: false
system: false,
protected: false
})).to.be.true;
});
@ -533,7 +686,8 @@ describe('Browser: SelectionState', function() {
size: 999999999,
mountpoint: '/media/UNTITLED',
name: '/dev/sdb',
system: false
system: false,
protected: false
})).to.be.false;
});
@ -553,7 +707,8 @@ describe('Browser: SelectionState', function() {
size: 999999999,
mountpoint: '/media/UNTITLED',
name: '/dev/sdb',
system: false
system: false,
protected: false
};
SelectionStateModel.setDrive(this.drive);
@ -569,7 +724,8 @@ describe('Browser: SelectionState', function() {
const drive = {
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
};
m.chai.expect(SelectionStateModel.getDrive()).to.deep.equal(this.drive);
@ -590,7 +746,8 @@ describe('Browser: SelectionState', function() {
const drive = {
device: '/dev/disk2',
name: 'USB Drive',
size: 999999999
size: 999999999,
protected: false
};
m.chai.expect(SelectionStateModel.hasDrive()).to.be.false;