btrfs-mkfs 0.5.0

Create btrfs filesystems
Documentation
use anyhow::{Result, bail};
use btrfs_mkfs::{
    args::{Arguments, ChecksumArg, Profile},
    mkfs::{self, DeviceInfo},
    write::ChecksumType,
};
use clap::Parser;
use std::os::unix::fs::FileTypeExt;
use uuid::Uuid;

fn main() -> Result<()> {
    let args = Arguments::parse();

    // Resolve sizes.
    let nodesize = args.nodesize.map(|s| s.0 as u32).unwrap_or(16384);
    let sectorsize = args.sectorsize.map(|s| s.0 as u32).unwrap_or(4096);

    // Validate nodesize/sectorsize.
    if !nodesize.is_power_of_two() || nodesize < sectorsize || nodesize > 65536
    {
        bail!(
            "invalid nodesize {nodesize}: must be a power of 2, \
             >= sectorsize ({sectorsize}), and <= 64K"
        );
    }
    if !sectorsize.is_power_of_two() || sectorsize < 4096 {
        bail!("invalid sectorsize {sectorsize}: must be a power of 2 >= 4096");
    }

    // Map checksum argument to ChecksumType.
    let csum_type = match args.checksum {
        None | Some(ChecksumArg::Crc32c) => ChecksumType::Crc32c,
        Some(ChecksumArg::Xxhash) => ChecksumType::Xxhash64,
        Some(ChecksumArg::Sha256) => ChecksumType::Sha256,
        Some(ChecksumArg::Blake2) => ChecksumType::Blake2b,
    };

    // Validate label.
    if let Some(ref label) = args.label
        && label.len() >= 256
    {
        bail!("label too long: {} bytes (max 255)", label.len());
    }

    // Profile defaults: DUP metadata for single device, RAID1 for multi-device.
    let num_devices = args.devices.len();
    let metadata_profile =
        args.metadata_profile.unwrap_or(if num_devices > 1 {
            Profile::Raid1
        } else {
            Profile::Dup
        });
    let data_profile = args.data_profile.unwrap_or(Profile::Single);

    // Validate profile vs device count.
    if num_devices < metadata_profile.min_devices() {
        bail!(
            "metadata profile {} requires at least {} devices, got {}",
            metadata_profile,
            metadata_profile.min_devices(),
            num_devices
        );
    }
    if num_devices < data_profile.min_devices() {
        bail!(
            "data profile {} requires at least {} devices, got {}",
            data_profile,
            data_profile.min_devices(),
            num_devices
        );
    }

    // Build device list.
    let mut devices = Vec::with_capacity(num_devices);
    for (i, dev_path) in args.devices.iter().enumerate() {
        let devid = (i + 1) as u64;
        let total_bytes = if let Some(byte_count) = args.byte_count {
            byte_count.0
        } else {
            mkfs::device_size(dev_path)?
        };

        // Minimum size check.
        let min_size = mkfs::minimum_device_size(nodesize);
        if total_bytes < min_size {
            bail!(
                "device '{}' too small: {} bytes, need at least {} bytes ({} MiB)",
                dev_path.display(),
                total_bytes,
                min_size,
                min_size / (1024 * 1024)
            );
        }

        // Device safety checks (block devices only).
        let is_block = std::fs::metadata(dev_path)
            .ok()
            .is_some_and(|m| m.file_type().is_block_device());
        if is_block {
            if mkfs::is_device_mounted(dev_path)? {
                bail!(
                    "'{}' is mounted; refusing to format a mounted device",
                    dev_path.display()
                );
            }
            if !args.force && mkfs::has_btrfs_superblock(dev_path) {
                bail!(
                    "'{}' already contains a btrfs filesystem; use -f to force",
                    dev_path.display()
                );
            }
        }

        let dev_uuid = if i == 0 {
            args.device_uuid.unwrap_or_else(Uuid::new_v4)
        } else {
            Uuid::new_v4()
        };

        devices.push(DeviceInfo {
            devid,
            path: dev_path.clone(),
            total_bytes,
            dev_uuid,
        });
    }

    // Generate or parse filesystem UUID.
    let fs_uuid = args.filesystem_uuid.unwrap_or_else(Uuid::new_v4);
    let chunk_tree_uuid = Uuid::new_v4();

    let mut cfg = mkfs::MkfsConfig {
        nodesize,
        sectorsize,
        devices,
        label: args.label,
        fs_uuid,
        chunk_tree_uuid,
        incompat_flags: mkfs::MkfsConfig::default_incompat_flags(),
        compat_ro_flags: mkfs::MkfsConfig::default_compat_ro_flags(),
        data_profile,
        metadata_profile,
        csum_type,
        creation_time: None,
    };

    // Apply user-specified feature flags.
    cfg.apply_features(&args.features)?;

    if !args.quiet {
        let device_names: Vec<_> = cfg
            .devices
            .iter()
            .map(|d| d.path.display().to_string())
            .collect();
        eprintln!("Creating btrfs filesystem on {}", device_names.join(", "));
        eprintln!(
            "  Label:          {}",
            cfg.label.as_deref().unwrap_or("(none)")
        );
        eprintln!("  UUID:           {}", cfg.fs_uuid);
        eprintln!("  Node size:      {}", cfg.nodesize);
        eprintln!("  Sector size:    {}", cfg.sectorsize);
        eprintln!(
            "  Filesystem size: {} ({} bytes)",
            human_size(cfg.total_bytes()),
            cfg.total_bytes()
        );
        if num_devices > 1 {
            eprintln!("  Data profile:   {}", cfg.data_profile);
            eprintln!("  Metadata profile: {}", cfg.metadata_profile);
        }
    }

    // TRIM each device before writing (unless -K).
    if !args.nodiscard {
        for dev in &cfg.devices {
            let is_block = std::fs::metadata(&dev.path)
                .ok()
                .is_some_and(|m| m.file_type().is_block_device());
            if is_block {
                if !args.quiet {
                    eprintln!(
                        "Performing full device TRIM on {}...",
                        dev.path.display()
                    );
                }
                if let Err(e) = mkfs::discard_device(&dev.path, dev.total_bytes)
                {
                    eprintln!(
                        "WARNING: discard failed on {}: {e:#}",
                        dev.path.display()
                    );
                }
            }
        }
    }

    mkfs::make_btrfs(&cfg)?;

    if !args.quiet {
        eprintln!("Done.");
    }

    Ok(())
}

fn human_size(bytes: u64) -> String {
    const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
    let mut value = bytes as f64;
    for &unit in UNITS {
        if value < 1024.0 {
            return if value.fract() == 0.0 {
                format!("{:.0} {unit}", value)
            } else {
                format!("{:.2} {unit}", value)
            };
        }
        value /= 1024.0;
    }
    format!("{:.2} EiB", value)
}