volumeData.mjs

/**
 * @description Provides read/write access to data metrics of interest.
 * @copyright December 2020
 * @author Mike Price <dev.grumptech@gmail.com>
 * @module VolumeDataModule
 * @requires debug
 * @see {@link https://github.com/debug-js/debug#readme}
 */

// External dependencies and imports.
import _debugModule from 'debug';

/**
 * @description Debugging function pointer for runtime related diagnostics.
 * @private
 */
const _debug = _debugModule('vol_data');

// Bind debug to console.log
// eslint-disable-next-line no-console
_debug.log = console.log.bind(console);

// Helpful constants and conversion factors.
/**
 * @description Factor for converting from bytes to gigabytes (base-2)
 * @type {number}
 * @private
 */
const BYTES_TO_GB_BASE2     = (1024.0 * 1024.0 * 1024.0);
/**
 * @description Factor for converting from bytes to gigabytes (base-10)
 * @type {number}
 * @private
 */
const BYTES_TO_GB_BASE10    = (1000.0 * 1000.0 * 1000.0);
/**
 * @description Factor for converting from kilobytes to bytes (base-2)
 * @type {number}
 * @private
 */
const BLOCK_1K_TO_BYTES     = 1024.0;

/**
 * @description Enumeration of volume types (file systems).
 * @readonly
 * @enum {string}
 * @property {string} TYPE_UNKNOWN - Unknown volume type
 * @property {string} TYPE_HFS_PLUS - HGS Plus volume (legacy Apple file system)
 * @property {string} TYPE_APFS - APFS vvolume (current Apple file system)
 * @property {string} TYPE_UDF - Universal Disk Format (ISO, etc)
 * @property {string} TYPE_MSDOS - Legacy volume (Typically used for EFI & FAT32)
 * @property {string} TYPE_NTFS - Windows volume
 * @property {string} TYPE_SMBFS - Server Message Block volume (Remote File Share)
 * @property {string} TYPE_EXT4 - Linux volume
 * @property {string} TYPE_VFAT - Linux volume
 */
export const VOLUME_TYPES = {
    /* eslint-disable key-spacing */
    TYPE_UNKNOWN  : 'unknown',
    TYPE_HFS_PLUS : 'hfs',
    TYPE_APFS     : 'apfs',
    TYPE_UDF      : 'udf',
    TYPE_MSDOS    : 'msdos',
    TYPE_NTFS     : 'ntfs',
    TYPE_SMBFS    : 'smbfs',
    TYPE_EXT4     : 'ext4',
    TYPE_VFAT     : 'vfat',
    /* eslint-enable key-spacing */
};

/**
 * @description Enumeration of supported conversion factors
 * @readonly
 * @enum {number}
 * @property {number} BASE_2 - Two's complement conversion factor.
 * @property {number} BASE_10 - Base 10 conversion factor.
 */
export const CONVERSION_BASES = {
    /* eslint-disable key-spacing */
    BASE_2  : 2,
    BASE_10 : 10,
    /* eslint-enable key-spacing */
};

/**
 * @description Provides data of interest for volumes.
 */
export class VolumeData {
    /**
     * @description Constructor
     * @class
     * @param {object} [data] - The settings to use for creating the object.
     * @param {string} [data.name] - Name of the volume.
     * @param {string} [data.disk_id] - Disk identifier of the volume.
     * @param {VOLUME_TYPES | string} [data.volume_type] - File system type of the volume.
     * @param {string} [data.mount_point] - Mount point of the volume.
     * @param {string} [data.device_node] - Device node of the volume.
     * @param {string} [data.volume_uuid] - Unique identifier of the volume.
     * @param {number} [data.capacity_bytes] - Total size (in bytes) of the volume.
     * @param {number} [data.free_space_bytes] - Remaining space (in bytes) of the volume.
     * @param {number} [data.used_space_bytes] - Actively used space (in bytes) of the volume.
     * @param {boolean} [data.visible] - Flag indicating that the volume is visible to the user.
     * @param {boolean} [data.shown] - Flag indicating that the volume should be shown.
     * @param {boolean} [data.low_space_alert] - Flag indicating that the low space alert threshold has been exceeded.
     * @throws {TypeError}  - thrown if the configuration item is not the expected type.
     * @throws {RangeError} - thrown if the configuration parameters are out of bounds.
     */
    constructor(data) {
        // Initialize default values
        let name;
        let diskIdentifier;
        let volumeType = VOLUME_TYPES.TYPE_UNKNOWN;
        let mountPoint;
        let capacityBytes = 0;
        let deviceNode;
        let volumeUUID;
        let freeSpaceBytes = 0;
        let usedSpaceBytes;
        let visible = false;
        let shown = false;
        let lowSpaceAlert = false;

        // Update values from data passed in.
        if (data !== undefined) {
            if (typeof(data) !== 'object') {
                throw new TypeError('\'data\' must be an object');
            }
            if (Object.prototype.hasOwnProperty.call(data, 'name') &&
                (typeof(data.name) === 'string')) {
                name = data.name;
            }
            if (Object.prototype.hasOwnProperty.call(data, 'disk_id') &&
                (typeof(data.disk_id) === 'string')) {
                diskIdentifier = data.disk_id;
            }
            if (Object.prototype.hasOwnProperty.call(data, 'volume_type') &&
                (typeof(data.volume_type) === 'string')) {
                if (Object.values(VOLUME_TYPES).includes(data.volume_type)) {
                    volumeType = data.volume_type;
                }
                else {
                    throw new RangeError(`Unrecognized volume type specified. (${data.volume_type})`);
                }
            }
            if (Object.prototype.hasOwnProperty.call(data, 'mount_point') &&
                (typeof(data.mount_point) === 'string')) {
                mountPoint = data.mount_point;
            }
            if (Object.prototype.hasOwnProperty.call(data, 'capacity_bytes') &&
                (typeof(data.capacity_bytes) === 'number')) {
                if (data.capacity_bytes >= 0) {
                    capacityBytes = data.capacity_bytes;
                }
                else {
                    throw new RangeError(`Volume capacity size must be greater than or equal to 0. (${data.capacity_bytes})`);
                }
            }
            if (Object.prototype.hasOwnProperty.call(data, 'device_node') &&
                (typeof(data.device_node) === 'string')) {
                deviceNode = data.device_node;
            }
            if (Object.prototype.hasOwnProperty.call(data, 'volume_uuid') &&
                (typeof(data.volume_uuid) === 'string')) {
                volumeUUID = data.volume_uuid;
            }
            if (Object.prototype.hasOwnProperty.call(data, 'free_space_bytes') &&
                (typeof(data.free_space_bytes) === 'number')) {
                if (data.free_space_bytes >= 0) {
                    freeSpaceBytes = data.free_space_bytes;
                }
                else {
                    throw new RangeError(`Volume free space size must be greater than or equal to 0. (${data.free_space_bytes})`);
                }
            }
            if (Object.prototype.hasOwnProperty.call(data, 'used_space_bytes') &&
                (typeof(data.used_space_bytes) === 'number')) {
                if (data.used_space_bytes >= 0) {
                    usedSpaceBytes = data.used_space_bytes;
                }
                else {
                    throw new RangeError(`Volume used space size must be greater than or equal to 0. (${data.used_space_bytes})`);
                }
            }
            if (Object.prototype.hasOwnProperty.call(data, 'visible') &&
                (typeof(data.visible) === 'boolean')) {
                visible = data.visible;
            }
            if (Object.prototype.hasOwnProperty.call(data, 'shown') &&
                (typeof(data.shown) === 'boolean')) {
                shown = data.shown;
            }
            if (Object.prototype.hasOwnProperty.call(data, 'low_space_alert') &&
                (typeof(data.low_space_alert) === 'boolean')) {
                lowSpaceAlert = data.low_space_alert;
            }
        }

        // Initialize data members.
        this._name              = name;
        this._disk_identifier   = diskIdentifier;
        this._volume_type       = volumeType;
        this._mount_point       = mountPoint;
        this._capacity_bytes    = capacityBytes;
        this._device_node       = deviceNode;
        this._volume_uuid       = volumeUUID;
        this._free_space_bytes  = freeSpaceBytes;
        this._visible           = visible;
        this._shown             = shown;
        this._low_space_alert   = lowSpaceAlert;
        if (usedSpaceBytes === undefined) {
            // Compute the used space as the difference between the capacity and free space.
            this._used_space_bytes  = (capacityBytes - freeSpaceBytes);
        }
        else {
            // Use the used space as provided.
            this._used_space_bytes = usedSpaceBytes;
        }
        if (this._used_space_bytes < 0) {
            throw new RangeError(`Used space cannot be negative. ${this._used_space_bytes}`);
        }
    }

    /**
     * @description Read-only property accessor for the name of the volume
     * @returns {string} - Name of the volume
     */
    get Name() {
        return (this._name);
    }

    /**
     * @description Read-only property accessor for the disk identifier of the volume.
     * @returns {string} - Disk identifier of the volume
     */
    get DiskId() {
        return (this._disk_identifier);
    }

    /**
     * @description Read-only property accessor for the file system of the volume.
     * @returns {VOLUME_TYPES} - File system of the volume
     */
    get VolumeType() {
        return (this._volume_type);
    }

    /**
     * @description Read-Only Property accessor for the mount point of the volume.
     * @returns {string} - Mount point of the volume. Undefined if not mounted.
     */
    get MountPoint() {
        return (this._mount_point);
    }

    /**
     * @description Read-Only Property accessor for the device node of the volume.
     * @returns {string} - Device node of the volume.
     */
    get DeviceNode() {
        return (this._device_node);
    }

    /**
     * @description Read-Only Property accessor for the UUID of the volume.
     * @returns {string} - Unique identifier of the volume.
     */
    get VolumeUUID() {
        return (this._volume_uuid);
    }

    /**
     * @description Read-Only Property accessor for the size (in bytes) of the volume.
     * @returns {number} - Size (in bytes) of the volume.
     */
    get Size() {
        return (this._capacity_bytes);
    }

    /**
     * @description Read-Only Property accessor for free space (in bytes) of the volume.
     * @returns {number} - Free space (in bytes) of the volume.
     */
    get FreeSpace() {
        return (this._free_space_bytes);
    }

    /**
     * @description Read-Only Property accessor for used space (in bytes) of the volume.
     * @returns {number} - Used space (in bytes) of the volume. Excludes purgable space. Example APFS Snapshots.
     */
    get UsedSpace() {
        return (this._used_space_bytes);
    }

    /**
     * @description Read-Only Property accessor indicating if the volume is mounted.
     * @returns {boolean} - true if the volume is mounted.
     */
    get IsMounted() {
        return ((this._mount_point !== undefined) &&
                (this._mount_point.length > 0));
    }

    /**
     * @description Read-Only Property accessor indicating if the volume is visible to the user.
     * @returns {boolean} - true if the volume is visible.
     */
    get IsVisible() {
        return (this._visible);
    }

    /**
     * @description Read-Only Property accessor indicating if the low space alert threshold has been exceeded.
     * @returns {boolean} - true if the low space threshold has been exceeded.
     */
    get LowSpaceAlert() {
        return (this._low_space_alert);
    }

    /**
     * @description Read-Only Property accessor indicating the percentage of free space.
     * @returns {number} - percentage of space remaining (0...100)
     */
    get PercentFree() {
        return ((this.FreeSpace / this.Size) * 100.0);
    }

    /**
     * @description Read-Only Property accessor indicating is the volume should be shown.
     * @returns {boolean} - true if the volume should be shown.
     */
    get IsShown() {
        return (this._shown);
    }

    /**
     * @description Helper to determine if the supplied object is equivalent to this one.
     * @param {object} compareTarget - Object used as the target or the comparison.
     * @returns {boolean} -  true if the supplied object is a match. false otherwise.
     */
    IsMatch(compareTarget) {
        /* eslint-disable indent, space-in-parens */
                          // Ensure 'compareTarget' is indeed an instance of VolumeData.
        const result = (  (compareTarget instanceof VolumeData) &&
                          // A subset of the volume data properties are used to establish
                          // equivalence.
                          (this.Name === compareTarget.Name) &&
                          (this.VolumeType === compareTarget.VolumeType) &&
                          (this.DeviceNode === compareTarget.DeviceNode) &&
                          (this.MountPoint === compareTarget.MountPoint)   );
       /* eslint-enable indent, space-in-parens */

        return (result);
    }

    /**
     * @description Helper to convert from bytes to GB
     * @param {number} bytes - Size in bytes to be converted
     * @param {number | CONVERSION_BASES} [base] - Base to use for the conversion.
                                                 (Default=CONVERSION_BASES.BASE_2)
     * @returns {number} - Size in GB
     * @throws {TypeError}  - thrown if the bytes or base is not a number
     * @throws {RangeError} - thrown if the base is not valid.
     */
    static ConvertFromBytesToGB(bytes, base) {
        if ((bytes === undefined) || (typeof(bytes) !== 'number')) {
            throw new TypeError('\'bytes\' must be a number.');
        }
        let convFactor = BYTES_TO_GB_BASE2;
        if (base !== undefined) {
            if (Object.values(CONVERSION_BASES).includes(base)) {
                if (CONVERSION_BASES.BASE_10 === base) {
                    convFactor = BYTES_TO_GB_BASE10;
                }
            }
            else {
                throw new RangeError(`'base' has an unsupported value. {${base}}`);
            }
        }

        return (bytes / convFactor);
    }

    /**
     * @description Helper to convert from 1k Blocks to bytes
     * @param {number} blocks - Number of 1k blocks.
     * @returns {number} - Size in bytes
     * @throws {TypeError}  - thrown if the blocks is not a number
     * @throws {RangeError} - thrown if the blocks <= 0
     */
    static ConvertFrom1KBlockaToBytes(blocks) {
        if ((blocks === undefined) || (typeof(blocks) !== 'number')) {
            throw new TypeError('\'blocks\' must be a number.');
        }
        if (blocks < 0) {
            throw new RangeError(`'blocks' must be a positive number. (${blocks})`);
        }

        return (blocks * BLOCK_1K_TO_BYTES);
    }
}
export default VolumeData;