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

Popular posts from this blog

πŸš€ Simple Login & Registration System in Python Tkinter πŸ“±

πŸ”₯ Advanced MP3 Music Player in Python | CustomTkinter + Pygame | Free Source Code

πŸš€ Create a Python Screen Recorder with Audio (Complete Code)