chore: revise ESLint built-in configuration (#1149)

There are a lot of new rules since the last time I revised the ESLint
rules documentation.

I've updated the main `.eslintrc.yml` to include some newer additions,
plus I added another ESLint configuration file inside `tests`, so we can
add some stricted rules to the production code while relaxing them for
the test suite (due to the fact that Mocha is not very ES6 friendly and
Angular tests require a bit of dark magic to setup).

This is a summary of the most important changes:

- Disallow "magic numbers"

These should now be extracted to constants, which forces us to think of
a good name for them, and thus make the code more self-documenting (I
had to Google up the meaning of some existing magic numbers, so I guess
this will be great for readability purposes).

- Require consistent `return` statements

Some functions relied on JavaScript relaxed casting mechanism to work,
which now have explicit return values. This flag also helped me detect
some promises that were not being returned, and therefore risked not
being caught by the exception handlers in case of errors.

- Disallow redefining function arguments

Immutability makes functions easier to reason about.

- Enforce JavaScript string templates instead of string concatenation

We were heavily mixing boths across the codebase.

There are some extra rules that I tweaked, however most of codebase
changes in this commit are related to the rules mentioned above.

Signed-off-by: Juan Cruz Viotti <jviotti@openmailbox.org>
This commit is contained in:
Juan Cruz Viotti 2017-03-07 23:46:44 -04:00 committed by GitHub
parent c93f528f96
commit 6c8bc117ab
52 changed files with 514 additions and 174 deletions

View File

@ -9,9 +9,6 @@ rules:
# Possible Errors # Possible Errors
comma-dangle:
- error
- never
no-cond-assign: no-cond-assign:
- error - error
no-console: no-console:
@ -59,6 +56,8 @@ rules:
- error - error
no-sparse-arrays: no-sparse-arrays:
- error - error
no-template-curly-in-string:
- error
no-unexpected-multiline: no-unexpected-multiline:
- error - error
no-unreachable: no-unreachable:
@ -93,8 +92,12 @@ rules:
- error - error
block-scoped-var: block-scoped-var:
- error - error
class-methods-use-this:
- error
complexity: complexity:
- off - off
consistent-return:
- error
curly: curly:
- error - error
default-case: default-case:
@ -136,6 +139,8 @@ rules:
- error - error
no-floating-decimal: no-floating-decimal:
- error - error
no-global-assign:
- error
no-implicit-coercion: no-implicit-coercion:
- error - error
no-implicit-globals: no-implicit-globals:
@ -150,6 +155,8 @@ rules:
- error - error
no-loop-func: no-loop-func:
- error - error
no-magic-numbers:
- error
no-multi-spaces: no-multi-spaces:
- error - error
no-multi-str: no-multi-str:
@ -166,12 +173,19 @@ rules:
- error - error
no-octal-escape: no-octal-escape:
- error - error
no-param-reassign:
- error
no-proto: no-proto:
- error - error
no-redeclare: no-redeclare:
- error - error
no-restricted-properties:
- error
- property: __proto__
no-return-assign: no-return-assign:
- error - error
no-return-await:
- error
no-script-url: no-script-url:
- error - error
no-self-assign: no-self-assign:
@ -184,6 +198,8 @@ rules:
- error - error
no-unmodified-loop-condition: no-unmodified-loop-condition:
- error - error
no-unused-expressions:
- error
no-unused-labels: no-unused-labels:
- error - error
no-useless-call: no-useless-call:
@ -216,12 +232,18 @@ rules:
# Variables # Variables
init-declarations:
- error
- always
no-catch-shadow: no-catch-shadow:
- error - error
no-delete-var: no-delete-var:
- error - error
no-label-var: no-label-var:
- error - error
no-restricted-globals:
- error
- event
no-shadow: no-shadow:
- error - error
no-shadow-restricted-names: no-shadow-restricted-names:
@ -230,6 +252,8 @@ rules:
- error - error
no-undef-init: no-undef-init:
- error - error
no-undefined:
- error
no-unused-vars: no-unused-vars:
- error - error
no-use-before-define: no-use-before-define:
@ -268,6 +292,13 @@ rules:
- 1tbs - 1tbs
camelcase: camelcase:
- error - error
capitalized-comments:
- error
- always
- ignoreConsecutiveComments: true
comma-dangle:
- error
- never
comma-spacing: comma-spacing:
- error - error
- before: false - before: false
@ -283,6 +314,12 @@ rules:
- self - self
eol-last: eol-last:
- error - error
func-call-spacing:
- error
- never
func-name-matching:
- error
- always
func-names: func-names:
- error - error
- never - never
@ -291,6 +328,11 @@ rules:
- expression - expression
id-blacklist: id-blacklist:
- error - error
id-length:
- error
- min: 2
exceptions:
- "_"
indent: indent:
- error - error
- 2 - 2
@ -304,6 +346,12 @@ rules:
- error - error
- before: true - before: true
after: true after: true
line-comment-position:
- error
- position: above
linebreak-style:
- error
- unix
lines-around-comment: lines-around-comment:
- error - error
- beforeBlockComment: true - beforeBlockComment: true
@ -316,6 +364,9 @@ rules:
allowObjectEnd: false allowObjectEnd: false
allowArrayStart: true allowArrayStart: true
allowArrayEnd: false allowArrayEnd: false
lines-around-directive:
- error
- always
max-len: max-len:
- error - error
- code: 130 - code: 130
@ -323,13 +374,20 @@ rules:
ignoreComments: false ignoreComments: false
ignoreTrailingComments: false ignoreTrailingComments: false
ignoreUrls: true ignoreUrls: true
max-params:
- off
max-statements-per-line: max-statements-per-line:
- error - error
- max: 1 - max: 1
multiline-ternary:
- error
- never
new-cap: new-cap:
- error - error
new-parens: new-parens:
- error - error
newline-per-chained-call:
- off
no-array-constructor: no-array-constructor:
- error - error
no-bitwise: no-bitwise:
@ -338,10 +396,14 @@ rules:
- error - error
no-inline-comments: no-inline-comments:
- error - error
no-lonely-if:
- error
no-mixed-operators: no-mixed-operators:
- error - error
no-mixed-spaces-and-tabs: no-mixed-spaces-and-tabs:
- error - error
no-multi-assign:
- error
no-multiple-empty-lines: no-multiple-empty-lines:
- error - error
- max: 1 - max: 1
@ -355,8 +417,14 @@ rules:
- error - error
no-plusplus: no-plusplus:
- error - error
no-restricted-syntax:
- error
- WithStatement
- ForInStatement
no-spaced-func: no-spaced-func:
- error - error
no-tabs:
- error
no-trailing-spaces: no-trailing-spaces:
- error - error
no-underscore-dangle: no-underscore-dangle:
@ -374,6 +442,9 @@ rules:
- always - always
object-property-newline: object-property-newline:
- error - error
one-var-declaration-per-line:
- error
- always
one-var: one-var:
- error - error
- never - never
@ -383,6 +454,9 @@ rules:
operator-linebreak: operator-linebreak:
- error - error
- before - before
padded-blocks:
- error
- classes: always
quote-props: quote-props:
- error - error
- as-needed - as-needed
@ -395,6 +469,7 @@ rules:
FunctionDeclaration: true FunctionDeclaration: true
ClassDeclaration: true ClassDeclaration: true
MethodDefinition: true MethodDefinition: true
ArrowFunctionExpression: true
semi: semi:
- error - error
- always - always
@ -412,9 +487,16 @@ rules:
- never - never
space-infix-ops: space-infix-ops:
- error - error
space-unary-ops:
- error
- words: true
nonwords: false
spaced-comment: spaced-comment:
- error - error
- always - always
template-tag-spacing:
- error
- always
unicode-bom: unicode-bom:
- error - error
@ -458,12 +540,24 @@ rules:
- error - error
no-var: no-var:
- error - error
object-shorthand:
- error
- always
prefer-const: prefer-const:
- error - error
prefer-reflect: prefer-reflect:
- error - error
prefer-spread: prefer-spread:
- error - error
prefer-numeric-literals:
- error
prefer-rest-params:
- error
prefer-template:
- error
prefer-arrow-callback:
- error
- allowNamedFunctions: false
require-yield: require-yield:
- error - error
rest-spread-spacing: rest-spread-spacing:
@ -471,6 +565,8 @@ rules:
template-curly-spacing: template-curly-spacing:
- error - error
- never - never
symbol-description:
- error
yield-star-spacing: yield-star-spacing:
- error - error
- before: true - before: true

View File

@ -47,7 +47,8 @@ exports.getBooleanArgumentForm = (argumentName, value) => {
return '--no-'; return '--no-';
} }
if (_.size(argumentName) === 1) { const SHORT_OPTION_LENGTH = 1;
if (_.size(argumentName) === SHORT_OPTION_LENGTH) {
return '-'; return '-';
} }

View File

@ -61,7 +61,7 @@ exports.write = (image, drive, options) => {
const argv = cli.getArguments({ const argv = cli.getArguments({
entryPoint: rendererUtils.getApplicationEntryPoint(), entryPoint: rendererUtils.getApplicationEntryPoint(),
image: image, image,
device: drive.device, device: drive.device,
validateWriteOnSuccess: options.validateWriteOnSuccess, validateWriteOnSuccess: options.validateWriteOnSuccess,
unmountOnSuccess: options.unmountOnSuccess unmountOnSuccess: options.unmountOnSuccess
@ -77,6 +77,14 @@ exports.write = (image, drive, options) => {
ipc.config.silent = true; ipc.config.silent = true;
ipc.serve(); ipc.serve();
/**
* @summary Safely terminate the IPC server
* @function
* @private
*
* @example
* terminateServer();
*/
const terminateServer = () => { const terminateServer = () => {
// Turns out we need to destroy all sockets for // Turns out we need to destroy all sockets for
@ -90,6 +98,16 @@ exports.write = (image, drive, options) => {
ipc.server.stop(); ipc.server.stop();
}; };
/**
* @summary Emit an error to the client
* @function
* @private
*
* @param {Error} error - error
*
* @example
* emitError(new Error('foo bar'));
*/
const emitError = (error) => { const emitError = (error) => {
terminateServer(); terminateServer();
emitter.emit('error', error); emitter.emit('error', error);
@ -97,7 +115,7 @@ exports.write = (image, drive, options) => {
ipc.server.on('error', emitError); ipc.server.on('error', emitError);
ipc.server.on('message', (data) => { ipc.server.on('message', (data) => {
let message; let message = null;
try { try {
message = robot.parseMessage(data); message = robot.parseMessage(data);
@ -112,7 +130,7 @@ exports.write = (image, drive, options) => {
return emitError(robot.recomposeErrorMessage(message)); return emitError(robot.recomposeErrorMessage(message));
} }
emitter.emit(robot.getCommand(message), robot.getData(message)); return emitter.emit(robot.getCommand(message), robot.getData(message));
}); });
ipc.server.on('start', () => { ipc.server.on('start', () => {
@ -144,9 +162,13 @@ exports.write = (image, drive, options) => {
}); });
} }
if (code !== EXIT_CODES.SUCCESS && code !== EXIT_CODES.VALIDATION_ERROR) { // We shouldn't emit the `done` event manually here
return emitError(new Error(`Child process exited with error code: ${code}`)); // since the writer process will take care of it.
if (code === EXIT_CODES.SUCCESS || code === EXIT_CODES.VALIDATION_ERROR) {
return null;
} }
return emitError(new Error(`Child process exited with error code: ${code}`));
}); });
}); });

View File

@ -40,9 +40,12 @@ exports.getApplicationEntryPoint = () => {
return path.join(process.resourcesPath, 'app.asar'); return path.join(process.resourcesPath, 'app.asar');
} }
const ENTRY_POINT_ARGV_INDEX = 1;
const relativeEntryPoint = electron.remote.process.argv[ENTRY_POINT_ARGV_INDEX];
// On GNU/Linux, `pkexec` resolves relative paths // On GNU/Linux, `pkexec` resolves relative paths
// from `/root`, therefore we pass an absolute path, // from `/root`, therefore we pass an absolute path,
// in order to be on the safe side. // in order to be on the safe side.
return path.join(CONSTANTS.PROJECT_ROOT, electron.remote.process.argv[1]); return path.join(CONSTANTS.PROJECT_ROOT, relativeEntryPoint);
}; };

View File

@ -37,8 +37,32 @@ const packageJSON = require('../../package.json');
// and `stderr` to the parent process using IPC communication, // and `stderr` to the parent process using IPC communication,
// taking care of the writer elevation as needed. // taking care of the writer elevation as needed.
const EXECUTABLE = process.argv[0]; /**
const ETCHER_ARGUMENTS = process.argv.slice(2); * @summary The Etcher executable file path
* @constant
* @private
* @type {String}
*/
const executable = _.first(process.argv);
/**
* @summary The first index that represents an actual option argument
* @constant
* @private
* @type {Number}
*
* @description
* The first arguments are usually the program executable itself, etc.
*/
const OPTIONS_INDEX_START = 2;
/**
* @summary The list of Etcher argument options
* @constant
* @private
* @type {String[]}
*/
const etcherArguments = process.argv.slice(OPTIONS_INDEX_START);
return isElevated().then((elevated) => { return isElevated().then((elevated) => {
@ -153,7 +177,7 @@ return isElevated().then((elevated) => {
ipc.of[process.env.IPC_SERVER_ID].on('error', reject); ipc.of[process.env.IPC_SERVER_ID].on('error', reject);
ipc.of[process.env.IPC_SERVER_ID].on('connect', () => { ipc.of[process.env.IPC_SERVER_ID].on('connect', () => {
const child = childProcess.spawn(EXECUTABLE, ETCHER_ARGUMENTS, { const child = childProcess.spawn(executable, etcherArguments, {
env: { env: {
// The CLI might call operating system utilities (like `diskutil`), // The CLI might call operating system utilities (like `diskutil`),
@ -169,6 +193,18 @@ return isElevated().then((elevated) => {
child.on('error', reject); child.on('error', reject);
child.on('close', resolve); child.on('close', resolve);
/**
* @summary Emit an object message to the IPC server
* @function
* @private
*
* @param {Buffer} data - json message data
*
* @example
* emitMessage(Buffer.from(JSON.stringify({
* foo: 'bar'
* })));
*/
const emitMessage = (data) => { const emitMessage = (data) => {
// Output from stdout/stderr coming from the CLI might be buffered, // Output from stdout/stderr coming from the CLI might be buffered,

View File

@ -108,7 +108,7 @@ exports.getErrorMessage = (error) => {
}); });
if (error.description) { if (error.description) {
return message + '\n\n' + error.description; return `${message}\n\n${error.description}`;
} }
return message; return message;

View File

@ -29,6 +29,9 @@ const robot = require('../shared/robot');
const messages = require('../shared/messages'); const messages = require('../shared/messages');
const EXIT_CODES = require('../shared/exit-codes'); const EXIT_CODES = require('../shared/exit-codes');
const ARGV_IMAGE_PATH_INDEX = 0;
const imagePath = options._[ARGV_IMAGE_PATH_INDEX];
isElevated().then((elevated) => { isElevated().then((elevated) => {
if (!elevated) { if (!elevated) {
throw new Error(messages.error.elevationRequired()); throw new Error(messages.error.elevationRequired());
@ -50,10 +53,10 @@ isElevated().then((elevated) => {
override: { override: {
drive: options.drive, drive: options.drive,
// If `options.yes` is `false`, pass `undefined`, // If `options.yes` is `false`, pass `null`,
// otherwise the question will not be asked because // otherwise the question will not be asked because
// `false` is a defined value. // `false` is a defined value.
yes: robot.isEnabled(process.env) || options.yes || undefined yes: robot.isEnabled(process.env) || options.yes || null
} }
}); });
@ -76,7 +79,7 @@ isElevated().then((elevated) => {
throw new Error(`Drive not found: ${answers.drive}`); throw new Error(`Drive not found: ${answers.drive}`);
} }
return writer.writeImage(options._[0], selectedDrive, { return writer.writeImage(imagePath, selectedDrive, {
unmountOnSuccess: options.unmount, unmountOnSuccess: options.unmount,
validateWriteOnSuccess: options.check validateWriteOnSuccess: options.check
}, (state) => { }, (state) => {
@ -109,6 +112,7 @@ isElevated().then((elevated) => {
console.log(`Checksum: ${results.sourceChecksum}`); console.log(`Checksum: ${results.sourceChecksum}`);
} }
return Bluebird.resolve();
}).then(() => { }).then(() => {
process.exit(EXIT_CODES.SUCCESS); process.exit(EXIT_CODES.SUCCESS);
}); });
@ -121,6 +125,7 @@ isElevated().then((elevated) => {
} }
errors.print(error); errors.print(error);
return Bluebird.resolve();
}).then(() => { }).then(() => {
if (error.code === 'EVALIDATION') { if (error.code === 'EVALIDATION') {
process.exit(EXIT_CODES.VALIDATION_ERROR); process.exit(EXIT_CODES.VALIDATION_ERROR);

View File

@ -24,6 +24,33 @@ const robot = require('../shared/robot');
const EXIT_CODES = require('../shared/exit-codes'); const EXIT_CODES = require('../shared/exit-codes');
const packageJSON = require('../../package.json'); const packageJSON = require('../../package.json');
/**
* @summary The minimum required number of CLI arguments
* @constant
* @private
* @type {Number}
*/
const MINIMUM_NUMBER_OF_ARGUMENTS = 1;
/**
* @summary The index of the image argument
* @constant
* @private
* @type {Number}
*/
const IMAGE_PATH_ARGV_INDEX = 0;
/**
* @summary The first index that represents an actual option argument
* @constant
* @private
* @type {Number}
*
* @description
* The first arguments are usually the program executable itself, etc.
*/
const OPTIONS_INDEX_START = 2;
/** /**
* @summary Parsed CLI options and arguments * @summary Parsed CLI options and arguments
* @type {Object} * @type {Object}
@ -34,7 +61,7 @@ module.exports = yargs
// Don't wrap at all // Don't wrap at all
.wrap(null) .wrap(null)
.demand(1, 'Missing image') .demand(MINIMUM_NUMBER_OF_ARGUMENTS, 'Missing image')
// Usage help // Usage help
.usage('Usage: $0 [options] <image>') .usage('Usage: $0 [options] <image>')
@ -42,7 +69,7 @@ module.exports = yargs
'Exit codes:', 'Exit codes:',
_.map(EXIT_CODES, (value, key) => { _.map(EXIT_CODES, (value, key) => {
const reason = _.map(_.split(key, '_'), _.capitalize).join(' '); const reason = _.map(_.split(key, '_'), _.capitalize).join(' ');
return ' ' + value + ' - ' + reason; return ` ${value} - ${reason}`;
}).join('\n'), }).join('\n'),
'', '',
'If you need help, don\'t hesitate in contacting us at:', 'If you need help, don\'t hesitate in contacting us at:',
@ -64,7 +91,7 @@ module.exports = yargs
.version(_.constant(packageJSON.version)) .version(_.constant(packageJSON.version))
// Error reporting // Error reporting
.fail(function(message, error) { .fail((message, error) => {
if (robot.isEnabled(process.env)) { if (robot.isEnabled(process.env)) {
robot.printError(error || message); robot.printError(error || message);
} else { } else {
@ -72,12 +99,12 @@ module.exports = yargs
errors.print(error || message); errors.print(error || message);
} }
process.exit(1); process.exit(EXIT_CODES.GENERAL_ERROR);
}) })
// Assert that image exists // Assert that image exists
.check((argv) => { .check((argv) => {
fs.accessSync(argv._[0]); fs.accessSync(argv._[IMAGE_PATH_ARGV_INDEX]);
return true; return true;
}) })
@ -123,4 +150,4 @@ module.exports = yargs
default: true default: true
} }
}) })
.parse(process.argv.slice(2)); .parse(process.argv.slice(OPTIONS_INDEX_START));

View File

@ -59,7 +59,7 @@ exports.writeImage = (imagePath, drive, options, onProgress) => {
// Unmounting a drive in Windows means we can't write to it anymore // Unmounting a drive in Windows means we can't write to it anymore
if (os.platform() === 'win32') { if (os.platform() === 'win32') {
return; return Bluebird.resolve();
} }
return unmount.unmountDrive(drive); return unmount.unmountDrive(drive);
@ -96,7 +96,7 @@ exports.writeImage = (imagePath, drive, options, onProgress) => {
return fs.closeAsync(driveFileDescriptor).then(() => { return fs.closeAsync(driveFileDescriptor).then(() => {
if (!options.unmountOnSuccess) { if (!options.unmountOnSuccess) {
return; return Bluebird.resolve();
} }
return unmount.unmountDrive(drive); return unmount.unmountDrive(drive);

View File

@ -24,6 +24,7 @@
var angular = require('angular'); var angular = require('angular');
const electron = require('electron'); const electron = require('electron');
const Bluebird = require('bluebird');
const EXIT_CODES = require('../shared/exit-codes'); const EXIT_CODES = require('../shared/exit-codes');
const messages = require('../shared/messages'); const messages = require('../shared/messages');
@ -98,6 +99,8 @@ app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateMo
AnalyticsService.logEvent('Notifying update'); AnalyticsService.logEvent('Notifying update');
return UpdateNotifierService.notify(); return UpdateNotifierService.notify();
} }
return Bluebird.resolve();
}).catch(ErrorService.reportException); }).catch(ErrorService.reportException);
} }
@ -158,7 +161,7 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe
// Don't open any more popups // Don't open any more popups
popupExists = true; popupExists = true;
return OSDialogService.showWarning({ OSDialogService.showWarning({
confirmationLabel: 'Yes, quit', confirmationLabel: 'Yes, quit',
rejectionLabel: 'Cancel', rejectionLabel: 'Cancel',
title: 'Are you sure you want to close Etcher?', title: 'Are you sure you want to close Etcher?',

View File

@ -82,7 +82,7 @@ module.exports = function(
description: [ description: [
messages.warning.unrecommendedDriveSize({ messages.warning.unrecommendedDriveSize({
image: SelectionStateModel.getImage(), image: SelectionStateModel.getImage(),
drive: drive drive
}), }),
'Are you sure you want to continue?' 'Are you sure you want to continue?'
].join(' ') ].join(' ')

View File

@ -59,18 +59,12 @@ module.exports = function($uibModal, $q) {
.then(resolve) .then(resolve)
.catch((error) => { .catch((error) => {
// Bootstrap doesn't 'resolve' these but cancels the dialog; // Bootstrap doesn't 'resolve' these but cancels the dialog
// therefore call 'resolve' here applied to 'false'.
if (error === 'escape key press' || error === 'backdrop click') { if (error === 'escape key press' || error === 'backdrop click') {
resolve(); return resolve();
// For some annoying reason, UI Bootstrap Modal rejects
// the result reason if the user clicks on the backdrop
// (e.g: the area surrounding the modal).
} else if (error !== 'backdrop click') {
return reject(error);
} }
return reject(error);
}); });
}) })
}; };

View File

@ -16,7 +16,7 @@
'use strict'; 'use strict';
module.exports = function($uibModalInstance, SettingsModel, options) { module.exports = function($uibModalInstance, SettingsModel, UPDATE_NOTIFIER_SLEEP_DAYS, options) {
// We update this value in this controller since its the only place // We update this value in this controller since its the only place
// where we can be sure the modal was really presented to the user. // where we can be sure the modal was really presented to the user.
@ -25,6 +25,14 @@ module.exports = function($uibModalInstance, SettingsModel, options) {
// have been called, but the modal could have failed to be shown. // have been called, but the modal could have failed to be shown.
SettingsModel.set('lastUpdateNotify', Date.now()); SettingsModel.set('lastUpdateNotify', Date.now());
/**
* @summary The number of days the update notified can be put to sleep
* @constant
* @public
* @type {Number}
*/
this.sleepDays = UPDATE_NOTIFIER_SLEEP_DAYS;
/** /**
* @summary Settings model * @summary Settings model
* @type {Object} * @type {Object}

View File

@ -19,8 +19,9 @@
const _ = require('lodash'); const _ = require('lodash');
const semver = require('semver'); const semver = require('semver');
const etcherLatestVersion = require('etcher-latest-version'); const etcherLatestVersion = require('etcher-latest-version');
const units = require('../../../../shared/units');
module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, ManifestBindService, SettingsModel) { module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_DAYS, ManifestBindService, SettingsModel) {
/** /**
* @summary The current application version * @summary The current application version
@ -57,10 +58,12 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M
}, (error, latestVersion) => { }, (error, latestVersion) => {
if (error) { if (error) {
// The error status equals -1 if the request couldn't // The error status equals this number if the request
// be made successfully, for example, because of a // couldn't be made successfuly, for example, because
// timeout on an unstable network connection. // of a timeout on an unstable network connection.
if (error.status === -1) { const ERROR_CODE_UNSUCCESSFUL_REQUEST = -1;
if (error.status === ERROR_CODE_UNSUCCESSFUL_REQUEST) {
return resolve(CURRENT_VERSION); return resolve(CURRENT_VERSION);
} }
@ -114,7 +117,7 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M
return true; return true;
} }
if (lastUpdateNotify - Date.now() > UPDATE_NOTIFIER_SLEEP_TIME) { if (lastUpdateNotify - Date.now() > units.daysToMilliseconds(UPDATE_NOTIFIER_SLEEP_DAYS)) {
SettingsModel.set('sleepUpdateCheck', false); SettingsModel.set('sleepUpdateCheck', false);
return true; return true;
} }
@ -140,7 +143,7 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M
size: 'update-notifier', size: 'update-notifier',
resolve: { resolve: {
options: _.constant({ options: _.constant({
version: version version
}) })
} }
}).result; }).result;

View File

@ -11,7 +11,7 @@
<input type="checkbox" <input type="checkbox"
ng-model="modal.sleepUpdateCheck" ng-model="modal.sleepUpdateCheck"
ng-change="modal.settings.set('sleepUpdateCheck', modal.sleepUpdateCheck)"> ng-change="modal.settings.set('sleepUpdateCheck', modal.sleepUpdateCheck)">
<span>Remind me again in 7 days</span> <span>Remind me again in {{::modal.sleepDays}} days</span>
</label> </label>
</div> </div>
</div> </div>

View File

@ -29,7 +29,15 @@ const UpdateNotifier = angular.module(MODULE_NAME, [
require('../../os/open-external/open-external') require('../../os/open-external/open-external')
]); ]);
UpdateNotifier.constant('UPDATE_NOTIFIER_SLEEP_TIME', 7 * 24 * 60 * 60 * 100); /**
* @summary The number of days the update notifier can be put to sleep
* @constant
* @private
* @type {Number}
*/
const UPDATE_NOTIFIER_SLEEP_DAYS = 7;
UpdateNotifier.constant('UPDATE_NOTIFIER_SLEEP_DAYS', UPDATE_NOTIFIER_SLEEP_DAYS);
UpdateNotifier.controller('UpdateNotifierController', require('./controllers/update-notifier')); UpdateNotifier.controller('UpdateNotifierController', require('./controllers/update-notifier'));
UpdateNotifier.service('UpdateNotifierService', require('./services/update-notifier')); UpdateNotifier.service('UpdateNotifierService', require('./services/update-notifier'));

View File

@ -41,12 +41,13 @@ electron.app.on('ready', () => {
// Prevent flash of white when starting the application // Prevent flash of white when starting the application
// https://github.com/atom/electron/issues/2172 // https://github.com/atom/electron/issues/2172
mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.on('did-finish-load', () => {
const WEBVIEW_LOAD_TIMEOUT_MS = 100;
// The flash of white is still present for a very short // The flash of white is still present for a very short
// while after the WebView reports it finished loading // while after the WebView reports it finished loading
setTimeout(() => { setTimeout(() => {
mainWindow.show(); mainWindow.show();
}, 100); }, WEBVIEW_LOAD_TIMEOUT_MS);
}); });

View File

@ -64,19 +64,36 @@ Drives.service('DrivesModel', function() {
}); });
}; };
// This workaround is needed to avoid AngularJS from getting /**
// caught in an infinite digest loop when using `ngRepeat` * @summary Memoize ImmutableJS list reference
// over a function that returns a mutable version of an * @function
// ImmutableJS object. * @private
// *
// The problem is that every time you call `myImmutableObject.toJS()` * @description
// you will get a new object, whose reference is different from * This workaround is needed to avoid AngularJS from getting
// the one you previously got, even if the data is exactly the same. * caught in an infinite digest loop when using `ngRepeat`
* over a function that returns a mutable version of an
* ImmutableJS object.
*
* The problem is that every time you call `myImmutableObject.toJS()`
* you will get a new object, whose reference is different from
* the one you previously got, even if the data is exactly the same.
*
* @param {Function} func - function that returns an ImmutableJS list
* @returns {Function} memoized function
*
* @example
* const getList = () => {
* return Store.getState().toJS().myList;
* };
*
* const memoizedFunction = memoizeImmutableListReference(getList);
*/
const memoizeImmutableListReference = (func) => { const memoizeImmutableListReference = (func) => {
let previous = []; let previous = [];
return () => { return (...args) => {
const list = Reflect.apply(func, this, arguments); const list = Reflect.apply(func, this, args);
if (!_.isEqual(list, previous)) { if (!_.isEqual(list, previous)) {
previous = list; previous = list;

View File

@ -134,9 +134,12 @@ FlashState.service('FlashStateModel', function() {
if (_.isNumber(state.speed) && !_.isNaN(state.speed)) { if (_.isNumber(state.speed) && !_.isNaN(state.speed)) {
// Preserve only two decimal places // Preserve only two decimal places
return Math.floor(units.bytesToMegabytes(state.speed) * 100) / 100; const PRECISION = 2;
return _.round(units.bytesToMegabytes(state.speed), PRECISION);
} }
return null;
}) })
} }
}); });

View File

@ -286,9 +286,7 @@ SelectionStateModel.service('SelectionStateModel', function(DrivesModel) {
* @example * @example
* SelectionStateModel.clear({ preserveImage: true }); * SelectionStateModel.clear({ preserveImage: true });
*/ */
this.clear = (options) => { this.clear = (options = {}) => {
options = options || {};
if (!options.preserveImage) { if (!options.preserveImage) {
Store.dispatch({ Store.dispatch({
type: Store.Actions.REMOVE_IMAGE type: Store.Actions.REMOVE_IMAGE

View File

@ -75,9 +75,21 @@ const ACTIONS = _.fromPairs(_.map([
return [ message, message ]; return [ message, message ];
})); }));
const storeReducer = (state, action) => { /**
state = state || DEFAULT_STATE; * @summary The redux store reducer
* @function
* @private
*
* @param {Object} state - application state
* @param {Object} action - dispatched action
* @returns {Object} new application state
*
* @example
* const newState = storeReducer(DEFAULT_STATE, {
* type: ACTIONS.REMOVE_DRIVE
* });
*/
const storeReducer = (state = DEFAULT_STATE, action) => {
switch (action.type) { switch (action.type) {
case ACTIONS.SET_AVAILABLE_DRIVES: { case ACTIONS.SET_AVAILABLE_DRIVES: {
@ -91,7 +103,9 @@ const storeReducer = (state, action) => {
const newState = state.set('availableDrives', Immutable.fromJS(action.data)); const newState = state.set('availableDrives', Immutable.fromJS(action.data));
if (action.data.length === 1) { const AUTOSELECT_DRIVE_COUNT = 1;
const numberOfDrives = action.data.length;
if (numberOfDrives === AUTOSELECT_DRIVE_COUNT) {
const drive = _.first(action.data); const drive = _.first(action.data);
@ -150,7 +164,7 @@ const storeReducer = (state, action) => {
throw new Error(`Invalid state percentage: ${action.data.percentage}`); throw new Error(`Invalid state percentage: ${action.data.percentage}`);
} }
if (!action.data.eta && action.data.eta !== 0) { if (_.isNil(action.data.eta)) {
throw new Error('Missing state eta'); throw new Error('Missing state eta');
} }
@ -352,9 +366,9 @@ module.exports = _.merge(redux.createStore(
// In the first run, there will be no information // In the first run, there will be no information
// to deserialize. In this case, we avoid merging, // to deserialize. In this case, we avoid merging,
// otherwise we will be basically erasing the property // otherwise we will be basically erasing the property
// we aim the keep serialising the in future. // we aim to keep serialising the in future.
if (!subset) { if (!subset) {
return; return state;
} }
// Blindly setting the state to the deserialised subset // Blindly setting the state to the deserialised subset

View File

@ -115,7 +115,7 @@ SupportedFormats.service('SupportedFormatsModel', function() {
* } * }
*/ */
this.isSupportedImage = (imagePath) => { this.isSupportedImage = (imagePath) => {
const extension = path.extname(imagePath).slice(1).toLowerCase(); const extension = _.replace(path.extname(imagePath), '.', '').toLowerCase();
if (_.some([ if (_.some([
_.includes(this.getNonCompressedExtensions(), extension), _.includes(this.getNonCompressedExtensions(), extension),

View File

@ -93,13 +93,13 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
* AnalyticsService.log('Hello World'); * AnalyticsService.log('Hello World');
*/ */
this.logDebug = (message) => { this.logDebug = (message) => {
message = new Date() + ' ' + message; const debugMessage = `${new Date()} ${message}`;
if (SettingsModel.get('errorReporting') && isRunningInAsar()) { if (SettingsModel.get('errorReporting') && isRunningInAsar()) {
$window.trackJs.console.debug(message); $window.trackJs.console.debug(debugMessage);
} }
$log.debug(message); $log.debug(debugMessage);
}; };
/** /**
@ -119,7 +119,6 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
* }); * });
*/ */
this.logEvent = (message, data) => { this.logEvent = (message, data) => {
if (SettingsModel.get('errorReporting') && isRunningInAsar()) { if (SettingsModel.get('errorReporting') && isRunningInAsar()) {
// Clone data before passing it to `mixpanel.track` // Clone data before passing it to `mixpanel.track`
@ -129,11 +128,15 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
} }
const debugMessage = _.attempt(() => {
if (data) { if (data) {
message += ` (${JSON.stringify(data)})`; return `${message} (${JSON.stringify(data)})`;
} }
this.logDebug(message); return message;
});
this.logDebug(debugMessage);
}; };
/** /**

View File

@ -34,16 +34,20 @@ const driveScanner = angular.module(MODULE_NAME, [
driveScanner.factory('DriveScannerService', (SettingsModel) => { driveScanner.factory('DriveScannerService', (SettingsModel) => {
const DRIVE_SCANNER_INTERVAL_MS = 2000; const DRIVE_SCANNER_INTERVAL_MS = 2000;
const DRIVE_SCANNER_FIRST_SCAN_DELAY_MS = 0;
const emitter = new EventEmitter(); const emitter = new EventEmitter();
const availableDrives = Rx.Observable.timer(0, DRIVE_SCANNER_INTERVAL_MS) const availableDrives = Rx.Observable.timer(
DRIVE_SCANNER_FIRST_SCAN_DELAY_MS,
DRIVE_SCANNER_INTERVAL_MS
)
.flatMap(() => { .flatMap(() => {
return Rx.Observable.fromNodeCallback(drivelist.list)(); return Rx.Observable.fromNodeCallback(drivelist.list)();
}) })
.map((drives) => {
// Calculate an appropriate "display name" // Build human friendly "description"
drives = _.map(drives, (drive) => { .map((drives) => {
return _.map(drives, (drive) => {
drive.name = drive.device; drive.name = drive.device;
if (os.platform() === 'win32' && !_.isEmpty(drive.mountpoints)) { if (os.platform() === 'win32' && !_.isEmpty(drive.mountpoints)) {
@ -52,7 +56,9 @@ driveScanner.factory('DriveScannerService', (SettingsModel) => {
return drive; return drive;
}); });
})
.map((drives) => {
if (SettingsModel.get('unsafeMode')) { if (SettingsModel.get('unsafeMode')) {
return drives; return drives;
} }

View File

@ -78,7 +78,7 @@ module.exports = function($q, SupportedFormatsModel) {
return resolve(); return resolve();
} }
imageStream.getImageMetadata(imagePath).then((metadata) => { return imageStream.getImageMetadata(imagePath).then((metadata) => {
metadata.path = imagePath; metadata.path = imagePath;
metadata.size = metadata.size.final.value; metadata.size = metadata.size.final.value;
return resolve(metadata); return resolve(metadata);
@ -95,8 +95,8 @@ module.exports = function($q, SupportedFormatsModel) {
* @param {Object} options - options * @param {Object} options - options
* @param {String} options.title - dialog title * @param {String} options.title - dialog title
* @param {String} options.description - dialog description * @param {String} options.description - dialog description
* @param {String} options.confirmationLabel - confirmation label * @param {String} [options.confirmationLabel="OK"] - confirmation label
* @param {String} options.rejectionLabel - rejection label * @param {String} [options.rejectionLabel="Cancel"] - rejection label
* @fulfil {Boolean} - whether the dialog was confirmed or not * @fulfil {Boolean} - whether the dialog was confirmed or not
* @returns {Promise}; * @returns {Promise};
* *
@ -113,20 +113,30 @@ module.exports = function($q, SupportedFormatsModel) {
* }); * });
*/ */
this.showWarning = (options) => { this.showWarning = (options) => {
_.defaults(options, {
confirmationLabel: 'OK',
rejectionLabel: 'Cancel'
});
const BUTTONS = [
options.confirmationLabel,
options.rejectionLabel
];
const BUTTON_CONFIRMATION_INDEX = _.indexOf(BUTTONS, options.confirmationLabel);
const BUTTON_REJECTION_INDEX = _.indexOf(BUTTONS, options.rejectionLabel);
return $q((resolve) => { return $q((resolve) => {
electron.remote.dialog.showMessageBox(currentWindow, { electron.remote.dialog.showMessageBox(currentWindow, {
type: 'warning', type: 'warning',
buttons: [ buttons: BUTTONS,
options.confirmationLabel, defaultId: BUTTON_REJECTION_INDEX,
options.rejectionLabel cancelId: BUTTON_REJECTION_INDEX,
],
defaultId: 1,
cancelId: 1,
title: 'Attention', title: 'Attention',
message: options.title, message: options.title,
detail: options.description detail: options.description
}, (response) => { }, (response) => {
return resolve(response === 0); return resolve(response === BUTTON_CONFIRMATION_INDEX);
}); });
}); });
}; };
@ -149,19 +159,19 @@ module.exports = function($q, SupportedFormatsModel) {
* OSDialogService.showError('Foo Bar', 'An error happened!'); * OSDialogService.showError('Foo Bar', 'An error happened!');
*/ */
this.showError = (error, description) => { this.showError = (error, description) => {
error = error || {}; const errorObject = error || {};
// Try to get as most information as possible about the error // Try to get as most information as possible about the error
// rather than falling back to generic messages right away. // rather than falling back to generic messages right away.
const title = _.attempt(() => { const title = _.attempt(() => {
if (_.isString(error)) { if (_.isString(errorObject)) {
return error; return errorObject;
} }
return error.message || error.code || 'An error ocurred'; return errorObject.message || errorObject.code || 'An error ocurred';
}); });
const message = description || error.stack || JSON.stringify(error) || ''; const message = description || errorObject.stack || JSON.stringify(errorObject) || '';
// Ensure the parameters are strings to prevent the following // Ensure the parameters are strings to prevent the following
// types of obscure errors: // types of obscure errors:

View File

@ -40,8 +40,8 @@ module.exports = ($timeout) => {
scope: { scope: {
osDropzone: '&' osDropzone: '&'
}, },
link: (scope, element) => { link: (scope, $element) => {
const domElement = element[0]; const domElement = _.first($element);
// See https://github.com/electron/electron/blob/master/docs/api/file-object.md // See https://github.com/electron/electron/blob/master/docs/api/file-object.md
@ -52,7 +52,7 @@ module.exports = ($timeout) => {
domElement.ondrop = (event) => { domElement.ondrop = (event) => {
event.preventDefault(); event.preventDefault();
const filename = event.dataTransfer.files[0].path; const filename = _.first(event.dataTransfer.files).path;
// Safely bring this to the word of Angular // Safely bring this to the word of Angular
$timeout(() => { $timeout(() => {

View File

@ -48,7 +48,7 @@ module.exports = function() {
} }
return new Notification(title, { return new Notification(title, {
body: body body
}); });
}; };

View File

@ -28,7 +28,7 @@ const OSOpenExternal = angular.module(MODULE_NAME, []);
OSOpenExternal.service('OSOpenExternalService', require('./services/open-external')); OSOpenExternal.service('OSOpenExternalService', require('./services/open-external'));
OSOpenExternal.directive('osOpenExternal', require('./directives/open-external')); OSOpenExternal.directive('osOpenExternal', require('./directives/open-external'));
OSOpenExternal.run(function(OSOpenExternalService) { OSOpenExternal.run((OSOpenExternalService) => {
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {
const target = event.target; const target = event.target;
if (target.tagName === 'A' && angular.isDefined(target.href)) { if (target.tagName === 'A' && angular.isDefined(target.href)) {

View File

@ -26,12 +26,9 @@ module.exports = function() {
* @protected * @protected
* *
* @description * @description
* Since electron only has one renderer view, we can assume the
* current window is the one with id == 1.
*
* We expose this property to `this` for testability purposes. * We expose this property to `this` for testability purposes.
*/ */
this.currentWindow = electron.remote.BrowserWindow.fromId(1); this.currentWindow = electron.remote.getCurrentWindow();
/** /**
* @summary Set operating system window progress * @summary Set operating system window progress
@ -47,11 +44,14 @@ module.exports = function() {
* OSWindowProgressService.set(85); * OSWindowProgressService.set(85);
*/ */
this.set = (percentage) => { this.set = (percentage) => {
if (percentage > 100 || percentage < 0) { const PERCENTAGE_MINIMUM = 0;
const PERCENTAGE_MAXIMUM = 100;
if (percentage > PERCENTAGE_MAXIMUM || percentage < PERCENTAGE_MINIMUM) {
throw new Error(`Invalid window progress percentage: ${percentage}`); throw new Error(`Invalid window progress percentage: ${percentage}`);
} }
this.currentWindow.setProgressBar(percentage / 100); this.currentWindow.setProgressBar(percentage / PERCENTAGE_MAXIMUM);
}; };
/** /**
@ -65,8 +65,9 @@ module.exports = function() {
this.clear = () => { this.clear = () => {
// Passing 0 or null/undefined doesn't work. // Passing 0 or null/undefined doesn't work.
this.currentWindow.setProgressBar(-1); const ELECTRON_PROGRESS_BAR_RESET_VALUE = -1;
this.currentWindow.setProgressBar(ELECTRON_PROGRESS_BAR_RESET_VALUE);
}; };
}; };

View File

@ -58,7 +58,7 @@ module.exports = function(
DriveScannerService.stop(); DriveScannerService.stop();
AnalyticsService.logEvent('Flash', { AnalyticsService.logEvent('Flash', {
image: image, image,
device: drive.device device: drive.device
}); });
@ -106,22 +106,20 @@ module.exports = function(
this.getProgressButtonLabel = () => { this.getProgressButtonLabel = () => {
const flashState = FlashStateModel.getFlashState(); const flashState = FlashStateModel.getFlashState();
const isChecking = flashState.type === 'check'; const isChecking = flashState.type === 'check';
const PERCENTAGE_MINIMUM = 0;
const PERCENTAGE_MAXIMUM = 100;
if (!FlashStateModel.isFlashing()) { if (!FlashStateModel.isFlashing()) {
return 'Flash!'; return 'Flash!';
} } else if (flashState.percentage === PERCENTAGE_MINIMUM && !flashState.speed) {
if (flashState.percentage === 0 && !flashState.speed) {
return 'Starting...'; return 'Starting...';
} else if (flashState.percentage === 100) { } else if (flashState.percentage === PERCENTAGE_MAXIMUM) {
if (isChecking && SettingsModel.get('unmountOnSuccess')) { if (isChecking && SettingsModel.get('unmountOnSuccess')) {
return 'Unmounting...'; return 'Unmounting...';
} }
return 'Finishing...'; return 'Finishing...';
} } else if (isChecking) {
if (isChecking) {
return `${flashState.percentage}% Validating...`; return `${flashState.percentage}% Validating...`;
} }

View File

@ -66,7 +66,7 @@ module.exports = function(
this.selectImage = (image) => { this.selectImage = (image) => {
if (!SupportedFormatsModel.isSupportedImage(image.path)) { if (!SupportedFormatsModel.isSupportedImage(image.path)) {
OSDialogService.showError('Invalid image', messages.error.invalidImage({ OSDialogService.showError('Invalid image', messages.error.invalidImage({
image: image image
})); }));
AnalyticsService.logEvent('Invalid image', image); AnalyticsService.logEvent('Invalid image', image);
@ -100,9 +100,8 @@ module.exports = function(
image.logo = Boolean(image.logo); image.logo = Boolean(image.logo);
image.bmap = Boolean(image.bmap); image.bmap = Boolean(image.bmap);
AnalyticsService.logEvent('Select image', image); return AnalyticsService.logEvent('Select image', image);
}).catch(ErrorService.reportException); }).catch(ErrorService.reportException);
}; };
/** /**

View File

@ -18,7 +18,7 @@
const os = require('os'); const os = require('os');
module.exports = function(WarningModalService, SettingsModel) { module.exports = function(WarningModalService, SettingsModel, ErrorService) {
/** /**
* @summary Client platform * @summary Client platform
@ -78,12 +78,12 @@ module.exports = function(WarningModalService, SettingsModel) {
// Keep the checkbox unchecked until the user confirms // Keep the checkbox unchecked until the user confirms
this.currentData[name] = false; this.currentData[name] = false;
WarningModalService.display(options).then((userAccepted) => { return WarningModalService.display(options).then((userAccepted) => {
if (userAccepted) { if (userAccepted) {
this.model.set(name, true); this.model.set(name, true);
this.refreshSettings(); this.refreshSettings();
} }
}); }).catch(ErrorService.reportException);
}; };
}; };

View File

@ -25,7 +25,8 @@ const MODULE_NAME = 'Etcher.Pages.Settings';
const SettingsPage = angular.module(MODULE_NAME, [ const SettingsPage = angular.module(MODULE_NAME, [
require('angular-ui-router'), require('angular-ui-router'),
require('../../components/warning-modal/warning-modal'), require('../../components/warning-modal/warning-modal'),
require('../../models/settings') require('../../models/settings'),
require('../../modules/error')
]); ]);
SettingsPage.controller('SettingsController', require('./controllers/settings')); SettingsPage.controller('SettingsController', require('./controllers/settings'));

View File

@ -39,7 +39,7 @@ module.exports = (ManifestBindService) => {
const value = ManifestBindService.get(attributes.manifestBind); const value = ManifestBindService.get(attributes.manifestBind);
if (!value) { if (!value) {
throw new Error('ManifestBind: Unknown property `' + attributes.manifestBind + '`'); throw new Error(`ManifestBind: Unknown property \`${attributes.manifestBind}\``);
} }
element.html(value); element.html(value);

View File

@ -33,7 +33,7 @@ module.exports = () => {
*/ */
return (input) => { return (input) => {
if (!input) { if (!input) {
return; return '';
} }
return path.basename(input); return path.basename(input);

View File

@ -48,9 +48,11 @@ exports.getEntries = (archive) => {
zip.on('error', reject); zip.on('error', reject);
zip.on('ready', () => { zip.on('ready', () => {
const EMPTY_ENTRY_SIZE = 0;
return resolve(_.chain(zip.entries()) return resolve(_.chain(zip.entries())
.omitBy((entry) => { .omitBy((entry) => {
return entry.size === 0; return entry.size === EMPTY_ENTRY_SIZE;
}) })
.map((metadata) => { .map((metadata) => {
return { return {
@ -99,7 +101,7 @@ exports.extractFile = (archive, entries, file) => {
return zipfile.readEntry(); return zipfile.readEntry();
} }
zipfile.openReadStream(entry, (error, readStream) => { return zipfile.openReadStream(entry, (error, readStream) => {
if (error) { if (error) {
return reject(error); return reject(error);
} }

View File

@ -170,11 +170,12 @@ exports.extractImage = (archive, hooks) => {
return hooks.getEntries(archive).then((entries) => { return hooks.getEntries(archive).then((entries) => {
const imageEntries = _.filter(entries, (entry) => { const imageEntries = _.filter(entries, (entry) => {
const extension = path.extname(entry.name).slice(1); const extension = _.replace(path.extname(entry.name), '.', '').toLowerCase();
return _.includes(IMAGE_EXTENSIONS, extension); return _.includes(IMAGE_EXTENSIONS, extension);
}); });
if (imageEntries.length !== 1) { const VALID_NUMBER_OF_IMAGE_ENTRIES = 1;
if (imageEntries.length !== VALID_NUMBER_OF_IMAGE_ENTRIES) {
const error = new Error('Invalid archive image'); const error = new Error('Invalid archive image');
error.description = 'The archive image should contain one and only one top image file.'; error.description = 'The archive image should contain one and only one top image file.';
error.report = false; error.report = false;

View File

@ -33,9 +33,11 @@ const archiveType = require('archive-type');
*/ */
exports.getArchiveMimeType = (file) => { exports.getArchiveMimeType = (file) => {
// archive-type only needs the first 261 bytes // `archive-type` only needs the first 261 bytes
// See https://github.com/kevva/archive-type // See https://github.com/kevva/archive-type
const chunk = readChunk.sync(file, 0, 261); const MAGIC_NUMBER_BUFFER_START = 0;
const MAGIC_NUMBER_BUFFER_END = 261;
const chunk = readChunk.sync(file, MAGIC_NUMBER_BUFFER_START, MAGIC_NUMBER_BUFFER_END);
return _.get(archiveType(chunk), 'mime', 'application/octet-stream'); return _.get(archiveType(chunk), 'mime', 'application/octet-stream');
}; };

View File

@ -19,6 +19,14 @@
const _ = require('lodash'); const _ = require('lodash');
const pathIsInside = require('path-is-inside'); const pathIsInside = require('path-is-inside');
/**
* @summary The default unknown size for things such as images and drives
* @constant
* @private
* @type {Number}
*/
const UNKNOWN_SIZE = 0;
/** /**
* @summary Check if a drive is locked * @summary Check if a drive is locked
* @function * @function
@ -134,7 +142,7 @@ exports.isSourceDrive = (drive, image) => {
* } * }
*/ */
exports.isDriveLargeEnough = (drive, image) => { exports.isDriveLargeEnough = (drive, image) => {
return _.get(drive, 'size', 0) >= _.get(image, 'size', 0); return _.get(drive, 'size', UNKNOWN_SIZE) >= _.get(image, 'size', UNKNOWN_SIZE);
}; };
/** /**
@ -198,5 +206,5 @@ exports.isDriveValid = (drive, image) => {
* } * }
*/ */
exports.isDriveSizeRecommended = (drive, image) => { exports.isDriveSizeRecommended = (drive, image) => {
return _.get(drive, 'size', 0) >= _.get(image, 'recommendedDriveSize', 0); return _.get(drive, 'size', UNKNOWN_SIZE) >= _.get(image, 'recommendedDriveSize', UNKNOWN_SIZE);
}; };

View File

@ -60,7 +60,7 @@ exports.buildMessage = (title, data = {}) => {
return JSON.stringify({ return JSON.stringify({
command: title, command: title,
data: data data
}); });
}; };
@ -83,7 +83,7 @@ exports.buildMessage = (title, data = {}) => {
* > } * > }
*/ */
exports.parseMessage = (string) => { exports.parseMessage = (string) => {
let output; let output = null;
try { try {
output = JSON.parse(string); output = JSON.parse(string);
@ -124,15 +124,13 @@ exports.parseMessage = (string) => {
* > true * > true
*/ */
exports.buildErrorMessage = (error) => { exports.buildErrorMessage = (error) => {
if (_.isString(error)) { const errorObject = _.isString(error) ? new Error(error) : error;
error = new Error(error);
}
return exports.buildMessage('error', { return exports.buildMessage('error', {
message: error.message, message: errorObject.message,
description: error.description, description: errorObject.description,
stacktrace: error.stack, stacktrace: errorObject.stack,
code: error.code code: errorObject.code
}); });
}; };

View File

@ -16,6 +16,39 @@
'use strict'; 'use strict';
/**
* @summary Gigabyte to byte ratio
* @constant
* @private
* @type {Number}
*
* @description
* 1 GB = 1e+9 B
*/
const GIGABYTE_TO_BYTE_RATIO = 1e+9;
/**
* @summary Megabyte to byte ratio
* @constant
* @private
* @type {Number}
*
* @description
* 1 MB = 1e+6 B
*/
const MEGABYTE_TO_BYTE_RATIO = 1e+6;
/**
* @summary Milliseconds in a day
* @constant
* @private
* @type {Number}
*
* @description
* From 24 * 60 * 60 * 1000
*/
const MILLISECONDS_IN_A_DAY = 86400000;
/** /**
* @summary Convert bytes to gigabytes * @summary Convert bytes to gigabytes
* @function * @function
@ -28,7 +61,7 @@
* const result = units.bytesToGigabytes(7801405440); * const result = units.bytesToGigabytes(7801405440);
*/ */
exports.bytesToGigabytes = (bytes) => { exports.bytesToGigabytes = (bytes) => {
return bytes / 1e+9; return bytes / GIGABYTE_TO_BYTE_RATIO;
}; };
/** /**
@ -43,5 +76,20 @@ exports.bytesToGigabytes = (bytes) => {
* const result = units.bytesToMegabytes(7801405440); * const result = units.bytesToMegabytes(7801405440);
*/ */
exports.bytesToMegabytes = (bytes) => { exports.bytesToMegabytes = (bytes) => {
return bytes / 1e+6; return bytes / MEGABYTE_TO_BYTE_RATIO;
};
/**
* @summary Convert days to milliseconds
* @function
* @public
*
* @param {Number} days - days
* @returns {Number} milliseconds
*
* @example
* const result = units.daysToMilliseconds(2);
*/
exports.daysToMilliseconds = (days) => {
return days * MILLISECONDS_IN_A_DAY;
}; };

View File

@ -111,7 +111,7 @@
"electron-mocha": "^3.1.1", "electron-mocha": "^3.1.1",
"electron-packager": "^7.0.1", "electron-packager": "^7.0.1",
"electron-prebuilt": "1.4.4", "electron-prebuilt": "1.4.4",
"eslint": "^2.13.1", "eslint": "^3.16.1",
"file-exists": "^1.0.0", "file-exists": "^1.0.0",
"html-angular-validate": "^0.1.9", "html-angular-validate": "^0.1.9",
"jsonfile": "^2.3.1", "jsonfile": "^2.3.1",

View File

@ -21,6 +21,7 @@ const jsonfile = require('jsonfile');
const childProcess = require('child_process'); const childProcess = require('child_process');
const packageJSON = require('../package.json'); const packageJSON = require('../package.json');
const shrinkwrapIgnore = _.union(packageJSON.shrinkwrapIgnore, _.keys(packageJSON.optionalDependencies)); const shrinkwrapIgnore = _.union(packageJSON.shrinkwrapIgnore, _.keys(packageJSON.optionalDependencies));
const EXIT_CODES = require('../lib/shared/exit-codes');
const SHRINKWRAP_PATH = path.join(__dirname, '..', 'npm-shrinkwrap.json'); const SHRINKWRAP_PATH = path.join(__dirname, '..', 'npm-shrinkwrap.json');
try { try {
@ -29,7 +30,7 @@ try {
})); }));
} catch (error) { } catch (error) {
console.error(error.stderr.toString()); console.error(error.stderr.toString());
process.exit(1); process.exit(EXIT_CODES.GENERAL_ERROR);
} }
const shrinkwrapContents = jsonfile.readFileSync(SHRINKWRAP_PATH); const shrinkwrapContents = jsonfile.readFileSync(SHRINKWRAP_PATH);

View File

@ -14,6 +14,7 @@ const chalk = require('chalk');
const path = require('path'); const path = require('path');
const _ = require('lodash'); const _ = require('lodash');
const angularValidate = require('html-angular-validate'); const angularValidate = require('html-angular-validate');
const EXIT_CODES = require('../lib/shared/exit-codes');
const PROJECT_ROOT = path.join(__dirname, '..'); const PROJECT_ROOT = path.join(__dirname, '..');
const FILENAME = path.relative(PROJECT_ROOT, __filename); const FILENAME = path.relative(PROJECT_ROOT, __filename);
@ -45,16 +46,14 @@ angularValidate.validate(
reportCheckstylePath: null reportCheckstylePath: null
} }
).then((result) => { ).then((result) => {
// console.log(result);
_.each(result.failed, (failure) => { _.each(result.failed, (failure) => {
// The module has a typo in the "numbers" property // The module has a typo in the "numbers" property
console.error(chalk.red(`${failure.numerrs} errors at ${path.relative(PROJECT_ROOT, failure.filepath)}`)); console.error(chalk.red(`${failure.numerrs} errors at ${path.relative(PROJECT_ROOT, failure.filepath)}`));
_.each(failure.errors, (error) => { _.each(failure.errors, (error) => {
console.error(' ' + chalk.yellow(`[${error.line}:${error.col}]`) + ` ${error.msg}`); const errorPosition = `[${error.line}:${error.col}]`;
console.error(` ${chalk.yellow(errorPosition)} ${error.msg}`);
if (/^Attribute (.*) not allowed on/.test(error.msg)) { if (/^Attribute (.*) not allowed on/.test(error.msg)) {
console.error(chalk.dim(` If this is a valid directive attribute, add it to the whitelist at ${FILENAME}`)); console.error(chalk.dim(` If this is a valid directive attribute, add it to the whitelist at ${FILENAME}`));
@ -71,16 +70,17 @@ angularValidate.validate(
} }
if (!result.allpassed) { if (!result.allpassed) {
const EXIT_TIMEOUT_MS = 500;
// Add a small timeout, otherwise the scripts exits // Add a small timeout, otherwise the scripts exits
// before every string was printed on the screen. // before every string was printed on the screen.
setTimeout(() => { setTimeout(() => {
process.exit(1); process.exit(EXIT_CODES.GENERAL_ERROR);
}, 500); }, EXIT_TIMEOUT_MS);
} }
}, (error) => { }, (error) => {
console.error(error); console.error(error);
process.exit(1); process.exit(EXIT_CODES.GENERAL_ERROR);
}); });

View File

@ -22,19 +22,19 @@ console.log(_.flatten([
packageJSON.packageIgnore, packageJSON.packageIgnore,
// Development dependencies // Development dependencies
_.map(_.keys(packageJSON.devDependencies), function(dependency) { _.map(_.keys(packageJSON.devDependencies), (dependency) => {
return path.join('node_modules', dependency); return path.join('node_modules', dependency);
}), }),
// Top level hidden files // Top level hidden files
_.map(_.filter(topLevelFiles, function(file) { _.map(_.filter(topLevelFiles, (file) => {
return _.startsWith(file, '.'); return _.startsWith(file, '.');
}), function(file) { }), (file) => {
return '\\' + file; return `\\${file}`;
}), }),
// Top level markdown files // Top level markdown files
_.filter(topLevelFiles, function(file) { _.filter(topLevelFiles, (file) => {
return _.endsWith(file, '.md'); return _.endsWith(file, '.md');
}) })

19
tests/.eslintrc.yml Normal file
View File

@ -0,0 +1,19 @@
rules:
require-jsdoc:
- off
no-undefined:
- off
init-declarations:
- off
no-unused-expressions:
- off
prefer-arrow-callback:
- off
no-magic-numbers:
- off
id-length:
- error
- min: 2
exceptions:
- "_"
- "m"

View File

@ -39,7 +39,7 @@ describe('Browser: SVGIcon', function() {
// Injecting XML as HTML causes the XML header to be commented out. // Injecting XML as HTML causes the XML header to be commented out.
// Modify here to ease assertions later on. // Modify here to ease assertions later on.
iconContents[0] = '<!--' + iconContents[0].slice(1, iconContents[0].length - 1) + '-->'; iconContents[0] = `<!--${iconContents[0].slice(1, iconContents[0].length - 1)}-->`;
iconContents = iconContents.join('\n'); iconContents = iconContents.join('\n');
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope); const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);

View File

@ -2,6 +2,7 @@
const m = require('mochainon'); const m = require('mochainon');
const angular = require('angular'); const angular = require('angular');
const units = require('../../../lib/shared/units');
require('angular-mocks'); require('angular-mocks');
describe('Browser: UpdateNotifier', function() { describe('Browser: UpdateNotifier', function() {
@ -16,12 +17,12 @@ describe('Browser: UpdateNotifier', function() {
let UpdateNotifierService; let UpdateNotifierService;
let SettingsModel; let SettingsModel;
let UPDATE_NOTIFIER_SLEEP_TIME; let UPDATE_NOTIFIER_SLEEP_DAYS;
beforeEach(angular.mock.inject(function(_UpdateNotifierService_, _SettingsModel_, _UPDATE_NOTIFIER_SLEEP_TIME_) { beforeEach(angular.mock.inject(function(_UpdateNotifierService_, _SettingsModel_, _UPDATE_NOTIFIER_SLEEP_DAYS_) {
UpdateNotifierService = _UpdateNotifierService_; UpdateNotifierService = _UpdateNotifierService_;
SettingsModel = _SettingsModel_; SettingsModel = _SettingsModel_;
UPDATE_NOTIFIER_SLEEP_TIME = _UPDATE_NOTIFIER_SLEEP_TIME_; UPDATE_NOTIFIER_SLEEP_DAYS = _UPDATE_NOTIFIER_SLEEP_DAYS_;
})); }));
describe('given the `sleepUpdateCheck` is disabled', function() { describe('given the `sleepUpdateCheck` is disabled', function() {
@ -72,7 +73,8 @@ describe('Browser: UpdateNotifier', function() {
describe('given the `lastUpdateNotify` was updated long ago', function() { describe('given the `lastUpdateNotify` was updated long ago', function() {
beforeEach(function() { beforeEach(function() {
SettingsModel.set('lastUpdateNotify', Date.now() + UPDATE_NOTIFIER_SLEEP_TIME + 1000); const SLEEP_MS = units.daysToMilliseconds(UPDATE_NOTIFIER_SLEEP_DAYS);
SettingsModel.set('lastUpdateNotify', Date.now() + SLEEP_MS + 1000);
}); });
it('should return true', function() { it('should return true', function() {

View File

@ -65,7 +65,7 @@ describe('Browser: SettingsModel', function() {
const keyUnderTest = _.first(SUPPORTED_KEYS); const keyUnderTest = _.first(SUPPORTED_KEYS);
m.chai.expect(function() { m.chai.expect(function() {
SettingsModel.set(keyUnderTest, { SettingsModel.set(keyUnderTest, {
x: 1 setting: 1
}); });
}).to.throw('Invalid setting value: [object Object]'); }).to.throw('Invalid setting value: [object Object]');
}); });

View File

@ -73,35 +73,35 @@ describe('Browser: SupportedFormats', function() {
it('should return true if the extension is included in .getAllExtensions()', function() { it('should return true if the extension is included in .getAllExtensions()', function() {
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
const imagePath = '/path/to/foo.' + nonCompressedExtension; const imagePath = `/path/to/foo.${nonCompressedExtension}`;
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
m.chai.expect(isSupported).to.be.true; m.chai.expect(isSupported).to.be.true;
}); });
it('should ignore casing when determining extension validity', function() { it('should ignore casing when determining extension validity', function() {
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
const imagePath = '/path/to/foo.' + nonCompressedExtension.toUpperCase(); const imagePath = `/path/to/foo.${nonCompressedExtension.toUpperCase()}`;
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
m.chai.expect(isSupported).to.be.true; m.chai.expect(isSupported).to.be.true;
}); });
it('should not consider an extension before a non compressed extension', function() { it('should not consider an extension before a non compressed extension', function() {
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
const imagePath = '/path/to/foo.1234.' + nonCompressedExtension; const imagePath = `/path/to/foo.1234.${nonCompressedExtension}`;
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
m.chai.expect(isSupported).to.be.true; m.chai.expect(isSupported).to.be.true;
}); });
it('should return true if the extension is supported and the file name includes dots', function() { it('should return true if the extension is supported and the file name includes dots', function() {
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
const imagePath = '/path/to/foo.1.2.3-bar.' + nonCompressedExtension; const imagePath = `/path/to/foo.1.2.3-bar.${nonCompressedExtension}`;
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
m.chai.expect(isSupported).to.be.true; m.chai.expect(isSupported).to.be.true;
}); });
it('should return true if the extension is only a supported archive extension', function() { it('should return true if the extension is only a supported archive extension', function() {
const archiveExtension = _.first(SupportedFormatsModel.getArchiveExtensions()); const archiveExtension = _.first(SupportedFormatsModel.getArchiveExtensions());
const imagePath = '/path/to/foo.' + archiveExtension; const imagePath = `/path/to/foo.${archiveExtension}`;
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
m.chai.expect(isSupported).to.be.true; m.chai.expect(isSupported).to.be.true;
}); });
@ -109,14 +109,14 @@ describe('Browser: SupportedFormats', function() {
it('should return true if the extension is a supported one plus a supported compressed extensions', function() { it('should return true if the extension is a supported one plus a supported compressed extensions', function() {
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions()); const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions()); const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions());
const imagePath = '/path/to/foo.' + nonCompressedExtension + '.' + compressedExtension; const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}`;
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
m.chai.expect(isSupported).to.be.true; m.chai.expect(isSupported).to.be.true;
}); });
it('should return false if the extension is an unsupported one plus a supported compressed extensions', function() { it('should return false if the extension is an unsupported one plus a supported compressed extensions', function() {
const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions()); const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions());
const imagePath = '/path/to/foo.jpg.' + compressedExtension; const imagePath = `/path/to/foo.jpg.${compressedExtension}`;
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath); const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
m.chai.expect(isSupported).to.be.false; m.chai.expect(isSupported).to.be.false;
}); });

View File

@ -19,8 +19,8 @@ describe('Browser: Path', function() {
basenameFilter = _basenameFilter_; basenameFilter = _basenameFilter_;
})); }));
it('should return undefined if no input', function() { it('should return an empty string if no input', function() {
m.chai.expect(basenameFilter()).to.be.undefined; m.chai.expect(basenameFilter()).to.equal('');
}); });
it('should return the basename', function() { it('should return the basename', function() {

View File

@ -39,6 +39,8 @@ const deleteIfExists = (file) => {
if (fileExists(file)) { if (fileExists(file)) {
return fs.unlinkAsync(file); return fs.unlinkAsync(file);
} }
return Bluebird.resolve();
}); });
}; };
@ -63,7 +65,7 @@ exports.extractFromFilePath = function(file, image) {
results.size.original === fs.statSync(file).size, results.size.original === fs.statSync(file).size,
results.size.original === fs.statSync(image).size results.size.original === fs.statSync(image).size
])) { ])) {
throw new Error('Invalid size: ' + results.size.original); throw new Error(`Invalid size: ${results.size.original}`);
} }
const stream = results.stream const stream = results.stream