In this tutorial, we’ll build FileOMate, a smart file organizer that automatically sorts your files in real time. It includes a GUI, drag-and-drop support, customizable rules, preview mode, and real-time monitoring using watchdog.
We’ll go step by step, breaking down the code into small, understandable chunks.
Step 1: Setting Up Dependencies
FileOMate relies on a few Python packages:
pip install ttkbootstrap watchdog tkinterdnd2
Explanation:
ttkbootstrap → Modern themes for Tkinter GUI.
watchdog → Real-time file system monitoring.
tkinterdnd2 → Drag-and-drop folder support (optional).
Step 2: Import Required Modules
We start by importing the standard library modules and installed packages:
import sys
import time
import json
import queue
import shutil
import threading
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox
import ttkbootstrap as tb
from ttkbootstrap.constants import *
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
Optional drag & drop support
try:
from tkinterdnd2 import TkinterDnD, DND_FILES
DND = True
except ImportError:
DND = False
Explanation:
queue → Thread-safe communication between background threads and GUI.
shutil → For moving files safely.
watchdog → Observes file changes in real time.
TkinterDnD → Enables drag-and-drop folder functionality.
Step 3: Create a Watchdog Handler
FolderHandler responds when a new file is created in a watched folder.
class FolderHandler(FileSystemEventHandler):
def __init__(self, app):
self.app = app
def on_created(self, event):
if not event.is_directory:
self.app.handle_new_file(Path(event.src_path))
Explanation:
on_created is called whenever a file is added.
self.app.handle_new_file processes the new file safely.
Step 4: Building the Main App Class
The FileOMateApp class handles GUI, file rules, and real-time monitoring.
class FileOMateApp:
APP_NAME = "FileOMate"
APP_VERSION = "1.2.0"
def __init__(self):
self.root = TkinterDnD.Tk() if DND else tk.Tk()
tb.Style(theme="darkly")
self.root.title(f"{self.APP_NAME} v{self.APP_VERSION}")
self.root.geometry("1180x680")
Explanation:
Supports drag-and-drop if TkinterDnD is installed.
Uses a modern dark theme with ttkbootstrap.
Sets window size and title.
Step 5: Adding State Variables
These variables track the app’s state, counters, and options.
self.watch_folders = []
self.unmatched_folder = None
self.running = False
self.preview_mode = True
self.ui_queue = queue.Queue()
self.moved = tk.IntVar(value=0)
self.unmatched = tk.IntVar(value=0)
self.errors = tk.IntVar(value=0)
self.preview_moved = 0
self.preview_unmatched = 0
self.auto_unmatched_var = tk.BooleanVar(value=True)
self.recursive_var = tk.BooleanVar(value=True)
self.status_var = tk.StringVar(value="Preview Mode")
Explanation:
watch_folders → List of folders to monitor.
ui_queue → Thread-safe messages to display in the GUI log.
IntVar and BooleanVar → Tkinter variables that automatically update the UI.
Step 6: Safe File Move Utility
A helper function that moves files safely, avoiding overwrites.
def safe_move(self, src: Path, dst: Path) -> Path | None:
try:
dst.parent.mkdir(parents=True, exist_ok=True)
base, ext = dst.stem, dst.suffix
final_dst = dst
i = 1
while final_dst.exists():
final_dst = dst.with_name(f"{base} ({i}){ext}")
i += 1
shutil.move(str(src), str(final_dst))
return final_dst
except Exception as e:
self.ui_queue.put(f"❌ Failed to move {src.name}: {e}\n")
if not self.preview_mode:
self.errors.set(self.errors.get() + 1)
return None
Explanation:
Creates missing folders automatically.
Automatically renames duplicates like file (1).txt.
Reports errors safely to the UI log.
Step 7: Matching Files to Rules
We need a way to decide where each file goes:
def match_rule(self, file: Path):
for r in self.rule_table.get_children():
keyword, target_folder = self.rule_table.item(r)["values"]
if keyword in file.name.lower():
return Path(target_folder)
return None
Explanation:
Each rule has a keyword and a target folder.
If a file name contains a keyword, it is moved to the target folder.
Returns None if no match is found.
Step 8: Handling a New File
def handle_new_file(self, file_path: Path):
f = Path(file_path).resolve()
if not f.is_file():
return
target_folder = self.match_rule(f)
if not target_folder:
if self.auto_unmatched_var.get() and self.unmatched_folder:
target_folder = self.unmatched_folder
else:
if not self.preview_mode:
self.unmatched.set(self.unmatched.get() + 1)
self.ui_queue.put(f"⚠ Skipped (no rule): {f.name}\n")
return
if self.preview_mode:
self.ui_queue.put(f"{f.name} → {target_folder}\n")
return
final_dst = self.safe_move(f, target_folder / f.name)
if final_dst:
self.ui_queue.put(f"✔ Moved: {f.name} → {final_dst}\n")
self.moved.set(self.moved.get() + 1)
Explanation:
Checks if file matches a rule or goes to the "unmatched" folder.
Logs actions in preview mode without moving files.
Uses safe_move for real moves and updates counters.
Step 9: Preview vs Real Moves
def preview_scan(self):
self.preview_mode = True
self.status_var.set("Preview Mode")
self.ui_queue.put("--- Preview ---\n")
for folder in self.watch_folders:
for f in Path(folder).rglob("*") if self.recursive_var.get() else Path(folder).iterdir():
if f.is_file():
target_folder = self.match_rule(f)
if target_folder:
self.ui_queue.put(f"{f.name} → {target_folder}\n")
else:
self.ui_queue.put(f"{f.name} → unmatched\n")
Explanation:
Shows where files would be moved without actually moving them.
Helps users confirm rules before running.
Step 10: Running the Observer
def start(self):
self.running = True
self.preview_mode = False
self.status_var.set("Running")
self.observer = Observer()
handler = FolderHandler(self)
for folder in self.watch_folders:
self.observer.schedule(handler, str(folder), recursive=self.recursive_var.get())
self.observer.start()
Explanation:
Observer monitors the file system in real-time.
Calls FolderHandler whenever a file is added.
Runs in the background while the GUI remains responsive.
Step 11: Building the GUI
We use ttkbootstrap for modern styling. Example: Watch folders panel.
lf = tb.Labelframe(self.root, text="Watch Folders", padding=10)
lf.pack(fill="x", padx=10, pady=5)
self.folder_list = tk.Listbox(lf, height=4)
self.folder_list.pack(side="left", fill="x", expand=True)
tb.Button(lf, text="Add Folder", command=self.add_watch_folder).pack(side="left", padx=6)
Explanation:
Labelframe → Group UI elements.
Listbox → Displays folders being monitored.
Button → Allows adding new folders.
Step 12: Running the App
Finally, we run the main loop:
if __name__ == "__main__":
FileOMateApp().run()
Explanation:
root.mainloop() keeps the GUI running.
All threads (file processing, watchdog, UI queue) run in the background.
✅ At this point, you have a working FileOMate app that can:
Monitor folders in real time.
Organize files by rules.
Preview changes.
Safely move files with duplicate handling.
Clone full repository here: https://github.com/rogers-cyber/FileOMate

Top comments (0)