def scan_storage(root: Path, derivatives: Path) -> SummaryStorage:
"""Count regular-file apparent bytes, excluding symlinks and incomplete scans."""
counts: Counter[str] = Counter()
children: dict[str, Counter[str]] = {}
total = 0
deadline = time.monotonic() + 60
try:
root = root.resolve()
derivatives = derivatives.resolve()
from tit.storage import classify, kind_prefixes
prefixes = kind_prefixes(PathManager(str(root)))
stack = [root]
while stack:
if time.monotonic() > deadline:
raise OSError("Storage scan exceeded its one-minute budget")
directory = stack.pop()
if not directory.resolve().is_relative_to(root):
raise OSError("Directory moved outside the project during scan")
with os.scandir(directory) as entries:
for entry in entries:
if time.monotonic() > deadline:
raise OSError("Storage scan exceeded its one-minute budget")
if entry.is_symlink():
continue
path = Path(entry.path)
if entry.is_dir(follow_symlinks=False):
stack.append(path)
elif entry.is_file(follow_symlinks=False):
size = entry.stat(follow_symlinks=False).st_size
total += size
kind = classify(str(path), prefixes)
if (
path.is_relative_to(derivatives)
and len(path.relative_to(derivatives).parts) > 1
):
folder = path.relative_to(derivatives).parts[0]
if folder == "SimNIBS":
counts["SimNIBS"] += size
label = {
"head_models": "Head models",
"flex_search": "Flex search",
"ex_search": "Ex search",
"simulations": "Simulations",
"leadfields": "Leadfields",
}.get(kind, "Other")
children.setdefault(
"SimNIBS",
Counter({"Flex search": 0, "Leadfields": 0}),
)[label] += size
elif folder in ("qsiprep", ".qsiprep_work"):
counts["QSIPrep"] += size
elif folder in ("qsirecon", ".qsirecon_work"):
counts["QSIRecon"] += size
elif folder in ("freesurfer", "fastsurfer"):
counts["FreeSurfer / FastSurfer"] += size
elif kind in ("raw", "sourcedata"):
counts["Input data"] += size
except (OSError, RuntimeError):
logger.warning("Project storage scan incomplete", exc_info=True)
return SummaryStorage(
state="error",
total_bytes=None,
other_bytes=None,
derivatives=[],
scanned_at=None,
)
return SummaryStorage(
state="ready",
total_bytes=total,
other_bytes=total - sum(counts.values()),
derivatives=[
{
"name": name,
"bytes": size,
"children": [
{"name": label, "bytes": value}
for label, value in sorted(
children.get(name, {}).items(),
key=lambda item: (
{
"Head models": 0,
"Flex search": 1,
"Ex search": 2,
"Simulations": 3,
"Leadfields": 4,
"Other": 5,
}.get(item[0], 6),
item[0],
),
)
],
}
for name, size in sorted(
counts.items(), key=lambda item: (item[0] != "SimNIBS", item[0])
)
],
scanned_at=datetime.now(timezone.utc).isoformat(),
)