From ba16891d46e51e9a95989a7924188ca079dbb68d Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 19:52:20 +0100 Subject: [PATCH] wip --- lib/shared/drive-constraints.js | 475 -------------------------------- lib/shared/drive-constraints.ts | 319 +++++++++++++++++++++ 2 files changed, 319 insertions(+), 475 deletions(-) delete mode 100644 lib/shared/drive-constraints.js create mode 100644 lib/shared/drive-constraints.ts diff --git a/lib/shared/drive-constraints.js b/lib/shared/drive-constraints.js deleted file mode 100644 index a719de87c98..00000000000 --- a/lib/shared/drive-constraints.js +++ /dev/null @@ -1,475 +0,0 @@ -/* - * Copyright 2016 balena.io - * - * 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. - */ - -'use strict' - -const _ = require('lodash') -const pathIsInside = require('path-is-inside') -const prettyBytes = require('pretty-bytes') -// eslint-disable-next-line node/no-missing-require -const messages = require('./messages') - -/** - * @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 - * @public - * - * @description - * This usually points out a locked SD Card. - * - * @param {Object} drive - drive - * @returns {Boolean} whether the drive is locked - * - * @example - * if (constraints.isDriveLocked({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 123456789, - * isReadOnly: true - * })) { - * console.log('This drive is locked (e.g: write-protected)'); - * } - */ -exports.isDriveLocked = (drive) => { - return Boolean(_.get(drive, [ 'isReadOnly' ], false)) -} - -/** - * @summary Check if a drive is a system drive - * @function - * @public - * @param {Object} drive - drive - * @returns {Boolean} whether the drive is a system drive - * - * @example - * if (constraints.isSystemDrive({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 123456789, - * isReadOnly: true, - * system: true - * })) { - * console.log('This drive is a system drive!'); - * } - */ -exports.isSystemDrive = (drive) => { - return Boolean(_.get(drive, [ 'isSystem' ], false)) -} - -/** - * @summary Check if a drive is source drive - * @function - * @public - * - * @description - * In the context of Etcher, a source drive is a drive - * containing the image. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive is a source drive - * - * - * @example - * if (constraints.isSourceDrive({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 123456789, - * isReadOnly: true, - * system: true, - * mountpoints: [ - * { - * path: '/Volumes/Untitled' - * } - * ] - * }, { - * path: '/Volumes/Untitled/image.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * })) { - * console.log('This drive is a source drive!'); - * } - */ -exports.isSourceDrive = (drive, image) => { - const mountpoints = _.get(drive, [ 'mountpoints' ], []) - const imagePath = _.get(image, [ 'path' ]) - - if (!imagePath || _.isEmpty(mountpoints)) { - return false - } - - return _.some(_.map(mountpoints, (mountpoint) => { - return pathIsInside(imagePath, mountpoint.path) - })) -} - -/** - * @summary Check if a drive is large enough for an image - * @function - * @public - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive is large enough - * - * @example - * if (constraints.isDriveLargeEnough({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 1000000000 - * }, { - * path: 'rpi.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * })) { - * console.log('We can flash the image to this drive!'); - * } - */ -exports.isDriveLargeEnough = (drive, image) => { - const driveSize = _.get(drive, [ 'size' ], UNKNOWN_SIZE) - - if (_.get(image, [ 'isSizeEstimated' ])) { - // If the drive size is smaller than the original image size, and - // the final image size is just an estimation, then we stop right - // here, based on the assumption that the final size will never - // be less than the original size. - if (driveSize < _.get(image, [ 'compressedSize' ], UNKNOWN_SIZE)) { - return false - } - - // If the final image size is just an estimation then consider it - // large enough. In the worst case, the user gets an error saying - // the drive has ran out of space, instead of prohibiting the flash - // at all, when the estimation may be wrong. - return true - } - - return driveSize >= _.get(image, [ 'size' ], UNKNOWN_SIZE) -} - -/** - * @summary Check if a drive is disabled (i.e. not ready for selection) - * @function - * @public - * - * @param {Object} drive - drive - * @returns {Boolean} whether the drive is disabled - * - * @example - * if (constraints.isDriveDisabled({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 1000000000, - * disabled: true - * })) { - * console.log('The drive is disabled'); - * } - */ -exports.isDriveDisabled = (drive) => { - return _.get(drive, [ 'disabled' ], false) -} - -/** - * @summary Check if a drive is valid, i.e. not locked and large enough for an image - * @function - * @public - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive is valid - * - * @example - * if (constraints.isDriveValid({ - * device: '/dev/disk2', - * name: 'My Drive', - * size: 1000000000, - * isReadOnly: false - * }, { - * path: 'rpi.img', - * size: 1000000000, - * compressedSize: 1000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 2000000000 - * })) { - * console.log('This drive is valid!'); - * } - */ -exports.isDriveValid = (drive, image) => { - return !this.isDriveLocked(drive) && - this.isDriveLargeEnough(drive, image) && - !this.isSourceDrive(drive, image) && - !this.isDriveDisabled(drive) -} - -/** - * @summary Check if a drive meets the recommended drive size suggestion - * @function - * @public - * - * @description - * If the image doesn't have a recommended size, this function returns true. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} whether the drive size is recommended - * - * @example - * const drive = { - * device: '/dev/disk2', - * name: 'My Drive', - * size: 4000000000 - * }; - * - * const image = { - * path: 'rpi.img', - * size: 2000000000, - * compressedSize: 2000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 4000000000 - * }); - * - * if (constraints.isDriveSizeRecommended(drive, image)) { - * console.log('We meet the recommended drive size!'); - * } - */ -exports.isDriveSizeRecommended = (drive, image) => { - return _.get(drive, [ 'size' ], UNKNOWN_SIZE) >= _.get(image, [ 'recommendedDriveSize' ], UNKNOWN_SIZE) -} - -/** - * @summary 64GB - * @private - * @constant - */ -exports.LARGE_DRIVE_SIZE = 64e9 - -/** - * @summary Check whether a drive's size is 'large' - * @public - * - * @param {Object} drive - drive - * @returns {Boolean} whether drive size is large - * - * @example - * if (constraints.isDriveSizeLarge(drive)) { - * console.log('Impressive') - * } - */ -exports.isDriveSizeLarge = (drive) => { - return _.get(drive, [ 'size' ], UNKNOWN_SIZE) > exports.LARGE_DRIVE_SIZE -} - -/** - * @summary Drive/image compatibility status types. - * @public - * @type {Object} - * - * @description - * Status types classifying what kind of message it is, i.e. error, warning. - */ -exports.COMPATIBILITY_STATUS_TYPES = { - WARNING: 1, - ERROR: 2 -} - -/** - * @summary Get drive/image compatibility in an object - * @function - * @public - * - * @description - * Given an image and a drive, return their compatibility status object - * containing the status type (ERROR, WARNING), and accompanying - * status message. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Object[]} list of compatibility status objects - * - * @example - * const drive = { - * device: '/dev/disk2', - * name: 'My Drive', - * size: 4000000000 - * }; - * - * const image = { - * path: '/path/to/rpi.img', - * size: 2000000000, - * compressedSize: 2000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 4000000000 - * }); - * - * const statuses = constraints.getDriveImageCompatibilityStatuses(drive, image); - * - * for ({ type, message } of statuses) { - * if (type === constraints.COMPATIBILITY_STATUS_TYPES.WARNING) { - * // do something - * } else if (type === constraints.COMPATIBILITY_STATUS_TYPES.ERROR) { - * // do something else - * } - * } - */ -exports.getDriveImageCompatibilityStatuses = (drive, image) => { - const statusList = [] - - // Mind the order of the if-statements if you modify. - if (exports.isSourceDrive(drive, image)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, - message: messages.compatibility.containsImage() - }) - } else if (exports.isDriveLocked(drive)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, - message: messages.compatibility.locked() - }) - } else if (!_.isNil(drive) && !_.isNil(drive.size) && !exports.isDriveLargeEnough(drive, image)) { - const imageSize = image.isSizeEstimated ? image.compressedSize : image.size - const relativeBytes = imageSize - drive.size - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, - message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)) - }) - } else { - if (exports.isSystemDrive(drive)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, - message: messages.compatibility.system() - }) - } - - if (exports.isDriveSizeLarge(drive)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, - message: messages.compatibility.largeDrive() - }) - } - - if (!_.isNil(drive) && !exports.isDriveSizeRecommended(drive, image)) { - statusList.push({ - type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, - message: messages.compatibility.sizeNotRecommended() - }) - } - } - - return statusList -} - -/** - * @summary Get drive/image compatibility status for many drives - * @function - * @public - * - * @description - * Given an image and a list of drives, return all compatibility status objects, - * containing the status type (ERROR, WARNING), and accompanying status message. - * - * @param {Object[]} drives - drives - * @param {Object} image - image - * @returns {Object[]} list of compatibility status objects - * - * @example - * const drives = [ - * { - * device: '/dev/disk2', - * name: 'My Drive', - * size: 4000000000 - * }, - * { - * device: '/dev/disk1', - * name: 'My Other Drive', - * size: 780000000 - * } - * ] - * - * const image = { - * path: '/path/to/rpi.img', - * size: 2000000000, - * compressedSize: 2000000000, - * isSizeEstimated: false, - * recommendedDriveSize: 4000000000 - * }) - * - * const statuses = constraints.getListDriveImageCompatibilityStatuses(drives, image) - * - * for ({ type, message } of statuses) { - * if (type === constraints.COMPATIBILITY_STATUS_TYPES.WARNING) { - * // do something - * } else if (type === constraints.COMPATIBILITY_STATUS_TYPES.ERROR) { - * // do something else - * } - * } - */ -exports.getListDriveImageCompatibilityStatuses = (drives, image) => { - return _.flatMap(drives, (drive) => { - return exports.getDriveImageCompatibilityStatuses(drive, image) - }) -} - -/** - * @summary Does the drive/image pair have at least one compatibility status? - * @function - * @public - * - * @description - * Given an image and a drive, return whether they have a connected compatibility status object. - * - * @param {Object} drive - drive - * @param {Object} image - image - * @returns {Boolean} - * - * @example - * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) { - * console.log('This drive-image pair has a compatibility status message!') - * } - */ -exports.hasDriveImageCompatibilityStatus = (drive, image) => { - return Boolean(exports.getDriveImageCompatibilityStatuses(drive, image).length) -} - -/** - * @summary Does any drive/image pair have at least one compatibility status? - * @function - * @public - * - * @description - * Given an image and a drive, return whether they have a connected compatibility status object. - * - * @param {Object[]} drives - drives - * @param {Object} image - image - * @returns {Boolean} - * - * @example - * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) { - * console.log('This drive-image pair has a compatibility status message!') - * } - */ -exports.hasListDriveImageCompatibilityStatus = (drives, image) => { - return Boolean(exports.getListDriveImageCompatibilityStatuses(drives, image).length) -} diff --git a/lib/shared/drive-constraints.ts b/lib/shared/drive-constraints.ts new file mode 100644 index 00000000000..91638dae55c --- /dev/null +++ b/lib/shared/drive-constraints.ts @@ -0,0 +1,319 @@ +/* + * Copyright 2016 balena.io + * + * 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 { Drive as DrivelistDrive } from 'drivelist'; +import * as _ from 'lodash'; +import * as pathIsInside from 'path-is-inside'; +import * as prettyBytes from 'pretty-bytes'; + +import * as messages from './messages'; + +/** + * @summary The default unknown size for things such as images and drives + */ +const UNKNOWN_SIZE = 0; + +/** + * @summary Check if a drive is locked + * + * @description + * This usually points out a locked SD Card. + */ +export function isDriveLocked(drive: DrivelistDrive): boolean { + return Boolean(_.get(drive, ['isReadOnly'], false)); +} + +/** + * @summary Check if a drive is a system drive + */ +export function isSystemDrive(drive: DrivelistDrive): boolean { + return Boolean(_.get(drive, ['isSystem'], false)); +} + +/** + * @summary Check if a drive is source drive + * + * @description + * In the context of Etcher, a source drive is a drive + * containing the image. + */ +export function isSourceDrive( + drive: DrivelistDrive, + image: { path: string }, +): boolean { + const mountpoints = _.get(drive, ['mountpoints'], []); + const imagePath = _.get(image, ['path']); + + if (!imagePath || _.isEmpty(mountpoints)) { + return false; + } + + return _.some( + _.map(mountpoints, mountpoint => { + return pathIsInside(imagePath, mountpoint.path); + }), + ); +} + +/** + * @summary Check if a drive is large enough for an image + */ +export function isDriveLargeEnough( + drive: DrivelistDrive, + image: { compressedSize?: number; size?: number }, +): boolean { + const driveSize = _.get(drive, ['size'], UNKNOWN_SIZE); + + if (_.get(image, ['isSizeEstimated'])) { + // If the drive size is smaller than the original image size, and + // the final image size is just an estimation, then we stop right + // here, based on the assumption that the final size will never + // be less than the original size. + if (driveSize < _.get(image, ['compressedSize'], UNKNOWN_SIZE)) { + return false; + } + + // If the final image size is just an estimation then consider it + // large enough. In the worst case, the user gets an error saying + // the drive has ran out of space, instead of prohibiting the flash + // at all, when the estimation may be wrong. + return true; + } + + return driveSize >= _.get(image, ['size'], UNKNOWN_SIZE); +} + +/** + * @summary Check if a drive is disabled (i.e. not ready for selection) + */ +export function isDriveDisabled(drive: DrivelistDrive): boolean { + return _.get(drive, ['disabled'], false); +} + +/** + * @summary Check if a drive is valid, i.e. not locked and large enough for an image + */ +export function isDriveValid( + drive: DrivelistDrive, + image: { compressedSize?: number; size?: number; path: string }, +): boolean { + return ( + !isDriveLocked(drive) && + isDriveLargeEnough(drive, image) && + !isSourceDrive(drive, image) && + !isDriveDisabled(drive) + ); +} + +/** + * @summary Check if a drive meets the recommended drive size suggestion + * + * @description + * If the image doesn't have a recommended size, this function returns true. + */ +export function isDriveSizeRecommended( + drive: DrivelistDrive, + image: { recommendedDriveSize?: number }, +): boolean { + return ( + _.get(drive, ['size'], UNKNOWN_SIZE) >= + _.get(image, ['recommendedDriveSize'], UNKNOWN_SIZE) + ); +} + +/** + * @summary 64GB + */ +export const LARGE_DRIVE_SIZE = 64e9; + +/** + * @summary Check whether a drive's size is 'large' + */ +export function isDriveSizeLarge(drive: DrivelistDrive): boolean { + return _.get(drive, ['size'], UNKNOWN_SIZE) > LARGE_DRIVE_SIZE; +} + +/** + * @summary Drive/image compatibility status types. + * + * @description + * Status types classifying what kind of message it is, i.e. error, warning. + */ +export const COMPATIBILITY_STATUS_TYPES = { + WARNING: 1, + ERROR: 2, +}; + +/** + * @summary Get drive/image compatibility in an object + * + * @description + * Given an image and a drive, return their compatibility status object + * containing the status type (ERROR, WARNING), and accompanying + * status message. + * + * @returns {Object[]} list of compatibility status objects + */ +export function getDriveImageCompatibilityStatuses( + drive: DrivelistDrive, + image: {}, +) { + const statusList = []; + + // Mind the order of the if-statements if you modify. + if (exports.isSourceDrive(drive, image)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + message: messages.compatibility.containsImage(), + }); + } else if (exports.isDriveLocked(drive)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + message: messages.compatibility.locked(), + }); + } else if ( + !_.isNil(drive) && + !_.isNil(drive.size) && + !exports.isDriveLargeEnough(drive, image) + ) { + const imageSize = image.isSizeEstimated ? image.compressedSize : image.size; + const relativeBytes = imageSize - drive.size; + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.ERROR, + message: messages.compatibility.tooSmall(prettyBytes(relativeBytes)), + }); + } else { + if (exports.isSystemDrive(drive)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + message: messages.compatibility.system(), + }); + } + + if (exports.isDriveSizeLarge(drive)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + message: messages.compatibility.largeDrive(), + }); + } + + if (!_.isNil(drive) && !exports.isDriveSizeRecommended(drive, image)) { + statusList.push({ + type: exports.COMPATIBILITY_STATUS_TYPES.WARNING, + message: messages.compatibility.sizeNotRecommended(), + }); + } + } + + return statusList; +} + +/** + * @summary Get drive/image compatibility status for many drives + * @function + * @public + * + * @description + * Given an image and a list of drives, return all compatibility status objects, + * containing the status type (ERROR, WARNING), and accompanying status message. + * + * @param {Object[]} drives - drives + * @param {Object} image - image + * @returns {Object[]} list of compatibility status objects + * + * @example + * const drives = [ + * { + * device: '/dev/disk2', + * name: 'My Drive', + * size: 4000000000 + * }, + * { + * device: '/dev/disk1', + * name: 'My Other Drive', + * size: 780000000 + * } + * ] + * + * const image = { + * path: '/path/to/rpi.img', + * size: 2000000000, + * compressedSize: 2000000000, + * isSizeEstimated: false, + * recommendedDriveSize: 4000000000 + * }) + * + * const statuses = constraints.getListDriveImageCompatibilityStatuses(drives, image) + * + * for ({ type, message } of statuses) { + * if (type === constraints.COMPATIBILITY_STATUS_TYPES.WARNING) { + * // do something + * } else if (type === constraints.COMPATIBILITY_STATUS_TYPES.ERROR) { + * // do something else + * } + * } + */ +exports.getListDriveImageCompatibilityStatuses = (drives, image) => { + return _.flatMap(drives, drive => { + return exports.getDriveImageCompatibilityStatuses(drive, image); + }); +}; + +/** + * @summary Does the drive/image pair have at least one compatibility status? + * @function + * @public + * + * @description + * Given an image and a drive, return whether they have a connected compatibility status object. + * + * @param {Object} drive - drive + * @param {Object} image - image + * @returns {Boolean} + * + * @example + * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) { + * console.log('This drive-image pair has a compatibility status message!') + * } + */ +exports.hasDriveImageCompatibilityStatus = (drive, image) => { + return Boolean( + exports.getDriveImageCompatibilityStatuses(drive, image).length, + ); +}; + +/** + * @summary Does any drive/image pair have at least one compatibility status? + * @function + * @public + * + * @description + * Given an image and a drive, return whether they have a connected compatibility status object. + * + * @param {Object[]} drives - drives + * @param {Object} image - image + * @returns {Boolean} + * + * @example + * if (constraints.hasDriveImageCompatibilityStatus(drive, image)) { + * console.log('This drive-image pair has a compatibility status message!') + * } + */ +exports.hasListDriveImageCompatibilityStatus = (drives, image) => { + return Boolean( + exports.getListDriveImageCompatibilityStatuses(drives, image).length, + ); +};