Sistem Asistanı (v1.1) / sayfalar/temizlik.py
temizlik.py 339 satır • 14.64 KB
# sayfalar/temizlik.py

from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, 
                             QPushButton, QListWidget, QListWidgetItem, 
                             QCheckBox, QMessageBox, QProgressBar, QTextEdit) # QTextEdit eklendi
from PyQt6.QtCore import Qt, QThread, pyqtSignal
from PyQt6.QtGui import QColor, QFont
from gorsel_araclar import SayfaBasligi, SvgIkonOlusturucu
import subprocess
import os
import shutil
import re 
import time # Loglama için zaman gecikmesi ekleyebiliriz

# TaramaWorker sınıfı değişmiyor. (Yukarıdaki son haliyle aynı kalabilir.)
class TaramaWorker(QThread):
    sonuc_sinyali = pyqtSignal(list) 
    # ... (get_dir_size, size_fmt ve run metotları aynı kalıyor)
    def get_dir_size(self, path):
        total = 0
        try:
            for dirpath, dirnames, filenames in os.walk(path):
                for f in filenames:
                    fp = os.path.join(dirpath, f)
                    if not os.path.islink(fp):
                        total += os.path.getsize(fp)
        except Exception: 
            pass
        return total

    def size_fmt(self, num):
        for unit in ['B', 'KB', 'MB', 'GB']:
            if abs(num) < 1024.0: return f"{num:3.1f} {unit}"
            num /= 1024.0
        return f"{num:.1f} TB"

    def run(self):
        items = []
        
        # 1. Apt Cache (Paket Önbelleği)
        try:
            output = subprocess.check_output("du -sb /var/cache/apt/archives 2>/dev/null", shell=True).strip().split()[0].decode()
            apt_size = int(output)
            if apt_size > 10240:
                items.append(("APT Paket Önbelleği", self.size_fmt(apt_size), "/var/cache/apt/archives", "apt"))
        except: pass

        # 2. Thumbnail Cache (~/.cache/thumbnails)
        thumb_path = os.path.expanduser("~/.cache/thumbnails")
        s = self.get_dir_size(thumb_path)
        if s > 0: 
            items.append(("Küçük Resim (Thumbnail) Önbelleği", self.size_fmt(s), thumb_path, "delete_user"))
        else:
            items.append(("Küçük Resim (Thumbnail) Önbelleği", self.size_fmt(0), thumb_path, "delete_user_empty")) # Boş da olsa göster

        # 3. Tarayıcı ve Uygulama Önbellekleri (~/.cache/mozilla, ~/.cache/google-chrome vb.)
        cache_root = os.path.expanduser("~/.cache")
        targets = ["mozilla", "google-chrome", "chromium", "vlc", "pip", "flatpak"]
        browser_paths = []
        browser_size = 0
        
        for t in targets:
            p = os.path.join(cache_root, t)
            if os.path.exists(p) and os.path.isdir(p):
                size = self.get_dir_size(p)
                if size > 0:
                    browser_size += size
                    browser_paths.append(p) 
        
        path_str = "|".join(browser_paths) 
        display_path = ", ".join(t for t in targets if os.path.join(cache_root, t) in browser_paths)

        if browser_size > 0:
            items.append(("Tarayıcı ve Uygulama Önbellekleri", self.size_fmt(browser_size), path_str, "browser_cache"))
        else:
            items.append(("Tarayıcı ve Uygulama Önbellekleri", self.size_fmt(0), path_str, "browser_cache_empty")) # Boş da olsa göster


        # 4. Journald (Eski Sistem Günlükleri)
        try:
            out = subprocess.check_output("journalctl --disk-usage 2>/dev/null", shell=True, text=True)
            match = re.search(r"take up\s+([\d\.]+)\s*([KMGT]B)", out, re.IGNORECASE)
            if match:
                size_str = match.group(1) + " " + match.group(2)
                items.append(("Eski Sistem Günlükleri (Journald)", size_str.strip(), "/var/log/journal", "journal"))
            else:
                 items.append(("Eski Sistem Günlükleri (Journald)", self.size_fmt(0), "/var/log/journal", "journal_empty"))
        except: pass
        
        # 5. Çöp Kutusu (Kullanıcı Çöpü)
        trash_root = os.path.expanduser("~/.local/share/Trash")
        trash_files = os.path.join(trash_root, "files")
        trash_info = os.path.join(trash_root, "info")
        
        total_trash = 0
        if os.path.exists(trash_files): total_trash += self.get_dir_size(trash_files)
        if os.path.exists(trash_info): total_trash += self.get_dir_size(trash_info)
            
        if total_trash > 0:
            items.append(("Çöp Kutusu", self.size_fmt(total_trash), trash_root, "trash"))
        else:
            items.append(("Çöp Kutusu", self.size_fmt(0), trash_root, "trash_empty")) # Boş da olsa göster

        self.sonuc_sinyali.emit(items)


class TemizlikSayfasi(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        layout = QVBoxLayout(self)
        icon = SvgIkonOlusturucu.clean_ikonu("#33AADD", 32)
        layout.addWidget(SayfaBasligi("Sistem Temizliği", icon))

        # Bilgi
        info = QLabel("Sisteminizde yer kaplayan gereksiz dosyaları tarayın ve temizleyin.")
        info.setStyleSheet("color: palette(mid); margin-bottom: 10px;")
        layout.addWidget(info)

        # Liste
        self.list_widget = QListWidget()
        self.list_widget.setStyleSheet("QListWidget { font-size: 11pt; } QCheckBox { padding: 5px; }")
        # Listeyi sabit bir yükseklikte tutalım
        self.list_widget.setMaximumHeight(250) 
        layout.addWidget(self.list_widget)

        # Log Konsolu
        self.console_log = QTextEdit()
        self.console_log.setReadOnly(True)
        self.console_log.setStyleSheet("background-color: #2c3e50; color: #ecf0f1; font-family: monospace; padding: 5px;")
        self.console_log.setPlaceholderText("Temizlik işlemleri burada listelenecektir...")
        self.console_log.setMaximumHeight(150)
        layout.addWidget(self.console_log)


        # Alt Panel
        h_bot = QHBoxLayout()
        self.lbl_total = QLabel("Toplam Kazanç: 0 MB")
        self.lbl_total.setStyleSheet("font-weight: bold; color: #2ecc71; font-size: 12pt;")
        
        self.btn_tara = QPushButton("🔍 Tekrar Tara")
        self.btn_tara.clicked.connect(self.taramayi_baslat)
        
        self.btn_temizle = QPushButton("🧹 Seçilileri Temizle")
        self.btn_temizle.setStyleSheet("background-color: #e67e22; color: white; font-weight: bold; padding: 8px 20px;")
        self.btn_temizle.clicked.connect(self.temizle)
        self.btn_temizle.setEnabled(False)

        h_bot.addWidget(self.lbl_total)
        h_bot.addStretch()
        h_bot.addWidget(self.btn_tara)
        h_bot.addWidget(self.btn_temizle)
        layout.addLayout(h_bot)

        self.taramayi_baslat()

    def taramayi_baslat(self):
        self.list_widget.clear()
        self.console_log.clear() # Logu da temizle
        self.lbl_total.setText("Taranıyor...")
        self.btn_temizle.setEnabled(False)
        self.btn_tara.setEnabled(False)
        
        # Loga bildirim ekle
        self.append_log("Tarama işlemi başlatılıyor...")
        
        self.worker = TaramaWorker()
        self.worker.sonuc_sinyali.connect(self.tarama_bitti)
        self.worker.start()

    def tarama_bitti(self, items):
        self.items_data = items
        self.btn_tara.setEnabled(True)
        self.append_log("Tarama tamamlandı.")
        
        total_found_size = 0.0
        
        if not items:
            self.lbl_total.setText("Sistem Temiz!")
            return

        self.btn_temizle.setEnabled(True)
        
        for ad, boyut, yol, tip in items:
            item = QListWidgetItem()
            widget = QWidget()
            h = QHBoxLayout(widget); h.setContentsMargins(5, 5, 5, 5)
            
            chk = QCheckBox(ad)
            
            # Boyutu sadece doluysa listede göster, boşsa gri yap
            if "_empty" in tip:
                chk.setChecked(False) # Boş olanları varsayılan olarak seçme
                chk.setStyleSheet("font-weight: normal; color: #7f8c8d;")
                lbl_size = QLabel(boyut)
                lbl_size.setStyleSheet("color: #7f8c8d; font-weight: normal;")
            else:
                chk.setChecked(True)
                chk.setStyleSheet("font-weight: bold;")
                lbl_size = QLabel(boyut)
                lbl_size.setStyleSheet("color: #33AADD; font-weight: bold;")

            # Boyut bilgisini toplayalım (sadece dolu olanları)
            if "_empty" not in tip:
                try:
                    # Basit bir boyut toplama tahmini
                    val = float(boyut.split()[0].replace(',', '.'))
                    unit = boyut.split()[1]
                    if unit == 'KB': total_found_size += val / 1024.0
                    elif unit == 'MB': total_found_size += val
                    elif unit == 'GB': total_found_size += val * 1024.0
                except:
                    pass
            
            # Yolu sadece tarayıcı önbelleği ise kısalt
            if tip.startswith("browser_cache"):
                 lbl_desc = QLabel(f"Önbellek Yolları: {yol.replace('|', ', ')}")
            else:
                 lbl_desc = QLabel(f"{yol}")

            lbl_desc.setStyleSheet("color: #7f8c8d; font-size: 9pt;")
            
            v = QVBoxLayout()
            v.addWidget(chk)
            v.addWidget(lbl_desc)
            
            h.addLayout(v)
            h.addStretch()
            h.addWidget(lbl_size)
            
            item.setSizeHint(widget.sizeHint())
            self.list_widget.addItem(item)
            self.list_widget.setItemWidget(item, widget)
            
            chk.setProperty("data_type", tip)
            chk.setProperty("data_path", yol)
            
            # Boş klasörler seçilemez olmalı
            if "_empty" in tip:
                 chk.setEnabled(False) 

        self.lbl_total.setText(f"Tahmini Kazanç: {self.worker.size_fmt(total_found_size * 1024 * 1024)}")


    def append_log(self, message, is_error=False):
        """Konsola log mesajı ekler."""
        color = "#e74c3c" if is_error else "#2ecc71"
        html_message = f'<span style="color:{color}; font-weight:bold;">[{time.strftime("%H:%M:%S")}] </span> {message}<br>'
        self.console_log.insertHtml(html_message)
        self.console_log.verticalScrollBar().setValue(self.console_log.verticalScrollBar().maximum())
        
    def temizle(self):
        komutlar = []
        user_paths_to_delete = [] 
        trash_mode = False
        
        for i in range(self.list_widget.count()):
            item = self.list_widget.item(i)
            widget = self.list_widget.itemWidget(item)
            chk = widget.findChild(QCheckBox)
            
            # Sadece seçili ve boş olmayan (yani silinecek bir şey olan) öğeleri topla
            if chk.isChecked() and "_empty" not in chk.property("data_type"):
                tip = chk.property("data_type")
                yol = chk.property("data_path")
                
                if tip == "apt": 
                    komutlar.append("apt-get clean")
                    self.append_log("Sistem: APT Paket Önbelleği temizleme komutu eklendi.")
                elif tip == "journal": 
                    komutlar.append("journalctl --vacuum-time=1s") 
                    self.append_log("Sistem: Eski Sistem Günlükleri (Journald) temizleme komutu eklendi.")
                elif tip == "delete_user": 
                    user_paths_to_delete.append(yol)
                    self.append_log(f"Kullanıcı: Küçük Resim Önbelleği ({yol}) silinmek üzere işaretlendi.")
                elif tip == "browser_cache":
                    browser_paths = yol.split("|")
                    user_paths_to_delete.extend(browser_paths)
                    self.append_log(f"Kullanıcı: Tarayıcı/Uygulama Önbellekleri ({len(browser_paths)} klasör) silinmek üzere işaretlendi.")
                elif tip == "trash":
                    trash_mode = True
                    self.append_log("Kullanıcı: Çöp Kutusu içeriği silinmek üzere işaretlendi (Root yetkisi gerekebilir).")


        if not komutlar and not user_paths_to_delete and not trash_mode:
            self.append_log("Temizlenecek seçili öğe bulunamadı.", is_error=True)
            return

        reply = QMessageBox.question(self, "Onay", "Seçili dosyalar kalıcı olarak silinecek. Devam edilsin mi?", 
                                     QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
        if reply == QMessageBox.StandardButton.No: return
        
        clean_success = True
        self.append_log("Temizlik işlemi başlatılıyor...")


        # 1. Çöp Kutusu ve Sistem Komutları (pkexec ile root yetkisi gerektirenler)
        if trash_mode:
            trash_root = os.path.expanduser("~/.local/share/Trash")
            # rm -rf komutu, içindeki dosyaların root yetkisi olsa bile silinmesini sağlar.
            trash_clean_cmd = f"rm -rf {trash_root}/files/* {trash_root}/info/*"
            komutlar.append(trash_clean_cmd)
        
        if komutlar:
            full_cmd = " && ".join(komutlar)
            self.append_log(f"ROOT KOMUTU ÇALIŞTIRILIYOR: {full_cmd}")
            try: 
                # pkexec ile root yetkisi iste
                subprocess.run(["pkexec", "sh", "-c", full_cmd], check=True, 
                               stdout=subprocess.PIPE, stderr=subprocess.PIPE)
                self.append_log("Sistem/Çöp Kutusu temizliği başarılı.")
            except Exception as e: 
                QMessageBox.warning(self, "Hata", f"Sistem ve Çöp Kutusu temizliği sırasında yetki/komut hatası.")
                self.append_log(f"HATA: Sistem temizliği başarısız oldu. Yetki hatası olabilir.", is_error=True)
                clean_success = False

        # 2. Normal Kullanıcı Klasörleri (Tarayıcı önbellekleri, vb.)
        if user_paths_to_delete:
            self.append_log(f"{len(user_paths_to_delete)} adet kullanıcı dizini temizleniyor...")
            for p in user_paths_to_delete:
                try:
                    self.append_log(f"  Siliniyor: {p}")
                    if os.path.islink(p) or os.path.isfile(p): 
                        os.unlink(p)
                    elif os.path.isdir(p): 
                        shutil.rmtree(p)
                except Exception as e: 
                    self.append_log(f"HATA: {p} silinemedi: İzin hatası.", is_error=True)
                    clean_success = False
            self.append_log("Kullanıcı dizini temizliği tamamlandı.")


        # Temizlik sonucunu bildir
        if clean_success:
            QMessageBox.information(self, "Tamamlandı", "Temizlik işlemi başarıyla tamamlandı.")
        else:
            QMessageBox.warning(self, "Kısmen Tamamlandı", "Temizlik işlemi tamamlandı, ancak bazı dosyalara ulaşılamadığı için silinemedi (izin hatası olabilir).")
            
        # Temizlik sonrası listeyi güncelle
        self.taramayi_baslat()