232 lines
11 KiB
Python
232 lines
11 KiB
Python
import customtkinter as ctk
|
|
import psutil
|
|
import subprocess
|
|
import shutil
|
|
import threading
|
|
import json
|
|
import os
|
|
|
|
class SevenDiskApp(ctk.CTk):
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Configuration de la fenêtre principale
|
|
self.title("7Disk - Moniteur de Stockage Pro")
|
|
self.geometry("900x600")
|
|
self.minsize(800, 500)
|
|
ctk.set_appearance_mode("System")
|
|
ctk.set_default_color_theme("blue")
|
|
|
|
# Configuration de la grille (Sidebar + Contenu principal)
|
|
self.grid_rowconfigure(0, weight=1)
|
|
self.grid_columnconfigure(1, weight=1)
|
|
|
|
# --- BARRE LATÉRALE (SIDEBAR) ---
|
|
self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0)
|
|
self.sidebar_frame.grid(row=0, column=0, sticky="nsew")
|
|
self.sidebar_frame.grid_rowconfigure(4, weight=1)
|
|
|
|
self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="7Disk ⚡", font=ctk.CTkFont(size=24, weight="bold"))
|
|
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 30))
|
|
|
|
self.btn_tab_overview = ctk.CTkButton(self.sidebar_frame, text="📊 Vue d'ensemble", fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"), anchor="w", command=lambda: self.select_tab("overview"))
|
|
self.btn_tab_overview.grid(row=1, column=0, padx=20, pady=10, sticky="ew")
|
|
|
|
self.btn_tab_smart = ctk.CTkButton(self.sidebar_frame, text="🩺 Santé S.M.A.R.T.", fg_color="transparent", text_color=("gray10", "gray90"), hover_color=("gray70", "gray30"), anchor="w", command=lambda: self.select_tab("smart"))
|
|
self.btn_tab_smart.grid(row=2, column=0, padx=20, pady=10, sticky="ew")
|
|
|
|
self.appearance_mode_label = ctk.CTkLabel(self.sidebar_frame, text="Thème:", anchor="w")
|
|
self.appearance_mode_label.grid(row=5, column=0, padx=20, pady=(10, 0))
|
|
self.appearance_mode_menu = ctk.CTkOptionMenu(self.sidebar_frame, values=["System", "Dark", "Light"], command=self.change_appearance_mode)
|
|
self.appearance_mode_menu.grid(row=6, column=0, padx=20, pady=(10, 20))
|
|
|
|
# --- ZONE DE CONTENU PRINCIPAL ---
|
|
self.main_frame = ctk.CTkFrame(self, corner_radius=10, fg_color="transparent")
|
|
self.main_frame.grid(row=0, column=1, padx=20, pady=20, sticky="nsew")
|
|
self.main_frame.grid_rowconfigure(1, weight=1)
|
|
self.main_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
# Création des différentes vues
|
|
self.create_overview_tab()
|
|
self.create_smart_tab()
|
|
|
|
# Sélection de l'onglet par défaut
|
|
self.select_tab("overview")
|
|
|
|
def change_appearance_mode(self, new_appearance_mode: str):
|
|
ctk.set_appearance_mode(new_appearance_mode)
|
|
|
|
def select_tab(self, tab_name):
|
|
# Cache toutes les frames
|
|
self.frame_overview.grid_forget()
|
|
self.frame_smart.grid_forget()
|
|
|
|
# Réinitialise les couleurs des boutons
|
|
self.btn_tab_overview.configure(fg_color="transparent")
|
|
self.btn_tab_smart.configure(fg_color="transparent")
|
|
|
|
# Affiche la frame demandée
|
|
if tab_name == "overview":
|
|
self.frame_overview.grid(row=0, column=0, sticky="nsew")
|
|
self.btn_tab_overview.configure(fg_color=("gray75", "gray25"))
|
|
self.load_overview_data()
|
|
elif tab_name == "smart":
|
|
self.frame_smart.grid(row=0, column=0, sticky="nsew")
|
|
self.btn_tab_smart.configure(fg_color=("gray75", "gray25"))
|
|
|
|
# ==========================================
|
|
# ONGLET 1 : VUE D'ENSEMBLE (Partitions)
|
|
# ==========================================
|
|
def create_overview_tab(self):
|
|
self.frame_overview = ctk.CTkScrollableFrame(self.main_frame, fg_color="transparent")
|
|
self.frame_overview.grid_columnconfigure(0, weight=1)
|
|
|
|
self.lbl_overview_title = ctk.CTkLabel(self.frame_overview, text="Stockage Local", font=ctk.CTkFont(size=20, weight="bold"))
|
|
self.lbl_overview_title.grid(row=0, column=0, sticky="w", pady=(0, 20))
|
|
|
|
self.partitions_frame = ctk.CTkFrame(self.frame_overview, fg_color="transparent")
|
|
self.partitions_frame.grid(row=1, column=0, sticky="nsew")
|
|
self.partitions_frame.grid_columnconfigure(0, weight=1)
|
|
|
|
def load_overview_data(self):
|
|
# Nettoyage de l'affichage existant
|
|
for widget in self.partitions_frame.winfo_children():
|
|
widget.destroy()
|
|
|
|
partitions = psutil.disk_partitions(all=False)
|
|
row_idx = 0
|
|
|
|
for p in partitions:
|
|
# Ignorer les systèmes de fichiers virtuels et les snaps de Linux
|
|
if p.fstype == "" or "snap" in p.mountpoint or "boot" in p.mountpoint:
|
|
continue
|
|
|
|
try:
|
|
usage = psutil.disk_usage(p.mountpoint)
|
|
total_gb = usage.total // (2**30)
|
|
used_gb = usage.used // (2**30)
|
|
free_gb = usage.free // (2**30)
|
|
percent = usage.percent
|
|
|
|
card = ctk.CTkFrame(self.partitions_frame, corner_radius=10)
|
|
card.grid(row=row_idx, column=0, sticky="ew", pady=10)
|
|
card.grid_columnconfigure(1, weight=1)
|
|
|
|
lbl_mount = ctk.CTkLabel(card, text=f"Point de montage : {p.mountpoint} ({p.fstype})", font=ctk.CTkFont(weight="bold"))
|
|
lbl_mount.grid(row=0, column=0, columnspan=2, sticky="w", padx=15, pady=(10, 5))
|
|
|
|
lbl_info = ctk.CTkLabel(card, text=f"{used_gb} Go utilisés sur {total_gb} Go | Libre: {free_gb} Go")
|
|
lbl_info.grid(row=1, column=0, sticky="w", padx=15, pady=0)
|
|
|
|
lbl_percent = ctk.CTkLabel(card, text=f"{percent}%", font=ctk.CTkFont(weight="bold"))
|
|
lbl_percent.grid(row=1, column=1, sticky="e", padx=15, pady=0)
|
|
|
|
prog = ctk.CTkProgressBar(card)
|
|
prog.grid(row=2, column=0, columnspan=2, sticky="ew", padx=15, pady=(5, 15))
|
|
prog.set(percent / 100)
|
|
|
|
if percent > 90:
|
|
prog.configure(progress_color="#e74c3c") # Rouge
|
|
elif percent > 75:
|
|
prog.configure(progress_color="#f39c12") # Orange
|
|
else:
|
|
prog.configure(progress_color="#2ecc71") # Vert
|
|
|
|
row_idx += 1
|
|
except PermissionError:
|
|
continue
|
|
|
|
# ==========================================
|
|
# ONGLET 2 : SANTÉ S.M.A.R.T (Physique)
|
|
# ==========================================
|
|
def create_smart_tab(self):
|
|
self.frame_smart = ctk.CTkFrame(self.main_frame, fg_color="transparent")
|
|
self.frame_smart.grid_columnconfigure(0, weight=1)
|
|
self.frame_smart.grid_rowconfigure(2, weight=1)
|
|
|
|
self.lbl_smart_title = ctk.CTkLabel(self.frame_smart, text="Diagnostic Matériel (S.M.A.R.T.)", font=ctk.CTkFont(size=20, weight="bold"))
|
|
self.lbl_smart_title.grid(row=0, column=0, sticky="w", pady=(0, 10))
|
|
|
|
# Contrôles du haut
|
|
self.controls_frame = ctk.CTkFrame(self.frame_smart, fg_color="transparent")
|
|
self.controls_frame.grid(row=1, column=0, sticky="ew", pady=10)
|
|
|
|
self.disk_selector = ctk.CTkOptionMenu(self.controls_frame, values=["/dev/nvme0n1", "/dev/sda", "/dev/sdb"])
|
|
self.disk_selector.pack(side="left", padx=(0, 10))
|
|
|
|
self.btn_scan = ctk.CTkButton(self.controls_frame, text="🔍 Lancer le Scan Profond", command=self.start_smart_scan)
|
|
self.btn_scan.pack(side="left")
|
|
|
|
self.lbl_status = ctk.CTkLabel(self.controls_frame, text="", text_color="gray")
|
|
self.lbl_status.pack(side="left", padx=10)
|
|
|
|
# Zone d'affichage des résultats
|
|
self.textbox_smart = ctk.CTkTextbox(self.frame_smart, font=ctk.CTkFont(family="monospace", size=12))
|
|
self.textbox_smart.grid(row=2, column=0, sticky="nsew", pady=10)
|
|
self.textbox_smart.insert("0.0", "Sélectionnez un disque et lancez le scan.\nLes droits administrateurs vous seront demandés via une fenêtre Zorin OS.")
|
|
self.textbox_smart.configure(state="disabled")
|
|
|
|
def start_smart_scan(self):
|
|
disk = self.disk_selector.get()
|
|
self.btn_scan.configure(state="disabled")
|
|
self.lbl_status.configure(text="Analyse en cours, veuillez patienter...")
|
|
self.textbox_smart.configure(state="normal")
|
|
self.textbox_smart.delete("0.0", "end")
|
|
self.textbox_smart.configure(state="disabled")
|
|
|
|
# Lancement de l'analyse dans un Thread séparé pour ne pas figer l'interface
|
|
threading.Thread(target=self.fetch_smart_data, args=(disk,), daemon=True).start()
|
|
|
|
def fetch_smart_data(self, disk_path):
|
|
try:
|
|
# Utilisation de -j pour récupérer du JSON, beaucoup plus propre à traiter
|
|
result = subprocess.run(['pkexec', 'smartctl', '-a', '-j', disk_path], capture_output=True, text=True)
|
|
|
|
if result.returncode == 126 or result.returncode == 127:
|
|
output_text = "❌ Erreur : Droits administrateur refusés ou smartctl non trouvé."
|
|
else:
|
|
try:
|
|
data = json.loads(result.stdout)
|
|
|
|
model = data.get("model_name", "Inconnu")
|
|
serial = data.get("serial_number", "Inconnu")
|
|
passed = data.get("smart_status", {}).get("passed", False)
|
|
temp = data.get("temperature", {}).get("current", "N/A")
|
|
|
|
status_icon = "✅ BON" if passed else "❌ DÉFAILLANT"
|
|
|
|
output_text = f"=== RAPPPORT 7DISK : {disk_path} ===\n\n"
|
|
output_text += f"Modèle : {model}\n"
|
|
output_text += f"N° Série : {serial}\n"
|
|
output_text += f"Statut S.M.A.R.T: {status_icon}\n"
|
|
output_text += f"Température : {temp}°C\n\n"
|
|
output_text += "--- Détails Techniques Bruts ---\n"
|
|
|
|
# Ajout des informations brutes formatées
|
|
if "nvme_smart_health_information_log" in data:
|
|
for key, val in data["nvme_smart_health_information_log"].items():
|
|
output_text += f"{key}: {val}\n"
|
|
elif "ata_smart_attributes" in data:
|
|
output_text += "ID Attribut Valeur\n"
|
|
for attr in data["ata_smart_attributes"].get("table", []):
|
|
output_text += f"{attr['id']:<4} {attr['name']:<30} {attr['raw']['value']}\n"
|
|
|
|
except json.JSONDecodeError:
|
|
output_text = f"⚠️ Résultat brut (Impossible de parser le JSON):\n\n{result.stdout}"
|
|
|
|
except Exception as e:
|
|
output_text = f"❌ Erreur inattendue : {e}"
|
|
|
|
# Mise à jour de l'interface graphique (doit être fait via mainloop, d'où self.after)
|
|
self.after(0, self.update_smart_ui, output_text)
|
|
|
|
def update_smart_ui(self, text):
|
|
self.textbox_smart.configure(state="normal")
|
|
self.textbox_smart.insert("0.0", text)
|
|
self.textbox_smart.configure(state="disabled")
|
|
self.lbl_status.configure(text="Analyse terminée.")
|
|
self.btn_scan.configure(state="normal")
|
|
|
|
if __name__ == "__main__":
|
|
app = SevenDiskApp()
|
|
app.mainloop() |