2026-07-14 11:27:49 +02:00
|
|
|
"""Block-device selection and low-level device helpers."""
|
|
|
|
|
|
|
|
|
|
import stat as stat_module
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
from lim import runner, ui
|
|
|
|
|
from lim.errors import LimError
|
|
|
|
|
|
|
|
|
|
SYS_BLOCK_PATH = Path("/sys/block")
|
|
|
|
|
|
|
|
|
|
PARTITION_TABLE_INFO = """\
|
|
|
|
|
##########################################################################################
|
|
|
|
|
Note on Partition Table Deletion:
|
|
|
|
|
---------------------------------------------
|
|
|
|
|
• MBR (Master Boot Record):
|
|
|
|
|
- Typically occupies the first sector (512 bytes), i.e., 1 block.
|
|
|
|
|
|
|
|
|
|
• GPT (GUID Partition Table):
|
|
|
|
|
- Uses a protective MBR (1 block), a GPT header (1 block),
|
|
|
|
|
and usually a partition entry array that takes up about 32 blocks.
|
|
|
|
|
- Total: approximately 34 blocks (assuming a 512-byte block size).
|
|
|
|
|
|
|
|
|
|
Recommendation: For deleting a GPT partition table, use a block size of 512 bytes
|
|
|
|
|
and overwrite at least 34 blocks to ensure the entire table is cleared.
|
|
|
|
|
##########################################################################################"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def optimal_blocksize(name: str, sys_block_path: Path = SYS_BLOCK_PATH) -> str:
|
|
|
|
|
"""64 * physical block size, or "4K" when the size cannot be read.
|
|
|
|
|
|
|
|
|
|
See https://www.heise.de/ct/hotline/Optimale-Blockgroesse-fuer-dd-2056768.html
|
|
|
|
|
"""
|
|
|
|
|
size_path = sys_block_path / name / "queue" / "physical_block_size"
|
|
|
|
|
try:
|
|
|
|
|
return str(64 * int(size_path.read_text().strip()))
|
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
return "4K"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Device:
|
|
|
|
|
name: str # e.g. "sda" or "mmcblk0"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def path(self) -> str:
|
|
|
|
|
return f"/dev/{self.name}"
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def optimal_blocksize(self) -> str:
|
|
|
|
|
return optimal_blocksize(self.name)
|
|
|
|
|
|
|
|
|
|
def partition(self, number: int) -> str:
|
|
|
|
|
"""/dev/sda -> /dev/sda1, /dev/mmcblk0 -> /dev/mmcblk0p1."""
|
|
|
|
|
if self.name[-1].isdigit():
|
|
|
|
|
return f"{self.path}p{number}"
|
|
|
|
|
return f"{self.path}{number}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_block_device(path: str) -> bool:
|
|
|
|
|
try:
|
|
|
|
|
return stat_module.S_ISBLK(Path(path).stat().st_mode)
|
|
|
|
|
except OSError:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def select_device() -> Device:
|
|
|
|
|
ui.info("Available devices:")
|
|
|
|
|
runner.run(["lsblk", "-o", "NAME,SIZE,TYPE,MODEL"], check=False)
|
|
|
|
|
name = ui.ask("Please type in the name of the device: /dev/")
|
|
|
|
|
device = Device(name)
|
|
|
|
|
if not name or not is_block_device(device.path):
|
|
|
|
|
raise LimError(f"{device.path} is not a valid device.")
|
|
|
|
|
ui.info(f"Device path set to: {device.path}")
|
|
|
|
|
ui.info(f"Optimal blocksize set to: {device.optimal_blocksize}")
|
|
|
|
|
return device
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def overwrite_device(device: Device) -> None:
|
|
|
|
|
"""Optionally overwrite the device (or its first blocks) with zeros."""
|
|
|
|
|
print(PARTITION_TABLE_INFO)
|
|
|
|
|
answer = ui.ask(
|
|
|
|
|
f"Should {device.path} be overwritten with zeros before copying? (y/N/block count)"
|
|
|
|
|
)
|
|
|
|
|
if answer == "y":
|
|
|
|
|
ui.info("Overwriting entire device...")
|
|
|
|
|
runner.run(
|
|
|
|
|
[
|
|
|
|
|
"dd",
|
|
|
|
|
"if=/dev/zero",
|
|
|
|
|
f"of={device.path}",
|
|
|
|
|
f"bs={device.optimal_blocksize}",
|
|
|
|
|
"status=progress",
|
|
|
|
|
],
|
|
|
|
|
sudo=True,
|
|
|
|
|
error_msg=f"Overwriting {device.path} failed.",
|
|
|
|
|
)
|
|
|
|
|
runner.sync_disks()
|
|
|
|
|
elif answer in ("", "N"):
|
|
|
|
|
ui.info("Skipping Overwriting...")
|
|
|
|
|
elif answer.isdigit():
|
|
|
|
|
ui.info(f"Overwriting {answer} blocks...")
|
|
|
|
|
runner.run(
|
|
|
|
|
[
|
|
|
|
|
"dd",
|
|
|
|
|
"if=/dev/zero",
|
|
|
|
|
f"of={device.path}",
|
|
|
|
|
f"bs={device.optimal_blocksize}",
|
|
|
|
|
f"count={answer}",
|
|
|
|
|
"status=progress",
|
|
|
|
|
],
|
|
|
|
|
sudo=True,
|
|
|
|
|
error_msg=f"Overwriting {device.path} failed.",
|
|
|
|
|
)
|
|
|
|
|
runner.sync_disks()
|
|
|
|
|
else:
|
|
|
|
|
raise LimError("Invalid input. Block count must be a number.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def blkid_value(path: str, tag: str) -> str:
|
|
|
|
|
"""Return a blkid tag value ("TYPE", "UUID", ...) or "" when unavailable."""
|
2026-07-22 17:47:32 +02:00
|
|
|
return runner.output(["blkid", path, "-s", tag, "-o", "value"], sudo=True, check=False)
|
2026-07-14 11:27:49 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_mounted(path_fragment: str) -> bool:
|
|
|
|
|
"""Whether any current mount line contains the given fragment."""
|
|
|
|
|
mounts = runner.output(["mount"], check=False)
|
|
|
|
|
return any(path_fragment in line for line in mounts.splitlines())
|