diff --git a/src/disk_checker/core/drive_scanner.py b/src/disk_checker/core/drive_scanner.py new file mode 100644 index 0000000..1f453d5 --- /dev/null +++ b/src/disk_checker/core/drive_scanner.py @@ -0,0 +1,127 @@ +"""Drive detection module for disk-checker.""" + +import json +import subprocess +from typing import Dict, List +from dataclasses import dataclass +from pathlib import Path + + +@dataclass +class DriveInfo: + """Information about a block device.""" + name: str # e.g., "sda", "sdb" + path: str # e.g., "/dev/sda" + size: str # e.g., "232.9G" + model: str # e.g., "CT250MX500SSD1" + type: str # e.g., "disk", "part" + mounted: bool # True if currently mounted + mount_point: str = "" # e.g., "/", "/home" + serial: str = "" # e.g., "2131G5F25137" + + +def get_mounted_devices() -> set: + """ + Get set of mounted device paths. + + Returns: + Set of device paths that are currently mounted + """ + mounted = set() + + # Method 1: Parse lsblk with mount points + try: + result = subprocess.run( + ["lsblk", "-J", "-o", "NAME,MOUNTPOINT"], + capture_output=True, + text=True, + check=True + ) + data = json.loads(result.stdout) + for device in data.get("blockdevices", []): + mountpoint = device.get("mountpoint") + if mountpoint: + # Add both full path and device name + mounted.add(f"/dev/{device['name']}") + mounted.add(device["name"]) + except (subprocess.CalledProcessError, json.JSONDecodeError): + print("lsblk failed to retrieve mounted devices information") + pass + + # Method 2: Parse /proc/mounts + try: + with open("/proc/mounts", "r") as f: + for line in f: + parts = line.split() + if len(parts) >= 2 and parts[0].startswith("/dev/"): + mounted.add(parts[0]) + # Also add device name without /dev/ + mounted.add(parts[0].replace("/dev/", "")) + except (FileNotFoundError, IOError): + print("/proc/mounts failed to retrieve mounted devices information") + pass + + return mounted + + +def scan_drives() -> List[DriveInfo]: + """ + Scan host system for available block devices. + + Returns: + List of DriveInfo objects for physical disks + """ + mounted_devices = get_mounted_devices() + drives = [] + + try: + # Get block devices with lsblk + result = subprocess.run( + [ + "lsblk", + "-J", # JSON output + "-d", # Only disks (not partitions) + "-b", # Show size in bytes + "-o", "NAME,SIZE,TYPE,MODEL,Serial,MOUNTPOINT" + ], + capture_output=True, + text=True, + check=True + ) + + data = json.loads(result.stdout) + + for device in data.get("blockdevices", []): + # Only include physical disks + if device.get("type") != "disk": + continue + + name = device.get("name", "") + path = f"/dev/{name}" + + # Check if mounted + is_mounted = path in mounted_devices or name in mounted_devices + + drive = DriveInfo( + name=name, + path=path, + size=device.get("size", "Unknown"), + model=device.get("model") or "", + type="disk", + mounted=is_mounted, + mount_point=device.get("mountpoint", ""), + serial=device.get("serial") or "" + ) + drives.append(drive) + + except subprocess.CalledProcessError as e: + # If lsblk fails, return empty list + print("lsblk failed to parse drives data") + print(e) + pass + except json.JSONDecodeError as e: + print("json decoder error") + print(e) + pass + + return drives \ No newline at end of file diff --git a/src/disk_checker/web/routes.py b/src/disk_checker/web/routes.py index 96fa619..3f1a0b7 100644 --- a/src/disk_checker/web/routes.py +++ b/src/disk_checker/web/routes.py @@ -3,6 +3,7 @@ from fastapi import APIRouter, Request from disk_checker.core.tool_scanner import ToolInfo +from disk_checker.core.drive_scanner import DriveInfo, scan_drives api_router = APIRouter() @@ -19,4 +20,26 @@ async def get_available_tools(request: Request): "version" : tool.version } for tool_name, tool in tools.items() + } + +@api_router.get("/drives") +async def get_available_drives(request: Request): + """ + Get list of available block devices + Mounted drives are marked as unavailable + """ + drives = scan_drives() + return { + "drives": [ + { + "name": drive.name, + "path": drive.path, + "size": drive.size, + "model": drive.model, + "mounted": drive.mounted, + "mount_point": drive.mount_point, + "serial": drive.serial + } + for drive in drives + ] } \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css index c7651d5..d668371 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -14,4 +14,32 @@ .badge { font-size: 0.9rem; +} + +/* Drive cards styling */ +.drive-card { + transition: all 0.3s ease; +} + +.drive-card.available { + border-left: 4px solid #28a745; +} + +.drive-card.disabled { + border-left: 4px solid #6c757d; + opacity: 0.7; +} + +.drive-card:hover:not(.disabled) { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.1); +} + +/* Make disabled cards look unavailable */ +.drive-card.disabled .card-title { + color: #6c757d; +} + +.drive-card.disabled .card-text { + color: #6c757d; } \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index c0ca987..a686e65 100644 --- a/templates/base.html +++ b/templates/base.html @@ -16,9 +16,9 @@ -
{% block content %}{% endblock %} + {% block extra_js %}{% endblock %}
diff --git a/templates/index.html b/templates/index.html index 5d54f6a..8692da1 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,8 +2,10 @@ {% block content %}
+ +

Available Benchmarking Tools

- + {% if tools %}
@@ -14,12 +16,12 @@
{{ tool.name }}

{% if tool.available %} - Available - {% if tool.version %} - {{ tool.version }} - {% endif %} + Available + {% if tool.version %} + {{ tool.version }} + {% endif %} {% else %} - Not Installed + Not Installed {% endif %}

@@ -28,7 +30,64 @@ {% endfor %}
{% endif %} - -
+ + +

Available Drives

+

+ + Mounted drives are grayed out and cannot be benchmarked. +

+
+
+{% endblock %} + +{% block extra_js %} + {% endblock %} \ No newline at end of file