ZetaSync Python GUI

👤 Sharing: AI
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import shutil
import threading
import time

class ZetaSyncApp:
    def __init__(self, master):
        self.master = master
        master.title("ZetaSync - Your Ultimate File Synchronization Tool")
        master.geometry("800x600")
        master.configure(bg="#f0f0f0")

        # Style Configuration
        self.style = ttk.Style()
        self.style.configure("TButton", padding=6, relief="raised", background="#4CAF50", foreground="white")
        self.style.configure("TLabel", background="#f0f0f0", font=("Arial", 12))
        self.style.configure("TEntry", padding=5, font=("Arial", 12))
        self.style.configure("TProgressbar", thickness=20)

        # Source Directory
        self.source_label = ttk.Label(master, text="Source Directory:")
        self.source_label.grid(row=0, column=0, padx=10, pady=10, sticky="w")
        self.source_entry = ttk.Entry(master, width=60)
        self.source_entry.grid(row=0, column=1, padx=10, pady=10, sticky="we")
        self.source_button = ttk.Button(master, text="Browse", command=self.browse_source)
        self.source_button.grid(row=0, column=2, padx=10, pady=10, sticky="e")

        # Destination Directory
        self.dest_label = ttk.Label(master, text="Destination Directory:")
        self.dest_label.grid(row=1, column=0, padx=10, pady=10, sticky="w")
        self.dest_entry = ttk.Entry(master, width=60)
        self.dest_entry.grid(row=1, column=1, padx=10, pady=10, sticky="we")
        self.dest_button = ttk.Button(master, text="Browse", command=self.browse_dest)
        self.dest_button.grid(row=1, column=2, padx=10, pady=10, sticky="e")

        # Synchronization Options
        self.sync_label = ttk.Label(master, text="Synchronization Options:")
        self.sync_label.grid(row=2, column=0, padx=10, pady=10, sticky="w")

        self.one_way_var = tk.BooleanVar()
        self.one_way_check = tk.Checkbutton(master, text="One-Way Sync (Source -> Destination)", variable=self.one_way_var, bg="#f0f0f0", font=("Arial", 12))
        self.one_way_check.grid(row=2, column=1, padx=10, pady=5, sticky="w")

        self.delete_var = tk.BooleanVar()
        self.delete_check = tk.Checkbutton(master, text="Delete Extra Files in Destination", variable=self.delete_var, bg="#f0f0f0", font=("Arial", 12))
        self.delete_check.grid(row=3, column=1, padx=10, pady=5, sticky="w")

        # Exclude Patterns
        self.exclude_label = ttk.Label(master, text="Exclude Patterns (comma-separated):")
        self.exclude_label.grid(row=4, column=0, padx=10, pady=10, sticky="w")
        self.exclude_entry = ttk.Entry(master, width=60)
        self.exclude_entry.grid(row=4, column=1, padx=10, pady=10, sticky="we")

        # Sync Button
        self.sync_button = ttk.Button(master, text="Synchronize", command=self.start_sync)
        self.sync_button.grid(row=5, column=1, padx=10, pady=20, sticky="e")

        # Progress Bar
        self.progress = ttk.Progressbar(master, orient="horizontal", length=600, mode="determinate")
        self.progress.grid(row=6, column=0, columnspan=3, padx=10, pady=10, sticky="we")

        # Status Label
        self.status_label = ttk.Label(master, text="Status: Idle")
        self.status_label.grid(row=7, column=0, columnspan=3, padx=10, pady=10, sticky="w")

        # Details Button (Initially Hidden)
        self.details_button = ttk.Button(master, text="Details", command=self.show_details)
        self.details_button.grid(row=8, column=1, padx=10, pady=10, sticky="e")
        self.details_button.grid_remove() #hide it initially

        # Details Window (Will be created on demand)
        self.details_window = None


    def browse_source(self):
        dirname = filedialog.askdirectory(title="Select Source Directory")
        if dirname:
            self.source_entry.delete(0, tk.END)
            self.source_entry.insert(0, dirname)

    def browse_dest(self):
        dirname = filedialog.askdirectory(title="Select Destination Directory")
        if dirname:
            self.dest_entry.delete(0, tk.END)
            self.dest_entry.insert(0, dirname)

    def start_sync(self):
        source = self.source_entry.get()
        dest = self.dest_entry.get()
        one_way = self.one_way_var.get()
        delete = self.delete_var.get()
        exclude_patterns = [p.strip() for p in self.exclude_entry.get().split(',') if p.strip()]

        if not source or not dest:
            messagebox.showerror("Error", "Please specify both source and destination directories.")
            return

        self.sync_button.config(state=tk.DISABLED)
        self.status_label.config(text="Status: Synchronizing...")
        self.progress["value"] = 0
        self.master.update_idletasks()

        # Start sync in a separate thread to avoid freezing the GUI
        threading.Thread(target=self.synchronize, args=(source, dest, one_way, delete, exclude_patterns)).start()

    def synchronize(self, source, dest, one_way, delete, exclude_patterns):
        try:
            file_list = self.get_file_list(source, exclude_patterns)
            total_files = len(file_list)
            self.progress["maximum"] = total_files

            copied_files = 0 # Track number of files copied

            for i, file_path in enumerate(file_list):
                relative_path = os.path.relpath(file_path, source)
                dest_path = os.path.join(dest, relative_path)

                # Ensure destination directory exists
                os.makedirs(os.path.dirname(dest_path), exist_ok=True)

                # Copy file if it doesn't exist or is newer in source
                if not os.path.exists(dest_path) or os.path.getmtime(file_path) > os.path.getmtime(dest_path):
                    shutil.copy2(file_path, dest_path)  # copy2 preserves metadata
                    copied_files += 1 # increment counter


                self.progress["value"] = i + 1
                self.status_label.config(text=f"Status: Synchronizing... ({i + 1}/{total_files} files)")
                self.master.update_idletasks()

            if delete and one_way:
                self.delete_extra_files(source, dest, exclude_patterns)

            self.status_label.config(text="Status: Synchronization Complete!")
            self.details_button.grid() #Make the details button visible
            messagebox.showinfo("Success", f"Synchronization complete! {copied_files} files copied.") # Include the number of files copied.

        except Exception as e:
            self.status_label.config(text="Status: Error during synchronization.")
            messagebox.showerror("Error", f"An error occurred: {e}")
        finally:
            self.sync_button.config(state=tk.NORMAL)


    def get_file_list(self, source, exclude_patterns):
        file_list = []
        for root, _, files in os.walk(source):
            for file in files:
                file_path = os.path.join(root, file)
                relative_path = os.path.relpath(file_path, source)
                if not any(pattern in relative_path for pattern in exclude_patterns):
                    file_list.append(file_path)
        return file_list

    def delete_extra_files(self, source, dest, exclude_patterns):
        for root, _, files in os.walk(dest):
            for file in files:
                dest_file_path = os.path.join(root, file)
                relative_path = os.path.relpath(dest_file_path, dest)
                source_file_path = os.path.join(source, relative_path)

                if not os.path.exists(source_file_path) and not any(pattern in relative_path for pattern in exclude_patterns):
                    try:
                        os.remove(dest_file_path)
                        print(f"Deleted extra file: {dest_file_path}")
                    except Exception as e:
                        print(f"Error deleting file {dest_file_path}: {e}")

    def show_details(self):
        if self.details_window is None or not tk.Toplevel.winfo_exists(self.details_window):
          self.details_window = tk.Toplevel(self.master)
          self.details_window.title("ZetaSync Details")

          details_text = tk.Text(self.details_window, wrap=tk.WORD, height=20, width=80)
          details_text.pack(padx=10, pady=10)
          details_text.insert(tk.END, self.get_details_text())
          details_text.config(state=tk.DISABLED) # Make it read-only

        self.details_window.focus()

    def get_details_text(self):
        return """ZetaSync is a powerful and versatile file synchronization tool designed as an alternative to complex, paid software.

        Key Features:

        - One-Way and Two-Way Synchronization: Choose between one-way (source to destination) or two-way synchronization to keep your files consistent.
        - Selective File Deletion: Option to delete extra files in the destination directory during one-way synchronization.
        - Exclusion Patterns: Exclude specific files or directories from synchronization using comma-separated patterns (e.g., *.tmp, .git/).
        - Real-Time Progress Tracking: Monitor the synchronization progress with a visual progress bar and status updates.
        - Threaded Operation: Performs synchronization in a separate thread to prevent the GUI from freezing.
        - Metadata Preservation: Uses shutil.copy2 to preserve file metadata during synchronization.
        - Robust Error Handling: Handles potential errors during synchronization and provides informative error messages.

        How to Use:

        1. Select the source and destination directories.
        2. Choose your synchronization options (one-way, delete extra files).
        3. Specify any exclusion patterns.
        4. Click the 'Synchronize' button to start the synchronization process.
        
        ZetaSync offers a user-friendly interface and comprehensive features, making it an ideal solution for individuals and professionals who need reliable file synchronization without the cost and complexity of commercial software.
        """


root = tk.Tk()
app = ZetaSyncApp(root)
root.mainloop()
👁️ Viewed: 7

Comments