×
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()
Copy