Secure File Shredder — Python + customtkinter (Safe Test Mode, DRY_RUN)
Demo :
Click Video πππ
Features :
-
Modern customtkinter UI with file & folder add.
-
Overwrite passes (1–3) with cryptographically random bytes (secrets.token_bytes).
-
Chunked 4MB writes + fsync for reliability.
-
Dry-run quarantine mode (safe by default).
-
Timestamped logs & preview list.
-
Cross-platform (Windows / Linux).
-
Full source code link + usage instructions + safety checklist.
Code :
"""
Python Tkinter Secure File Shredder (Modern GUI with customtkinter)
SAFE BY DEFAULT: DRY_RUN = True (moves files to ./quarantine)
Change DRY_RUN to False only after thorough testing and when you own the files.
Author: FuzzuTech example
"""
import os
import threading
import time
import math
import secrets
import shutil
from pathlib import Path
from functools import partial
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import customtkinter as ctk
# --------- CONFIG ----------
DRY_RUN = True # !!! Default safe mode. Set False to actually overwrite & delete.
CHUNK_SIZE = 4 * 1024 * 1024 # 4 MB chunks when overwriting
QUARANTINE_DIR = Path("quarantine")
# ---------------------------
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
class ShredderApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("FuzzuTech — Secure File Shredder (Test Mode)")
self.geometry("1050x600")
self.minsize(620, 620)
# Data
self.files = [] # list of Path objects
self.total_bytes = 0
# UI layout frames
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.side_frame = ctk.CTkFrame(self, width=260, corner_radius=12)
self.side_frame.grid(row=0, column=0, sticky="nswe", padx=12, pady=12)
self.main_frame = ctk.CTkFrame(self, corner_radius=12)
self.main_frame.grid(row=0, column=1, sticky="nswe", padx=(0,12), pady=12)
self._build_side()
self._build_main()
# Ensure quarantine exists if dry-run
if DRY_RUN:
QUARANTINE_DIR.mkdir(parents=True, exist_ok=True)
def _build_side(self):
ctk.CTkLabel(self.side_frame, text="FuzzuTech Shredder", font=ctk.CTkFont(size=18, weight="bold")).pack(pady=(12,6))
ctk.CTkLabel(self.side_frame, text="Modern Secure File Shredder\nSafe-by-default", justify="left").pack(pady=(0,12))
# Buttons
ctk.CTkButton(self.side_frame, text="Add Files", command=self.add_files).pack(fill="x", padx=12, pady=6)
ctk.CTkButton(self.side_frame, text="Add Folder", command=self.add_folder).pack(fill="x", padx=12, pady=6)
ctk.CTkButton(self.side_frame, text="Clear List", fg_color="#e74c3c", hover_color="#ff6b6b", command=self.clear_list).pack(fill="x", padx=12, pady=6)
# Options
ctk.CTkLabel(self.side_frame, text="Options", anchor="w", font=ctk.CTkFont(size=14, weight="bold")).pack(fill="x", padx=12, pady=(18,6))
self.passes_var = ctk.IntVar(value=1)
ctk.CTkSlider(self.side_frame, from_=1, to=3, number_of_steps=2, command=self._on_slider, variable=self.passes_var).pack(fill="x", padx=12)
self.passes_label = ctk.CTkLabel(self.side_frame, text="Overwrite passes: 1")
self.passes_label.pack(padx=12, pady=(6,0))
self.dry_run_var = tk.BooleanVar(value=DRY_RUN)
self.dry_run_check = ctk.CTkCheckBox(self.side_frame, text="Dry Run (Move to quarantine)", variable=self.dry_run_var)
self.dry_run_check.pack(padx=12, pady=8)
# Theme Toggle
self.theme_button = ctk.CTkSegmentedButton(self.side_frame, values=["Light", "Dark"], command=self._on_theme_change)
self.theme_button.set("Dark" if ctk.get_appearance_mode()=="Dark" else "Light")
self.theme_button.pack(padx=12, pady=(12,6))
# Spacer
ctk.CTkLabel(self.side_frame, text=" ", height=12).pack()
ctk.CTkLabel(self.side_frame, text="Log Preview", anchor="w").pack(padx=12, pady=(6,4))
self.log_preview = scrolledtext.ScrolledText(self.side_frame, height=10, state="disabled")
self.log_preview.pack(fill="both", padx=12, pady=(0,12), expand=True)
def _on_slider(self, val):
self.passes_label.configure(text=f"Overwrite passes: {int(float(val))}")
def _on_theme_change(self, value):
ctk.set_appearance_mode("Dark" if value=="Dark" else "Light")
def _build_main(self):
top_row = ctk.CTkFrame(self.main_frame)
top_row.pack(fill="x", padx=12, pady=12)
self.listbox = ctk.CTkTextbox(self.main_frame, width=600, height=260, corner_radius=8)
self.listbox.pack(fill="both", padx=12, pady=(0,12), expand=True)
bottom_row = ctk.CTkFrame(self.main_frame)
bottom_row.pack(fill="x", padx=12, pady=12)
self.overall_progress = ctk.CTkProgressBar(bottom_row)
self.overall_progress.pack(fill="x", side="top", pady=(0,10))
btn_frame = ctk.CTkFrame(bottom_row)
btn_frame.pack(fill="x", side="top")
ctk.CTkButton(btn_frame, text="Preview List", command=self.preview_list).pack(side="left", padx=6)
ctk.CTkButton(btn_frame, text="Start Shred (CONFIRM)", fg_color="#ff4757", hover_color="#ff6b6b", command=self.confirm_and_start).pack(side="right", padx=6)
# small hint
ctk.CTkLabel(self.main_frame, text="Tip: Use small test files first. Dry Run ON moves files to ./quarantine").pack(anchor="w", padx=12, pady=(6,0))
# ---------------- file management ----------------
def add_files(self):
paths = filedialog.askopenfilenames(title="Select files to add")
if not paths:
return
for p in paths:
self._add_path(Path(p))
def add_folder(self):
folder = filedialog.askdirectory(title="Select folder to add (all files inside will be added)")
if not folder:
return
for root, _, files in os.walk(folder):
for f in files:
self._add_path(Path(root) / f)
def _add_path(self, path: Path):
if not path.exists():
return
if path.is_file():
if path not in self.files:
self.files.append(path)
self.total_bytes += path.stat().st_size
self._log(f"Added: {path} ({self._size_fmt(path.stat().st_size)})")
self._refresh_listbox()
def clear_list(self):
if messagebox.askyesno("Clear list", "Remove all files from list?"):
self.files.clear()
self.total_bytes = 0
self._refresh_listbox()
self._log("Cleared file list.")
def preview_list(self):
self._refresh_listbox()
messagebox.showinfo("Preview", f"{len(self.files)} files queued, total size: {self._size_fmt(self.total_bytes)}")
def _refresh_listbox(self):
self.listbox.configure(state="normal")
self.listbox.delete("0.0", tk.END)
for i, p in enumerate(self.files, 1):
self.listbox.insert(tk.END, f"{i}. {p} — {self._size_fmt(p.stat().st_size)}\n")
self.listbox.configure(state="disabled")
# ---------------- shredding logic ----------------
def confirm_and_start(self):
if not self.files:
messagebox.showwarning("No files", "Add files first.")
return
passes = self.passes_var.get()
is_dry = self.dry_run_var.get()
confirm_text = "DRY RUN (quarantine)" if is_dry else f"REAL SHRED (Permanently destroy files) with {passes} passes"
if not messagebox.askyesno("Confirm Shred", f"You're about to start:\n\n{confirm_text}\n\nFiles: {len(self.files)}\nTotal size: {self._size_fmt(self.total_bytes)}\n\nProceed?"):
return
# run in thread
t = threading.Thread(target=self._start_shred, args=(passes, is_dry), daemon=True)
t.start()
def _start_shred(self, passes: int, dry_run: bool):
total_files = len(self.files)
processed = 0
self.overall_progress.set(0)
for p in list(self.files): # copy to avoid mutation issues
try:
self._log(f"Processing: {p}")
if dry_run:
dest = QUARANTINE_DIR / p.name
# ensure unique
dest = self._unique_path(dest)
shutil.move(str(p), str(dest))
self._log(f"[DRY] Moved to quarantine: {dest}")
else:
self._secure_overwrite_file(p, passes)
os.remove(p)
self._log(f"Shredded and removed: {p}")
except Exception as e:
self._log(f"ERROR processing {p}: {e}")
processed += 1
self.overall_progress.set(processed / total_files)
# refresh UI list
if p in self.files:
try:
self.files.remove(p)
except Exception:
pass
self._refresh_listbox()
self._log("Operation complete.")
messagebox.showinfo("Done", "Shredding operation completed. Check log for details.")
def _secure_overwrite_file(self, path: Path, passes: int):
"""Overwrite file content with random bytes passes times (chunked) — then flush."""
size = path.stat().st_size
if size == 0:
self._log(f"Skipping zero-length file: {path}")
return
with open(path, "r+b", buffering=0) as f:
for pass_no in range(1, passes+1):
f.seek(0)
bytes_written = 0
start = time.time()
# write random bytes in chunks
while bytes_written < size:
to_write = min(CHUNK_SIZE, size - bytes_written)
# generate cryptographically random bytes
chunk = secrets.token_bytes(to_write)
f.write(chunk)
bytes_written += to_write
# Optional: small sleep to allow UI update in heavy IO systems
f.flush()
os.fsync(f.fileno())
elapsed = time.time() - start
self._log(f"Overwritten pass {pass_no}/{passes} for {path} ({self._size_fmt(size)}) in {elapsed:.2f}s")
# final pass: zeros (optional)
f.seek(0)
zeros_written = 0
while zeros_written < size:
to_write = min(CHUNK_SIZE, size - zeros_written)
f.write(b'\x00' * to_write)
zeros_written += to_write
f.flush()
os.fsync(f.fileno())
# -------------- helpers ---------------
def _size_fmt(self, num):
# human readable
for unit in ['B','KB','MB','GB','TB']:
if num < 1024.0:
return f"{num:3.1f}{unit}"
num /= 1024.0
return f"{num:.1f}PB"
def _unique_path(self, p: Path) -> Path:
base = p.stem
ext = p.suffix
parent = p.parent
i = 1
while p.exists():
p = parent / f"{base}_{i}{ext}"
i += 1
return p
def _log(self, text: str):
timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
line = f"[{timestamp}] {text}\n"
# append to side log preview
self.log_preview.configure(state="normal")
self.log_preview.insert(tk.END, line)
self.log_preview.see(tk.END)
self.log_preview.configure(state="disabled")
# also print to console
print(line.strip())
if __name__ == "__main__":
app = ShredderApp()
app.mainloop()
Comments
Post a Comment