Haiku BVolume: Capacity, Free-Space Snapshots, and Filesystem Capabilities
Inspect Haiku volume capacity and filesystem capabilities without confusing a free-space snapshot with a write guarantee or mount notification.
BVolume is a small Storage Kit wrapper around a device identifier and filesystem information. It can report capacity, free bytes, block size, the volume name, its root directory, and capabilities such as read-only state or support for attributes and queries. Those values are useful for UI, diagnostics, and choosing an operation, but they are observations, not reservations. A successful free-space check cannot guarantee that a later write will fit.
The most reliable usage pattern is to validate the BVolume object first, read only the properties the operation needs, and still handle errors from the actual filesystem operation. If the requirement is to react when volumes appear or disappear, BVolume alone is not the event source; use BVolumeRoster for enumeration and watch notifications, then refresh the corresponding BVolume state.
Initialize and validate the volume identity
A default-constructed BVolume is uninitialized. Constructing from a device ID or calling SetTo() attempts to bind the object to a volume. Check InitCheck() before interpreting the remaining accessors. That check proves the wrapper initialized; it cannot guarantee that a removable device will still answer a later filesystem query.
BVolume volume(device);
status_t status = volume.InitCheck();
if (status != B_OK)
return status;
fs_info info;
if (fs_stat_dev(volume.Device(), &info) != 0) {
// Capture errno here if the caller needs a detailed diagnostic.
return B_ERROR;
}
off_t capacity = info.total_blocks * info.block_size;
off_t freeBytes = info.free_blocks * info.block_size;
The example uses fs_stat_dev() when the caller needs a separate success indicator and errno-based error handling. BVolume::Capacity() and FreeBytes() return off_t rather than status_t plus an output parameter. Their documentation describes B_BAD_VALUE for an uninitialized object, and the current implementation also forwards an errno value if its underlying filesystem query fails. Do not assume every failure is a negative size or cast a possible error to unsigned. If a brief UI display uses the BVolume getters, treat the result as an observation and accept that the getters are not a structured error-reporting interface.
A dev_t identifies the volume object that the BVolume wraps at the time it is initialized. Do not hard-code a device ID from one boot or retain it as a permanent user-facing identifier. Volumes can change as devices mount and unmount. Use BVolumeRoster to enumerate current volumes and receive lifecycle notifications, then call SetTo() or construct a fresh BVolume when you need a current observation.
Treat capacity and free bytes as observations
Capacity() reports total storage capacity in bytes, and FreeBytes() reports unused space in bytes. The interval between reading those values and using them is a race: another process can write, remove data, change mounts, or otherwise alter available space. Your write can still fail for permissions, read-only state, quota-like behavior, filesystem errors, or insufficient space despite a recent successful check.
Use free-space data to improve user feedback, select a likely target, or emit a metric. Do not use it as the sole safety condition for an operation that must be atomic. For a large copy, handle short writes and error statuses from BFile or the relevant I/O API. For a package installer, stage data and verify the final commit behavior instead of assuming an estimate makes the operation safe.
For a display percentage, avoid multiplying a potentially large off_t value before dividing. Promote to a floating-point type after validation, or use overflow-safe integer arithmetic. Treat zero capacity as an invalid denominator. Round for presentation, not for deciding whether a write fits.
double freePercent = 0.0;
if (capacity > 0 && freeBytes >= 0 && freeBytes <= capacity) {
freePercent = 100.0 * static_cast<double>(freeBytes)
/ static_cast<double>(capacity);
}
The bounds check is appropriate for a simple display but not a universal statement about every filesystem’s accounting behavior. Record the raw values and the observation time if the values feed diagnostics. Avoid presenting a stale number as a live guarantee after a mount event or a long-running task.
Interpret block size and capability queries narrowly
BlockSize() returns a filesystem-dependent block-size value. It is not necessarily a physical disk sector size, a cluster size that applications should choose for all I/O, or a guarantee about alignment of every file operation. Use it for diagnostics or algorithms that specifically need the filesystem’s reported value, and consult the I/O API for its actual buffering and alignment contract.
Capability methods such as IsReadOnly(), IsRemovable(), IsPersistent(), IsShared(), KnowsAttr(), KnowsMime(), and KnowsQuery() describe properties the volume reports. They are useful for tailoring UI and avoiding unsupported operations. A false value from a boolean capability method can mean the object is uninitialized, its current filesystem query failed, or the capability is not available; call InitCheck() first, but do not treat it as proof that a later query succeeded.
if (volume.InitCheck() != B_OK)
return B_NO_INIT;
if (volume.IsReadOnly())
return B_READ_ONLY_DEVICE;
if (volume.KnowsAttr()) {
// Attribute-aware behavior is available on this volume.
}
The capability check does not make a later operation immune to state changes. A volume can become unavailable after inspection, and filesystem operations still return authoritative results. Prefer checking a capability to avoid a predictable unsupported request, then handle the actual status returned by the request.
KnowsAttr() and KnowsQuery() are especially useful when software is portable across filesystems. Do not assume every mounted filesystem supports BFS attributes, MIME type metadata, or live queries just because the application is running on Haiku. Provide a fallback based on ordinary files or an explicit “feature unavailable” path rather than writing metadata and assuming it was stored.
Use the root directory as an API boundary
GetRootDirectory() initializes a caller-provided BDirectory to the volume’s root. This is preferable to constructing a path string from a display name or guessing a mount path. The method reports failure through status_t; check it before enumerating. The resulting BDirectory is still an object tied to a filesystem state that can change, so subsequent operations must handle their own errors.
BDirectory root;
status = volume.GetRootDirectory(&root);
if (status != B_OK)
return status;
BEntry entry;
while ((status = root.GetNextEntry(&entry, false)) == B_OK) {
// Process one root entry without following symlinks.
}
if (status != B_ENTRY_NOT_FOUND)
return status;
This pattern uses BDirectory iteration rather than concatenating a volume name with a slash. A volume name is presentation metadata, not a stable path component. Avoid assuming a boot volume, removable disk, or network-backed filesystem has the same root path on every machine.
Separate polling from mount lifecycle notifications
BVolume answers questions about a volume you already identified. BVolumeRoster owns the enumeration and watching workflow: it can iterate volumes, identify the boot volume, and send volume lifecycle messages to a BMessenger. A robust monitor treats those messages as hints to refresh state, not as a permanent cache of every property.
On startup, enumerate the current roster and create fresh BVolume observations. Start watching using a live BMessenger target, then enumerate and reconcile again so a mount change between the first scan and watch registration is not silently missed. If notifications are delayed while the application is busy or the app restarts, enumerate again. This handles the ordinary gap between an initial scan and event subscription without pretending the two operations are atomic.
When a device is removed, discard cached assumptions and report operations that fail because the underlying volume vanished. When it is mounted again, do not assume its previous device ID or path remains appropriate; resolve the current roster entry. Keep long-lived work attached to a file or entry reference according to the relevant Storage Kit contract, and handle the case where the source volume no longer exists.
Operational acceptance checks
Test a valid writable volume, a read-only volume, a removable volume, an uninitialized BVolume, an unsupported capability, and a device that disappears during a read or copy. Check that negative size results are never converted to unsigned values, zero capacity never divides, and a failed GetRootDirectory() does not lead to iteration on an invalid object.
For a free-space warning, test concurrent consumption that reduces available capacity after the reading. The application should surface the actual write failure and avoid destructive cleanup based solely on an old metric. For a monitor, test application startup before and after a volume is mounted, removal during a task, notification handling, and full roster reconciliation after a missed event.
BVolume is a focused observation API, not a storage reservation manager. Validate initialization, interpret values and capabilities conservatively, use BVolumeRoster for volume lifecycle, and check real I/O results at the point where data is read or written. That boundary keeps capacity displays helpful without turning them into false guarantees.
Related:
- Haiku BVolumeRoster: Volume Enumeration, Mount Events, and Safe Reconciliation
- Fixing Disk Mounting and BFS Volume Check Issues on Haiku
Sources: