Storage Tool

Free Windows utility to quickly free disk space: clear temp/cache, empty Recycle Bin, purge Windows Update cache, keep only the latest LCU, scan and remove largest files, and more. Dry-run mode by default.

Download for Windows (.exe)

Source Code


#!/usr/bin/env python3
import ctypes.wintypes
import os, sys, re, ctypes, threading, queue, time, math, shutil, subprocess
from pathlib import Path
from datetime import datetime, timedelta
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
APP_TITLE = "Storage Tool"
VERSION = "1.1.0"
def is_windows() -> bool:
    return os.name == 'nt'
if not is_windows():
    print("This tool is Windows-only.")
    sys.exit(1)
try:
    IS_ADMIN = ctypes.windll.shell32.IsUserAnAdmin()
except Exception:
    IS_ADMIN = False
class SHFILEOPSTRUCT(ctypes.Structure):
    _fields_ = [
        ("hwnd", ctypes.wintypes.HWND),
        ("wFunc", ctypes.wintypes.UINT),
        ("pFrom", ctypes.wintypes.LPCWSTR),
        ("pTo", ctypes.wintypes.LPCWSTR),
        ("fFlags", ctypes.wintypes.USHORT),
        ("fAnyOperationsAborted", ctypes.wintypes.BOOL),
        ("hNameMappings", ctypes.wintypes.LPVOID),
        ("lpszProgressTitle", ctypes.wintypes.LPCWSTR),
    ]
FO_DELETE = 3
FOF_ALLOWUNDO = 0x0040
FOF_NOCONFIRMATION = 0x0010
FOF_SILENT = 0x0004
SHERB_NOCONFIRMATION = 0x00000001
SHERB_NOPROGRESSUI = 0x00000002
SHERB_NOSOUND = 0x00000004
USERPROFILE = Path(os.environ.get('USERPROFILE', str(Path.home())))
LOCALAPPDATA = Path(os.environ.get('LOCALAPPDATA', USERPROFILE / 'AppData/Local'))
TEMP = Path(os.environ.get('TEMP', str(LOCALAPPDATA / 'Temp')))
WINTEMP = Path(r"C:\Windows\Temp")
SOFTWAREDISTRIBUTION = Path(r"C:\Windows\SoftwareDistribution\Download")
SD_INSTALL = SOFTWAREDISTRIBUTION / "Install"
WER = Path(r"C:\ProgramData\Microsoft\Windows\WER")
LOGS = Path(r"C:\Windows\Logs")
PREFETCH = Path(r"C:\Windows\Prefetch")
BROWSER_CACHES = [
    LOCALAPPDATA / r"Microsoft/Edge/User Data/Default/Cache",
    LOCALAPPDATA / r"Google/Chrome/User Data/Default/Cache",
    LOCALAPPDATA / r"Mozilla/Firefox/Profiles",
]
def bytes_fmt(n: int) -> str:
    for unit in ['B','KB','MB','GB','TB']:
        if n < 1024:
            return f"{n:.0f} {unit}"
        n /= 1024
    return f"{n:.1f} PB"
def get_drives():
    drives = []
    bitmask = ctypes.windll.kernel32.GetLogicalDrives()
    for i in range(26):
        if bitmask & (1 << i):
            root = f"{chr(65+i)}:\\"
            type_ = ctypes.windll.kernel32.GetDriveTypeW(ctypes.c_wchar_p(root))
            if type_ == 3:
                total, used, free = shutil.disk_usage(root)
                drives.append((root, total, used, free))
    return drives
def can_recycle(path: Path) -> bool:
    return path.exists()
def recycle_path(path: Path) -> tuple[bool, str]:
    try:
        pFrom = f"{str(path)}\0\0"
        fos = SHFILEOPSTRUCT()
        fos.hwnd = 0
        fos.wFunc = FO_DELETE
        fos.pFrom = pFrom
        fos.pTo = None
        fos.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT
        res = ctypes.windll.shell32.SHFileOperationW(ctypes.byref(fos))
        if res == 0 and not fos.fAnyOperationsAborted:
            return True, "recycled"
        return False, f"shell op failed: code {res}, aborted={bool(fos.fAnyOperationsAborted)}"
    except Exception as e:
        return False, f"recycle error: {e}"
def rm_tree(path: Path) -> tuple[int, list[str]]:
    freed = 0
    errors: list[str] = []
    if not path.exists():
        return 0, []
    try:
        if path.is_file() or path.is_symlink():
            try:
                size = path.stat().st_size
            except Exception:
                size = 0
            try:
                path.unlink(missing_ok=True)
                freed += size
            except Exception as e:
                errors.append(f"{path}: {e}")
        else:
            for root, dirs, files in os.walk(path, topdown=False):
                for name in files:
                    fp = Path(root) / name
                    try:
                        size = fp.stat().st_size
                    except Exception:
                        size = 0
                    try:
                        fp.unlink(missing_ok=True)
                        freed += size
                    except Exception as e:
                        errors.append(f"{fp}: {e}")
                for name in dirs:
                    dp = Path(root) / name
                    try:
                        dp.rmdir()
                    except Exception as e:
                        errors.append(f"{dp}: {e}")
            try:
                path.rmdir()
            except Exception:
                pass
    except Exception as e:
        errors.append(f"{path}: {e}")
    return freed, errors
def dir_size(path: Path) -> int:
    total = 0
    for root, _, files in os.walk(path):
        for f in files:
            try:
                total += (Path(root)/f).stat().st_size
            except Exception:
                pass
    return total
def find_largest(start: Path, limit: int = 100, skip_system=True):
    sizes = []
    start = Path(start)
    sys_roots = {Path(r"C:\Windows"), Path(r"C:\Program Files"), Path(r"C:\Program Files (x86)")}
    for root, dirs, files in os.walk(start):
        rp = Path(root)
        if skip_system and any(str(rp).startswith(str(s)) for s in sys_roots):
            dirs[:] = []
            continue
        for name in files:
            p = rp / name
            try:
                s = p.stat().st_size
                sizes.append((p, s))
            except Exception:
                continue
    sizes.sort(key=lambda x: x[1], reverse=True)
    return sizes[:limit]
def estimate_folder(paths: list[Path]) -> int:
    total = 0
    for p in paths:
        if p.exists():
            try:
                if p.is_file():
                    total += p.stat().st_size
                else:
                    total += dir_size(p)
            except Exception:
                pass
    return total
def empty_recycle_bin() -> tuple[int, str]:
    try:
        freed_before = get_recycle_bin_size()
    except Exception:
        freed_before = 0
    try:
        ctypes.windll.shell32.SHEmptyRecycleBinW(None, None, SHERB_NOCONFIRMATION | SHERB_NOPROGRESSUI | SHERB_NOSOUND)
        return freed_before, "Recycle Bin emptied"
    except Exception as e:
        return 0, f"Failed to empty Recycle Bin: {e}"
def get_recycle_bin_size() -> int:
    total = 0
    for root, _, _ in get_drives():
        rb = Path(root) / "$Recycle.Bin"
        if rb.exists():
            total += dir_size(rb)
    return total
def stop_service(name: str) -> tuple[bool, str]:
    try:
        res = subprocess.run(["sc", "stop", name], capture_output=True, text=True, timeout=30)
        ok = res.returncode == 0 or "STOP_PENDING" in res.stdout
        return ok, res.stdout.strip() or res.stderr.strip()
    except Exception as e:
        return False, str(e)
def start_service(name: str) -> tuple[bool, str]:
    try:
        res = subprocess.run(["sc", "start", name], capture_output=True, text=True, timeout=30)
        ok = res.returncode == 0 or "RUNNING" in res.stdout
        return ok, res.stdout.strip() or res.stderr.strip()
    except Exception as e:
        return False, str(e)
def purge_software_distribution() -> tuple[int, list[str]]:
    errors = []
    freed = 0
    ok1, msg1 = stop_service("wuauserv")
    ok2, msg2 = stop_service("bits")
    if not ok1:
        errors.append(f"Stop wuauserv: {msg1}")
    if not ok2:
        errors.append(f"Stop BITS: {msg2}")
    if ok1 and ok2:
        if SOFTWAREDISTRIBUTION.exists():
            f, e = rm_tree(SOFTWAREDISTRIBUTION)
            freed += f
            errors += e
    start_service("bits")
    start_service("wuauserv")
    return freed, errors
def toggle_hibernation(enable: bool) -> tuple[bool, str]:
    if not IS_ADMIN:
        return False, "Admin required to toggle hibernation."
    try:
        arg = "/h on" if enable else "/h off"
        res = subprocess.run(["powercfg", arg], shell=True, capture_output=True, text=True)
        if res.returncode == 0:
            return True, ("Enabled" if enable else "Disabled")
        return False, res.stderr.strip() or res.stdout.strip()
    except Exception as e:
        return False, str(e)
def lcu_keep_latest_estimate() -> tuple[int, int]:
    if not SD_INSTALL.exists():
        return 0, 0
    items = list(SD_INSTALL.glob("*"))
    if not items:
        return 0, 0
    items = [p for p in items if p.exists()]
    if len(items) <= 1:
        return 0, 0
    items.sort(key=lambda p: p.stat().st_mtime, reverse=True)
    to_delete = items[1:]
    total = 0
    count = 0
    for p in to_delete:
        try:
            total += p.stat().st_size if p.is_file() else dir_size(p)
            count += 1
        except Exception:
            pass
    return total, count
def lcu_keep_latest_delete(dry_run: bool) -> tuple[int, int, list[str]]:
    if not SD_INSTALL.exists():
        return 0, 0, []
    items = list(SD_INSTALL.glob("*"))
    if len(items) <= 1:
        return 0, 0, []
    items.sort(key=lambda p: p.stat().st_mtime, reverse=True)
    to_delete = items[1:]
    total = 0
    count = 0
    errs = []
    for p in to_delete:
        try:
            if dry_run:
                total += p.stat().st_size if p.is_file() else dir_size(p)
                count += 1
            else:
                if p.is_file():
                    ok, note = recycle_path(p)
                    if ok:
                        total += p.stat().st_size
                        count += 1
                    else:
                        f, e = rm_tree(p)
                        total += f
                        count += 1
                        errs += e
                else:
                    f, e = rm_tree(p)
                    total += f
                    count += 1
                    errs += e
        except Exception as e:
            errs.append(f"{p}: {e}")
    return total, count, errs
class Logger:
    def __init__(self, widget: tk.Text):
        self.widget = widget
        self.lock = threading.Lock()
    def log(self, msg: str):
        ts = datetime.now().strftime('%H:%M:%S')
        line = f"[{ts}] {msg}\n"
        with self.lock:
            self.widget.insert(tk.END, line)
            self.widget.see(tk.END)
class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.overrideredirect(True)
        self.geometry('980x700')
        self.minsize(900, 620)
        try:
            self.iconbitmap(default='')
        except Exception:
            pass
        self.style = ttk.Style(self)
        self.style.theme_use('clam')
        self.dry_run = tk.BooleanVar(value=True)
        self.scan_limit = tk.IntVar(value=100)
        self.days_threshold = tk.IntVar(value=30)
        self._build_chrome()
        self._build_ui()
        self.center_on_screen()
        self.refresh_overview()
    def _build_chrome(self):
        self.border = tk.Frame(self, bg="#0b1220", bd=0, highlightthickness=1, highlightbackground="#2c3448")
        self.border.pack(fill=tk.BOTH, expand=True)
        self.titlebar = tk.Frame(self.border, bg="#111827")
        self.titlebar.pack(fill=tk.X, side=tk.TOP)
        self.title_label = tk.Label(self.titlebar, text=f"{APP_TITLE} — v{VERSION}", bg="#111827", fg="#e5e7eb", font=("Segoe UI", 10, "bold"))
        self.title_label.pack(side=tk.LEFT, padx=10, pady=4)
        self.dry_cb = ttk.Checkbutton(self.titlebar, text='Dry-run', variable=self.dry_run)
        self.dry_cb.pack(side=tk.LEFT, padx=8)
        self.btn_min = tk.Button(self.titlebar, text="—", command=self.minimize, bg="#111827", fg="#e5e7eb", bd=0, width=3, activebackground="#1f2937", activeforeground="#e5e7eb")
        self.btn_min.pack(side=tk.RIGHT, padx=(0,2), pady=2)
        self.btn_close = tk.Button(self.titlebar, text="✕", command=self.close, bg="#111827", fg="#e5e7eb", bd=0, width=3, activebackground="#b91c1c", activeforeground="#fff")
        self.btn_close.pack(side=tk.RIGHT, padx=(0,6), pady=2)
        self.titlebar.bind("", self.start_move)
        self.titlebar.bind("", self.do_move)
        self.title_label.bind("", self.start_move)
        self.title_label.bind("", self.do_move)
        self.body = tk.Frame(self.border, bg="#0b1220")
        self.body.pack(fill=tk.BOTH, expand=True)
        self.sizegrip = ttk.Sizegrip(self.body)
        self.sizegrip.place(relx=1.0, rely=1.0, anchor="se")
    def minimize(self):
        self.update_idletasks()
        self.overrideredirect(False)
        self.iconify()
        def reover(event):
            self.overrideredirect(True)
            self.unbind("")
        self.bind("", reover)
    def close(self):
        self.destroy()
    def start_move(self, event):
        self._x = event.x
        self._y = event.y
    def do_move(self, event):
        x = self.winfo_pointerx() - self._x
        y = self.winfo_pointery() - self._y
        self.geometry(f"+{x}+{y}")
    def center_on_screen(self):
        self.update_idletasks()
        sw = self.winfo_screenwidth()
        sh = self.winfo_screenheight()
        w = self.winfo_width()
        h = self.winfo_height()
        x = (sw - w) // 2
        y = (sh - h) // 2
        self.geometry(f"{w}x{h}+{x}+{y}")
    def _build_ui(self):
        top = ttk.Frame(self.body)
        top.pack(fill=tk.X, padx=10, pady=8)
        nb = ttk.Notebook(self.body)
        nb.pack(fill=tk.BOTH, expand=True, padx=10, pady=(0,10))
        self.tab_overview = ttk.Frame(nb)
        self.tab_quick = ttk.Frame(nb)
        self.tab_deep = ttk.Frame(nb)
        self.tab_tools = ttk.Frame(nb)
        self.tab_log = ttk.Frame(nb)
        nb.add(self.tab_overview, text='Overview')
        nb.add(self.tab_quick, text='Quick Clean')
        nb.add(self.tab_deep, text='Deep Scan')
        nb.add(self.tab_tools, text='Tools')
        nb.add(self.tab_log, text='Log')
        self.log_text = tk.Text(self.tab_log, height=12, wrap='word')
        self.log_text.pack(fill=tk.BOTH, expand=True)
        self.logger = Logger(self.log_text)
        self.tree = ttk.Treeview(self.tab_overview, columns=('drive', 'total', 'used', 'free', 'pct'), show='headings')
        for col, title, w in [('drive','Drive',140),('total','Total',140),('used','Used',140),('free','Free',140),('pct','% Free',80)]:
            self.tree.heading(col, text=title)
            self.tree.column(col, width=w, anchor='center' if col!='drive' else 'w')
        self.tree.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
        btn_row = ttk.Frame(self.tab_overview)
        btn_row.pack(fill=tk.X, padx=8, pady=(0,8))
        ttk.Button(btn_row, text='Refresh', command=self.refresh_overview).pack(side=tk.LEFT)
        ttk.Button(btn_row, text='Run Quick Clean', command=self.run_quick_clean).pack(side=tk.RIGHT)
        qc = self.tab_quick
        sec1 = ttk.LabelFrame(qc, text='Targets')
        sec1.pack(fill=tk.X, padx=8, pady=8)
        self.var_temp = tk.BooleanVar(value=True)
        self.var_wintemp = tk.BooleanVar(value=True)
        self.var_browser = tk.BooleanVar(value=True)
        self.var_wer = tk.BooleanVar(value=True)
        self.var_logs = tk.BooleanVar(value=False)
        self.var_prefetch = tk.BooleanVar(value=False)
        self.var_recycle = tk.BooleanVar(value=True)
        row = ttk.Frame(sec1); row.pack(fill=tk.X, padx=8, pady=6)
        ttk.Checkbutton(row, text=f"User Temp ({TEMP})", variable=self.var_temp).pack(anchor='w')
        ttk.Checkbutton(row, text=f"Windows Temp ({WINTEMP}) [admin for some files]", variable=self.var_wintemp).pack(anchor='w')
        ttk.Checkbutton(row, text="Browser caches (Edge/Chrome/Firefox)", variable=self.var_browser).pack(anchor='w')
        ttk.Checkbutton(row, text=f"Windows Error Reporting ({WER})", variable=self.var_wer).pack(anchor='w')
        ttk.Checkbutton(row, text=f"Windows Logs ({LOGS})", variable=self.var_logs).pack(anchor='w')
        ttk.Checkbutton(row, text=f"Prefetch ({PREFETCH})", variable=self.var_prefetch).pack(anchor='w')
        ttk.Checkbutton(row, text="Empty Recycle Bin", variable=self.var_recycle).pack(anchor='w')
        sec2 = ttk.LabelFrame(qc, text='Actions')
        sec2.pack(fill=tk.X, padx=8, pady=(0,8))
        ttk.Button(sec2, text='Estimate Savings', command=self.estimate_quick_clean).pack(side=tk.LEFT, padx=6, pady=6)
        ttk.Button(sec2, text='Run Quick Clean', command=self.run_quick_clean).pack(side=tk.LEFT, padx=6, pady=6)
        self.qc_est_label = ttk.Label(qc, text='Estimated savings: -')
        self.qc_est_label.pack(anchor='w', padx=16)
        ds_top = ttk.Frame(self.tab_deep)
        ds_top.pack(fill=tk.X, padx=8, pady=6)
        ttk.Label(ds_top, text='Scan folder:').pack(side=tk.LEFT)
        self.scan_path_var = tk.StringVar(value='C:\\')
        ttk.Entry(ds_top, textvariable=self.scan_path_var, width=40).pack(side=tk.LEFT, padx=6)
        ttk.Button(ds_top, text='Browse', command=self.browse_scan_folder).pack(side=tk.LEFT)
        ttk.Label(ds_top, text='Top N:').pack(side=tk.LEFT, padx=(10,2))
        ttk.Spinbox(ds_top, from_=20, to=1000, textvariable=self.scan_limit, width=6).pack(side=tk.LEFT)
        ttk.Button(ds_top, text='Scan Largest Files', command=self.start_deep_scan).pack(side=tk.RIGHT)
        self.scan_progress = ttk.Progressbar(self.tab_deep, mode='indeterminate')
        self.scan_progress.pack(fill=tk.X, padx=8)
        self.deep_tree = ttk.Treeview(self.tab_deep, columns=('path','size'), show='headings', selectmode='extended')
        self.deep_tree.heading('path', text='Path')
        self.deep_tree.heading('size', text='Size')
        self.deep_tree.column('path', width=700)
        self.deep_tree.column('size', width=120, anchor='e')
        self.deep_tree.pack(fill=tk.BOTH, expand=True, padx=8, pady=8)
        ds_btns = ttk.Frame(self.tab_deep)
        ds_btns.pack(fill=tk.X, padx=8, pady=(0,8))
        ttk.Button(ds_btns, text='Delete Selected (Recycle if possible)', command=self.delete_selected).pack(side=tk.RIGHT)
        tools = self.tab_tools
        lf1 = ttk.LabelFrame(tools, text='Downloads cleanup (user)')
        lf1.pack(fill=tk.X, padx=8, pady=8)
        ttk.Label(lf1, text='Delete files in Downloads older than N days:').pack(side=tk.LEFT, padx=6)
        ttk.Spinbox(lf1, from_=7, to=3650, textvariable=self.days_threshold, width=6).pack(side=tk.LEFT)
        ttk.Button(lf1, text='Estimate', command=self.estimate_old_downloads).pack(side=tk.LEFT, padx=6)
        ttk.Button(lf1, text='Delete', command=self.delete_old_downloads).pack(side=tk.LEFT)
        lf2 = ttk.LabelFrame(tools, text='Windows Update cache (admin)')
        lf2.pack(fill=tk.X, padx=8, pady=8)
        ttk.Button(lf2, text='Purge SoftwareDistribution/Download', command=self.purge_wu_cache).pack(side=tk.LEFT, padx=6, pady=6)
        lf3 = ttk.LabelFrame(tools, text='Hibernation (admin)')
        lf3.pack(fill=tk.X, padx=8, pady=8)
        ttk.Button(lf3, text='Disable Hibernation (free hiberfil.sys)', command=lambda: self.set_hibernation(False)).pack(side=tk.LEFT, padx=6)
        ttk.Button(lf3, text='Enable Hibernation', command=lambda: self.set_hibernation(True)).pack(side=tk.LEFT, padx=6)
        lf4 = ttk.LabelFrame(tools, text='LCU cleanup (admin)')
        lf4.pack(fill=tk.X, padx=8, pady=8)
        ttk.Button(lf4, text='Estimate LCU (keep latest)', command=self.estimate_lcu).pack(side=tk.LEFT, padx=6, pady=6)
        ttk.Button(lf4, text='Delete LCU (keep latest)', command=self.delete_lcu).pack(side=tk.LEFT, padx=6, pady=6)
    def refresh_overview(self):
        for i in self.tree.get_children():
            self.tree.delete(i)
        for root, total, used, free in get_drives():
            pct = 100 * free / total if total else 0
            self.tree.insert('', tk.END, values=(root, bytes_fmt(total), bytes_fmt(used), bytes_fmt(free), f"{pct:.1f}%"))
        if not IS_ADMIN:
            self.logger.log("Not running as Administrator. Some actions may be skipped or partially cleaned.")
    def estimate_quick_clean(self):
        targets = []
        if self.var_temp.get():
            targets.append(TEMP)
        if self.var_wintemp.get():
            targets.append(WINTEMP)
        if self.var_browser.get():
            for p in BROWSER_CACHES:
                if 'Firefox' in str(p) and p.exists():
                    for prof in p.glob('*.default*'):
                        targets.append(prof / 'cache2')
                else:
                    targets.append(p)
        if self.var_wer.get():
            targets.append(WER)
        if self.var_logs.get():
            targets.append(LOGS)
        if self.var_prefetch.get():
            targets.append(PREFETCH)
        est = estimate_folder(targets)
        if self.var_recycle.get():
            try:
                est += get_recycle_bin_size()
            except Exception:
                pass
        self.qc_est_label.config(text=f"Estimated savings: {bytes_fmt(est)} (includes Recycle Bin)")
        self.logger.log(f"Quick Clean estimate: {bytes_fmt(est)}")
    def run_quick_clean(self):
        if not messagebox.askyesno(APP_TITLE, "Proceed with Quick Clean? Review your selections first."):
            return
        total_freed = 0
        errors = []
        def try_delete(p: Path):
            nonlocal total_freed, errors
            if not p.exists():
                return
            self.logger.log(f"Deleting: {p}")
            if self.dry_run.get():
                size = dir_size(p) if p.is_dir() else p.stat().st_size
                total_freed += size
                return
            if p.is_file() and can_recycle(p):
                ok, note = recycle_path(p)
                if ok:
                    try:
                        total_freed += p.stat().st_size
                    except Exception:
                        pass
                else:
                    self.logger.log(f"Recycle failed ({note}); deleting permanently…")
                    f, e = rm_tree(p)
                    total_freed += f
                    errors += e
            else:
                f, e = rm_tree(p)
                total_freed += f
                errors += e
        targets = []
        if self.var_temp.get():
            targets.append(TEMP)
        if self.var_wintemp.get():
            targets.append(WINTEMP)
        if self.var_browser.get():
            for p in BROWSER_CACHES:
                if 'Firefox' in str(p) and p.exists():
                    for prof in p.glob('*.default*'):
                        targets.append(prof / 'cache2')
                else:
                    targets.append(p)
        if self.var_wer.get():
            targets.append(WER)
        if self.var_logs.get():
            targets.append(LOGS)
        if self.var_prefetch.get():
            targets.append(PREFETCH)
        for p in targets:
            try_delete(p)
        if self.var_recycle.get():
            self.logger.log("Emptying Recycle Bin…")
            if self.dry_run.get():
                try:
                    total_freed += get_recycle_bin_size()
                except Exception:
                    pass
            else:
                freed, msg = empty_recycle_bin()
                total_freed += freed
                if freed:
                    self.logger.log(f"{msg}: {bytes_fmt(freed)} freed")
                else:
                    self.logger.log(msg)
        self.logger.log(f"Quick Clean completed. {'Estimated freed: ' if self.dry_run.get() else 'Freed: '}{bytes_fmt(total_freed)}")
        self.refresh_overview()

    def browse_scan_folder(self):
        p = filedialog.askdirectory(initialdir=self.scan_path_var.get() or 'C:\\')
        if p:
            self.scan_path_var.set(p)
    def start_deep_scan(self):
        path = Path(self.scan_path_var.get()).resolve()
        if not path.exists():
            messagebox.showerror(APP_TITLE, f"Path not found: {path}")
            return
        self.deep_tree.delete(*self.deep_tree.get_children())
        self.scan_progress.start(10)
        self.logger.log(f"Scanning largest files in: {path} (top {self.scan_limit.get()})")
        t = threading.Thread(target=self._scan_thread, args=(path, self.scan_limit.get()), daemon=True)
        t.start()
    def _scan_thread(self, path: Path, limit: int):
        try:
            items = find_largest(path, limit=limit)
            self.after(0, self._populate_deep_tree, items)
        finally:
            self.after(0, self.scan_progress.stop)
    def _populate_deep_tree(self, items):
        for p, s in items:
            self.deep_tree.insert('', tk.END, values=(str(p), bytes_fmt(s)))
        self.logger.log(f"Deep Scan found {len(items)} items.")
    def delete_selected(self):
        sel = self.deep_tree.selection()
        if not sel:
            messagebox.showinfo(APP_TITLE, "Select one or more files first.")
            return
        if not messagebox.askyesno(APP_TITLE, f"Delete {len(sel)} selected item(s)? Files go to Recycle Bin when possible."):
            return
        total_freed = 0
        errors = []
        for iid in sel:
            path_str = self.deep_tree.item(iid, 'values')[0]
            p = Path(path_str)
            self.logger.log(f"Deleting: {p}")
            if self.dry_run.get():
                try:
                    total_freed += p.stat().st_size
                except Exception:
                    pass
                continue
            if p.exists():
                ok, note = recycle_path(p)
                if ok:
                    try:
                        total_freed += p.stat().st_size
                    except Exception:
                        pass
                else:
                    self.logger.log(f"Recycle failed ({note}); deleting permanently…")
                    f, e = rm_tree(p)
                    total_freed += f
                    errors += e
        if errors:
            self.logger.log("Errors:\n" + "\n".join(errors))
        self.logger.log(f"Deletion complete. {'Estimated ' if self.dry_run.get() else ''}Freed: {bytes_fmt(total_freed)}")
        self.refresh_overview()
    def estimate_old_downloads(self):
        days = self.days_threshold.get()
        cutoff = datetime.now() - timedelta(days=days)
        downloads = USERPROFILE / 'Downloads'
        total = 0
        count = 0
        if downloads.exists():
            for p in downloads.rglob('*'):
                try:
                    if p.is_file() and datetime.fromtimestamp(p.stat().st_mtime) < cutoff:
                        total += p.stat().st_size
                        count += 1
                except Exception:
                    pass
        messagebox.showinfo(APP_TITLE, f"Files older than {days} days: {count}\nEstimated size: {bytes_fmt(total)}")
        self.logger.log(f"Downloads estimate: {count} files, {bytes_fmt(total)}")
    def delete_old_downloads(self):
        days = self.days_threshold.get()
        if not messagebox.askyesno(APP_TITLE, f"Delete files in Downloads older than {days} days?"):
            return
        cutoff = datetime.now() - timedelta(days=days)
        downloads = USERPROFILE / 'Downloads'
        total = 0
        count = 0
        for p in downloads.rglob('*'):
            try:
                if p.is_file() and datetime.fromtimestamp(p.stat().st_mtime) < cutoff:
                    if self.dry_run.get():
                        total += p.stat().st_size
                        count += 1
                    else:
                        ok, note = recycle_path(p)
                        if ok:
                            try:
                                total += p.stat().st_size
                            except Exception:
                                pass
                            count += 1
                        else:
                            f, e = rm_tree(p)
                            total += f
            except Exception:
                continue
        self.logger.log(f"Old downloads deleted: {count} files, {'estimated ' if self.dry_run.get() else ''}{bytes_fmt(total)} freed")
        self.refresh_overview()
    def purge_wu_cache(self):
        if not IS_ADMIN:
            messagebox.showwarning(APP_TITLE, "Admin rights required to purge Windows Update cache.")
            return
        if not messagebox.askyesno(APP_TITLE, "Purge Windows Update cache now? This may temporarily affect Windows Update."):
            return
        self.logger.log("Purging Windows Update cache…")
        if self.dry_run.get():
            est = estimate_folder([SOFTWAREDISTRIBUTION])
            self.logger.log(f"[DRY-RUN] Would free about {bytes_fmt(est)}")
            return
        freed, errors = purge_software_distribution()
        self.logger.log(f"Windows Update cache purged. Freed: {bytes_fmt(freed)}")
        if errors:
            self.logger.log("Errors:\n" + "\n".join(errors))
        self.refresh_overview()
    def set_hibernation(self, enable: bool):
        action = 'enable' if enable else 'disable'
        if not IS_ADMIN:
            messagebox.showwarning(APP_TITLE, f"Admin rights required to {action} hibernation.")
            return
        ok = messagebox.askyesno(APP_TITLE, f"Are you sure you want to {action} hibernation?")
        if not ok:
            return
        self.logger.log(f"Attempting to {action} hibernation…")
        if self.dry_run.get():
            self.logger.log("[DRY-RUN] powercfg would be invoked.")
            return
        ok2, msg = toggle_hibernation(enable)
        if ok2:
            self.logger.log(f"Hibernation {('enabled' if enable else 'disabled')}.")
        else:
            self.logger.log(f"Failed to {action} hibernation: {msg}")
        self.refresh_overview()
    def estimate_lcu(self):
        if not IS_ADMIN:
            messagebox.showwarning(APP_TITLE, "Admin rights required for LCU cleanup.")
            return
        total, count = lcu_keep_latest_estimate()
        self.logger.log(f"LCU estimate (keep latest): {count} items, {bytes_fmt(total)}")
    def delete_lcu(self):
        if not IS_ADMIN:
            messagebox.showwarning(APP_TITLE, "Admin rights required for LCU cleanup.")
            return
        if not messagebox.askyesno(APP_TITLE, "Delete old LCU contents in SoftwareDistribution\\Download\\Install (keep latest)?"):
            return
        total, count, errs = lcu_keep_latest_delete(self.dry_run.get())
        self.logger.log(f"LCU cleanup {'[DRY-RUN] ' if self.dry_run.get() else ''}removed {count} items, {bytes_fmt(total)}")
        if errs:
            self.logger.log("Errors:\n" + "\n".join(errs))
        self.refresh_overview()
if __name__ == '__main__':
    app = App()
    app.logger.log("Dry-run is enabled by default. Uncheck it to actually delete.")
    app.mainloop()
          
Version 1.1.0 Updated 2025-08-28 Size ~11 MB

Why it’s useful

Quick Clean

Clear Temp, Windows Temp, browser caches, WER, logs, prefetch; empty Recycle Bin.

Deep Scan

Find and delete the largest files anywhere you choose.

Windows Update cache

Purge SoftwareDistribution\\Download safely.

LCU cleanup

Keep the most recent LCU in ...\\Download\\Install, remove the rest.

Hibernation toggle

Enable/disable hibernation to free hiberfil.sys (admin).

Dry-run mode

Estimate space savings without deleting a thing.

What it can free

User Temp, Windows Temp, browser caches (Edge/Chrome/Firefox), Windows Error Reporting, Windows Logs, Prefetch, Recycle Bin
Windows Update cache, old LCU contents (keep latest)
Old files in Downloads by age (configurable)
          

How it works

1
Run

Launch the EXE. Dry-run is on by default so you can preview savings.

2
Select targets

Choose Quick Clean targets or run a Deep Scan on any folder/drive.

3
Apply

Uncheck Dry-run to actually delete. Admin elevates certain actions.

FAQ

Does it require admin?

No for most tasks. Admin enables Windows Temp, WU cache, LCU, and hibernation toggle.

Does it send data?

No network use. All operations are local.

Can I undo deletes?

When possible it sends files to Recycle Bin; some system paths delete directly.

Will it affect Windows Update?

Purging WU cache only clears temporary downloads. Windows will re-fetch if needed.

Download

Windows (.exe)
Version 1.1.0 Updated 2025-08-28 Size ~11 MB

System requirements

  • Windows 10 or later
  • ~11 MB free space
  • No extra installs required
Want more free utilities? Visit the home page.

← Back to Home