From c85896845fe13a3263d65778b8f320bed0668131 Mon Sep 17 00:00:00 2001 From: Alexis Svinartchouk Date: Wed, 8 Jan 2020 19:52:20 +0100 Subject: [PATCH] Convert drive-constraints.js to typescript Change-type: patch --- lib/gui/app/models/store.js | 1 + lib/shared/drive-constraints.js | 475 ---------------------- lib/shared/drive-constraints.ts | 278 +++++++++++++ lib/shared/messages.ts | 2 +- tests/gui/models/available-drives.spec.js | 1 + tests/shared/drive-constraints.spec.js | 1 + typings/path-is-inside/index.d.ts | 1 + 7 files changed, 283 insertions(+), 476 deletions(-) delete mode 100644 lib/shared/drive-constraints.js create mode 100644 lib/shared/drive-constraints.ts create mode 100644 typings/path-is-inside/index.d.ts diff --git a/lib/gui/app/models/store.js b/lib/gui/app/models/store.js index 7078a34b..f4938d96 100644 --- a/lib/gui/app/models/store.js +++ b/lib/gui/app/models/store.js @@ -20,6 +20,7 @@ const Immutable = require('immutable') const _ = require('lodash') const redux = require('redux') const uuidV4 = require('uuid/v4') +// eslint-disable-next-line node/no-missing-require const constraints = require('../../../shared/drive-constraints') // eslint-disable-next-line node/no-missing-require const supportedFormats = require('../../../shared/supported-formats') diff --git a/lib/shared/drive-constraints.js b/lib/shared/drive-constraints.js deleted file mode 100644 index a719de87..00000000 --- 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 00000000..e2bf1e32 --- /dev/null +++ b/lib/shared/drive-constraints.ts @@ -0,0 +1,278 @@ +/* + * 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 | undefined, + 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 | undefined, + image: { recommendedDriveSize?: number }, +): boolean { + const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; + return driveSize >= _.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 { + const driveSize = _.get(drive, 'size') || UNKNOWN_SIZE; + return driveSize > 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: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + 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) as number; + 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 + * + * @description + * Given an image and a list of drives, return all compatibility status objects, + * containing the status type (ERROR, WARNING), and accompanying status message. + */ +export function getListDriveImageCompatibilityStatuses( + drives: DrivelistDrive[], + image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + return _.flatMap(drives, drive => { + return getDriveImageCompatibilityStatuses(drive, image); + }); +} + +/** + * @summary Does the drive/image pair have at least one compatibility status? + * + * @description + * Given an image and a drive, return whether they have a connected compatibility status object. + */ +export function hasDriveImageCompatibilityStatus( + drive: DrivelistDrive, + image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + return Boolean(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!') + * } + */ +export function hasListDriveImageCompatibilityStatus( + drives: DrivelistDrive[], + image: { isSizeEstimated?: boolean; compressedSize?: number; size?: number }, +) { + return Boolean( + exports.getListDriveImageCompatibilityStatuses(drives, image).length, + ); +} diff --git a/lib/shared/messages.ts b/lib/shared/messages.ts index 322a3de0..71351da5 100644 --- a/lib/shared/messages.ts +++ b/lib/shared/messages.ts @@ -54,7 +54,7 @@ export const compatibility = { return 'Not Recommended'; }, - tooSmall(additionalSpace: number) { + tooSmall(additionalSpace: string) { return `Insufficient space, additional ${additionalSpace} required`; }, diff --git a/tests/gui/models/available-drives.spec.js b/tests/gui/models/available-drives.spec.js index eacc5c69..7149640f 100644 --- a/tests/gui/models/available-drives.spec.js +++ b/tests/gui/models/available-drives.spec.js @@ -20,6 +20,7 @@ const m = require('mochainon') const path = require('path') const availableDrives = require('../../../lib/gui/app/models/available-drives') const selectionState = require('../../../lib/gui/app/models/selection-state') +// eslint-disable-next-line node/no-missing-require const constraints = require('../../../lib/shared/drive-constraints') describe('Model: availableDrives', function () { diff --git a/tests/shared/drive-constraints.spec.js b/tests/shared/drive-constraints.spec.js index f15fb82e..bcaa167e 100644 --- a/tests/shared/drive-constraints.spec.js +++ b/tests/shared/drive-constraints.spec.js @@ -19,6 +19,7 @@ const m = require('mochainon') const _ = require('lodash') const path = require('path') +// eslint-disable-next-line node/no-missing-require const constraints = require('../../lib/shared/drive-constraints') // eslint-disable-next-line node/no-missing-require const messages = require('../../lib/shared/messages') diff --git a/typings/path-is-inside/index.d.ts b/typings/path-is-inside/index.d.ts new file mode 100644 index 00000000..79217ed4 --- /dev/null +++ b/typings/path-is-inside/index.d.ts @@ -0,0 +1 @@ +declare module 'path-is-inside';