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 b80f5269..c798c0d7 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
@@ -40,6 +40,11 @@
System Drive
+
+
+ Source Drive
+
{
const drive = _.first(action.data);
- // Even if there's no image selectected, we need to call `isDriveValid`
- // to check if the drive is locked, and `{}` works fine with it
+ // Even if there's no image selected, we need to call several
+ // drive/image related checks, and `{}` works fine with them
const image = state.getIn([ 'selection', 'image' ], Immutable.fromJS({})).toJS();
if (_.every([
@@ -268,7 +268,7 @@ const storeReducer = (state, action) => {
return _.attempt(() => {
if (selectedDrive && !_.every([
- constraints.isDriveLargeEnough(selectedDrive.toJS(), action.data),
+ constraints.isDriveValid(selectedDrive.toJS(), action.data),
constraints.isDriveSizeRecommended(selectedDrive.toJS(), action.data)
])) {
return storeReducer(state, {
diff --git a/lib/shared/drive-constraints.js b/lib/shared/drive-constraints.js
index 00acf6a6..20fa6a58 100644
--- a/lib/shared/drive-constraints.js
+++ b/lib/shared/drive-constraints.js
@@ -17,6 +17,7 @@
'use strict';
const _ = require('lodash');
+const pathIsInside = require('path-is-inside');
/**
* @summary Check if a drive is locked
@@ -65,6 +66,52 @@ exports.isSystemDrive = (drive) => {
return Boolean(_.get(drive, 'system', false));
};
+/**
+ * @summary Check if a drive is source drive
+ * @function
+ * @public
+ *
+ * @description
+ * In the context of Etcher, a source drive is a drive
+ * containing the image.
+ *
+ * @param {Object} drive - drive
+ * @param {Object} image - image
+ * @returns {Boolean} whether the drive is a source drive
+ *
+ *
+ * @example
+ * if (constraints.isSourceDrive({
+ * device: '/dev/disk2',
+ * name: 'My Drive',
+ * size: 123456789,
+ * protected: true,
+ * system: true,
+ * mountpoints: [
+ * {
+ * path: '/Volumes/Untitled'
+ * }
+ * ]
+ * }, {
+ * path: '/Volumes/Untitled/image.img',
+ * size: 1000000000
+ * })) {
+ * console.log('This drive is a source drive!');
+ * }
+ */
+exports.isSourceDrive = (drive, image) => {
+ const mountpoints = _.get(drive, 'mountpoints', []);
+ const imagePath = _.get(image, 'path');
+
+ if (!imagePath || _.isEmpty(mountpoints)) {
+ return false;
+ }
+
+ return _.some(_.map(mountpoints, (mountpoint) => {
+ return pathIsInside(imagePath, mountpoint.path);
+ }));
+};
+
/**
* @summary Check if a drive is large enough for an image
* @function
@@ -114,7 +161,11 @@ exports.isDriveLargeEnough = (drive, image) => {
* }
*/
exports.isDriveValid = (drive, image) => {
- return !this.isDriveLocked(drive) && this.isDriveLargeEnough(drive, image);
+ return _.every([
+ !this.isDriveLocked(drive),
+ this.isDriveLargeEnough(drive, image),
+ !this.isSourceDrive(drive, image)
+ ]);
};
/**
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 6c588afe..9f955d26 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1298,6 +1298,11 @@
"from": "path-exists@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz"
},
+ "path-is-inside": {
+ "version": "1.0.2",
+ "from": "path-is-inside@>=1.0.2 <2.0.0",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz"
+ },
"path-key": {
"version": "1.0.0",
"from": "path-key@>=1.0.0 <2.0.0",
diff --git a/package.json b/package.json
index 007b5202..b562ad70 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"lzma-native": "^1.5.2",
"node-ipc": "^8.9.2",
"node-stream-zip": "^1.3.4",
+ "path-is-inside": "^1.0.2",
"read-chunk": "^2.0.0",
"redux": "^3.5.2",
"redux-localstorage": "^0.4.1",
diff --git a/tests/gui/models/drives.spec.js b/tests/gui/models/drives.spec.js
index 880fbe8a..9cb193e5 100644
--- a/tests/gui/models/drives.spec.js
+++ b/tests/gui/models/drives.spec.js
@@ -1,6 +1,7 @@
'use strict';
const m = require('mochainon');
+const path = require('path');
const angular = require('angular');
require('angular-mocks');
@@ -108,9 +109,15 @@ describe('Browser: DrivesModel', function() {
describe('given a selected image and no selected drive', function() {
beforeEach(function() {
+ if (process.platform === 'win32') {
+ this.imagePath = 'E:\\bar\\foo.img';
+ } else {
+ this.imagePath = '/mnt/bar/foo.img';
+ }
+
SelectionStateModel.removeDrive();
SelectionStateModel.setImage({
- path: 'foo.img',
+ path: this.imagePath,
size: 999999999,
recommendedDriveSize: 2000000000
});
@@ -220,6 +227,27 @@ describe('Browser: DrivesModel', function() {
m.chai.expect(SelectionStateModel.hasDrive()).to.be.false;
});
+ it('should not auto-select a source drive', function() {
+ m.chai.expect(SelectionStateModel.hasDrive()).to.be.false;
+
+ DrivesModel.setDrives([
+ {
+ device: '/dev/sdb',
+ name: 'Foo',
+ size: 2000000000,
+ mountpoints: [
+ {
+ path: path.dirname(this.imagePath)
+ }
+ ],
+ system: false,
+ protected: false
+ }
+ ]);
+
+ m.chai.expect(SelectionStateModel.hasDrive()).to.be.false;
+ });
+
});
});
diff --git a/tests/gui/models/selection-state.spec.js b/tests/gui/models/selection-state.spec.js
index 4a35113c..15c18b89 100644
--- a/tests/gui/models/selection-state.spec.js
+++ b/tests/gui/models/selection-state.spec.js
@@ -1,6 +1,8 @@
'use strict';
const m = require('mochainon');
+const _ = require('lodash');
+const path = require('path');
const angular = require('angular');
require('angular-mocks');
@@ -485,6 +487,41 @@ describe('Browser: SelectionState', function() {
SelectionStateModel.removeImage();
});
+ it('should de-select a previously selected source drive', function() {
+ const imagePath = _.attempt(() => {
+ if (process.platform === 'win32') {
+ return 'E:\\bar\\foo.img';
+ }
+
+ return '/mnt/bar/foo.img';
+ });
+
+ DrivesModel.setDrives([
+ {
+ device: '/dev/disk1',
+ name: 'USB Drive',
+ size: 1200000000,
+ mountpoints: [
+ {
+ path: path.dirname(imagePath)
+ }
+ ],
+ protected: false
+ }
+ ]);
+
+ SelectionStateModel.setDrive('/dev/disk1');
+ m.chai.expect(SelectionStateModel.hasDrive()).to.be.true;
+
+ SelectionStateModel.setImage({
+ path: imagePath,
+ size: 999999999
+ });
+
+ m.chai.expect(SelectionStateModel.hasDrive()).to.be.false;
+ SelectionStateModel.removeImage();
+ });
+
});
});
diff --git a/tests/shared/drive-constraints.spec.js b/tests/shared/drive-constraints.spec.js
index 056421ca..8f6bc15c 100644
--- a/tests/shared/drive-constraints.spec.js
+++ b/tests/shared/drive-constraints.spec.js
@@ -1,6 +1,7 @@
'use strict';
const m = require('mochainon');
+const path = require('path');
const constraints = require('../../lib/shared/drive-constraints');
describe('Shared: DriveConstraints', function() {
@@ -92,6 +93,214 @@ describe('Shared: DriveConstraints', function() {
});
+ describe('.isSourceDrive()', function() {
+
+ it('should return false if no image', function() {
+ const result = constraints.isSourceDrive({
+ device: '/dev/disk2',
+ name: 'USB Drive',
+ size: 999999999,
+ protected: true,
+ system: false
+ }, undefined);
+
+ m.chai.expect(result).to.be.false;
+ });
+
+ it('should return false if no drive', function() {
+ const result = constraints.isSourceDrive(undefined, {
+ path: '/Volumes/Untitled/image.img'
+ });
+
+ m.chai.expect(result).to.be.false;
+ });
+
+ it('should return false if there are no mount points', function() {
+ const result = constraints.isSourceDrive({
+ device: '/dev/disk2',
+ name: 'USB Drive',
+ size: 999999999,
+ protected: true,
+ system: false
+ }, {
+ path: '/Volumes/Untitled/image.img'
+ });
+
+ m.chai.expect(result).to.be.false;
+ });
+
+ describe('given Windows paths', function() {
+
+ beforeEach(function() {
+ this.separator = path.sep;
+ path.sep = '\\';
+ });
+
+ afterEach(function() {
+ path.sep = this.separator;
+ });
+
+ it('should return true if the image lives directly inside a mount point of the drive', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: 'E:'
+ },
+ {
+ path: 'F:'
+ }
+ ]
+ }, {
+ path: 'E:\\image.img'
+ });
+
+ m.chai.expect(result).to.be.true;
+ });
+
+ it('should return true if the image lives inside a mount point of the drive', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: 'E:'
+ },
+ {
+ path: 'F:'
+ }
+ ]
+ }, {
+ path: 'E:\\foo\\bar\\image.img'
+ });
+
+ m.chai.expect(result).to.be.true;
+ });
+
+ it('should return false if the image does not live inside a mount point of the drive', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: 'E:'
+ },
+ {
+ path: 'F:'
+ }
+ ]
+ }, {
+ path: 'G:\\image.img'
+ });
+
+ m.chai.expect(result).to.be.false;
+ });
+
+ it('should return false if the image is in a mount point that is a substring of the image mount point', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: 'E:\\fo'
+ }
+ ]
+ }, {
+ path: 'E:\\foo/image.img'
+ });
+
+ m.chai.expect(result).to.be.false;
+ });
+
+ });
+
+ describe('given UNIX paths', function() {
+
+ beforeEach(function() {
+ this.separator = path.sep;
+ path.sep = '/';
+ });
+
+ afterEach(function() {
+ path.sep = this.separator;
+ });
+
+ it('should return true if the mount point is / and the image lives directly inside it', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: '/'
+ }
+ ]
+ }, {
+ path: '/image.img'
+ });
+
+ m.chai.expect(result).to.be.true;
+ });
+
+ it('should return true if the image lives directly inside a mount point of the drive', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: '/Volumes/A'
+ },
+ {
+ path: '/Volumes/B'
+ }
+ ]
+ }, {
+ path: '/Volumes/A/image.img'
+ });
+
+ m.chai.expect(result).to.be.true;
+ });
+
+ it('should return true if the image lives inside a mount point of the drive', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: '/Volumes/A'
+ },
+ {
+ path: '/Volumes/B'
+ }
+ ]
+ }, {
+ path: '/Volumes/A/foo/bar/image.img'
+ });
+
+ m.chai.expect(result).to.be.true;
+ });
+
+ it('should return false if the image does not live inside a mount point of the drive', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: '/Volumes/A'
+ },
+ {
+ path: '/Volumes/B'
+ }
+ ]
+ }, {
+ path: '/Volumes/C/image.img'
+ });
+
+ m.chai.expect(result).to.be.false;
+ });
+
+ it('should return false if the image is in a mount point that is a substring of the image mount point', function() {
+ const result = constraints.isSourceDrive({
+ mountpoints: [
+ {
+ path: '/Volumes/fo'
+ }
+ ]
+ }, {
+ path: '/Volumes/foo/image.img'
+ });
+
+ m.chai.expect(result).to.be.false;
+ });
+
+ });
+
+ });
+
describe('.isDriveLargeEnough()', function() {
it('should return true if the drive size is greater than the image size', function() {
@@ -254,48 +463,93 @@ describe('Shared: DriveConstraints', function() {
describe('.isDriveValid()', function() {
- describe('given drive is large enough', function() {
+ beforeEach(function() {
+ if (process.platform === 'win32') {
+ this.mountpoint = 'E:\\foo';
+ } else {
+ this.mountpoint = '/mnt/foo';
+ }
+
+ this.drive = {
+ device: '/dev/disk2',
+ name: 'My Drive',
+ mountpoints: [
+ {
+ path: this.mountpoint
+ }
+ ],
+ size: 4000000000
+ };
+ });
+
+ describe('given the drive is locked', function() {
beforeEach(function() {
- this.drive = {
- device: '/dev/disk2',
- name: 'My Drive',
- size: 4000000000
- };
- this.image = {
- path: 'rpi.img',
- size: 2000000000
- };
- });
-
- it('should return true if drive is not locked', function() {
- this.drive.protected = false;
- m.chai.expect(constraints.isDriveValid(this.drive, this.image)).to.be.true;
- });
-
- it('should return false if drive is locked', function() {
this.drive.protected = true;
- m.chai.expect(constraints.isDriveValid(this.drive, this.image)).to.be.false;
+ });
+
+ it('should return false if the drive is not large enough and is a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.join(this.mountpoint, 'rpi.img'),
+ size: 5000000000
+ })).to.be.false;
+ });
+
+ it('should return false if the drive is not large enough and is not a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.resolve(this.mountpoint, '../bar/rpi.img'),
+ size: 5000000000
+ })).to.be.false;
+ });
+
+ it('should return false if the drive is large enough and is a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.join(this.mountpoint, 'rpi.img'),
+ size: 2000000000
+ })).to.be.false;
+ });
+
+ it('should return false if the drive is large enough and is not a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.resolve(this.mountpoint, '../bar/rpi.img'),
+ size: 2000000000
+ })).to.be.false;
});
});
- describe('given drive is not large enough', function() {
+ describe('given the drive is not locked', function() {
beforeEach(function() {
- this.drive = {
- device: '/dev/disk2',
- name: 'My Drive',
- size: 1000000000
- };
- this.image = {
- path: 'rpi.img',
- size: 2000000000
- };
+ this.drive.protected = false;
});
- it('should return false', function() {
- m.chai.expect(constraints.isDriveValid(this.drive, this.image)).to.be.false;
+ it('should return false if the drive is not large enough and is a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.join(this.mountpoint, 'rpi.img'),
+ size: 5000000000
+ })).to.be.false;
+ });
+
+ it('should return false if the drive is not large enough and is not a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.resolve(this.mountpoint, '../bar/rpi.img'),
+ size: 5000000000
+ })).to.be.false;
+ });
+
+ it('should return false if the drive is large enough and is a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.join(this.mountpoint, 'rpi.img'),
+ size: 2000000000
+ })).to.be.false;
+ });
+
+ it('should return true if the drive is large enough and is not a source drive', function() {
+ m.chai.expect(constraints.isDriveValid(this.drive, {
+ path: path.resolve(this.mountpoint, '../bar/rpi.img'),
+ size: 2000000000
+ })).to.be.true;
});
});