mirror of
https://github.com/balena-io/balena-cli.git
synced 2024-12-18 21:27:51 +00:00
Update lib/actions/local/flash.coffee
* switch to typescript * replace etcher-image-stream with etcher-sdk Change-type: patch
This commit is contained in:
parent
bc41ff0540
commit
f9390ceb10
@ -1,120 +0,0 @@
|
||||
###
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
###
|
||||
|
||||
module.exports =
|
||||
signature: 'local flash <image>'
|
||||
description: 'Flash an image to a drive'
|
||||
help: '''
|
||||
Use this command to flash a balenaOS image to a drive.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local flash path/to/balenaos.img
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
||||
'''
|
||||
options: [
|
||||
signature: 'yes'
|
||||
boolean: true
|
||||
description: 'confirm non-interactively'
|
||||
alias: 'y'
|
||||
,
|
||||
signature: 'drive'
|
||||
parameter: 'drive'
|
||||
description: 'drive'
|
||||
alias: 'd'
|
||||
]
|
||||
root: true
|
||||
action: (params, options, done) ->
|
||||
|
||||
_ = require('lodash')
|
||||
os = require('os')
|
||||
Promise = require('bluebird')
|
||||
umountAsync = Promise.promisify(require('umount').umount)
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
driveListAsync = Promise.promisify(require('drivelist').list)
|
||||
chalk = require('chalk')
|
||||
visuals = require('resin-cli-visuals')
|
||||
form = require('resin-cli-form')
|
||||
imageWrite = require('etcher-image-write')
|
||||
|
||||
form.run [
|
||||
{
|
||||
message: 'Select drive'
|
||||
type: 'drive'
|
||||
name: 'drive'
|
||||
},
|
||||
{
|
||||
message: 'This will erase the selected drive. Are you sure?'
|
||||
type: 'confirm'
|
||||
name: 'yes'
|
||||
default: false
|
||||
}
|
||||
],
|
||||
override:
|
||||
drive: options.drive
|
||||
|
||||
# If `options.yes` is `false`, pass `undefined`,
|
||||
# otherwise the question will not be asked because
|
||||
# `false` is a defined value.
|
||||
yes: options.yes || undefined
|
||||
|
||||
# TODO: dedupe with the resin-device-operations
|
||||
.then (answers) ->
|
||||
if answers.yes isnt true
|
||||
console.log(chalk.red.bold('Aborted image flash'))
|
||||
process.exit(0)
|
||||
|
||||
driveListAsync().then (drives) ->
|
||||
selectedDrive = _.find(drives, device: answers.drive)
|
||||
|
||||
if not selectedDrive?
|
||||
throw new Error("Drive not found: #{answers.drive}")
|
||||
|
||||
return selectedDrive
|
||||
.then (selectedDrive) ->
|
||||
progressBars =
|
||||
write: new visuals.Progress('Flashing')
|
||||
check: new visuals.Progress('Validating')
|
||||
|
||||
umountAsync(selectedDrive.device).then ->
|
||||
Promise.props
|
||||
imageSize: fs.statAsync(params.image).get('size'),
|
||||
imageStream: Promise.resolve(fs.createReadStream(params.image))
|
||||
driveFileDescriptor: fs.openAsync(selectedDrive.raw, 'rs+')
|
||||
.then (results) ->
|
||||
imageWrite.write
|
||||
fd: results.driveFileDescriptor
|
||||
device: selectedDrive.raw
|
||||
size: selectedDrive.size
|
||||
,
|
||||
stream: results.imageStream,
|
||||
size: results.imageSize
|
||||
,
|
||||
check: true
|
||||
.then (writer) ->
|
||||
new Promise (resolve, reject) ->
|
||||
writer.on 'progress', (state) ->
|
||||
progressBars[state.type].update(state)
|
||||
writer.on('error', reject)
|
||||
writer.on('done', resolve)
|
||||
.then ->
|
||||
if (os.platform() is 'win32') and selectedDrive.mountpoint?
|
||||
ejectAsync = Promise.promisify(require('removedrive').eject)
|
||||
return ejectAsync(selectedDrive.mountpoint)
|
||||
|
||||
return umountAsync(selectedDrive.device)
|
||||
.asCallback(done)
|
121
lib/actions/local/flash.ts
Normal file
121
lib/actions/local/flash.ts
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
Copyright 2017 Balena
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { CommandDefinition } from 'capitano';
|
||||
import chalk from 'chalk';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
|
||||
import { DriveList } from '../../utils/visuals/drive-list';
|
||||
|
||||
async function getDrive(options: {
|
||||
drive?: string;
|
||||
}): Promise<sdk.sourceDestination.BlockDevice> {
|
||||
const sdk = await import('etcher-sdk');
|
||||
|
||||
const adapter = new sdk.scanner.adapters.BlockDeviceAdapter(() => false);
|
||||
const scanner = new sdk.scanner.Scanner([adapter]);
|
||||
await scanner.start();
|
||||
let drive: sdk.sourceDestination.BlockDevice;
|
||||
if (options.drive !== undefined) {
|
||||
const d = scanner.getBy('device', options.drive);
|
||||
if (d === undefined || !(d instanceof sdk.sourceDestination.BlockDevice)) {
|
||||
throw new Error(`Drive not found: ${options.drive}`);
|
||||
}
|
||||
drive = d;
|
||||
} else {
|
||||
const driveList = new DriveList(scanner);
|
||||
drive = await driveList.run();
|
||||
}
|
||||
scanner.stop();
|
||||
return drive;
|
||||
}
|
||||
|
||||
export const flash: CommandDefinition<
|
||||
{ image: string },
|
||||
{ drive: string; yes: boolean }
|
||||
> = {
|
||||
signature: 'local flash <image>',
|
||||
description: 'Flash an image to a drive',
|
||||
//root: true,
|
||||
help: stripIndent`
|
||||
Use this command to flash a balenaOS image to a drive.
|
||||
|
||||
Examples:
|
||||
|
||||
$ balena local flash path/to/balenaos.img
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2
|
||||
$ balena local flash path/to/balenaos.img --drive /dev/disk2 --yes
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
signature: 'yes',
|
||||
boolean: true,
|
||||
description: 'confirm non-interactively',
|
||||
alias: 'y',
|
||||
},
|
||||
{
|
||||
signature: 'drive',
|
||||
parameter: 'drive',
|
||||
description: 'drive',
|
||||
alias: 'd',
|
||||
},
|
||||
],
|
||||
async action(params, options) {
|
||||
const visuals = await import('resin-cli-visuals');
|
||||
const form = await import('resin-cli-form');
|
||||
const { sourceDestination, multiWrite } = await import('etcher-sdk');
|
||||
|
||||
const drive = await getDrive(options);
|
||||
|
||||
const yes =
|
||||
options.yes ||
|
||||
(await form.ask({
|
||||
message: 'This will erase the selected drive. Are you sure?',
|
||||
type: 'confirm',
|
||||
name: 'yes',
|
||||
default: false,
|
||||
}));
|
||||
if (yes !== true) {
|
||||
console.log(chalk.red.bold('Aborted image flash'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const source = new sourceDestination.File(
|
||||
params.image,
|
||||
sourceDestination.File.OpenFlags.Read,
|
||||
);
|
||||
|
||||
const progressBars: { [key: string]: any } = {
|
||||
flashing: new visuals.Progress('Flashing'),
|
||||
verifying: new visuals.Progress('Validating'),
|
||||
};
|
||||
|
||||
await multiWrite.pipeSourceToDestinations(
|
||||
source,
|
||||
[drive],
|
||||
(_, error) => {
|
||||
// onFail
|
||||
console.log(chalk.red.bold(error.message));
|
||||
},
|
||||
(progress: sdk.multiWrite.MultiDestinationProgress) => {
|
||||
// onProgress
|
||||
progressBars[progress.type].update(progress);
|
||||
},
|
||||
true, // verify
|
||||
);
|
||||
},
|
||||
};
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
###
|
||||
|
||||
exports.configure = require('./configure')
|
||||
exports.flash = require('./flash')
|
||||
exports.flash = require('./flash').flash
|
||||
exports.logs = require('./logs')
|
||||
exports.scan = require('./scan')
|
||||
exports.ssh = require('./ssh')
|
||||
|
25
lib/utils/visuals/custom-dynamic-list.ts
Normal file
25
lib/utils/visuals/custom-dynamic-list.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import DynamicList = require('inquirer-dynamic-list');
|
||||
|
||||
export abstract class CustomDynamicList<T> extends DynamicList {
|
||||
constructor(message: string, emptyMessage: string) {
|
||||
super({ message, emptyMessage, choices: [] });
|
||||
}
|
||||
|
||||
protected abstract getThings(): Iterable<T>;
|
||||
|
||||
protected abstract format(thing: T): string;
|
||||
|
||||
refresh(): void {
|
||||
this.opt.choices.choices = [];
|
||||
this.opt.choices.realChoices = [];
|
||||
for (const thing of this.getThings()) {
|
||||
this.addChoice({ name: this.format(thing), value: thing });
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
async run(): Promise<T> {
|
||||
this.refresh();
|
||||
return await super.run();
|
||||
}
|
||||
}
|
31
lib/utils/visuals/drive-list.ts
Normal file
31
lib/utils/visuals/drive-list.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import chalk from 'chalk';
|
||||
import * as sdk from 'etcher-sdk';
|
||||
|
||||
import { CustomDynamicList } from './custom-dynamic-list';
|
||||
|
||||
export class DriveList extends CustomDynamicList<
|
||||
sdk.sourceDestination.BlockDevice
|
||||
> {
|
||||
constructor(private scanner: sdk.scanner.Scanner) {
|
||||
super(
|
||||
'Select a drive',
|
||||
`${chalk.red('x')} No available drives were detected, plug one in!`,
|
||||
);
|
||||
const refresh = this.refresh.bind(this);
|
||||
scanner.on('attach', refresh);
|
||||
scanner.on('detach', refresh);
|
||||
}
|
||||
|
||||
protected *getThings() {
|
||||
for (const drive of this.scanner.drives) {
|
||||
if (drive instanceof sdk.sourceDestination.BlockDevice) {
|
||||
yield drive;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected format(drive: sdk.sourceDestination.BlockDevice) {
|
||||
const size = drive.size / 1e9;
|
||||
return `${drive.device} (${size.toFixed(1)} GB) - ${drive.description}`;
|
||||
}
|
||||
}
|
@ -128,7 +128,7 @@
|
||||
"dockerode-options": "^0.2.1",
|
||||
"drivelist": "^5.0.22",
|
||||
"ejs": "^2.5.7",
|
||||
"etcher-image-write": "^9.0.3",
|
||||
"etcher-sdk": "^0.2.0",
|
||||
"event-stream": "3.3.4",
|
||||
"express": "^4.13.3",
|
||||
"global-tunnel-ng": "^2.1.1",
|
||||
|
@ -24,6 +24,7 @@
|
||||
},
|
||||
"include": [
|
||||
"./typings/*.d.ts",
|
||||
"./node_modules/etcher-sdk/typings/**/*.d.ts",
|
||||
"./lib/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
26
typings/inquire-dynamic-list.d.ts
vendored
Normal file
26
typings/inquire-dynamic-list.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
declare module 'inquirer-dynamic-list' {
|
||||
interface Choice {
|
||||
name: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
class DynamicList {
|
||||
opt: {
|
||||
choices: {
|
||||
choices: Choice[];
|
||||
realChoices: Choice[];
|
||||
};
|
||||
};
|
||||
|
||||
constructor(options: {
|
||||
message?: string;
|
||||
emptyMessage?: string;
|
||||
choices: Choice[];
|
||||
});
|
||||
addChoice(choice: Choice): void;
|
||||
render(): void;
|
||||
run(): Promise<any>;
|
||||
}
|
||||
|
||||
export = DynamicList;
|
||||
}
|
Loading…
Reference in New Issue
Block a user