mirror of
https://github.com/balena-io/etcher.git
synced 2025-04-24 23:37:18 +00:00
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:
parent
c93f528f96
commit
6c8bc117ab
102
.eslintrc.yml
102
.eslintrc.yml
@ -9,9 +9,6 @@ rules:
|
||||
|
||||
# Possible Errors
|
||||
|
||||
comma-dangle:
|
||||
- error
|
||||
- never
|
||||
no-cond-assign:
|
||||
- error
|
||||
no-console:
|
||||
@ -59,6 +56,8 @@ rules:
|
||||
- error
|
||||
no-sparse-arrays:
|
||||
- error
|
||||
no-template-curly-in-string:
|
||||
- error
|
||||
no-unexpected-multiline:
|
||||
- error
|
||||
no-unreachable:
|
||||
@ -93,8 +92,12 @@ rules:
|
||||
- error
|
||||
block-scoped-var:
|
||||
- error
|
||||
class-methods-use-this:
|
||||
- error
|
||||
complexity:
|
||||
- off
|
||||
consistent-return:
|
||||
- error
|
||||
curly:
|
||||
- error
|
||||
default-case:
|
||||
@ -136,6 +139,8 @@ rules:
|
||||
- error
|
||||
no-floating-decimal:
|
||||
- error
|
||||
no-global-assign:
|
||||
- error
|
||||
no-implicit-coercion:
|
||||
- error
|
||||
no-implicit-globals:
|
||||
@ -150,6 +155,8 @@ rules:
|
||||
- error
|
||||
no-loop-func:
|
||||
- error
|
||||
no-magic-numbers:
|
||||
- error
|
||||
no-multi-spaces:
|
||||
- error
|
||||
no-multi-str:
|
||||
@ -166,12 +173,19 @@ rules:
|
||||
- error
|
||||
no-octal-escape:
|
||||
- error
|
||||
no-param-reassign:
|
||||
- error
|
||||
no-proto:
|
||||
- error
|
||||
no-redeclare:
|
||||
- error
|
||||
no-restricted-properties:
|
||||
- error
|
||||
- property: __proto__
|
||||
no-return-assign:
|
||||
- error
|
||||
no-return-await:
|
||||
- error
|
||||
no-script-url:
|
||||
- error
|
||||
no-self-assign:
|
||||
@ -184,6 +198,8 @@ rules:
|
||||
- error
|
||||
no-unmodified-loop-condition:
|
||||
- error
|
||||
no-unused-expressions:
|
||||
- error
|
||||
no-unused-labels:
|
||||
- error
|
||||
no-useless-call:
|
||||
@ -216,12 +232,18 @@ rules:
|
||||
|
||||
# Variables
|
||||
|
||||
init-declarations:
|
||||
- error
|
||||
- always
|
||||
no-catch-shadow:
|
||||
- error
|
||||
no-delete-var:
|
||||
- error
|
||||
no-label-var:
|
||||
- error
|
||||
no-restricted-globals:
|
||||
- error
|
||||
- event
|
||||
no-shadow:
|
||||
- error
|
||||
no-shadow-restricted-names:
|
||||
@ -230,6 +252,8 @@ rules:
|
||||
- error
|
||||
no-undef-init:
|
||||
- error
|
||||
no-undefined:
|
||||
- error
|
||||
no-unused-vars:
|
||||
- error
|
||||
no-use-before-define:
|
||||
@ -268,6 +292,13 @@ rules:
|
||||
- 1tbs
|
||||
camelcase:
|
||||
- error
|
||||
capitalized-comments:
|
||||
- error
|
||||
- always
|
||||
- ignoreConsecutiveComments: true
|
||||
comma-dangle:
|
||||
- error
|
||||
- never
|
||||
comma-spacing:
|
||||
- error
|
||||
- before: false
|
||||
@ -283,6 +314,12 @@ rules:
|
||||
- self
|
||||
eol-last:
|
||||
- error
|
||||
func-call-spacing:
|
||||
- error
|
||||
- never
|
||||
func-name-matching:
|
||||
- error
|
||||
- always
|
||||
func-names:
|
||||
- error
|
||||
- never
|
||||
@ -291,6 +328,11 @@ rules:
|
||||
- expression
|
||||
id-blacklist:
|
||||
- error
|
||||
id-length:
|
||||
- error
|
||||
- min: 2
|
||||
exceptions:
|
||||
- "_"
|
||||
indent:
|
||||
- error
|
||||
- 2
|
||||
@ -304,6 +346,12 @@ rules:
|
||||
- error
|
||||
- before: true
|
||||
after: true
|
||||
line-comment-position:
|
||||
- error
|
||||
- position: above
|
||||
linebreak-style:
|
||||
- error
|
||||
- unix
|
||||
lines-around-comment:
|
||||
- error
|
||||
- beforeBlockComment: true
|
||||
@ -316,6 +364,9 @@ rules:
|
||||
allowObjectEnd: false
|
||||
allowArrayStart: true
|
||||
allowArrayEnd: false
|
||||
lines-around-directive:
|
||||
- error
|
||||
- always
|
||||
max-len:
|
||||
- error
|
||||
- code: 130
|
||||
@ -323,13 +374,20 @@ rules:
|
||||
ignoreComments: false
|
||||
ignoreTrailingComments: false
|
||||
ignoreUrls: true
|
||||
max-params:
|
||||
- off
|
||||
max-statements-per-line:
|
||||
- error
|
||||
- max: 1
|
||||
multiline-ternary:
|
||||
- error
|
||||
- never
|
||||
new-cap:
|
||||
- error
|
||||
new-parens:
|
||||
- error
|
||||
newline-per-chained-call:
|
||||
- off
|
||||
no-array-constructor:
|
||||
- error
|
||||
no-bitwise:
|
||||
@ -338,10 +396,14 @@ rules:
|
||||
- error
|
||||
no-inline-comments:
|
||||
- error
|
||||
no-lonely-if:
|
||||
- error
|
||||
no-mixed-operators:
|
||||
- error
|
||||
no-mixed-spaces-and-tabs:
|
||||
- error
|
||||
no-multi-assign:
|
||||
- error
|
||||
no-multiple-empty-lines:
|
||||
- error
|
||||
- max: 1
|
||||
@ -355,8 +417,14 @@ rules:
|
||||
- error
|
||||
no-plusplus:
|
||||
- error
|
||||
no-restricted-syntax:
|
||||
- error
|
||||
- WithStatement
|
||||
- ForInStatement
|
||||
no-spaced-func:
|
||||
- error
|
||||
no-tabs:
|
||||
- error
|
||||
no-trailing-spaces:
|
||||
- error
|
||||
no-underscore-dangle:
|
||||
@ -374,6 +442,9 @@ rules:
|
||||
- always
|
||||
object-property-newline:
|
||||
- error
|
||||
one-var-declaration-per-line:
|
||||
- error
|
||||
- always
|
||||
one-var:
|
||||
- error
|
||||
- never
|
||||
@ -383,6 +454,9 @@ rules:
|
||||
operator-linebreak:
|
||||
- error
|
||||
- before
|
||||
padded-blocks:
|
||||
- error
|
||||
- classes: always
|
||||
quote-props:
|
||||
- error
|
||||
- as-needed
|
||||
@ -395,6 +469,7 @@ rules:
|
||||
FunctionDeclaration: true
|
||||
ClassDeclaration: true
|
||||
MethodDefinition: true
|
||||
ArrowFunctionExpression: true
|
||||
semi:
|
||||
- error
|
||||
- always
|
||||
@ -412,9 +487,16 @@ rules:
|
||||
- never
|
||||
space-infix-ops:
|
||||
- error
|
||||
space-unary-ops:
|
||||
- error
|
||||
- words: true
|
||||
nonwords: false
|
||||
spaced-comment:
|
||||
- error
|
||||
- always
|
||||
template-tag-spacing:
|
||||
- error
|
||||
- always
|
||||
unicode-bom:
|
||||
- error
|
||||
|
||||
@ -458,12 +540,24 @@ rules:
|
||||
- error
|
||||
no-var:
|
||||
- error
|
||||
object-shorthand:
|
||||
- error
|
||||
- always
|
||||
prefer-const:
|
||||
- error
|
||||
prefer-reflect:
|
||||
- error
|
||||
prefer-spread:
|
||||
- error
|
||||
prefer-numeric-literals:
|
||||
- error
|
||||
prefer-rest-params:
|
||||
- error
|
||||
prefer-template:
|
||||
- error
|
||||
prefer-arrow-callback:
|
||||
- error
|
||||
- allowNamedFunctions: false
|
||||
require-yield:
|
||||
- error
|
||||
rest-spread-spacing:
|
||||
@ -471,6 +565,8 @@ rules:
|
||||
template-curly-spacing:
|
||||
- error
|
||||
- never
|
||||
symbol-description:
|
||||
- error
|
||||
yield-star-spacing:
|
||||
- error
|
||||
- before: true
|
||||
|
@ -47,7 +47,8 @@ exports.getBooleanArgumentForm = (argumentName, value) => {
|
||||
return '--no-';
|
||||
}
|
||||
|
||||
if (_.size(argumentName) === 1) {
|
||||
const SHORT_OPTION_LENGTH = 1;
|
||||
if (_.size(argumentName) === SHORT_OPTION_LENGTH) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
|
@ -61,7 +61,7 @@ exports.write = (image, drive, options) => {
|
||||
|
||||
const argv = cli.getArguments({
|
||||
entryPoint: rendererUtils.getApplicationEntryPoint(),
|
||||
image: image,
|
||||
image,
|
||||
device: drive.device,
|
||||
validateWriteOnSuccess: options.validateWriteOnSuccess,
|
||||
unmountOnSuccess: options.unmountOnSuccess
|
||||
@ -77,6 +77,14 @@ exports.write = (image, drive, options) => {
|
||||
ipc.config.silent = true;
|
||||
ipc.serve();
|
||||
|
||||
/**
|
||||
* @summary Safely terminate the IPC server
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @example
|
||||
* terminateServer();
|
||||
*/
|
||||
const terminateServer = () => {
|
||||
|
||||
// Turns out we need to destroy all sockets for
|
||||
@ -90,6 +98,16 @@ exports.write = (image, drive, options) => {
|
||||
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) => {
|
||||
terminateServer();
|
||||
emitter.emit('error', error);
|
||||
@ -97,7 +115,7 @@ exports.write = (image, drive, options) => {
|
||||
|
||||
ipc.server.on('error', emitError);
|
||||
ipc.server.on('message', (data) => {
|
||||
let message;
|
||||
let message = null;
|
||||
|
||||
try {
|
||||
message = robot.parseMessage(data);
|
||||
@ -112,7 +130,7 @@ exports.write = (image, drive, options) => {
|
||||
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', () => {
|
||||
@ -144,9 +162,13 @@ exports.write = (image, drive, options) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (code !== EXIT_CODES.SUCCESS && code !== EXIT_CODES.VALIDATION_ERROR) {
|
||||
return emitError(new Error(`Child process exited with error code: ${code}`));
|
||||
// We shouldn't emit the `done` event manually here
|
||||
// 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}`));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -40,9 +40,12 @@ exports.getApplicationEntryPoint = () => {
|
||||
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
|
||||
// from `/root`, therefore we pass an absolute path,
|
||||
// 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);
|
||||
|
||||
};
|
||||
|
@ -37,8 +37,32 @@ const packageJSON = require('../../package.json');
|
||||
// and `stderr` to the parent process using IPC communication,
|
||||
// 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) => {
|
||||
|
||||
@ -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('connect', () => {
|
||||
|
||||
const child = childProcess.spawn(EXECUTABLE, ETCHER_ARGUMENTS, {
|
||||
const child = childProcess.spawn(executable, etcherArguments, {
|
||||
env: {
|
||||
|
||||
// The CLI might call operating system utilities (like `diskutil`),
|
||||
@ -169,6 +193,18 @@ return isElevated().then((elevated) => {
|
||||
child.on('error', reject);
|
||||
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) => {
|
||||
|
||||
// Output from stdout/stderr coming from the CLI might be buffered,
|
||||
|
@ -108,7 +108,7 @@ exports.getErrorMessage = (error) => {
|
||||
});
|
||||
|
||||
if (error.description) {
|
||||
return message + '\n\n' + error.description;
|
||||
return `${message}\n\n${error.description}`;
|
||||
}
|
||||
|
||||
return message;
|
||||
|
@ -29,6 +29,9 @@ const robot = require('../shared/robot');
|
||||
const messages = require('../shared/messages');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
|
||||
const ARGV_IMAGE_PATH_INDEX = 0;
|
||||
const imagePath = options._[ARGV_IMAGE_PATH_INDEX];
|
||||
|
||||
isElevated().then((elevated) => {
|
||||
if (!elevated) {
|
||||
throw new Error(messages.error.elevationRequired());
|
||||
@ -50,10 +53,10 @@ isElevated().then((elevated) => {
|
||||
override: {
|
||||
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
|
||||
// `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}`);
|
||||
}
|
||||
|
||||
return writer.writeImage(options._[0], selectedDrive, {
|
||||
return writer.writeImage(imagePath, selectedDrive, {
|
||||
unmountOnSuccess: options.unmount,
|
||||
validateWriteOnSuccess: options.check
|
||||
}, (state) => {
|
||||
@ -109,6 +112,7 @@ isElevated().then((elevated) => {
|
||||
console.log(`Checksum: ${results.sourceChecksum}`);
|
||||
}
|
||||
|
||||
return Bluebird.resolve();
|
||||
}).then(() => {
|
||||
process.exit(EXIT_CODES.SUCCESS);
|
||||
});
|
||||
@ -121,6 +125,7 @@ isElevated().then((elevated) => {
|
||||
}
|
||||
|
||||
errors.print(error);
|
||||
return Bluebird.resolve();
|
||||
}).then(() => {
|
||||
if (error.code === 'EVALIDATION') {
|
||||
process.exit(EXIT_CODES.VALIDATION_ERROR);
|
||||
|
@ -24,6 +24,33 @@ const robot = require('../shared/robot');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
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
|
||||
* @type {Object}
|
||||
@ -34,7 +61,7 @@ module.exports = yargs
|
||||
// Don't wrap at all
|
||||
.wrap(null)
|
||||
|
||||
.demand(1, 'Missing image')
|
||||
.demand(MINIMUM_NUMBER_OF_ARGUMENTS, 'Missing image')
|
||||
|
||||
// Usage help
|
||||
.usage('Usage: $0 [options] <image>')
|
||||
@ -42,7 +69,7 @@ module.exports = yargs
|
||||
'Exit codes:',
|
||||
_.map(EXIT_CODES, (value, key) => {
|
||||
const reason = _.map(_.split(key, '_'), _.capitalize).join(' ');
|
||||
return ' ' + value + ' - ' + reason;
|
||||
return ` ${value} - ${reason}`;
|
||||
}).join('\n'),
|
||||
'',
|
||||
'If you need help, don\'t hesitate in contacting us at:',
|
||||
@ -64,7 +91,7 @@ module.exports = yargs
|
||||
.version(_.constant(packageJSON.version))
|
||||
|
||||
// Error reporting
|
||||
.fail(function(message, error) {
|
||||
.fail((message, error) => {
|
||||
if (robot.isEnabled(process.env)) {
|
||||
robot.printError(error || message);
|
||||
} else {
|
||||
@ -72,12 +99,12 @@ module.exports = yargs
|
||||
errors.print(error || message);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
})
|
||||
|
||||
// Assert that image exists
|
||||
.check((argv) => {
|
||||
fs.accessSync(argv._[0]);
|
||||
fs.accessSync(argv._[IMAGE_PATH_ARGV_INDEX]);
|
||||
return true;
|
||||
})
|
||||
|
||||
@ -123,4 +150,4 @@ module.exports = yargs
|
||||
default: true
|
||||
}
|
||||
})
|
||||
.parse(process.argv.slice(2));
|
||||
.parse(process.argv.slice(OPTIONS_INDEX_START));
|
||||
|
@ -59,7 +59,7 @@ exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||
|
||||
// Unmounting a drive in Windows means we can't write to it anymore
|
||||
if (os.platform() === 'win32') {
|
||||
return;
|
||||
return Bluebird.resolve();
|
||||
}
|
||||
|
||||
return unmount.unmountDrive(drive);
|
||||
@ -96,7 +96,7 @@ exports.writeImage = (imagePath, drive, options, onProgress) => {
|
||||
return fs.closeAsync(driveFileDescriptor).then(() => {
|
||||
|
||||
if (!options.unmountOnSuccess) {
|
||||
return;
|
||||
return Bluebird.resolve();
|
||||
}
|
||||
|
||||
return unmount.unmountDrive(drive);
|
||||
|
@ -24,6 +24,7 @@
|
||||
|
||||
var angular = require('angular');
|
||||
const electron = require('electron');
|
||||
const Bluebird = require('bluebird');
|
||||
const EXIT_CODES = require('../shared/exit-codes');
|
||||
const messages = require('../shared/messages');
|
||||
|
||||
@ -98,6 +99,8 @@ app.run((AnalyticsService, ErrorService, UpdateNotifierService, SelectionStateMo
|
||||
AnalyticsService.logEvent('Notifying update');
|
||||
return UpdateNotifierService.notify();
|
||||
}
|
||||
|
||||
return Bluebird.resolve();
|
||||
}).catch(ErrorService.reportException);
|
||||
}
|
||||
|
||||
@ -158,7 +161,7 @@ app.run(($window, WarningModalService, ErrorService, FlashStateModel, OSDialogSe
|
||||
// Don't open any more popups
|
||||
popupExists = true;
|
||||
|
||||
return OSDialogService.showWarning({
|
||||
OSDialogService.showWarning({
|
||||
confirmationLabel: 'Yes, quit',
|
||||
rejectionLabel: 'Cancel',
|
||||
title: 'Are you sure you want to close Etcher?',
|
||||
|
@ -82,7 +82,7 @@ module.exports = function(
|
||||
description: [
|
||||
messages.warning.unrecommendedDriveSize({
|
||||
image: SelectionStateModel.getImage(),
|
||||
drive: drive
|
||||
drive
|
||||
}),
|
||||
'Are you sure you want to continue?'
|
||||
].join(' ')
|
||||
|
@ -59,18 +59,12 @@ module.exports = function($uibModal, $q) {
|
||||
.then(resolve)
|
||||
.catch((error) => {
|
||||
|
||||
// Bootstrap doesn't 'resolve' these but cancels the dialog;
|
||||
// therefore call 'resolve' here applied to 'false'.
|
||||
// Bootstrap doesn't 'resolve' these but cancels the dialog
|
||||
if (error === 'escape key press' || error === 'backdrop click') {
|
||||
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 resolve();
|
||||
}
|
||||
|
||||
return reject(error);
|
||||
});
|
||||
})
|
||||
};
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
'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
|
||||
// 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.
|
||||
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
|
||||
* @type {Object}
|
||||
|
@ -19,8 +19,9 @@
|
||||
const _ = require('lodash');
|
||||
const semver = require('semver');
|
||||
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
|
||||
@ -57,10 +58,12 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M
|
||||
}, (error, latestVersion) => {
|
||||
if (error) {
|
||||
|
||||
// The error status equals -1 if the request couldn't
|
||||
// be made successfully, for example, because of a
|
||||
// timeout on an unstable network connection.
|
||||
if (error.status === -1) {
|
||||
// The error status equals this number if the request
|
||||
// couldn't be made successfuly, for example, because
|
||||
// of a timeout on an unstable network connection.
|
||||
const ERROR_CODE_UNSUCCESSFUL_REQUEST = -1;
|
||||
|
||||
if (error.status === ERROR_CODE_UNSUCCESSFUL_REQUEST) {
|
||||
return resolve(CURRENT_VERSION);
|
||||
}
|
||||
|
||||
@ -114,7 +117,7 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M
|
||||
return true;
|
||||
}
|
||||
|
||||
if (lastUpdateNotify - Date.now() > UPDATE_NOTIFIER_SLEEP_TIME) {
|
||||
if (lastUpdateNotify - Date.now() > units.daysToMilliseconds(UPDATE_NOTIFIER_SLEEP_DAYS)) {
|
||||
SettingsModel.set('sleepUpdateCheck', false);
|
||||
return true;
|
||||
}
|
||||
@ -140,7 +143,7 @@ module.exports = function($http, $q, ModalService, UPDATE_NOTIFIER_SLEEP_TIME, M
|
||||
size: 'update-notifier',
|
||||
resolve: {
|
||||
options: _.constant({
|
||||
version: version
|
||||
version
|
||||
})
|
||||
}
|
||||
}).result;
|
||||
|
@ -11,7 +11,7 @@
|
||||
<input type="checkbox"
|
||||
ng-model="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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,7 +29,15 @@ const UpdateNotifier = angular.module(MODULE_NAME, [
|
||||
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.service('UpdateNotifierService', require('./services/update-notifier'));
|
||||
|
||||
|
@ -41,12 +41,13 @@ electron.app.on('ready', () => {
|
||||
// Prevent flash of white when starting the application
|
||||
// https://github.com/atom/electron/issues/2172
|
||||
mainWindow.webContents.on('did-finish-load', () => {
|
||||
const WEBVIEW_LOAD_TIMEOUT_MS = 100;
|
||||
|
||||
// The flash of white is still present for a very short
|
||||
// while after the WebView reports it finished loading
|
||||
setTimeout(() => {
|
||||
mainWindow.show();
|
||||
}, 100);
|
||||
}, WEBVIEW_LOAD_TIMEOUT_MS);
|
||||
|
||||
});
|
||||
|
||||
|
@ -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`
|
||||
// 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.
|
||||
/**
|
||||
* @summary Memoize ImmutableJS list reference
|
||||
* @function
|
||||
* @private
|
||||
*
|
||||
* @description
|
||||
* This workaround is needed to avoid AngularJS from getting
|
||||
* 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) => {
|
||||
let previous = [];
|
||||
|
||||
return () => {
|
||||
const list = Reflect.apply(func, this, arguments);
|
||||
return (...args) => {
|
||||
const list = Reflect.apply(func, this, args);
|
||||
|
||||
if (!_.isEqual(list, previous)) {
|
||||
previous = list;
|
||||
|
@ -134,9 +134,12 @@ FlashState.service('FlashStateModel', function() {
|
||||
if (_.isNumber(state.speed) && !_.isNaN(state.speed)) {
|
||||
|
||||
// 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;
|
||||
})
|
||||
}
|
||||
});
|
||||
|
@ -286,9 +286,7 @@ SelectionStateModel.service('SelectionStateModel', function(DrivesModel) {
|
||||
* @example
|
||||
* SelectionStateModel.clear({ preserveImage: true });
|
||||
*/
|
||||
this.clear = (options) => {
|
||||
options = options || {};
|
||||
|
||||
this.clear = (options = {}) => {
|
||||
if (!options.preserveImage) {
|
||||
Store.dispatch({
|
||||
type: Store.Actions.REMOVE_IMAGE
|
||||
|
@ -75,9 +75,21 @@ const ACTIONS = _.fromPairs(_.map([
|
||||
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) {
|
||||
|
||||
case ACTIONS.SET_AVAILABLE_DRIVES: {
|
||||
@ -91,7 +103,9 @@ const storeReducer = (state, action) => {
|
||||
|
||||
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);
|
||||
|
||||
@ -150,7 +164,7 @@ const storeReducer = (state, action) => {
|
||||
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');
|
||||
}
|
||||
|
||||
@ -352,9 +366,9 @@ module.exports = _.merge(redux.createStore(
|
||||
// In the first run, there will be no information
|
||||
// to deserialize. In this case, we avoid merging,
|
||||
// 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) {
|
||||
return;
|
||||
return state;
|
||||
}
|
||||
|
||||
// Blindly setting the state to the deserialised subset
|
||||
|
@ -115,7 +115,7 @@ SupportedFormats.service('SupportedFormatsModel', function() {
|
||||
* }
|
||||
*/
|
||||
this.isSupportedImage = (imagePath) => {
|
||||
const extension = path.extname(imagePath).slice(1).toLowerCase();
|
||||
const extension = _.replace(path.extname(imagePath), '.', '').toLowerCase();
|
||||
|
||||
if (_.some([
|
||||
_.includes(this.getNonCompressedExtensions(), extension),
|
||||
|
@ -93,13 +93,13 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
|
||||
* AnalyticsService.log('Hello World');
|
||||
*/
|
||||
this.logDebug = (message) => {
|
||||
message = new Date() + ' ' + message;
|
||||
const debugMessage = `${new Date()} ${message}`;
|
||||
|
||||
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) => {
|
||||
|
||||
if (SettingsModel.get('errorReporting') && isRunningInAsar()) {
|
||||
|
||||
// Clone data before passing it to `mixpanel.track`
|
||||
@ -129,11 +128,15 @@ analytics.service('AnalyticsService', function($log, $window, $mixpanel, Setting
|
||||
|
||||
}
|
||||
|
||||
if (data) {
|
||||
message += ` (${JSON.stringify(data)})`;
|
||||
}
|
||||
const debugMessage = _.attempt(() => {
|
||||
if (data) {
|
||||
return `${message} (${JSON.stringify(data)})`;
|
||||
}
|
||||
|
||||
this.logDebug(message);
|
||||
return message;
|
||||
});
|
||||
|
||||
this.logDebug(debugMessage);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -34,16 +34,20 @@ const driveScanner = angular.module(MODULE_NAME, [
|
||||
|
||||
driveScanner.factory('DriveScannerService', (SettingsModel) => {
|
||||
const DRIVE_SCANNER_INTERVAL_MS = 2000;
|
||||
const DRIVE_SCANNER_FIRST_SCAN_DELAY_MS = 0;
|
||||
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(() => {
|
||||
return Rx.Observable.fromNodeCallback(drivelist.list)();
|
||||
})
|
||||
.map((drives) => {
|
||||
|
||||
// Calculate an appropriate "display name"
|
||||
drives = _.map(drives, (drive) => {
|
||||
// Build human friendly "description"
|
||||
.map((drives) => {
|
||||
return _.map(drives, (drive) => {
|
||||
drive.name = drive.device;
|
||||
|
||||
if (os.platform() === 'win32' && !_.isEmpty(drive.mountpoints)) {
|
||||
@ -52,7 +56,9 @@ driveScanner.factory('DriveScannerService', (SettingsModel) => {
|
||||
|
||||
return drive;
|
||||
});
|
||||
})
|
||||
|
||||
.map((drives) => {
|
||||
if (SettingsModel.get('unsafeMode')) {
|
||||
return drives;
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ module.exports = function($q, SupportedFormatsModel) {
|
||||
return resolve();
|
||||
}
|
||||
|
||||
imageStream.getImageMetadata(imagePath).then((metadata) => {
|
||||
return imageStream.getImageMetadata(imagePath).then((metadata) => {
|
||||
metadata.path = imagePath;
|
||||
metadata.size = metadata.size.final.value;
|
||||
return resolve(metadata);
|
||||
@ -95,8 +95,8 @@ module.exports = function($q, SupportedFormatsModel) {
|
||||
* @param {Object} options - options
|
||||
* @param {String} options.title - dialog title
|
||||
* @param {String} options.description - dialog description
|
||||
* @param {String} options.confirmationLabel - confirmation label
|
||||
* @param {String} options.rejectionLabel - rejection label
|
||||
* @param {String} [options.confirmationLabel="OK"] - confirmation label
|
||||
* @param {String} [options.rejectionLabel="Cancel"] - rejection label
|
||||
* @fulfil {Boolean} - whether the dialog was confirmed or not
|
||||
* @returns {Promise};
|
||||
*
|
||||
@ -113,20 +113,30 @@ module.exports = function($q, SupportedFormatsModel) {
|
||||
* });
|
||||
*/
|
||||
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) => {
|
||||
electron.remote.dialog.showMessageBox(currentWindow, {
|
||||
type: 'warning',
|
||||
buttons: [
|
||||
options.confirmationLabel,
|
||||
options.rejectionLabel
|
||||
],
|
||||
defaultId: 1,
|
||||
cancelId: 1,
|
||||
buttons: BUTTONS,
|
||||
defaultId: BUTTON_REJECTION_INDEX,
|
||||
cancelId: BUTTON_REJECTION_INDEX,
|
||||
title: 'Attention',
|
||||
message: options.title,
|
||||
detail: options.description
|
||||
}, (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!');
|
||||
*/
|
||||
this.showError = (error, description) => {
|
||||
error = error || {};
|
||||
const errorObject = error || {};
|
||||
|
||||
// Try to get as most information as possible about the error
|
||||
// rather than falling back to generic messages right away.
|
||||
const title = _.attempt(() => {
|
||||
if (_.isString(error)) {
|
||||
return error;
|
||||
if (_.isString(errorObject)) {
|
||||
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
|
||||
// types of obscure errors:
|
||||
|
@ -40,8 +40,8 @@ module.exports = ($timeout) => {
|
||||
scope: {
|
||||
osDropzone: '&'
|
||||
},
|
||||
link: (scope, element) => {
|
||||
const domElement = element[0];
|
||||
link: (scope, $element) => {
|
||||
const domElement = _.first($element);
|
||||
|
||||
// See https://github.com/electron/electron/blob/master/docs/api/file-object.md
|
||||
|
||||
@ -52,7 +52,7 @@ module.exports = ($timeout) => {
|
||||
|
||||
domElement.ondrop = (event) => {
|
||||
event.preventDefault();
|
||||
const filename = event.dataTransfer.files[0].path;
|
||||
const filename = _.first(event.dataTransfer.files).path;
|
||||
|
||||
// Safely bring this to the word of Angular
|
||||
$timeout(() => {
|
||||
|
@ -48,7 +48,7 @@ module.exports = function() {
|
||||
}
|
||||
|
||||
return new Notification(title, {
|
||||
body: body
|
||||
body
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -28,7 +28,7 @@ const OSOpenExternal = angular.module(MODULE_NAME, []);
|
||||
OSOpenExternal.service('OSOpenExternalService', require('./services/open-external'));
|
||||
OSOpenExternal.directive('osOpenExternal', require('./directives/open-external'));
|
||||
|
||||
OSOpenExternal.run(function(OSOpenExternalService) {
|
||||
OSOpenExternal.run((OSOpenExternalService) => {
|
||||
document.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target.tagName === 'A' && angular.isDefined(target.href)) {
|
||||
|
@ -26,12 +26,9 @@ module.exports = function() {
|
||||
* @protected
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
this.currentWindow = electron.remote.BrowserWindow.fromId(1);
|
||||
this.currentWindow = electron.remote.getCurrentWindow();
|
||||
|
||||
/**
|
||||
* @summary Set operating system window progress
|
||||
@ -47,11 +44,14 @@ module.exports = function() {
|
||||
* OSWindowProgressService.set(85);
|
||||
*/
|
||||
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}`);
|
||||
}
|
||||
|
||||
this.currentWindow.setProgressBar(percentage / 100);
|
||||
this.currentWindow.setProgressBar(percentage / PERCENTAGE_MAXIMUM);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -65,8 +65,9 @@ module.exports = function() {
|
||||
this.clear = () => {
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
};
|
||||
|
@ -58,7 +58,7 @@ module.exports = function(
|
||||
DriveScannerService.stop();
|
||||
|
||||
AnalyticsService.logEvent('Flash', {
|
||||
image: image,
|
||||
image,
|
||||
device: drive.device
|
||||
});
|
||||
|
||||
@ -106,22 +106,20 @@ module.exports = function(
|
||||
this.getProgressButtonLabel = () => {
|
||||
const flashState = FlashStateModel.getFlashState();
|
||||
const isChecking = flashState.type === 'check';
|
||||
const PERCENTAGE_MINIMUM = 0;
|
||||
const PERCENTAGE_MAXIMUM = 100;
|
||||
|
||||
if (!FlashStateModel.isFlashing()) {
|
||||
return 'Flash!';
|
||||
}
|
||||
|
||||
if (flashState.percentage === 0 && !flashState.speed) {
|
||||
} else if (flashState.percentage === PERCENTAGE_MINIMUM && !flashState.speed) {
|
||||
return 'Starting...';
|
||||
} else if (flashState.percentage === 100) {
|
||||
} else if (flashState.percentage === PERCENTAGE_MAXIMUM) {
|
||||
if (isChecking && SettingsModel.get('unmountOnSuccess')) {
|
||||
return 'Unmounting...';
|
||||
}
|
||||
|
||||
return 'Finishing...';
|
||||
}
|
||||
|
||||
if (isChecking) {
|
||||
} else if (isChecking) {
|
||||
return `${flashState.percentage}% Validating...`;
|
||||
}
|
||||
|
||||
|
@ -66,7 +66,7 @@ module.exports = function(
|
||||
this.selectImage = (image) => {
|
||||
if (!SupportedFormatsModel.isSupportedImage(image.path)) {
|
||||
OSDialogService.showError('Invalid image', messages.error.invalidImage({
|
||||
image: image
|
||||
image
|
||||
}));
|
||||
|
||||
AnalyticsService.logEvent('Invalid image', image);
|
||||
@ -100,9 +100,8 @@ module.exports = function(
|
||||
image.logo = Boolean(image.logo);
|
||||
image.bmap = Boolean(image.bmap);
|
||||
|
||||
AnalyticsService.logEvent('Select image', image);
|
||||
return AnalyticsService.logEvent('Select image', image);
|
||||
}).catch(ErrorService.reportException);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
const os = require('os');
|
||||
|
||||
module.exports = function(WarningModalService, SettingsModel) {
|
||||
module.exports = function(WarningModalService, SettingsModel, ErrorService) {
|
||||
|
||||
/**
|
||||
* @summary Client platform
|
||||
@ -78,12 +78,12 @@ module.exports = function(WarningModalService, SettingsModel) {
|
||||
// Keep the checkbox unchecked until the user confirms
|
||||
this.currentData[name] = false;
|
||||
|
||||
WarningModalService.display(options).then((userAccepted) => {
|
||||
return WarningModalService.display(options).then((userAccepted) => {
|
||||
if (userAccepted) {
|
||||
this.model.set(name, true);
|
||||
this.refreshSettings();
|
||||
}
|
||||
});
|
||||
}).catch(ErrorService.reportException);
|
||||
};
|
||||
|
||||
};
|
||||
|
@ -25,7 +25,8 @@ const MODULE_NAME = 'Etcher.Pages.Settings';
|
||||
const SettingsPage = angular.module(MODULE_NAME, [
|
||||
require('angular-ui-router'),
|
||||
require('../../components/warning-modal/warning-modal'),
|
||||
require('../../models/settings')
|
||||
require('../../models/settings'),
|
||||
require('../../modules/error')
|
||||
]);
|
||||
|
||||
SettingsPage.controller('SettingsController', require('./controllers/settings'));
|
||||
|
@ -39,7 +39,7 @@ module.exports = (ManifestBindService) => {
|
||||
const value = ManifestBindService.get(attributes.manifestBind);
|
||||
|
||||
if (!value) {
|
||||
throw new Error('ManifestBind: Unknown property `' + attributes.manifestBind + '`');
|
||||
throw new Error(`ManifestBind: Unknown property \`${attributes.manifestBind}\``);
|
||||
}
|
||||
|
||||
element.html(value);
|
||||
|
@ -33,7 +33,7 @@ module.exports = () => {
|
||||
*/
|
||||
return (input) => {
|
||||
if (!input) {
|
||||
return;
|
||||
return '';
|
||||
}
|
||||
|
||||
return path.basename(input);
|
||||
|
@ -48,9 +48,11 @@ exports.getEntries = (archive) => {
|
||||
zip.on('error', reject);
|
||||
|
||||
zip.on('ready', () => {
|
||||
const EMPTY_ENTRY_SIZE = 0;
|
||||
|
||||
return resolve(_.chain(zip.entries())
|
||||
.omitBy((entry) => {
|
||||
return entry.size === 0;
|
||||
return entry.size === EMPTY_ENTRY_SIZE;
|
||||
})
|
||||
.map((metadata) => {
|
||||
return {
|
||||
@ -99,7 +101,7 @@ exports.extractFile = (archive, entries, file) => {
|
||||
return zipfile.readEntry();
|
||||
}
|
||||
|
||||
zipfile.openReadStream(entry, (error, readStream) => {
|
||||
return zipfile.openReadStream(entry, (error, readStream) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
@ -170,11 +170,12 @@ exports.extractImage = (archive, hooks) => {
|
||||
return hooks.getEntries(archive).then((entries) => {
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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');
|
||||
error.description = 'The archive image should contain one and only one top image file.';
|
||||
error.report = false;
|
||||
|
@ -33,9 +33,11 @@ const archiveType = require('archive-type');
|
||||
*/
|
||||
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
|
||||
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');
|
||||
};
|
||||
|
@ -19,6 +19,14 @@
|
||||
const _ = require('lodash');
|
||||
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
|
||||
* @function
|
||||
@ -134,7 +142,7 @@ exports.isSourceDrive = (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) => {
|
||||
return _.get(drive, 'size', 0) >= _.get(image, 'recommendedDriveSize', 0);
|
||||
return _.get(drive, 'size', UNKNOWN_SIZE) >= _.get(image, 'recommendedDriveSize', UNKNOWN_SIZE);
|
||||
};
|
||||
|
@ -60,7 +60,7 @@ exports.buildMessage = (title, data = {}) => {
|
||||
|
||||
return JSON.stringify({
|
||||
command: title,
|
||||
data: data
|
||||
data
|
||||
});
|
||||
};
|
||||
|
||||
@ -83,7 +83,7 @@ exports.buildMessage = (title, data = {}) => {
|
||||
* > }
|
||||
*/
|
||||
exports.parseMessage = (string) => {
|
||||
let output;
|
||||
let output = null;
|
||||
|
||||
try {
|
||||
output = JSON.parse(string);
|
||||
@ -124,15 +124,13 @@ exports.parseMessage = (string) => {
|
||||
* > true
|
||||
*/
|
||||
exports.buildErrorMessage = (error) => {
|
||||
if (_.isString(error)) {
|
||||
error = new Error(error);
|
||||
}
|
||||
const errorObject = _.isString(error) ? new Error(error) : error;
|
||||
|
||||
return exports.buildMessage('error', {
|
||||
message: error.message,
|
||||
description: error.description,
|
||||
stacktrace: error.stack,
|
||||
code: error.code
|
||||
message: errorObject.message,
|
||||
description: errorObject.description,
|
||||
stacktrace: errorObject.stack,
|
||||
code: errorObject.code
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -16,6 +16,39 @@
|
||||
|
||||
'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
|
||||
* @function
|
||||
@ -28,7 +61,7 @@
|
||||
* const result = units.bytesToGigabytes(7801405440);
|
||||
*/
|
||||
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);
|
||||
*/
|
||||
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;
|
||||
};
|
||||
|
@ -111,7 +111,7 @@
|
||||
"electron-mocha": "^3.1.1",
|
||||
"electron-packager": "^7.0.1",
|
||||
"electron-prebuilt": "1.4.4",
|
||||
"eslint": "^2.13.1",
|
||||
"eslint": "^3.16.1",
|
||||
"file-exists": "^1.0.0",
|
||||
"html-angular-validate": "^0.1.9",
|
||||
"jsonfile": "^2.3.1",
|
||||
|
@ -21,6 +21,7 @@ const jsonfile = require('jsonfile');
|
||||
const childProcess = require('child_process');
|
||||
const packageJSON = require('../package.json');
|
||||
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');
|
||||
|
||||
try {
|
||||
@ -29,7 +30,7 @@ try {
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error.stderr.toString());
|
||||
process.exit(1);
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
}
|
||||
|
||||
const shrinkwrapContents = jsonfile.readFileSync(SHRINKWRAP_PATH);
|
||||
|
@ -14,6 +14,7 @@ const chalk = require('chalk');
|
||||
const path = require('path');
|
||||
const _ = require('lodash');
|
||||
const angularValidate = require('html-angular-validate');
|
||||
const EXIT_CODES = require('../lib/shared/exit-codes');
|
||||
const PROJECT_ROOT = path.join(__dirname, '..');
|
||||
const FILENAME = path.relative(PROJECT_ROOT, __filename);
|
||||
|
||||
@ -45,16 +46,14 @@ angularValidate.validate(
|
||||
reportCheckstylePath: null
|
||||
}
|
||||
).then((result) => {
|
||||
|
||||
// console.log(result);
|
||||
|
||||
_.each(result.failed, (failure) => {
|
||||
|
||||
// The module has a typo in the "numbers" property
|
||||
console.error(chalk.red(`${failure.numerrs} errors at ${path.relative(PROJECT_ROOT, failure.filepath)}`));
|
||||
|
||||
_.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)) {
|
||||
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) {
|
||||
const EXIT_TIMEOUT_MS = 500;
|
||||
|
||||
// Add a small timeout, otherwise the scripts exits
|
||||
// before every string was printed on the screen.
|
||||
setTimeout(() => {
|
||||
process.exit(1);
|
||||
}, 500);
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
}, EXIT_TIMEOUT_MS);
|
||||
|
||||
}
|
||||
|
||||
}, (error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
process.exit(EXIT_CODES.GENERAL_ERROR);
|
||||
});
|
||||
|
@ -22,19 +22,19 @@ console.log(_.flatten([
|
||||
packageJSON.packageIgnore,
|
||||
|
||||
// Development dependencies
|
||||
_.map(_.keys(packageJSON.devDependencies), function(dependency) {
|
||||
_.map(_.keys(packageJSON.devDependencies), (dependency) => {
|
||||
return path.join('node_modules', dependency);
|
||||
}),
|
||||
|
||||
// Top level hidden files
|
||||
_.map(_.filter(topLevelFiles, function(file) {
|
||||
_.map(_.filter(topLevelFiles, (file) => {
|
||||
return _.startsWith(file, '.');
|
||||
}), function(file) {
|
||||
return '\\' + file;
|
||||
}), (file) => {
|
||||
return `\\${file}`;
|
||||
}),
|
||||
|
||||
// Top level markdown files
|
||||
_.filter(topLevelFiles, function(file) {
|
||||
_.filter(topLevelFiles, (file) => {
|
||||
return _.endsWith(file, '.md');
|
||||
})
|
||||
|
||||
|
19
tests/.eslintrc.yml
Normal file
19
tests/.eslintrc.yml
Normal 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"
|
@ -39,7 +39,7 @@ describe('Browser: SVGIcon', function() {
|
||||
|
||||
// Injecting XML as HTML causes the XML header to be commented out.
|
||||
// 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');
|
||||
|
||||
const element = $compile(`<svg-icon path="${icon}">Resin.io</svg-icon>`)($rootScope);
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
const m = require('mochainon');
|
||||
const angular = require('angular');
|
||||
const units = require('../../../lib/shared/units');
|
||||
require('angular-mocks');
|
||||
|
||||
describe('Browser: UpdateNotifier', function() {
|
||||
@ -16,12 +17,12 @@ describe('Browser: UpdateNotifier', function() {
|
||||
|
||||
let UpdateNotifierService;
|
||||
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_;
|
||||
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() {
|
||||
@ -72,7 +73,8 @@ describe('Browser: UpdateNotifier', function() {
|
||||
describe('given the `lastUpdateNotify` was updated long ago', 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() {
|
||||
|
@ -65,7 +65,7 @@ describe('Browser: SettingsModel', function() {
|
||||
const keyUnderTest = _.first(SUPPORTED_KEYS);
|
||||
m.chai.expect(function() {
|
||||
SettingsModel.set(keyUnderTest, {
|
||||
x: 1
|
||||
setting: 1
|
||||
});
|
||||
}).to.throw('Invalid setting value: [object Object]');
|
||||
});
|
||||
|
@ -73,35 +73,35 @@ describe('Browser: SupportedFormats', function() {
|
||||
|
||||
it('should return true if the extension is included in .getAllExtensions()', function() {
|
||||
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
|
||||
const imagePath = '/path/to/foo.' + nonCompressedExtension;
|
||||
const imagePath = `/path/to/foo.${nonCompressedExtension}`;
|
||||
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
|
||||
m.chai.expect(isSupported).to.be.true;
|
||||
});
|
||||
|
||||
it('should ignore casing when determining extension validity', function() {
|
||||
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
|
||||
const imagePath = '/path/to/foo.' + nonCompressedExtension.toUpperCase();
|
||||
const imagePath = `/path/to/foo.${nonCompressedExtension.toUpperCase()}`;
|
||||
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
|
||||
m.chai.expect(isSupported).to.be.true;
|
||||
});
|
||||
|
||||
it('should not consider an extension before a non compressed extension', function() {
|
||||
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
|
||||
const imagePath = '/path/to/foo.1234.' + nonCompressedExtension;
|
||||
const imagePath = `/path/to/foo.1234.${nonCompressedExtension}`;
|
||||
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
|
||||
m.chai.expect(isSupported).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true if the extension is supported and the file name includes dots', function() {
|
||||
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);
|
||||
m.chai.expect(isSupported).to.be.true;
|
||||
});
|
||||
|
||||
it('should return true if the extension is only a supported archive extension', function() {
|
||||
const archiveExtension = _.first(SupportedFormatsModel.getArchiveExtensions());
|
||||
const imagePath = '/path/to/foo.' + archiveExtension;
|
||||
const imagePath = `/path/to/foo.${archiveExtension}`;
|
||||
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
|
||||
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() {
|
||||
const nonCompressedExtension = _.first(SupportedFormatsModel.getNonCompressedExtensions());
|
||||
const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions());
|
||||
const imagePath = '/path/to/foo.' + nonCompressedExtension + '.' + compressedExtension;
|
||||
const imagePath = `/path/to/foo.${nonCompressedExtension}.${compressedExtension}`;
|
||||
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
|
||||
m.chai.expect(isSupported).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false if the extension is an unsupported one plus a supported compressed extensions', function() {
|
||||
const compressedExtension = _.first(SupportedFormatsModel.getCompressedExtensions());
|
||||
const imagePath = '/path/to/foo.jpg.' + compressedExtension;
|
||||
const imagePath = `/path/to/foo.jpg.${compressedExtension}`;
|
||||
const isSupported = SupportedFormatsModel.isSupportedImage(imagePath);
|
||||
m.chai.expect(isSupported).to.be.false;
|
||||
});
|
||||
|
@ -19,8 +19,8 @@ describe('Browser: Path', function() {
|
||||
basenameFilter = _basenameFilter_;
|
||||
}));
|
||||
|
||||
it('should return undefined if no input', function() {
|
||||
m.chai.expect(basenameFilter()).to.be.undefined;
|
||||
it('should return an empty string if no input', function() {
|
||||
m.chai.expect(basenameFilter()).to.equal('');
|
||||
});
|
||||
|
||||
it('should return the basename', function() {
|
||||
|
@ -39,6 +39,8 @@ const deleteIfExists = (file) => {
|
||||
if (fileExists(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(image).size
|
||||
])) {
|
||||
throw new Error('Invalid size: ' + results.size.original);
|
||||
throw new Error(`Invalid size: ${results.size.original}`);
|
||||
}
|
||||
|
||||
const stream = results.stream
|
||||
|
Loading…
x
Reference in New Issue
Block a user