5252import os
5353import os .path
5454import platform
55+ import re
56+ import shutil
5557import sys
5658import threading
5759
5860from collections .abc import Generator , Iterable , Iterator
59- from typing import IO , Any
61+ from typing import IO , Any , NamedTuple , Union
6062
6163# For device capacity reading in query_device_capacity().
6264if os .name == 'posix' :
6971 import msvcrt
7072
7173#: Platforms where :func:`query_device_capacity` is supported.
72- #: Corresponds to possible values of :data:`os.name`.
74+ #: Corresponds to possible values of :data:`os.name`. macOS (Darwin)
75+ #: is not supported due to SIP restrictions on raw block device access.
7376SUPPORTED_PLATFORMS = frozenset ({'posix' , 'nt' })
7477
7578__all__ = ['Bit' , 'Byte' , 'KiB' , 'MiB' , 'GiB' , 'TiB' , 'PiB' , 'EiB' , 'ZiB' , 'YiB' ,
7881 'Pb' , 'Eb' , 'Zb' , 'Yb' , 'getsize' , 'listdir' , 'format' ,
7982 'format_string' , 'format_plural' , 'parse_string' , 'parse_string_unsafe' ,
8083 'sum' , 'ALL_UNIT_TYPES' , 'NIST' , 'NIST_PREFIXES' , 'NIST_STEPS' ,
81- 'SI' , 'SI_PREFIXES' , 'SI_STEPS' ]
84+ 'SI' , 'SI_PREFIXES' , 'SI_STEPS' , 'Capacity' , 'query_capacity' ,
85+ 'query_device_capacity' ]
8286
8387#: A list of all the valid prefix unit types. Mostly for reference,
8488#: also used by the CLI tool as valid types
@@ -1342,31 +1346,33 @@ class DISK_GEOMETRY_EX(ctypes.Structure):
13421346
13431347
13441348def query_device_capacity (device_fd : IO [Any ]) -> Byte :
1345- """Create bitmath instances of the capacity of a system block device
1349+ """Query the raw physical capacity of a block device.
13461350
1347- Make one or more ioctl request to query the capacity of a block
1348- device. Perform any processing required to compute the final capacity
1349- value. Return the device capacity in bytes as a :class:`bitmath.Byte`
1350- instance.
1351-
1352- Thanks to the following resources for help figuring this out Linux/Mac
1353- ioctl's for querying block device sizes:
1351+ Most users should prefer :func:`query_capacity`. This function is for
1352+ callers who need raw physical device capacity (e.g. disk imaging tools).
1353+ Requires root on Linux and administrator on Windows. Not supported on
1354+ macOS (SIP restriction).
13541355
1355- * http://stackoverflow.com/a/12925285/263969
1356- * http://stackoverflow.com/a/9764508/263969
1357-
1358- :param file device_fd: A ``file`` object of the device to query the
1359- capacity of. On Linux/macOS: ``open("/dev/sda", "rb")``. On Windows:
1360- ``open(r'\\ \\ .\\ PhysicalDrive0', 'rb')`` (requires administrator privileges).
1356+ :param file device_fd: A ``file`` object of the device to query.
1357+ On Linux: ``open("/dev/sda", "rb")`` (requires root).
1358+ On Windows: ``open(r'\\ \\ .\\ PhysicalDrive0', 'rb')`` (requires administrator).
13611359
13621360 :return: a bitmath :class:`bitmath.Byte` instance equivalent to the
13631361 capacity of the target device in bytes.
1362+ :raises NotImplementedError: on macOS or any other unsupported platform.
1363+ :raises ValueError: if the file descriptor is not a block device.
13641364"""
13651365 if os .name not in SUPPORTED_PLATFORMS :
13661366 raise NotImplementedError (f"'bitmath.query_device_capacity' is not supported on this platform: { os .name } " )
13671367 if os .name == 'nt' :
13681368 return Byte (_query_device_capacity_windows (device_fd ))
13691369
1370+ if platform .system () == 'Darwin' :
1371+ raise NotImplementedError (
1372+ "query_device_capacity is not supported on macOS; "
1373+ "SIP blocks raw block device access. Use query_capacity() instead."
1374+ )
1375+
13701376 s = os .stat (device_fd .name ).st_mode
13711377 if not stat .S_ISBLK (s ):
13721378 raise ValueError ("The file descriptor provided is not of a device type" )
@@ -1411,45 +1417,6 @@ def query_device_capacity(device_fd: IO[Any]) -> Byte:
14111417 # BLKGETSIZE64.
14121418 "func" : lambda x : x ["BLKGETSIZE64" ]
14131419 },
1414- # ioctls for the "Darwin" (Mac OS X) platform
1415- "Darwin" : {
1416- "request_params" : [
1417- # A list of parameters to calculate the block size.
1418- #
1419- # ( PARAM_NAME , FORMAT_CHAR , REQUEST_CODE )
1420- ("DKIOCGETBLOCKCOUNT" , "L" , 0x40086419 ),
1421- # Per <sys/disk.h>: get media's block count - uint64_t
1422- #
1423- # As in the BLKGETSIZE64 example, an unsigned 64 bit
1424- # integer will use the 'L' formatting character
1425- ("DKIOCGETBLOCKSIZE" , "I" , 0x40046418 )
1426- # Per <sys/disk.h>: get media's block size - uint32_t
1427- #
1428- # This request returns an unsigned 32 bit integer, or
1429- # in other words: just a normal integer (or 'int' c
1430- # type). That should require 4 bytes of space for
1431- # buffering. According to the struct modules
1432- # 'Formatting Characters' chart:
1433- #
1434- # * Character 'I' - Unsigned Int C Type (uint32_t) - Loads into a Python int type
1435- ],
1436- # OS X doesn't have a direct equivalent to the Linux
1437- # BLKGETSIZE64 request. Instead, we must request how many
1438- # blocks (or "sectors") are on the disk, and the size (in
1439- # bytes) of each block. Finally, multiply the two together
1440- # to obtain capacity:
1441- #
1442- # n Block * y Byte
1443- # capacity (bytes) = -------
1444- # 1 Block
1445- "func" : lambda x : x ["DKIOCGETBLOCKCOUNT" ] * x ["DKIOCGETBLOCKSIZE" ]
1446- # This expression simply accepts a dictionary ``x`` as a
1447- # parameter, and then returns the result of multiplying
1448- # the two named dictionary items together. In this case,
1449- # that means multiplying ``DKIOCGETBLOCKCOUNT``, the total
1450- # number of blocks, by ``DKIOCGETBLOCKSIZE``, the size of
1451- # each block in bytes.
1452- }
14531420 }
14541421
14551422 platform_params = ioctl_map [platform .system ()]
@@ -1463,7 +1430,7 @@ def query_device_capacity(device_fd: IO[Any]) -> Byte:
14631430 # conditions for some possible errors. Really only for cases
14641431 # where it would add value to override the default exception
14651432 # message string.
1466- buffer = fcntl .ioctl (device_fd .fileno (), request_code , buffer_size )
1433+ buffer = fcntl .ioctl (device_fd .fileno (), request_code , b' \x00 ' * buffer_size )
14671434
14681435 # Unpack the raw result from the ioctl call into a familiar
14691436 # python data type according to the ``fmt`` rules.
@@ -1474,6 +1441,71 @@ def query_device_capacity(device_fd: IO[Any]) -> Byte:
14741441 return Byte (platform_params ['func' ](results ))
14751442
14761443
1444+ class Capacity (NamedTuple ):
1445+ """Capacity of a filesystem volume returned by :func:`query_capacity`."""
1446+ total : 'Bitmath'
1447+ used : 'Bitmath'
1448+ free : 'Bitmath'
1449+
1450+
1451+ # Matches a bare drive letter: "C", "c", "C:", "c:" — nothing else.
1452+ _DRIVE_LETTER_RE = re .compile (r'^[A-Za-z]:?$' )
1453+
1454+
1455+ def query_capacity (path : Union [str , os .PathLike ], bestprefix : bool = True ,
1456+ system : int = NIST ) -> Capacity :
1457+ """Return the total, used, and free capacity of the volume at ``path``.
1458+
1459+ This is the recommended API for querying volume or mount-point size. It
1460+ works cross-platform without elevated privileges.
1461+
1462+ :param path: A path on the filesystem volume to query. On Windows, a
1463+ bare drive letter (``"C"``, ``"C:"``) is normalized to ``"C:\\ "``.
1464+ :param bool bestprefix: When ``True`` (default), each field of the
1465+ returned :class:`Capacity` is already normalized via
1466+ :meth:`~bitmath.Bitmath.best_prefix` for human-readable output.
1467+ When ``False``, each field is a raw :class:`bitmath.Byte`.
1468+ :param int system: Unit system to use when ``bestprefix`` is ``True``.
1469+ Either :data:`bitmath.NIST` (default, binary prefixes like ``GiB``)
1470+ or :data:`bitmath.SI` (decimal prefixes like ``GB``). Ignored when
1471+ ``bestprefix`` is ``False``.
1472+
1473+ :return: A :class:`Capacity` NamedTuple with ``total``, ``used``, and
1474+ ``free`` fields, each a :class:`bitmath.Bitmath` instance.
1475+
1476+ :raises FileNotFoundError: if ``path`` does not exist.
1477+ :raises PermissionError: if the process lacks access to query ``path``.
1478+
1479+ Example — attribute access (human-readable by default)::
1480+
1481+ cap = bitmath.query_capacity("/")
1482+ print(cap.total) # e.g. 465.762 GiB
1483+
1484+ Example — tuple unpacking::
1485+
1486+ total, used, free = bitmath.query_capacity("/")
1487+
1488+ Example — raw bytes and SI prefixes::
1489+
1490+ cap_raw = bitmath.query_capacity("/", bestprefix=False)
1491+ cap_si = bitmath.query_capacity("/", system=bitmath.SI)
1492+ """
1493+ normalized : Union [str , os .PathLike ] = path
1494+ if os .name == 'nt' :
1495+ s = str (path ).upper ()
1496+ if _DRIVE_LETTER_RE .match (s ):
1497+ normalized = s .rstrip (':' ) + ':\\ '
1498+ usage = shutil .disk_usage (normalized )
1499+ total , used , free = Byte (usage .total ), Byte (usage .used ), Byte (usage .free )
1500+ if bestprefix :
1501+ return Capacity (
1502+ total .best_prefix (system = system ),
1503+ used .best_prefix (system = system ),
1504+ free .best_prefix (system = system ),
1505+ )
1506+ return Capacity (total , used , free )
1507+
1508+
14771509def getsize (path : str , bestprefix : bool = True , system : int = NIST ) -> Bitmath :
14781510 """Return a bitmath instance in the best human-readable representation
14791511of the file size at `path`. Optionally, provide a preferred unit
0 commit comments