JustPaste
HomeCategoriesAboutDonateContactTerms of UsePrivacy Policy
JustPaste

Free online notepad — write and share instantly

Navigate

  • Home
  • Timeline
  • Categories

Info

  • About
  • Donate
  • Contact

Legal

  • Terms of Use
  • Privacy Policy

© 2026 JustPaste.app. All rights reserved.

Made with ♥ by JustPaste

Erase35 | JustPaste.app
5 days ago4 views
👨‍💻Programming

Erase35

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
SecureWipe v5.3 - Effacement sécurisé renforcé.
- Fenêtre 480x560 (bouton EFFACER toujours visible)
- DnD via tkinterdnd2 (auto-install)
- Statut DnD fusionné dans la barre de statut du bas
"""

import os
import re
import sys
import stat
import shutil
import threading
import random
import string
import subprocess
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path

# =========================================================
#  AUTO-INSTALL tkinterdnd2
# =========================================================

def _try_import_dnd():
    try:
        from tkinterdnd2 import DND_FILES, TkinterDnD
        return True, None
    except Exception as e:
        return False, str(e)

def _auto_install_dnd():
    print("[DND] tkinterdnd2 absent → installation auto…")
    cmds = [
        [sys.executable, "-m", "pip", "install", "--user", "tkinterdnd2"],
        [sys.executable, "-m", "pip", "install", "--user", "--break-system-packages", "tkinterdnd2"],
    ]
    for cmd in cmds:
        try:
            r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
            if r.returncode == 0:
                print("[DND] Installation réussie.")
                return True
        except Exception as e:
            print(f"[DND] Erreur : {e}")
    return False

DND_AVAILABLE, DND_ERROR = _try_import_dnd()
if not DND_AVAILABLE:
    if _auto_install_dnd():
        DND_AVAILABLE, DND_ERROR = _try_import_dnd()

if DND_AVAILABLE:
    from tkinterdnd2 import DND_FILES, TkinterDnD


# =========================================================
#  MOTEUR
# =========================================================

CHUNK_SIZE = 1024 * 1024
DEFAULT_PASSES = 5
RENAME_ROUNDS = 3

PATTERNS = [
    lambda n: b"\x00" * n, lambda n: b"\xFF" * n,
    lambda n: b"\xAA" * n, lambda n: b"\x55" * n,
    lambda n: b"\x92" * n, lambda n: b"\x49" * n,
    lambda n: b"\x24" * n, lambda n: os.urandom(n),
]

def _sync_dir(path: Path):
    try:
        fd = os.open(str(path), os.O_RDONLY)
        try: os.fsync(fd)
        finally: os.close(fd)
    except OSError: pass

def _random_name(length=None):
    if length is None: length = random.randint(12, 24)
    return "".join(random.choices(string.ascii_letters + string.digits, k=length))

def _overwrite_file(path: Path, passes: int) -> bool:
    try: size = path.stat().st_size
    except OSError: return False
    if size == 0: return True
    try:
        with open(path, "r+b", buffering=0) as f:
            for p in range(passes):
                f.seek(0); written = 0
                while written < size:
                    block = min(CHUNK_SIZE, size - written)
                    f.write(PATTERNS[p % len(PATTERNS)](block))
                    written += block
                f.flush(); os.fsync(f.fileno())
            f.seek(0); f.truncate(0); f.flush(); os.fsync(f.fileno())
            f.write(os.urandom(256)); f.flush(); os.fsync(f.fileno())
            f.seek(0); f.truncate(0); f.flush(); os.fsync(f.fileno())
        return True
    except (OSError, PermissionError): return False

def _force_remove(path: Path):
    try: os.chmod(path, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC)
    except OSError: pass
    try:
        if path.is_dir() and not path.is_symlink():
            shutil.rmtree(path, ignore_errors=True)
        else:
            path.unlink(missing_ok=True)
    except OSError: pass

def _erase_single_file(fp: Path, passes: int) -> bool:
    if not _overwrite_file(fp, passes): return False
    current = fp
    for _ in range(RENAME_ROUNDS):
        try:
            new_name = current.parent / _random_name()
            current.rename(new_name)
            _sync_dir(current.parent)
            current = new_name
        except OSError: break
    _force_remove(current)
    _sync_dir(fp.parent)
    return True

def secure_erase(target: Path, passes=DEFAULT_PASSES, log_cb=None):
    target = Path(target)
    if not target.exists():
        if log_cb: log_cb(f"⚠ Introuvable : {target}")
        return 0, 0
    erased = failed = 0
    if target.is_file() or target.is_symlink():
        if log_cb: log_cb(f"→ {target.name} ({passes} passes)")
        if _erase_single_file(target, passes):
            erased += 1
            if log_cb: log_cb(f"   ✔ Effacé : {target.name}")
        else:
            failed += 1
            if log_cb: log_cb(f"   ✘ Échec : {target.name}")
        return erased, failed
    for root, dirs, files in os.walk(target, topdown=False):
        root_path = Path(root)
        for fname in files:
            fp = root_path / fname
            if log_cb: log_cb(f"→ {fp.relative_to(target)} ({passes} passes)")
            if _erase_single_file(fp, passes): erased += 1
            else:
                failed += 1
                if log_cb: log_cb(f"   ✘ Échec : {fp.relative_to(target)}")
        for dname in dirs:
            _force_remove(root_path / dname)
    _force_remove(target)
    _sync_dir(target.parent)
    if log_cb: log_cb(f"✔ Dossier supprimé : {target.name}")
    return erased, failed


def _parse_clipboard_paths(raw: str):
    if not raw: return []
    paths = []
    for m in re.finditer(r"file://([^\s\"'<>]+)", raw):
        p = m.group(1)
        try:
            from urllib.parse import unquote
            p = unquote(p)
        except Exception: pass
        paths.append(p)
    if paths: return paths
    for t in re.findall(r'"[^"]+"|\'[^\']+\'|\S+', raw):
        t = t.strip().strip('"').strip("'")
        if t: paths.append(t)
    return paths


# =========================================================
#  UI
# =========================================================

class SecureWipeApp:
    def __init__(self, root):
        self.root = root
        self.root.title("SecureWipe v5.3")
        self.root.geometry("480x560")
        self.root.minsize(440, 540)
        self.root.resizable(False, False)
        self.root.configure(bg="#1e1e2e")

        self.targets = []
        self.running = False
        self._build_ui()
        self._register_dnd()
        self._check_dnd_engine()

    def _build_ui(self):
        BG = "#1e1e2e"; CARD = "#2a2a3e"; DROP_BG = "#32324a"; DROP_HI = "#554488"
        ACCENT = "#ff5555"; ACCENT_H = "#ff7070"
        TEXT = "#f8f8f2"; SUBTEXT = "#a0a0b8"; GREEN = "#50fa7b"

        style = ttk.Style(); style.theme_use("clam")
        style.configure("Accent.TButton", background=ACCENT, foreground="white",
                        font=("Segoe UI", 9, "bold"), borderwidth=0, padding=6)
        style.map("Accent.TButton", background=[("active", ACCENT_H)])
        style.configure("Ghost.TButton", background=CARD, foreground=TEXT,
                        font=("Segoe UI", 8), borderwidth=0, padding=4)
        style.map("Ghost.TButton", background=[("active", "#3a3a52")])

        # Titre
        tk.Label(self.root, text="🗑  SecureWipe v5.3",
                 bg=BG, fg=TEXT, font=("Segoe UI", 13, "bold")).pack(pady=(8, 4))

        # Zone de drop
        self.drop_zone = tk.Label(
            self.root,
            text="⬇  GLISSEZ-DÉPOSEZ ICI  ⬇\n"
                 "fichiers ou dossiers\n"
                 "(ou Ctrl+V pour coller un chemin)",
            bg=DROP_BG, fg=SUBTEXT,
            font=("Segoe UI", 9, "bold"),
            relief="solid", bd=2,
            height=3, justify="center"
        )
        self.drop_zone.pack(padx=10, pady=(0, 4), fill="x")
        self.drop_zone.bind("<Control-v>", self._on_paste)
        self.drop_zone.bind("<Control-V>", self._on_paste)

        # Champ chemin
        path_frame = tk.Frame(self.root, bg=BG)
        path_frame.pack(padx=10, pady=(0, 4), fill="x")
        tk.Label(path_frame, text="Chemin :", bg=BG, fg=SUBTEXT,
                 font=("Segoe UI", 8)).pack(side="left")
        self.path_var = tk.StringVar()
        entry = tk.Entry(path_frame, textvariable=self.path_var,
                         bg=CARD, fg=TEXT, insertbackground=TEXT,
                         relief="flat", font=("Consolas", 8))
        entry.pack(side="left", fill="x", expand=True, padx=(4, 4))
        entry.bind("<Return>", lambda e: self._add_manual_path())
        ttk.Button(path_frame, text="+", style="Ghost.TButton", width=3,
                   command=self._add_manual_path).pack(side="right")

        # Boutons
        btn_frame = tk.Frame(self.root, bg=BG)
        btn_frame.pack(padx=10, pady=(2, 4), fill="x")
        ttk.Button(btn_frame, text="📂 Fichiers", style="Ghost.TButton",
                   command=lambda: self._browse(True)).pack(side="left")
        ttk.Button(btn_frame, text="📁 Dossier", style="Ghost.TButton",
                   command=lambda: self._browse(False)).pack(side="left", padx=4)
        ttk.Button(btn_frame, text="🧹 Vider", style="Ghost.TButton",
                   command=self._clear_list).pack(side="left")

        # Liste
        list_frame = tk.Frame(self.root, bg=CARD, bd=0)
        list_frame.pack(padx=10, pady=(2, 4), fill="both", expand=True)
        self.listbox = tk.Listbox(list_frame, bg=CARD, fg=TEXT,
                                   selectbackground="#3a3a52", selectforeground=TEXT,
                                   font=("Consolas", 8), borderwidth=0,
                                   highlightthickness=0, activestyle="none", height=4)
        self.listbox.pack(side="left", fill="both", expand=True, padx=(6, 0), pady=6)
        scroll = tk.Scrollbar(list_frame, command=self.listbox.yview)
        scroll.pack(side="right", fill="y", pady=6)
        self.listbox.config(yscrollcommand=scroll.set)

        # Passes
        passes_frame = tk.Frame(self.root, bg=BG)
        passes_frame.pack(padx=10, pady=(0, 2), fill="x")
        tk.Label(passes_frame, text="Passes :", bg=BG, fg=SUBTEXT,
                 font=("Segoe UI", 8)).pack(side="left")
        self.passes_var = tk.IntVar(value=DEFAULT_PASSES)
        tk.Spinbox(passes_frame, from_=1, to=35, textvariable=self.passes_var,
                   width=3, bg=CARD, fg=TEXT, insertbackground=TEXT,
                   buttonbackground=CARD, relief="flat",
                   font=("Segoe UI", 8)).pack(side="left", padx=4)
        tk.Label(passes_frame, text="5=défaut  7=DoD  35=Gutmann",
                 bg=BG, fg=SUBTEXT, font=("Segoe UI", 7)).pack(side="left")

        # Progression
        self.progress = ttk.Progressbar(self.root, mode="indeterminate")
        self.progress.pack(padx=10, pady=(2, 2), fill="x")

        # Statut (fusionne info DnD + état)
        self.status = tk.Label(self.root, text="Prêt.", bg=BG, fg=SUBTEXT,
                                font=("Segoe UI", 8), anchor="w")
        self.status.pack(padx=10, fill="x")

        # Journal
        self.log = tk.Text(self.root, height=3, bg=CARD, fg=TEXT,
                           font=("Consolas", 7), borderwidth=0,
                           highlightthickness=0, state="disabled")
        self.log.pack(padx=10, pady=(2, 4), fill="both")

        # Bouton principal
        self.erase_btn = ttk.Button(self.root, text="🔥  EFFACER (5 passes)",
                                     style="Accent.TButton", command=self._start_erase)
        self.erase_btn.pack(padx=10, pady=(0, 8), fill="x")

        self.root.bind("<Control-v>", self._on_paste)
        self.root.bind("<Control-V>", self._on_paste)

    # ---------- DnD ----------
    def _register_dnd(self):
        if not DND_AVAILABLE:
            print(f"[DND] Indisponible : {DND_ERROR}")
            return
        try:
            self.root.drop_target_register(DND_FILES)
            self.root.dnd_bind('<<Drop>>', self._on_drop)
            self.root.dnd_bind('<<DropEnter>>', self._on_drag_enter)
            self.root.dnd_bind('<<DropLeave>>', self._on_drag_leave)

            self.drop_zone.drop_target_register(DND_FILES)
            self.drop_zone.dnd_bind('<<Drop>>', self._on_drop)
            self.drop_zone.dnd_bind('<<DropEnter>>', self._on_drag_enter)
            self.drop_zone.dnd_bind('<<DropLeave>>', self._on_drag_leave)

            self.listbox.drop_target_register(DND_FILES)
            self.listbox.dnd_bind('<<Drop>>', self._on_drop)
            print("[DND] Enregistré (root + drop_zone + listbox)")
        except Exception as e:
            print(f"[DND] Échec enregistrement : {e}")

    def _check_dnd_engine(self):
        """Affiche l'état DnD dans la barre de statut (pas de ligne en plus)."""
        if not DND_AVAILABLE:
            self.status.config(
                text="⚠ DnD indisponible — utilisez Ctrl+V ou les boutons",
                fg="#ffaa55")
            return
        try:
            v = self.root.tk.call('package', 'require', 'tkdnd')
            print(f"[DND] tkdnd chargé : v{v}")
            self.status.config(text=f"✅ DnD actif (tkdnd v{v}) — prêt.",
                                fg="#50fa7b")
        except Exception as e:
            print(f"[DND] tkdnd NON chargé : {e}")
            self.status.config(
                text=f"⚠ tkdnd binaire non chargé — Ctrl+V ou boutons",
                fg="#ff5555")

    def _on_drag_enter(self, event):
        self.drop_zone.config(bg="#554488", fg="#ffffff")

    def _on_drag_leave(self, event):
        self.drop_zone.config(bg="#32324a", fg="#a0a0b8")

    def _on_drop(self, event):
        print(f"[DROP] reçu : {event.data!r}")
        try:
            paths = self.root.tk.splitlist(event.data)
        except Exception as e:
            print(f"[DROP] splitlist échec : {e}")
            paths = [event.data]
        self._add_paths(list(paths))
        self.drop_zone.config(bg="#32324a", fg="#a0a0b8")

    # ---------- Collage ----------
    def _on_paste(self, event=None):
        try: raw = self.root.clipboard_get()
        except tk.TclError:
            self._set_status("Presse-papiers vide.", "#ffaa55")
            return "break"
        paths = _parse_clipboard_paths(raw)
        if not paths:
            self._set_status("Aucun chemin détecté.", "#ffaa55")
            return "break"
        self._add_paths(paths)
        return "break"

    # ---------- Helpers ----------
    def _log(self, msg):
        def _do():
            self.log.config(state="normal")
            self.log.insert("end", msg + "\n")
            self.log.see("end")
            self.log.config(state="disabled")
        self.root.after(0, _do)

    def _set_status(self, msg, color="#a0a0b8"):
        self.root.after(0, lambda: self.status.config(text=msg, fg=color))

    def _add_manual_path(self):
        p = self.path_var.get().strip()
        if p: self._add_paths([p]); self.path_var.set("")

    def _add_paths(self, paths):
        added = skipped = 0
        for p in paths:
            try: p = Path(p.strip())
            except Exception: skipped += 1; continue
            if not p.exists(): skipped += 1; continue
            if p in self.targets: continue
            self.targets.append(p)
            kind = "📁" if p.is_dir() else "📄"
            self.listbox.insert("end", f"  {kind} {p.name}")
            added += 1
        if added: self._set_status(f"{len(self.targets)} élément(s).", "#50fa7b")
        elif skipped: self._set_status(f"{skipped} ignoré(s).", "#ffaa55")

    def _browse(self, is_file):
        if is_file:
            self._add_paths(filedialog.askopenfilenames(title="Choisir des fichiers"))
        else:
            folder = filedialog.askdirectory(title="Choisir un dossier")
            if folder: self._add_paths([folder])

    def _clear_list(self):
        self.targets.clear(); self.listbox.delete(0, "end")
        self._set_status("Liste vidée.")

    def _start_erase(self):
        if self.running: return
        if not self.targets:
            messagebox.showwarning("Rien à effacer", "Ajoutez au moins un élément.")
            return
        self.running = True
        self.erase_btn.config(state="disabled")
        self.progress.start(12)
        self._set_status("Effacement en cours…", "#ff5555")
        self._log("═" * 40)
        self._log(f"Démarrage — {int(self.passes_var.get())} passes")
        threading.Thread(target=self._worker, daemon=True).start()

    def _worker(self):
        passes = max(1, int(self.passes_var.get()))
        total_erased = total_failed = 0
        for target in list(self.targets):
            e, f = secure_erase(target, passes=passes, log_cb=self._log)
            total_erased += e; total_failed += f
        def _done():
            self.progress.stop(); self.running = False
            self.erase_btn.config(state="normal")
            self.targets.clear(); self.listbox.delete(0, "end")
            self._log("═" * 40)
            self._log(f"Terminé. OK:{total_erased} | Échecs:{total_failed}")
            if total_failed == 0:
                self._set_status(f"✔ {total_erased} élément(s) effacé(s).", "#50fa7b")
            else:
                self._set_status(f"⚠ {total_failed} échec(s).", "#ffaa55")
            messagebox.showinfo("Terminé",
                                f"Effacés : {total_erased}\nÉchecs : {total_failed}")
        self.root.after(0, _done)


def main():
    if DND_AVAILABLE:
        root = TkinterDnD.Tk()
        print("[DND] TkinterDnD.Tk() utilisé")
    else:
        root = tk.Tk()
        print("[DND] tk.Tk() simple (DnD désactivé)")
    SecureWipeApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()
← Back to timeline