Initial commit

This commit is contained in:
Juan Cruz Viotti 2015-10-29 09:01:12 -04:00
commit 4bfb161e5c
44 changed files with 59134 additions and 0 deletions

10
.editorconfig Normal file
View File

@ -0,0 +1,10 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directory
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
node_modules
bower_components
# Compiled Electron releases
release/

116
.jshintrc Normal file
View File

@ -0,0 +1,116 @@
{
// --------------------------------------------------------------------
// JSHint Configuration, Strict Edition
// --------------------------------------------------------------------
//
// This is a options template for [JSHint][1], using [JSHint example][2]
// and [Ory Band's example][3] as basis and setting config values to
// be most strict:
//
// * set all enforcing options to true
// * set all relaxing options to false
// * set all environment options to false, except the browser value
// * set all JSLint legacy options to false
//
// [1]: http://www.jshint.com/
// [2]: https://github.com/jshint/node-jshint/blob/master/example/config.json
// [3]: https://github.com/oryband/dotfiles/blob/master/jshintrc
//
// @author http://michael.haschke.biz/
// @license http://unlicense.org/
// == Enforcing Options ===============================================
//
// These options tell JSHint to be more strict towards your code. Use
// them if you want to allow only a safe subset of JavaScript, very
// useful when your codebase is shared with a big number of developers
// with different skill levels.
"bitwise" : true, // Prohibit bitwise operators (&, |, ^, etc.).
"curly" : true, // Require {} for every new block or scope.
"eqeqeq" : true, // Require triple equals i.e. `===`.
"forin" : true, // Tolerate `for in` loops without `hasOwnPrototype`.
"immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );`
"latedef" : true, // Prohibit variable use before definition.
"newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`.
"noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`.
"noempty" : true, // Prohibit use of empty blocks.
"nonew" : true, // Prohibit use of constructors for side-effects.
"plusplus" : true, // Prohibit use of `++` & `--`.
"regexp" : true, // Prohibit `.` and `[^...]` in regular expressions.
"undef" : true, // Require all non-global variables be declared before they are used.
"strict" : true, // Require `use strict` pragma in every file.
"trailing" : true, // Prohibit trailing whitespaces.
// == Relaxing Options ================================================
//
// These options allow you to suppress certain types of warnings. Use
// them only if you are absolutely positive that you know what you are
// doing.
"asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons).
"boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments.
"debug" : false, // Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // Tolerate use of `== null`.
"es5" : false, // Allow EcmaScript 5 syntax.
"esnext" : false, // Allow ES.next specific features such as `const` and `let`.
"evil" : false, // Tolerate use of `eval`.
"expr" : true, // Tolerate `ExpressionStatement` as Programs.
"funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside.
"globalstrict" : false, // Allow global "use strict" (also enables 'strict').
"iterator" : false, // Allow usage of __iterator__ property.
"lastsemic" : false, // Tolerat missing semicolons when the it is omitted for the last statement in a one-line block.
"laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons.
"laxcomma" : false, // Suppress warnings about comma-first coding style.
"loopfunc" : false, // Allow functions to be defined within loops.
"multistr" : false, // Tolerate multi-line strings.
"onecase" : false, // Tolerate switches with just one case.
"proto" : false, // Tolerate __proto__ property. This property is deprecated.
"regexdash" : false, // Tolerate unescaped last dash i.e. `[-...]`.
"scripturl" : false, // Tolerate script-targeted URLs.
"smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only.
"shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`.
"sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`.
"supernew" : false, // Tolerate `new function () { ... };` and `new Object;`.
"validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function.
// == Environments ====================================================
//
// These options pre-define global variables that are exposed by
// popular JavaScript libraries and runtime environments—such as
// browser or node.js.
"browser" : true, // Standard browser globals e.g. `window`, `document`.
"couch" : false, // Enable globals exposed by CouchDB.
"devel" : false, // Allow development statements e.g. `console.log();`.
"dojo" : false, // Enable globals exposed by Dojo Toolkit.
"esnext" : true, // Enable globals exposed by ES6.
"mocha" : true, // Enable globals exposed by Mocha.
"jquery" : false, // Enable globals exposed by jQuery JavaScript library.
"mootools" : false, // Enable globals exposed by MooTools JavaScript framework.
"node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment.
"nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape.
"prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework.
"rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment.
"wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host.
// == JSLint Legacy ===================================================
//
// These options are legacy from JSLint. Aside from bug fixes they will
// not be improved in any way and might be removed at any point.
"nomen" : false, // Prohibit use of initial or trailing underbars in names.
"onevar" : false, // Allow only one `var` statement per function.
"passfail" : false, // Stop on first error.
"white" : false, // Check against strict whitespace and indentation rules.
// == Undocumented Options ============================================
//
// While I've found these options in [example1][2] and [example2][3]
// they are not described in the [JSHint Options documentation][4].
//
// [4]: http://www.jshint.com/options/
"maxerr" : 100, // Maximum errors before stopping.
"indent" : 4 // Specify indentation spacing
}

8
.travis.yml Normal file
View File

@ -0,0 +1,8 @@
language: node_js
node_js:
- "4.0"
- "0.12"
- "0.10"
- "iojs"
before_script:
- npm install -g electron-prebuilt

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2015 Resin.io. https://resin.io.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

39
README.md Normal file
View File

@ -0,0 +1,39 @@
Herostratus
===========
[![dependencies](https://david-dm.org/resin-io/herostratus.png)](https://david-dm.org/resin-io/herostratus.png)
[![Build Status](https://travis-ci.org/resin-io/herostratus.svg)](https://travis-ci.org/resin-io/herostratus)
[![Build status](https://ci.appveyor.com/api/projects/status/jb66mkw45ypqvddg/branch/master?svg=true)](https://ci.appveyor.com/project/resin-io/herostratus/branch/master)
The easy way to burn images in all operating systems
----------------------------------------------------
An image burner with support for Windows, OS X and GNU/Linux.
![Herostratus](https://raw.githubusercontent.com/resin-io/herostratus/master/screenshot.png)
**Notice:** Herostratus is in a very early state and things might break or not work at all in certain setups.
Installation
------------
We're working on providing installers for all major operating systems.
For now you can manually run the application with the following commands:
```sh
git clone https://github.com/resin-io/herostratus
cd herostratus
npm install && bower install
npm start
```
Support
-------
If you're having any problem, please [raise an issue](https://github.com/resin-io/herostratus/issues/new) on GitHub and the Resin.io team will be happy to help.
License
-------
Herostratus is free software, and may be redistributed under the terms specified in the [license](https://github.com/resin-io/herostratus/blob/master/LICENSE).

31
appveyor.yml Normal file
View File

@ -0,0 +1,31 @@
# appveyor file
# http://www.appveyor.com/docs/appveyor-yml
init:
- git config --global core.autocrlf input
cache:
- C:\Users\appveyor\.node-gyp
- '%AppData%\npm-cache'
# what combinations to test
environment:
matrix:
- nodejs_version: 0.10
- nodejs_version: 0.12
- nodejs_version: 4
install:
- ps: Install-Product node $env:nodejs_version x64
- npm -g install npm@2
- set PATH=%APPDATA%\npm;%PATH%
- npm install -g electron-prebuilt
- npm install
build: off
test_script:
- node --version
- npm --version
- ps: npm test
- cmd: npm test

21
bower.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "herostratus",
"version": "0.0.1",
"homepage": "https://github.com/resin-io/herostratus",
"authors": [
"Juan Cruz Viotti <juan@resin.io>"
],
"main": "lib/templates/index.html",
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"iron-icon": "polymerelements/iron-icon#~1.0.7",
"polymer": "Polymer/polymer#^1.1.0"
}
}

50373
build/browser/app.js Normal file

File diff suppressed because it is too large Load Diff

5996
build/css/main.css Normal file

File diff suppressed because it is too large Load Diff

58
gulpfile.js Normal file
View File

@ -0,0 +1,58 @@
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var jshintStylish = require('jshint-stylish');
var sass = require('gulp-sass');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var buffer = require('vinyl-buffer');
var paths = {
scripts: [
'./tests/**/*.spec.js',
'./lib/**/*.js',
'gulpfile.js'
],
sass: [
'./lib/scss/**/*.scss'
]
};
gulp.task('sass', function() {
'use strict';
return gulp.src(paths.sass)
.pipe(sass().on('error', sass.logError))
.pipe(gulp.dest('./build/css'));
});
gulp.task('lint', function() {
'use strict';
return gulp.src(paths.scripts)
.pipe(jshint())
.pipe(jshint.reporter(jshintStylish));
});
gulp.task('javascript', function() {
'use strict';
var b = browserify({
entries: './lib/browser/app.js',
// No need for Browserify builtins since Electron
// has access to all NodeJS libraries
builtins: {}
});
return b.bundle()
.pipe(source('app.js'))
.pipe(buffer())
.pipe(gulp.dest('./build/browser/'));
});
gulp.task('watch', [ 'lint', 'javascript', 'sass' ], function() {
'use strict';
gulp.watch(paths.scripts, [ 'lint', 'javascript' ]);
gulp.watch(paths.sass, [ 'sass' ]);
});

78
lib/browser/app.js Normal file
View File

@ -0,0 +1,78 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* @module herostratus
*/
var angular = require('angular');
var remote = window.require('remote');
var shell = remote.require('shell');
var dialog = remote.require('./src/dialog');
require('angular-ui-bootstrap');
require('./modules/selection-state');
require('./modules/drive-scanner');
require('./modules/image-writer');
require('./modules/path');
var app = angular.module('Herostratus', [
'ui.bootstrap',
// Herostratus modules
'herostratus.path',
'herostratus.selection-state',
'herostratus.drive-scanner',
'herostratus.image-writer'
]);
app.controller('AppController', function($q, DriveScannerService, SelectionStateService, ImageWriterService) {
'use strict';
var self = this;
this.selection = SelectionStateService;
this.writer = ImageWriterService;
this.scanner = DriveScannerService;
this.scanner.start(2000);
this.selectImage = function() {
return $q.when(dialog.selectImage()).then(function(image) {
self.selection.setImage(image);
console.debug('Image selected: ' + image);
});
};
this.selectDrive = function(drive) {
self.selection.setDrive(drive);
console.debug('Drive selected: ' + drive);
};
this.burn = function(image, drive) {
console.debug('Burning ' + image + ' to ' + drive);
return self.writer.burn(image, drive).then(function() {
console.debug('Done!');
});
};
this.open = shell.openExternal;
});

View File

@ -0,0 +1,170 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* @module herostratus.drive-scanner
*/
var angular = require('angular');
var _ = require('lodash');
var remote = window.require('remote');
if (window.mocha) {
var drives = remote.require(require('path').join(__dirname, '..', '..', 'src', 'drives'));
} else {
var drives = remote.require('./src/drives');
}
var driveScanner = angular.module('herostratus.drive-scanner', []);
driveScanner.service('DriveScannerRefreshService', function($interval) {
'use strict';
var interval = null;
/**
* @summary Run a function every certain milliseconds
* @function
* @public
*
* @param {Function} fn - function
* @param {Number} ms - interval milliseconds
*
* @example
* DriveScannerRefreshService.every(function() {
* console.log('I get printed every 2 seconds!');
* }, 2000);
*/
this.every = function(fn, ms) {
fn();
interval = $interval(fn, ms);
};
/**
* @summary Stop the runnning interval
* @function
* @public
*
* @example
* DriveScannerRefreshService.stop();
*/
this.stop = function() {
$interval.cancel(interval);
};
});
driveScanner.service('DriveScannerService', function($q, DriveScannerRefreshService) {
'use strict';
var self = this;
/**
* @summary List of available drives
* @type {Object[]}
* @public
*/
this.drives = [];
/**
* @summary Check if there are available drives
* @function
* @public
*
* @returns {Boolean} whether there are available drives
*
* @example
* if (DriveScannerService.hasAvailableDrives()) {
* console.log('There are available drives!');
* }
*/
this.hasAvailableDrives = function() {
return !_.isEmpty(self.drives);
};
/**
* @summary Set the list of drives
* @function
* @public
*
* @param {Object[]} drives - drives
*
* @example
* DriveScannerService.scan().then(function(drives) {
* DriveScannerService.setDrives(drives);
* });
*/
this.setDrives = function(drives) {
// Only update if something has changed
// to avoid unnecessary DOM manipulations
// angular.equals ignores $$hashKey by default
if (!angular.equals(self.drives, drives)) {
self.drives = drives;
}
};
/**
* @summary Get available drives
* @function
* @public
*
* @fulfil {Object[]} - drives
* @returns {Promise}
*
* @example
* DriveScannerService.scan().then(function(drives) {
* console.log(drives);
* });
*/
this.scan = function() {
return $q.when(drives.listRemovable());
};
/**
* @summary Scan drives and populate `.drives`
* @function
* @public
*
* @param {Number} ms - interval milliseconds
*
* @example
* DriveScannerService.start(2000);
*/
this.start = function(ms) {
DriveScannerRefreshService.every(function() {
return self.scan().then(self.setDrives);
}, ms);
};
/**
* @summary Stop scanning drives
* @function
* @public
*
* @example
* DriveScannerService.stop();
*/
this.stop = DriveScannerRefreshService.stop;
});

View File

@ -0,0 +1,159 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* @module herostratus.image-writer
*/
var angular = require('angular');
var remote = window.require('remote');
if (window.mocha) {
var writer = remote.require(require('path').join(__dirname, '..', '..', 'src', 'writer'));
} else {
var writer = remote.require('./src/writer');
}
var imageWriter = angular.module('herostratus.image-writer', []);
imageWriter.service('ImageWriterService', function($q, $timeout) {
'use strict';
var self = this;
var burning = false;
/**
* @summary Progress percentage
* @type Number
* @public
*/
this.progress = 0;
/**
* @summary Set progress percentage
* @function
* @private
*
* @param {Number} progress
*
* @example
* ImageWriterService.setProgress(50);
*/
this.setProgress = function(progress) {
// Safely bring the state to the world of Angular
$timeout(function() {
self.progress = Math.floor(progress);
console.debug('Progress: ' + self.progress);
});
};
/**
* @summary Check if currently burning
* @function
* @private
*
* @returns {Boolean} whether is burning or not
*
* @example
* if (ImageWriterService.isBurning()) {
* console.log('We\'re currently burning');
* }
*/
this.isBurning = function() {
return burning;
};
/**
* @summary Set the burning status
* @function
* @private
*
* @param {Boolean} status - burning status
*
* @example
* ImageWriterService.setBurning(true);
*/
this.setBurning = function(status) {
burning = !!status;
};
/**
* @summary Perform write operation
* @function
* @private
*
* @description
* This function is extracted for testing purposes.
*
* @param {String} image - image path
* @param {String} drive - drive device
* @param {Function} onProgress - in progress callback (state)
*
* @returns {Promise}
*
* @example
* ImageWriter.performWrite('path/to/image.img', '/dev/disk2', function(state) {
* console.log(state.percentage);
* });
*/
this.performWrite = function(image, drive, onProgress) {
return $q.when(writer.writeImage(image, drive, onProgress));
};
/**
* @summary Burn an image to a drive
* @function
* @public
*
* @description
* This function will update `.progress` with the current writing percentage.
*
* @param {String} image - image path
* @param {String} drive - drive device
*
* @returns {Promise}
*
* @example
* ImageWriterService.burn('foo.img', '/dev/disk').then(function() {
* console.log('Write completed!');
* });
*/
this.burn = function(image, drive) {
// Avoid writing more than once
if (self.isBurning()) {
return;
}
self.setBurning(true);
return self.performWrite(image, drive, function(state) {
self.setProgress(state.percentage);
}).finally(function() {
self.setBurning(false);
});
};
});

View File

@ -0,0 +1,48 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* @module herostratus.path
*/
var angular = require('angular');
var path = require('path');
var pathModule = angular.module('herostratus.path', []);
pathModule.filter('basename', function() {
'use strict';
/**
* @summary Get the basename of a path
* @function
* @public
*
* @param {String} input - input path
* @returns {String} path basename
*
* @example
* {{ '/foo/bar/baz' | basename }}
*/
return path.basename;
});

View File

@ -0,0 +1,131 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
/**
* @module herostratus.selection-state
*/
var angular = require('angular');
var selectionState = angular.module('herostratus.selection-state', []);
selectionState.service('SelectionStateService', function() {
'use strict';
var self = this;
/**
* @summary Selection state
* @type Object
* @private
*/
var selection = {};
/**
* @summary Set a drive
* @function
* @public
*
* @param {String} drive - drive
*
* @example
* SelectionStateService.setDrive('/dev/disk2');
*/
this.setDrive = function(drive) {
selection.drive = drive;
};
/**
* @summary Set a image
* @function
* @public
*
* @param {String} image - image
*
* @example
* SelectionStateService.setImage('foo.img');
*/
this.setImage = function(image) {
selection.image = image;
};
/**
* @summary Get drive
* @function
* @public
*
* @returns {String} drive
*
* @example
* var drive = SelectionStateService.getDrive();
*/
this.getDrive = function() {
return selection.drive;
};
/**
* @summary Get image
* @function
* @public
*
* @returns {String} image
*
* @example
* var image = SelectionStateService.getImage();
*/
this.getImage = function() {
return selection.image;
};
/**
* @summary Check if there is a selected drive
* @function
* @public
*
* @returns {Boolean} whether there is a selected drive
*
* @example
* if (SelectionStateService.hasDrive()) {
* console.log('There is a drive!');
* }
*/
this.hasDrive = function() {
return !!self.getDrive();
};
/**
* @summary Check if there is a selected image
* @function
* @public
*
* @returns {Boolean} whether there is a selected image
*
* @example
* if (SelectionStateService.hasImage()) {
* console.log('There is an image!');
* }
*/
this.hasImage = function() {
return !!self.getImage();
};
});

View File

@ -0,0 +1,24 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<dom-module id="hero-badge">
<template>
<style is="custom-style">
:host ::content .badge {
border: 2px solid;
border-radius: 50%;
padding: 7px 10px;
position: relative;
z-index: 10;
letter-spacing: 0;
}
</style>
<span class="badge">
<content></content>
</span>
</template>
<script>
Polymer({ is: "hero-badge" });
</script>
</dom-module>

View File

@ -0,0 +1,44 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<dom-module id="hero-button">
<template>
<style>
:host ::content button.btn {
padding: 10px;
padding-top: 11px;
border-radius: 2px;
border: none;
letter-spacing: 0.5px;
outline: none;
min-width: 170px;
position: relative;
}
</style>
<button type="button" class="btn" disabled="{{disabled}}">
<content></content>
</button>
</template>
<script>
Polymer({
is: "hero-button",
properties: {
disabled: {
type: String
},
type: {
type: String,
value: 'primary'
}
},
ready: function() {
this.querySelector('.btn').className += ' btn-' + this.type;
}
});
</script>
</dom-module>

View File

@ -0,0 +1,20 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<dom-module id="hero-caption">
<template>
<style>
:host ::content strong {
font-size: 11px;
}
</style>
<div>
<strong>
<content></content>
</strong>
</div>
</template>
<script>
Polymer({ is: "hero-caption" });
</script>
</dom-module>

View File

@ -0,0 +1,36 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="../../bower_components/iron-icon/iron-icon.html">
<link rel="import" href="hero-caption.html">
<dom-module id="hero-icon">
<template>
<style>
:host {
--iron-icon-height: 40px;
--iron-icon-width: 40px;
}
:host ::content div {
margin-top: 10px;
}
</style>
<iron-icon src="{{path}}"></iron-icon>
<div>
<hero-caption>
<content></content>
</hero-caption>
</div>
</template>
<script>
Polymer({
is: "hero-icon",
properties: {
path: {
type: String
}
}
});
</script>
</dom-module>

View File

@ -0,0 +1,63 @@
<link rel="import" href="../../bower_components/polymer/polymer.html">
<link rel="import" href="hero-button.html">
<!-- From http://tympanus.net/Development/ProgressButtonStyles/ -->
<dom-module id="hero-progress-button">
<template>
<style>
:host:not([percentage="0"]) {
pointer-events: none;
}
:host ::content .text {
position: relative;
z-index: 10;
transition: transform 0.3s;
}
:host ::content .bar {
position: absolute;
left: 0;
top: 0;
width: 0;
height: 100%;
transition: width 0.3s, opacity 0.3s;
}
</style>
<hero-button disabled="{{disabled}}">
<span class="text">
<content></content>
</span>
<span class="bar"></span>
</hero-button>
</template>
<script>
Polymer({
is: "hero-progress-button",
properties: {
disabled: {
type: String
},
percentage: {
type: Number,
value: 0,
notify: true
}
},
setProgress: function(percentage) {
var bar = this.querySelector('.bar');
bar.style.width = percentage + '%';
},
ready: function() {
this.setProgress(this.percentage);
this.addEventListener('percentage-changed', function(event) {
this.setProgress(event.detail.value);
}.bind(this));
}
});
</script>
</dom-module>

79
lib/elevate.js Normal file
View File

@ -0,0 +1,79 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var _ = require('lodash');
var dialog = require('dialog');
var isElevated = require('is-elevated');
var sudoPrompt = require('sudo-prompt');
var windosu = require('windosu');
var os = require('os');
var platform = os.platform();
exports.require = function(callback) {
'use strict';
isElevated(function(error, elevated) {
if (error) {
return callback(error);
}
if (elevated) {
return callback();
}
if (!elevated) {
if (platform === 'darwin') {
sudoPrompt.setName('Herostratus');
sudoPrompt.exec(process.argv.join(' '), function(error) {
if (error) {
console.error(error.message);
process.exit(1);
}
// Don't keep the original parent process alive
process.exit(0);
});
}
else if (platform === 'win32') {
var command = _.map(process.argv, function(word) {
return '"' + word + '"';
});
windosu.exec(command.join(' '), null, function(error) {
if (error) {
console.error(error.message);
process.exit(1);
}
// Don't keep the original parent process alive
process.exit(0);
});
}
else {
dialog.showErrorBox('You don\'t have enough permissions', 'Please run this application as root or administrator');
process.exit(1);
}
}
});
};

54
lib/herostratus.js Normal file
View File

@ -0,0 +1,54 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var path = require('path');
var app = require('app');
var Menu = require('menu');
var Window = require('electron-window');
var elevate = require('./elevate');
app.on('window-all-closed', app.quit);
app.on('ready', function() {
'use strict';
// No menu bar
Menu.setApplicationMenu(null);
elevate.require(function(error) {
if (error) {
throw error;
}
var mainWindow = Window.createWindow({
width: 800,
height: 380,
resizable: false,
fullscreen: false
});
mainWindow.showUrl(path.join(__dirname, 'index.html'));
});
});

28
lib/images/burn.svg Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="999.944px" height="999.986px" viewBox="500.028 0 999.944 999.986" enable-background="new 500.028 0 999.944 999.986"
xml:space="preserve">
<g>
<g>
<path fill="#FFFFFF" d="M1174.844,499.923c0-96.492-78.443-174.991-174.991-174.991c-96.492,0-174.991,78.498-174.991,174.991
s78.498,174.991,174.991,174.991c50.949,0,96.548-22.254,128.442-57.25l-29.505-94.243l67.853,26.292
C1171.491,533.927,1174.844,517.428,1174.844,499.923z M999.853,599.922c-55.196,0-99.999-44.802-99.999-99.999
s44.802-99.999,99.999-99.999c55.196,0,99.999,44.802,99.999,99.999S1055.091,599.922,999.853,599.922z"/>
<path fill="#FFFFFF" d="M1109.296,675.808c7.055-8.843,11.693-15.255,15.395-20.802c-34.353,27.647-77.339,44.9-124.74,44.9
c-110.295,0-199.983-89.689-199.983-199.983S889.656,299.94,999.951,299.94s199.983,89.689,199.983,199.983
c0,20.648-4.051,40.248-9.849,58.996l5.7,2.151c15.395,5.951,29.854,13.495,43.098,22.255
c-2.249-40.542,6.147-85.302,44.649-129.587l72.1-83.192l15.2,108.995c4.904,34.995,42.847,82.899,76.389,125.187
c10.505,13.3,21.053,26.655,31.251,40.248c13.9-45.948,21.5-94.592,21.5-144.997C1499.972,223.886,1276.128,0,999.993,0
S500.028,223.886,500.028,499.979s223.83,499.965,499.965,499.965c44.048,0,86.489-6.245,127.24-17.002
c-44.802-45.096-72.24-106.593-72.24-173.286C1054.896,744.206,1086.245,704.712,1109.296,675.808z"/>
<path fill="#FFFFFF" d="M1283.476,756.709c-3.604-54.791-29.603-119.445-105.95-149.048
c26.948,86.252-72.743,101.438-72.743,202.037c0,91.589,62.698,167.838,147.246,190.288c-27.843-25.845,10.1-76.096-18.846-112.6
c36.35,11.553,69.097,38.153,69.097,84.799c36.392-24.755-8.955-92.497,58.354-137.341
c-20.592,61.092,34.995,90.639,25.901,130.887c-2.654,11.805-8.494,20.899-15.898,28.597
c75.243-27.954,129.196-99.691,129.196-184.644c0-109.093-164.289-219.933-178.637-323.228
C1233.183,587.921,1321.782,652.017,1283.476,756.709z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

18
lib/images/drive.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="134.229px" height="134.229px" viewBox="0 0 134.229 134.229" enable-background="new 0 0 134.229 134.229"
xml:space="preserve">
<g>
<g>
<path fill="#FFFFFF" d="M21.343,112.528c2.317,0,4.195,1.875,4.195,4.189c0,2.319-1.878,4.201-4.195,4.201
c-2.32,0-4.199-1.882-4.199-4.201C17.144,114.403,19.022,112.528,21.343,112.528z"/>
<path fill="#FFFFFF" d="M131.246,110.53L119.604,5.8C119.25,2.615,116.047,0,112.48,0H21.754c-3.568,0-6.777,2.615-7.127,5.8
L2.984,110.53c0,0.129-0.061,0.232-0.061,0.359v11.667c0,6.437,5.237,11.673,11.667,11.673h105.05
c6.431,0,11.667-5.236,11.667-11.673v-11.667C131.307,110.762,131.246,110.652,131.246,110.53z M125.474,122.556
c0,3.222-2.631,5.84-5.84,5.84H14.59c-3.206,0-5.836-2.618-5.836-5.84v-11.667c0-3.221,2.63-5.839,5.836-5.839h105.05
c3.203,0,5.834,2.618,5.834,5.839V122.556L125.474,122.556z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

16
lib/images/image.svg Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="41px" height="41px" viewBox="0 0 41 41" enable-background="new 0 0 41 41" xml:space="preserve">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M20.5,0C31.822,0,41,9.178,41,20.5S31.822,41,20.5,41
S0,31.822,0,20.5S9.178,0,20.5,0z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#555760" d="M20.5,12c4.694,0,8.5,3.806,8.5,8.5S25.194,29,20.5,29
S12,25.194,12,20.5S15.806,12,20.5,12z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" d="M20.5,12.625c4.349,0,7.875,3.526,7.875,7.875
s-3.526,7.875-7.875,7.875s-7.875-3.526-7.875-7.875S16.151,12.625,20.5,12.625z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#555760" d="M20.5,16c2.485,0,4.5,2.015,4.5,4.5S22.985,25,20.5,25
S16,22.985,16,20.5S18.015,16,20.5,16z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

104
lib/index.html Normal file
View File

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html>
<head>
<title>Herostratus</title>
<link rel="stylesheet" type="text/css" href="../node_modules/flexboxgrid/dist/flexboxgrid.css">
<link rel="stylesheet" type="text/css" href="../node_modules/angular/angular-csp.css">
<link rel="stylesheet" type="text/css" href="../build/css/main.css">
<link rel="import" href="components/hero-badge.html">
<link rel="import" href="components/hero-caption.html">
<link rel="import" href="components/hero-icon.html">
<link rel="import" href="components/hero-button.html">
<link rel="import" href="components/hero-progress-button.html">
<script src="../build/browser/app.js"></script>
</head>
<body ng-app="Herostratus" ng-controller="AppController as app" ng-cloak>
<div class="content row middle-xs space-horizontal-large">
<div class="col-xs">
<div class="row around-xs space-top-large">
<div class="col-xs">
<div class="box text-center">
<hero-icon path="images/image.svg">SELECT IMAGE</hero-icon>
<hero-badge class="block space-vertical-medium">1</hero-badge>
<div class="space-vertical-large">
<div ng-hide="app.selection.hasImage()">
<hero-button ng-click="app.selectImage()">Select image</hero-button>
</div>
<div ng-show="app.selection.hasImage()">
<span>{{ app.selection.getImage() | basename }}</span>
</div>
</div>
</div>
</div>
<div class="col-xs">
<div class="box text-center relative">
<div class="step-border-left" ng-disabled="!app.selection.hasImage()"></div>
<div class="step-border-right" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()"></div>
<hero-icon path="images/drive.svg" ng-disabled="!app.selection.hasImage()">SELECT DRIVE</hero-icon>
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage()">2</hero-badge>
<div class="space-vertical-large">
<div ng-hide="app.selection.hasDrive()">
<div ng-show="app.scanner.hasAvailableDrives() || !app.selection.hasImage()">
<div class="btn-group" uib-dropdown>
<hero-button ng-disabled="!app.selection.hasImage()"
uib-dropdown-toggle>Select drive</hero-button>
<ul class="dropdown-menu">
<li ng-repeat="drive in app.scanner.drives">
<a href="#" ng-click="app.selectDrive(drive.device)">{{ drive.device }} - {{ drive.size }}</a>
</li>
</ul>
</div>
</div>
<div ng-hide="app.scanner.hasAvailableDrives() || !app.selection.hasImage()">
<hero-button type="danger">Connect a drive</hero-button>
</div>
</div>
<div ng-show="app.selection.hasDrive()">
<span>{{ app.selection.getDrive() }}</span>
</div>
</div>
</div>
</div>
<div class="col-xs">
<div class="box text-center">
<hero-icon path="images/burn.svg" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">BURN IMAGE</hero-icon>
<hero-badge class="block space-vertical-medium" ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">3</hero-badge>
<div class="space-vertical-large">
<hero-progress-button percentage="{{ app.writer.progress }}"
ng-click="app.burn(app.selection.getImage(), app.selection.getDrive())"
ng-disabled="!app.selection.hasImage() || !app.selection.hasDrive()">
<span ng-show="app.writer.progress == 100">Done</span>
<span ng-show="app.writer.progress == 0 && !app.writer.isBurning()">Burn!</span>
<span ng-show="app.writer.progress == 0 && app.writer.isBurning()">Starting...</span>
<span ng-show="app.writer.progress != 0 && app.writer.progress != 100">{{ app.writer.progress }}%</span>
</hero-progress-button>
</div>
</div>
</div>
</div>
<div class="row around-xs section-footer">
<div class="col-xs">
<div class="box text-center">
<hero-caption ng-click="app.open('https://resin.io')">POWERED BY RESIN.IO</hero-caption>
</div>
</div>
</div>
</div>
</div>
</body>
</html>

27
lib/scss/_angular.scss Normal file
View File

@ -0,0 +1,27 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
[ng-click] {
cursor: pointer;
}

43
lib/scss/_desktop.scss Normal file
View File

@ -0,0 +1,43 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
// Prevent text selection
body {
-webkit-user-select: none;
}
// Prevent WebView bounce effect in OS X
html,
body {
height: 100%;
width: 100%;
}
html {
overflow: hidden;
}
body {
overflow: auto;
-webkit-overflow-scrolling: touch;
}

117
lib/scss/main.scss Normal file
View File

@ -0,0 +1,117 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
$body-bg: rgb(83, 87, 96);
$text-color: white;
$brand-primary: rgb(87, 147, 219);
$font-size-base: 13px;
$cursor-disabled: initial;
$color-disabled: rgb(120, 124, 127);
$btn-disabled: rgb(49, 51, 57);
$badge-disabled: rgb(92, 94, 92);
$btn-padding: 10px;
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap";
@import "./angular";
@import "./desktop";
@import "./modules/space";
hero-badge .badge {
background-color: $body-bg;
}
hero-badge[disabled] .badge {
background-color: $badge-disabled;
color: $color-disabled;
}
hero-icon[disabled] hero-caption {
color: $color-disabled;
}
hero-button .btn {
&[disabled] {
background-color: $btn-disabled;
color: $color-disabled;
&:hover {
background-color: lighten($btn-disabled, 2);
}
}
}
hero-progress-button .bar {
background: lighten($brand-primary, 5);
}
hero-progress-button[percentage="100"] .bar {
background-color: $brand-success;
}
.block {
display: block;
}
.dropdown-menu {
width: 170px;
}
body {
letter-spacing: 1px;
}
.content {
height: 100%;
}
.relative {
position: relative;
}
.section-footer {
color: $gray-dark;
margin-top: 50px;
}
.step-border {
height: 2px;
background-color: $text-color;
position: absolute;
width: 230px;
top: 55%;
&[disabled] {
background-color: $color-disabled;
}
}
.step-border-left {
@extend .step-border;
left: -120px;
}
.step-border-right {
@extend .step-border;
right: -120px;
}

View File

@ -0,0 +1,41 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
$spacing-large: 25px;
$spacing-medium: 15px;
.space-vertical-medium {
margin: $spacing-medium 0;
}
.space-vertical-large {
margin: $spacing-large 0;
}
.space-horizontal-large {
margin: 0 $spacing-large;
}
.space-top-large {
margin-top: $spacing-large;
}

56
lib/src/dialog.js Normal file
View File

@ -0,0 +1,56 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var dialog = require('dialog');
var _ = require('lodash');
/**
* @summary Open an image selection dialog
* @function
* @public
*
* @description
* Notice that by image, we mean *.img/*.iso files.
*
* @fulfil {String} - selected image
* @returns {Promise};
*
* @example
* dialog.selectImage().then(function(image) {
* console.log('The selected image is', image);
* });
*/
exports.selectImage = function() {
'use strict';
return new Promise(function(resolve, reject) {
dialog.showOpenDialog({
properties: [ 'openFile' ],
filters: [
{ name: 'IMG/ISO', extensions: [ 'img', 'iso' ] }
]
}, function(file) {
return resolve(_.first(file));
});
});
};

80
lib/src/drives.js Normal file
View File

@ -0,0 +1,80 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var drivelist = require('drivelist');
/**
* @summary List all available drives
* @function
* @public
*
* @description
* See https://github.com/resin-io/drivelist
*
* @fulfil {Object[]} - available drives
* @returns {Promise}
*
* @example
* drives.list().then(function(drives) {
* drives.forEach(function(drive) {
* console.log(drive.device);
* });
* });
*/
exports.list = function() {
'use strict';
return new Promise(function(resolve, reject) {
drivelist.list(function(error, drives) {
if (error) {
return reject(error);
}
return resolve(drives);
});
});
};
/**
* @summary List all available removable drives
* @function
* @public
*
* @fulfil {Object[]} - available removable drives
* @returns {Promise}
*
* @example
* drives.listRemovable().then(function(drives) {
* drives.forEach(function(drive) {
* console.log(drive.device);
* });
* });
*/
exports.listRemovable = function() {
'use strict';
return exports.list().then(function(drives) {
return drives.filter(function(drive) {
return !drive.system;
});
});
};

115
lib/src/writer.js Normal file
View File

@ -0,0 +1,115 @@
/* The MIT License
*
* Copyright (c) 2015 Resin.io. https://resin.io.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
var imageWrite = require('resin-image-write');
var umount = require('umount');
var fs = require('fs');
/**
* @summary Get image readable stream
* @function
* @private
*
* @description
* This function adds a custom `.length` property
* to the stream which equals the image size in bytes.
*
* @param {String} image - path to image
* @returns {ReadableStream} image stream
*
* @example
* var stream = writer.getImageStream('foo/bar/baz.img');
*/
exports.getImageStream = function(image) {
'use strict';
var stream = fs.createReadStream(image);
stream.length = fs.statSync(image).size;
return stream;
};
/**
* @summary Unmount a drive
* @function
* @private
*
* @param {String} disk - disk device
* @returns {Promise}
*
* @example
* writer.unmountDisk('/dev/disk2').then(function() {
* console.log('Disk unmounted!');
* });
*/
exports.unmountDisk = function(disk) {
'use strict';
return new Promise(function(resolve, reject) {
umount.umount(disk, function(error) {
if (error) {
return reject(error);
}
return resolve();
});
});
};
/**
* @summary Write an image to a disk drive
* @function
* @public
*
* @description
* See https://github.com/resin-io/resin-image-write for information
* about the `state` object passed to `onProgress` callback.
*
* @param {String} image - path to image
* @param {String} drive - drive device
* @param {Function} onProgress - on progress callback (state)
*
* @returns {Promise}
*
* @example
* writer.writeImage('path/to/image.img', '/dev/disk2', function(state) {
* console.log(state.percentage);
* }).then(function() {
* console.log('Done!');
* });
*/
exports.writeImage = function(image, drive, onProgress) {
'use strict';
return exports.unmountDisk(drive).then(function() {
var stream = exports.getImageStream(image);
return imageWrite.write(drive, stream);
}).then(function(writer) {
return new Promise(function(resolve, reject) {
writer.on('progress', onProgress);
writer.on('error', reject);
writer.on('done', resolve);
});
}).then(function() {
return exports.unmountDisk(drive);
});
};

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "herostratus",
"version": "0.0.1",
"main": "lib/herostratus.js",
"description": "An image burner with support for Windows, OS X and GNU/Linux.",
"homepage": "https://github.com/resin-io/herostratus",
"repository": {
"type": "git",
"url": "git@github.com:resin-io/herostratus.git"
},
"scripts": {
"test:main": "electron-mocha --recursive tests/src",
"test:browser": "electron-mocha --recursive --renderer tests/browser",
"test": "npm run-script test:main && npm run-script test:browser",
"start": "electron lib/herostratus.js"
},
"author": "Juan Cruz Viotti <juan@resin.io>",
"license": "MIT",
"dependencies": {
"angular": "^1.4.6",
"angular-ui-bootstrap": "^0.14.2",
"bootstrap-sass": "^3.3.5",
"drivelist": "^2.0.0",
"electron-window": "^0.6.0",
"flexboxgrid": "^6.3.0",
"is-elevated": "^1.0.0",
"lodash": "^3.10.1",
"resin-image-write": "^2.0.5",
"sudo-prompt": "^1.1.7",
"umount": "^1.1.1",
"windosu": "^0.1.3"
},
"devDependencies": {
"angular-mocks": "^1.4.7",
"browserify": "^12.0.1",
"electron-mocha": "^0.5.0",
"electron-prebuilt": "^0.33.0",
"gulp": "^3.9.0",
"gulp-jshint": "^1.11.2",
"gulp-sass": "^2.0.4",
"jshint-stylish": "^2.0.1",
"mochainon": "^1.0.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
}
}

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -0,0 +1,181 @@
var m = require('mochainon');
var angular = require('angular');
window.mocha = true;
require('../../../lib/browser/modules/drive-scanner');
require('angular-mocks');
describe('Browser: DriveScanner', function() {
'use strict';
beforeEach(angular.mock.module('herostratus.drive-scanner'));
describe('DriveScannerRefreshService', function() {
var DriveScannerRefreshService;
var $interval;
beforeEach(angular.mock.inject(function(_$interval_, _DriveScannerRefreshService_) {
$interval = _$interval_;
DriveScannerRefreshService = _DriveScannerRefreshService_;
}));
describe('.every()', function() {
it('should call the function right away', function() {
var spy = m.sinon.spy();
DriveScannerRefreshService.every(spy, 1000);
DriveScannerRefreshService.stop();
m.chai.expect(spy).to.have.been.calledOnce;
});
it('should call the function in an interval', function() {
var spy = m.sinon.spy();
DriveScannerRefreshService.every(spy, 100);
// 400ms = 100ms / 4 + 1 (the initial call)
$interval.flush(400);
DriveScannerRefreshService.stop();
m.chai.expect(spy).to.have.callCount(5);
});
});
});
describe('DriveScannerService', function() {
var $interval;
var $q;
var DriveScannerService;
beforeEach(angular.mock.inject(function(_$interval_, _$q_, _DriveScannerService_) {
$interval = _$interval_;
$q = _$q_;
DriveScannerService = _DriveScannerService_;
}));
it('should have no drives by default', function() {
m.chai.expect(DriveScannerService.drives).to.deep.equal([]);
});
describe('given no drives', function() {
describe('.hasAvailableDrives()', function() {
it('should return false', function() {
var hasDrives = DriveScannerService.hasAvailableDrives();
m.chai.expect(hasDrives).to.be.false;
});
});
describe('.setDrives()', function() {
it('should be able to set drives', function() {
var drives = [
{
device: '/dev/sdb',
description: 'Foo',
size: '14G',
mountpoint: '/mnt/foo',
system: false
}
];
DriveScannerService.setDrives(drives);
m.chai.expect(DriveScannerService.drives).to.deep.equal(drives);
});
});
});
describe('given drives', function() {
beforeEach(function() {
this.drives = [
{
device: '/dev/sdb',
description: 'Foo',
size: '14G',
mountpoint: '/mnt/foo',
system: false
},
{
device: '/dev/sdc',
description: 'Bar',
size: '14G',
mountpoint: '/mnt/bar',
system: false
}
];
DriveScannerService.drives = this.drives;
});
describe('.hasAvailableDrives()', function() {
it('should return true', function() {
var hasDrives = DriveScannerService.hasAvailableDrives();
m.chai.expect(hasDrives).to.be.true;
});
});
describe('.setDrives()', function() {
it('should keep the same drives if equal', function() {
DriveScannerService.setDrives(this.drives);
m.chai.expect(DriveScannerService.drives).to.deep.equal(this.drives);
});
it('should consider drives with different $$hashKey the same', function() {
this.drives[0].$$haskey = 1234;
DriveScannerService.setDrives(this.drives);
m.chai.expect(DriveScannerService.drives).to.deep.equal(this.drives);
});
});
});
describe('given available drives', function() {
beforeEach(function() {
this.drives = [
{
device: '/dev/sdb',
description: 'Foo',
size: '14G',
mountpoint: '/mnt/foo',
system: false
},
{
device: '/dev/sdc',
description: 'Bar',
size: '14G',
mountpoint: '/mnt/bar',
system: false
}
];
this.scanStub = m.sinon.stub(DriveScannerService, 'scan');
this.scanStub.returns($q.resolve(this.drives));
});
afterEach(function() {
this.scanStub.restore();
});
it('should set the drives to the scanned ones', function() {
DriveScannerService.start(200);
$interval.flush(400);
m.chai.expect(DriveScannerService.drives).to.deep.equal(this.drives);
DriveScannerService.stop();
});
});
});
});

View File

@ -0,0 +1,144 @@
var m = require('mochainon');
var angular = require('angular');
window.mocha = true;
require('../../../lib/browser/modules/image-writer');
require('angular-mocks');
describe('Browser: ImageWriter', function() {
'use strict';
beforeEach(angular.mock.module('herostratus.image-writer'));
describe('ImageWriterService', function() {
var $q;
var $timeout;
var $rootScope;
var ImageWriterService;
beforeEach(angular.mock.inject(function(_$q_, _$timeout_, _$rootScope_, _ImageWriterService_) {
$q = _$q_;
$timeout = _$timeout_;
$rootScope = _$rootScope_;
ImageWriterService = _ImageWriterService_;
}));
it('should set progress to zero by default', function() {
m.chai.expect(ImageWriterService.progress).to.equal(0);
});
describe('.isBurning()', function() {
it('should return false by default', function() {
m.chai.expect(ImageWriterService.isBurning()).to.be.false;
});
it('should return true if burning', function() {
ImageWriterService.setBurning(true);
m.chai.expect(ImageWriterService.isBurning()).to.be.true;
});
});
describe('.setBurning()', function() {
it('should be able to set burning to true', function() {
ImageWriterService.setBurning(true);
m.chai.expect(ImageWriterService.isBurning()).to.be.true;
});
it('should be able to set burning to false', function() {
ImageWriterService.setBurning(false);
m.chai.expect(ImageWriterService.isBurning()).to.be.false;
});
it('should cast to boolean by default', function() {
ImageWriterService.setBurning('hello');
m.chai.expect(ImageWriterService.isBurning()).to.be.true;
ImageWriterService.setBurning('');
m.chai.expect(ImageWriterService.isBurning()).to.be.false;
});
});
describe('.setProgress()', function() {
it('should be able to set the progress', function() {
ImageWriterService.setProgress(50);
$timeout.flush();
m.chai.expect(ImageWriterService.progress).to.equal(50);
});
it('should floor the percentage', function() {
ImageWriterService.setProgress(49.9999);
$timeout.flush();
m.chai.expect(ImageWriterService.progress).to.equal(49);
});
});
describe('.burn()', function() {
describe('given a succesful write', function() {
beforeEach(function() {
this.performWriteStub = m.sinon.stub(ImageWriterService, 'performWrite');
this.performWriteStub.returns($q.resolve());
});
afterEach(function() {
this.performWriteStub.restore();
});
it('should set burning to false when done', function() {
ImageWriterService.burn('foo.img', '/dev/disk2');
$rootScope.$apply();
m.chai.expect(ImageWriterService.isBurning()).to.be.false;
});
it('should prevent writing more than once', function() {
ImageWriterService.burn('foo.img', '/dev/disk2');
ImageWriterService.burn('foo.img', '/dev/disk2');
$rootScope.$apply();
m.chai.expect(this.performWriteStub).to.have.been.calledOnce;
});
});
describe('given an unsuccesful write', function() {
beforeEach(function() {
this.performWriteStub = m.sinon.stub(ImageWriterService, 'performWrite');
this.performWriteStub.returns($q.reject(new Error('write error')));
});
afterEach(function() {
this.performWriteStub.restore();
});
it('should set burning to false when done', function() {
ImageWriterService.burn('foo.img', '/dev/disk2');
$rootScope.$apply();
m.chai.expect(ImageWriterService.isBurning()).to.be.false;
});
it('should be rejected with the error', function() {
var rejection;
ImageWriterService.burn('foo.img', '/dev/disk2').catch(function(error) {
rejection = error;
});
$rootScope.$apply();
m.chai.expect(rejection).to.be.an.instanceof(Error);
m.chai.expect(rejection.message).to.equal('write error');
});
});
});
});
});

View File

@ -0,0 +1,35 @@
var m = require('mochainon');
var angular = require('angular');
var os = require('os');
window.mocha = true;
require('../../../lib/browser/modules/path');
require('angular-mocks');
describe('Browser: Path', function() {
'use strict';
beforeEach(angular.mock.module('herostratus.path'));
describe('BasenameFilter', function() {
var basenameFilter;
beforeEach(angular.mock.inject(function(_basenameFilter_) {
basenameFilter = _basenameFilter_;
}));
it('should return the basename', function() {
var isWindows = os.platform() === 'win32';
var basename;
if (isWindows) {
basename = basenameFilter('C:\\Users\\jviotti\\foo.img');
} else {
basename = basenameFilter('/Users/jviotti/foo.img');
}
m.chai.expect(basename).to.equal('foo.img');
});
});
});

View File

@ -0,0 +1,146 @@
var m = require('mochainon');
var angular = require('angular');
window.mocha = true;
require('../../../lib/browser/modules/selection-state');
require('angular-mocks');
describe('Browser: SelectionState', function() {
'use strict';
beforeEach(angular.mock.module('herostratus.selection-state'));
describe('SelectionStateService', function() {
var SelectionStateService;
beforeEach(angular.mock.inject(function(_SelectionStateService_) {
SelectionStateService = _SelectionStateService_;
}));
describe('given a clean state', function() {
it('getDrive() should return undefined', function() {
var drive = SelectionStateService.getDrive();
m.chai.expect(drive).to.be.undefined;
});
it('getImage() should return undefined', function() {
var image = SelectionStateService.getImage();
m.chai.expect(image).to.be.undefined;
});
it('hasDrive() should return false', function() {
var hasDrive = SelectionStateService.hasDrive();
m.chai.expect(hasDrive).to.be.false;
});
it('hasImage() should return false', function() {
var hasImage = SelectionStateService.hasImage();
m.chai.expect(hasImage).to.be.false;
});
});
describe('given a drive', function() {
beforeEach(function() {
SelectionStateService.setDrive('/dev/disk2');
});
describe('.getDrive()', function() {
it('should return the drive', function() {
var drive = SelectionStateService.getDrive();
m.chai.expect(drive).to.equal('/dev/disk2');
});
});
describe('.hasDrive()', function() {
it('should return true', function() {
var hasDrive = SelectionStateService.hasDrive();
m.chai.expect(hasDrive).to.be.true;
});
});
describe('.setDrive()', function() {
it('should override the drive', function() {
SelectionStateService.setDrive('/dev/disk5');
var drive = SelectionStateService.getDrive();
m.chai.expect(drive).to.equal('/dev/disk5');
});
});
});
describe('given no drive', function() {
describe('.setDrive()', function() {
it('should be able to set a drive', function() {
SelectionStateService.setDrive('/dev/disk5');
var drive = SelectionStateService.getDrive();
m.chai.expect(drive).to.equal('/dev/disk5');
});
});
});
describe('given an image', function() {
beforeEach(function() {
SelectionStateService.setImage('foo.img');
});
describe('.getImage()', function() {
it('should return the image', function() {
var image = SelectionStateService.getImage();
m.chai.expect(image).to.equal('foo.img');
});
});
describe('.hasImage()', function() {
it('should return true', function() {
var hasImage = SelectionStateService.hasImage();
m.chai.expect(hasImage).to.be.true;
});
});
describe('.setImage()', function() {
it('should override the image', function() {
SelectionStateService.setImage('bar.img');
var image = SelectionStateService.getImage();
m.chai.expect(image).to.equal('bar.img');
});
});
});
describe('given no image', function() {
describe('.setImage()', function() {
it('should be able to set an image', function() {
SelectionStateService.setImage('foo.img');
var image = SelectionStateService.getImage();
m.chai.expect(image).to.equal('foo.img');
});
});
});
});
});

32
tests/src/dialog.spec.js Normal file
View File

@ -0,0 +1,32 @@
var m = require('mochainon');
var electronDialog = require('dialog');
var dialog = require('../../lib/src/dialog');
describe('Dialog:', function() {
'use strict';
describe('.selectImage()', function() {
describe('given the users performs a selection', function() {
beforeEach(function() {
this.showOpenDialogStub = m.sinon.stub(electronDialog, 'showOpenDialog');
this.showOpenDialogStub.yields([ 'foo/bar' ]);
});
afterEach(function() {
this.showOpenDialogStub.restore();
});
it('should eventually equal the file', function(done) {
dialog.selectImage().then(function(image) {
m.chai.expect(image).to.equal('foo/bar');
done();
}).catch(done);
});
});
});
});

217
tests/src/drives.spec.js Normal file
View File

@ -0,0 +1,217 @@
var m = require('mochainon');
var drivelist = require('drivelist');
var drives = require('../../lib/src/drives');
describe('Drives:', function() {
'use strict';
describe('.list()', function() {
describe('given no available drives', function() {
beforeEach(function() {
this.drivelistListStub = m.sinon.stub(drivelist, 'list');
this.drivelistListStub.yields(null, []);
});
afterEach(function() {
this.drivelistListStub.restore();
});
it('should eventually equal an empty array', function(done) {
drives.list().then(function(drives) {
m.chai.expect(drives).to.deep.equal([]);
done();
}).catch(done);
});
});
describe('given available drives', function() {
beforeEach(function() {
this.drives = [
{
device: '/dev/sda',
description: 'WDC WD10JPVX-75J',
size: '931.5G',
mountpoint: '/',
system: true
}
];
this.drivelistListStub = m.sinon.stub(drivelist, 'list');
this.drivelistListStub.yields(null, this.drives);
});
afterEach(function() {
this.drivelistListStub.restore();
});
it('should eventually equal the drives', function(done) {
drives.list().then(function(drives) {
m.chai.expect(drives).to.deep.equal(this.drives);
done();
}.bind(this)).catch(done);
});
});
describe('given an error when listing the drives', function() {
beforeEach(function() {
this.drivelistListStub = m.sinon.stub(drivelist, 'list');
this.drivelistListStub.yields(new Error('scan error'));
});
afterEach(function() {
this.drivelistListStub.restore();
});
it('should be rejected with the error', function(done) {
drives.list().catch(function(error) {
m.chai.expect(error).to.be.an.instanceof(Error);
m.chai.expect(error.message).to.equal('scan error');
return done();
}).catch(done);
});
});
});
describe('.listRemovable()', function() {
describe('given no available drives', function() {
beforeEach(function() {
this.drivesListStub = m.sinon.stub(drives, 'list');
this.drivesListStub.returns(Promise.resolve([]));
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should eventually equal an empty array', function(done) {
drives.listRemovable().then(function(drives) {
m.chai.expect(drives).to.deep.equal([]);
done();
}).catch(done);
});
});
describe('given available system drives', function() {
beforeEach(function() {
this.drives = [
{
device: '/dev/sda',
description: 'WDC WD10JPVX-75J',
size: '931.5G',
mountpoint: '/',
system: true
}
];
this.drivesListStub = m.sinon.stub(drives, 'list');
this.drivesListStub.returns(Promise.resolve(this.drives));
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should eventually equal an empty array', function(done) {
drives.listRemovable().then(function(drives) {
m.chai.expect(drives).to.deep.equal([]);
done();
}).catch(done);
});
});
describe('given available system and removable drives', function() {
beforeEach(function() {
this.drives = [
{
device: '/dev/sda',
description: 'WDC WD10JPVX-75J',
size: '931.5G',
mountpoint: '/',
system: true
},
{
device: '/dev/sdb',
description: 'Foo',
size: '14G',
mountpoint: '/mnt/foo',
system: false
},
{
device: '/dev/sdc',
description: 'Bar',
size: '14G',
mountpoint: '/mnt/bar',
system: false
}
];
this.drivesListStub = m.sinon.stub(drives, 'list');
this.drivesListStub.returns(Promise.resolve(this.drives));
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should eventually become the removable drives', function(done) {
drives.listRemovable().then(function(drives) {
m.chai.expect(drives).to.deep.equal([
{
device: '/dev/sdb',
description: 'Foo',
size: '14G',
mountpoint: '/mnt/foo',
system: false
},
{
device: '/dev/sdc',
description: 'Bar',
size: '14G',
mountpoint: '/mnt/bar',
system: false
}
]);
done();
}).catch(done);
});
});
describe('given an error when listing the drives', function() {
beforeEach(function() {
this.drivesListStub = m.sinon.stub(drives, 'list');
this.drivesListStub.returns(Promise.reject(new Error('scan error')));
});
afterEach(function() {
this.drivesListStub.restore();
});
it('should be rejected with the error', function(done) {
drives.listRemovable().catch(function(error) {
m.chai.expect(error).to.be.an.instanceof(Error);
m.chai.expect(error.message).to.equal('scan error');
return done();
}).catch(done);
});
});
});
});

77
tests/src/writer.spec.js Normal file
View File

@ -0,0 +1,77 @@
var m = require('mochainon');
var ReadableStream = require('stream').Readable;
var path = require('path');
var umount = require('umount');
var writer = require('../../lib/src/writer');
describe('Writer:', function() {
'use strict';
describe('.getImageStream()', function() {
describe('given a valid image', function() {
beforeEach(function() {
this.image = path.join(__dirname, '..', 'utils', 'data.random');
});
it('should return a readable stream', function() {
var stream = writer.getImageStream(this.image);
m.chai.expect(stream).to.be.an.instanceof(ReadableStream);
});
it('should append a .length property with the correct size', function() {
var stream = writer.getImageStream(this.image);
m.chai.expect(stream.length).to.equal(2097152);
});
});
});
describe('.unmountDisk()', function() {
describe('given a successful unmount', function() {
beforeEach(function() {
this.umountStub = m.sinon.stub(umount, 'umount');
this.umountStub.yields(null);
});
afterEach(function() {
this.umountStub.restore();
});
it('should eventually resolve undefined', function(done) {
writer.unmountDisk('/dev/disk2').then(function() {
m.chai.expect(arguments[0]).to.be.undefined;
done();
}).catch(done);
});
});
describe('given an unsuccessful unmount', function() {
beforeEach(function() {
this.umountStub = m.sinon.stub(umount, 'umount');
this.umountStub.yields(new Error('unmount error'));
});
afterEach(function() {
this.umountStub.restore();
});
it('should be rejected with the error', function(done) {
writer.unmountDisk('/dev/disk2').catch(function(error) {
m.chai.expect(error).to.be.an.instanceof(Error);
m.chai.expect(error.message).to.equal('unmount error');
done();
}).catch(done);
});
});
});
});

BIN
tests/utils/data.random Normal file

Binary file not shown.