File Organizer

Overview

The Advanced File Organizer is a Python application with a Graphical User Interface (GUI) built using Tkinter. It helps users organize files and folders within a specified directory by categorizing them based on file extensions or folder content analysis and moving them into dated subdirectories within a designated output location. It includes features like a dry run mode for previewing changes, configurable options for handling subfolders and duplicates, and a detailed log of actions.

Screenshot

(Image shows the main interface with settings, proposed actions in the treeview, progress bar, and status log)

Features

Graphical User Interface

Easy-to-use interface built with Tkinter.

Directory Selection

Browse buttons to easily select Search and Output directories.

Dry Run Mode

Preview all proposed file/folder movements in a treeview before committing any actual changes. Items can be individually selected or deselected for commitment.

Categorization

  • Files are categorized based on their extensions using a customizable mapping.
  • Folders can be categorized based on the predominant file types they contain (“Smart Folder Analysis”).
  • Includes categories for common file types (Images, Documents, Videos, Code, etc.) and special categories like “Folders” and “Misc”.

Configurable Scanning

  • Option to exclude subfolders (process only top-level items).
  • Option to enable/disable “Smart Folder Analysis”.
  • Adjustable threshold (percentage) for Smart Folder Analysis majority rule.

Duplicate Handling

Choose how to handle items with the same name in the target destination during commit:

  • Rename with Number (e.g., file_1.txt)
  • Rename with DateTime (e.g., file_20231027_103000123.txt)
  • Rename adding ’ - Copy’ (e.g., file - Copy.txt)
  • Skip the item.
  • Overwrite the existing item (Use with caution!).

Hierarchical Treeview

Displays proposed actions grouped by target category/date folder. Allows expanding/collapsing groups and selecting/deselecting individual items or entire groups using checkboxes.

Context Menu

Right-click (or Ctrl-click on macOS) on items in the treeview for actions like:

  • Check/Uncheck item/group.
  • Change the proposed category for an item.
  • Revert to the originally detected category.
  • Re-analyze a specific item.
  • Open the source item’s location in the system’s file explorer.
  • Copy the source path to the clipboard.

Progress & Logging

  • Real-time progress bar during scanning and committing phases.
  • Status label providing current operation details.
  • Detailed status log window showing actions, warnings, and errors.

Asynchronous Operations

File moving operations are handled asynchronously in a separate thread to keep the GUI responsive.

Cross-Platform Compatibility

Designed to run on Windows, macOS, and Linux (core functionality and file explorer opening).

Requirements

Python 3.x

(Developed and tested with Python 3.7+, uses asyncio, tkinter)

Tkinter

Usually included with standard Python installations. On some Linux distributions, you might need to install it separately (e.g., sudo apt-get install python3-tk).

How to Use

1. Run the Script

Execute the Python script (e.g., python advanced_file_organizer.py).

2. Select Directories

  • Click “Browse” next to “Search Directory” and choose the folder containing the files/folders you want to organize.
  • Click “Browse” next to “Output Directory” and choose where the organized folders should be created. If the directory doesn’t exist, you’ll be prompted to create it. Note: The Output Directory cannot be the same as or inside the Search Directory.

3. Configure Options

  • Dry Run: Keep checked (default) to preview changes first. Uncheck to commit changes immediately after scanning (less common).
  • Exclude Subfolders: Check this if you only want to process items directly inside the Search Directory, ignoring any subfolders within it.
  • Analyze Folder Contents: Check this (default, unless ‘Exclude Subfolders’ is checked) to enable smart analysis of folder contents to determine their category. Set the Folder Majority Threshold (%) (default 80%) to define how dominant a file type must be to categorize the folder accordingly.
  • Duplicate Handling: Select your preferred method for resolving name conflicts when files/folders are moved.

4. Scan

Click the “Scan and Organize” button. The application will scan the Search Directory based on your settings. Progress will be shown.

5. Review (Dry Run)

If Dry Run was enabled, the “Dry Run Actions / Selection” treeview will populate with proposed actions.

  • Items are grouped by the target category folder (e.g., Images_20231027).
  • Expand groups to see individual files/folders.
  • Items marked [x] are selected for moving; items marked [ ] are not. Greyed-out items were skipped during analysis (e.g., empty folders, ignored types) and cannot be selected.
  • Click the checkbox [ ]/[x] next to an item or group header to toggle its selection state. Toggling a group header affects all enabled children within it.
  • Right-click items for more options (change category, open location, etc.).

6. Commit Changes

Once you have reviewed and selected the desired actions in the treeview, click the “Commit Selected Changes” button. You will be asked for confirmation.

7. Monitor Commit

The progress bar and status log will update as the selected files and folders are moved to the Output Directory according to your duplicate handling rules.

8. Completion

A final summary will appear in the status log. The treeview will clear, ready for a new scan if needed.

Configuration (Code)

While the GUI provides most options, the core categorization logic can be adjusted in the script:

extension_mapping

A dictionary mapping file extensions (lowercase, including the dot) to category names (strings). You can add, remove, or modify entries here to customize categorization.

self.extension_mapping = {
    # Images
    '.jpg': 'Images', '.jpeg': 'Images', '.png': 'Images',
    # Documents
    '.pdf': 'Documents', '.docx': 'Documents',
    # ... add more or change existing ones
    '.my_custom_ext': 'MyCategory',
}

folders_category

The default category name used for folders when analysis is disabled or content is too mixed.

misc_category

The default category name used for files with unrecognized extensions or other miscellaneous items.

CHECKED_SYMBOL / UNCHECKED_SYMBOL

Symbols used in the treeview.

Buy Me a Coffee

Don’t be shy; if you found this useful, say Hi!

Python Code

 
import os
import shutil
import datetime
import asyncio
from collections import defaultdict, Counter
import tkinter as tk
from tkinter import filedialog, ttk, messagebox, Menu, IntVar
import threading
import time
import math
import platform
import subprocess
import sys
 
CHECKED_SYMBOL = "[x]"
UNCHECKED_SYMBOL = "[ ]"
 
class AdvancedFileOrganizer:
    def __init__(self, master):
        self.master = master
        self.master.title("Advanced File Organizer v2.5.8 - Click Fix")
        self.master.geometry("1000x800")
 
        self.search_dir = tk.StringVar()
        self.output_dir = tk.StringVar()
        self.dry_run = tk.BooleanVar(value=True)
        self.exclude_subfolders = tk.BooleanVar(value=False)
        self.process_folders_smartly = tk.BooleanVar(value=True)
        self.folder_majority_threshold = tk.IntVar(value=80)
        self.duplicate_handling_mode = tk.StringVar(value="rename_number")
 
        self.extension_mapping = {
            '.jpg': 'Images', '.jpeg': 'Images', '.png': 'Images', '.gif': 'Images',
            '.bmp': 'Images', '.tiff': 'Images', '.tif': 'Images', '.eps': 'Images',
            '.psd': 'Images', '.webp': 'Images', '.svg': 'Images', '.ico': 'Images',
            '.heic': 'Images', '.raw': 'Images', '.xcf': 'Images', '.ai': 'Images',
            '.lnk': 'Shortcuts',
            '.pdf': 'Documents', '.txt': 'Documents', '.md': 'Documents', '.rtf': 'Documents',
            '.doc': 'Documents', '.docx': 'Documents', '.odt': 'Documents',
            '.csv': 'Spreadsheets', '.xlsx': 'Spreadsheets', '.xls': 'Spreadsheets', '.ods': 'Spreadsheets',
            '.wav': 'Audio', '.mp3': 'Audio', '.aiff': 'Audio', '.aac': 'Audio',
            '.wma': 'Audio', '.flac': 'Audio', '.mid': 'Audio', '.midi': 'Audio',
            '.m4a': 'Audio', '.ogg': 'Audio', '.m4p': 'Audio',
            '.mp4': 'Videos', '.avi': 'Videos', '.mov': 'Videos', '.wmv': 'Videos',
            '.flv': 'Videos', '.mkv': 'Videos', '.webm': 'Videos', '.mpg': 'Videos',
            '.mpeg': 'Videos', '.aep': 'Videos', '.prproj': 'Videos',
            '.stl': '3D_Models', '.ply': '3D_Models', '.3mf': '3D_Models',
            '.gcode': '3D_Prints_GCode', '.obj': '3D_Models', '.step': 'CAD', '.stp': 'CAD',
            '.iges': 'CAD', '.igs': 'CAD', '.dwg': 'CAD', '.dxf': 'CAD', '.sldprt': 'CAD', '.sldasm': 'CAD',
            '.ttc': 'Fonts', '.otf': 'Fonts', '.eot': 'Fonts', '.fon': 'Fonts',
            '.ttf': 'Fonts', '.woff': 'Fonts', '.woff2': 'Fonts',
            '.zip': 'Compressed', '.rar': 'Compressed', '.7z': 'Compressed',
            '.tar': 'Compressed', '.gz': 'Compressed', '.bz2': 'Compressed', '.xz': 'Compressed',
            '.exe': 'Executables', '.msi': 'Executables', '.app': 'Executables', '.dmg': 'Installers',
            '.py': 'Code', '.java': 'Code', '.cpp': 'Code', '.h': 'Code', '.cs': 'Code',
            '.js': 'Code', '.html': 'Code', '.css': 'Code', '.php': 'Code', '.rb': 'Code',
            '.sh': 'Scripts', '.bat': 'Scripts', '.ps1': 'Scripts', '.vbs': 'Scripts', '.bash': 'Scripts',
            '.ppt': 'Presentations', '.pptx': 'Presentations', '.key': 'Presentations', '.odp': 'Presentations',
            '.epub': 'eBooks', '.mobi': 'eBooks', '.azw': 'eBooks',
            '.db': 'Databases', '.sqlite': 'Databases', '.sql': 'Databases', '.mdb': 'Databases',
            '.vdi': 'Virtual_Machines', '.vmdk': 'Virtual_Machines', '.ova': 'Virtual_Machines', '.vhd': 'Virtual_Machines',
            '.indd': 'Design_Publishing', '.qxd': 'Design_Publishing', '.cdr': 'Design_Publishing',
            '.iso': 'Disk_Images',
            '.json': 'Data_Files', '.xml': 'Data_Files', '.yaml': 'Data_Files', '.yml': 'Data_Files',
            '.ini': 'ConfigFiles', '.cfg': 'ConfigFiles', '.conf': 'ConfigFiles', '.config': 'ConfigFiles',
            '.bak': 'Backups', '.old': 'Backups', '.tmp': 'Temporary',
            '.log': 'Logs', '.syslog': 'Logs'
        }
        self.folders_category = "Folders"
        self.misc_category = "Misc"
 
        self.proposed_moves = []
        self.treeview_item_states = {}
        self.all_categories = []
 
        self.create_widgets()
        self._update_all_categories()
 
 
    def _update_all_categories(self):
        """Helper to populate the list of unique category names."""
        cats = set(self.extension_mapping.values())
        cats.add(self.folders_category)
        cats.add(self.misc_category)
        self.all_categories = sorted(list(cats))
 
 
    def create_widgets(self):
        settings_frame = ttk.LabelFrame(self.master, text="Settings", padding=(10, 5))
        settings_frame.grid(row=0, column=0, columnspan=4, padx=10, pady=5, sticky="ew")
        settings_frame.columnconfigure(1, weight=1)
 
        ttk.Label(settings_frame, text="Search Directory:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
        ttk.Entry(settings_frame, textvariable=self.search_dir, width=80).grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        ttk.Button(settings_frame, text="Browse", command=self.browse_search_dir).grid(row=0, column=2, padx=5, pady=5)
 
        ttk.Label(settings_frame, text="Output Directory:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
        ttk.Entry(settings_frame, textvariable=self.output_dir, width=80).grid(row=1, column=1, padx=5, pady=5, sticky="ew")
        ttk.Button(settings_frame, text="Browse", command=self.browse_output_dir).grid(row=1, column=2, padx=5, pady=5)
 
        options_subframe = ttk.Frame(settings_frame)
        options_subframe.grid(row=2, column=0, columnspan=3, sticky="w", pady=5)
 
        scan_options_frame = ttk.Frame(options_subframe)
        scan_options_frame.pack(side=tk.LEFT, anchor='nw', padx=5)
        ttk.Checkbutton(scan_options_frame, text="Dry Run (Preview changes)", variable=self.dry_run).pack(anchor='w', pady=1)
        self.exclude_subfolders_cb = ttk.Checkbutton(scan_options_frame, text="Exclude Subfolders (Top Level Only)", variable=self.exclude_subfolders, command=self.toggle_subfolder_options)
        self.exclude_subfolders_cb.pack(anchor='w', pady=1)
        self.smart_folder_cb = ttk.Checkbutton(scan_options_frame, text="Analyze Folder Contents", variable=self.process_folders_smartly, command=self.toggle_threshold_option)
        self.smart_folder_cb.pack(anchor='w', pady=1)
        threshold_frame = ttk.Frame(scan_options_frame)
        threshold_frame.pack(anchor='w', padx=(20, 0))
        self.threshold_label = ttk.Label(threshold_frame, text="Folder Majority Threshold (%):")
        self.threshold_label.pack(side=tk.LEFT)
        self.threshold_spinbox = ttk.Spinbox(threshold_frame, from_=51, to=99, textvariable=self.folder_majority_threshold, width=5)
        self.threshold_spinbox.pack(side=tk.LEFT, padx=5)
 
        duplicate_options_frame = ttk.LabelFrame(options_subframe, text="Duplicate Handling (on Commit)")
        duplicate_options_frame.pack(side=tk.LEFT, anchor='nw', padx=15)
        ttk.Radiobutton(duplicate_options_frame, text="Rename with Number (item_1.ext)", variable=self.duplicate_handling_mode, value="rename_number").pack(anchor='w')
        ttk.Radiobutton(duplicate_options_frame, text="Rename with DateTime (item_timestamp.ext)", variable=self.duplicate_handling_mode, value="rename_datetime").pack(anchor='w')
        ttk.Radiobutton(duplicate_options_frame, text="Rename adding ' - Copy'", variable=self.duplicate_handling_mode, value="rename_copy").pack(anchor='w')
        ttk.Radiobutton(duplicate_options_frame, text="Skip", variable=self.duplicate_handling_mode, value="skip").pack(anchor='w')
        ttk.Radiobutton(duplicate_options_frame, text="Overwrite (Caution!)", variable=self.duplicate_handling_mode, value="overwrite").pack(anchor='w')
 
        self.toggle_subfolder_options()
 
        button_frame = ttk.Frame(self.master)
        button_frame.grid(row=1, column=0, columnspan=4, padx=10, pady=(0, 5))
        self.run_button = ttk.Button(button_frame, text="Scan and Organize", command=self.run_organizer, style="Accent.TButton")
        self.run_button.pack(side=tk.LEFT, padx=10)
        self.commit_button = ttk.Button(button_frame, text="Commit Selected Changes", command=self.run_commit_changes, state="disabled")
        self.commit_button.pack(side=tk.LEFT, padx=10)
        style = ttk.Style()
        try:
            style.configure("Accent.TButton", foreground="white", background="blue")
        except tk.TclError:
            print("Note: Could not apply custom button style (Accent.TButton).")
 
        dry_run_frame = ttk.LabelFrame(self.master, text="Dry Run Actions / Selection", padding=(10, 5))
        dry_run_frame.grid(row=2, column=0, columnspan=4, padx=10, pady=5, sticky="nsew")
        dry_run_frame.columnconfigure(0, weight=1)
        dry_run_frame.rowconfigure(0, weight=1)
        self.tree_columns = ("#0", "category", "files", "match", "folders", "source_rel", "destination")
        self.log_display_tree = ttk.Treeview(dry_run_frame, columns=self.tree_columns[1:], show="tree headings", selectmode="none")
        self.log_display_tree.grid(row=0, column=0, sticky="nsew")
        col_widths = {"#0": 320, "category": 110, "files": 50, "match": 60, "folders": 50, "source_rel": 200, "destination": 140}
        self.log_display_tree.heading("#0", text=" Action / Item", anchor='w'); self.log_display_tree.column("#0", width=col_widths["#0"], stretch=tk.YES, anchor='w')
        self.log_display_tree.heading("category", text="Category", anchor='w'); self.log_display_tree.column("category", width=col_widths["category"], stretch=tk.NO, anchor='w')
        self.log_display_tree.heading("files", text="Files", anchor='center'); self.log_display_tree.column("files", width=col_widths["files"], stretch=tk.NO, anchor='center')
        self.log_display_tree.heading("match", text="Match %", anchor='center'); self.log_display_tree.column("match", width=col_widths["match"], stretch=tk.NO, anchor='center')
        self.log_display_tree.heading("folders", text="Folders", anchor='center'); self.log_display_tree.column("folders", width=col_widths["folders"], stretch=tk.NO, anchor='center')
        self.log_display_tree.heading("source_rel", text="Source (Relative)", anchor='w'); self.log_display_tree.column("source_rel", width=col_widths["source_rel"], stretch=tk.YES, anchor='w')
        self.log_display_tree.heading("destination", text="Destination Folder", anchor='w'); self.log_display_tree.column("destination", width=col_widths["destination"], stretch=tk.YES, anchor='w')
        vsb = ttk.Scrollbar(dry_run_frame, orient="vertical", command=self.log_display_tree.yview); vsb.grid(row=0, column=1, sticky="ns")
        hsb = ttk.Scrollbar(dry_run_frame, orient="horizontal", command=self.log_display_tree.xview); hsb.grid(row=1, column=0, columnspan=2, sticky="ew")
        self.log_display_tree.configure(yscrollcommand=vsb.set, xscrollcommand=hsb.set)
        self.log_display_tree.bind('<ButtonRelease-1>', self.handle_tree_click)
        if platform.system() == "Darwin": self.log_display_tree.bind('<Button-2>', self.show_context_menu)
        else: self.log_display_tree.bind('<Button-3>', self.show_context_menu)
 
        bottom_frame = ttk.Frame(self.master, padding=(10, 5)); bottom_frame.grid(row=3, column=0, columnspan=4, padx=10, pady=5, sticky="ew"); bottom_frame.columnconfigure(0, weight=1)
        self.progress_label = ttk.Label(bottom_frame, text="Progress:"); self.progress_label.grid(row=0, column=0, columnspan=2, padx=5, pady=2, sticky="w"); self.progress_var = tk.DoubleVar(); self.progress_bar = ttk.Progressbar(bottom_frame, variable=self.progress_var, maximum=100); self.progress_bar.grid(row=1, column=0, columnspan=2, padx=5, pady=2, sticky="ew"); self.status_label = ttk.Label(bottom_frame, text=""); self.status_label.grid(row=2, column=0, columnspan=2, padx=5, pady=2, sticky="w")
        status_log_frame = ttk.Frame(bottom_frame); status_log_frame.grid(row=3, column=0, columnspan=2, padx=5, pady=(10,0), sticky="ew"); status_log_frame.columnconfigure(0, weight=1); ttk.Label(status_log_frame, text="Status Messages / Errors:").pack(anchor='w'); self.status_log = tk.Text(status_log_frame, wrap=tk.WORD, height=4, state=tk.DISABLED, relief=tk.SUNKEN, borderwidth=1); self.status_log.pack(fill=tk.X, expand=True, pady=(0,5))
 
        self.master.columnconfigure(0, weight=1); self.master.rowconfigure(2, weight=1); self.master.rowconfigure(3, weight=0)
 
    def browse_search_dir(self):
        directory = filedialog.askdirectory(title="Select Directory to Organize")
        if directory:
            self.search_dir.set(directory)
 
    def browse_output_dir(self):
        directory = filedialog.askdirectory(title="Select Output Directory")
        if directory:
            self.output_dir.set(directory)
 
    def toggle_subfolder_options(self):
        if self.exclude_subfolders.get():
            self.smart_folder_cb.config(state="disabled")
            self.process_folders_smartly.set(False)
        else:
            self.smart_folder_cb.config(state="normal")
        self.toggle_threshold_option()
 
    def toggle_threshold_option(self):
        if self.process_folders_smartly.get() and not self.exclude_subfolders.get():
            self.threshold_label.config(state="normal")
            self.threshold_spinbox.config(state="normal")
        else:
            self.threshold_label.config(state="disabled")
            self.threshold_spinbox.config(state="disabled")
 
    def log(self, message, tags=None):
        self.master.after(0, self._log_status_on_main_thread, message, tags)
 
    def _log_status_on_main_thread(self, message, tags=None):
        try:
            full_message = str(message)
            self.status_log.config(state=tk.NORMAL)
            if self.status_log.index("end-1c") != "1.0" and self.status_log.get("end-2c") != '\n':
                self.status_log.insert(tk.END, "\n")
            self.status_log.insert(tk.END, full_message, tags or ())
            self.status_log.see(tk.END)
            self.status_log.config(state=tk.DISABLED)
        except Exception as e:
            print(f"Error logging status: {e}")
 
    def clear_status_log(self):
        self.master.after(0, self._clear_status_log_on_main)
 
    def _clear_status_log_on_main(self):
        self.status_log.config(state=tk.NORMAL)
        self.status_log.delete(1.0, tk.END)
        self.status_log.config(state=tk.DISABLED)
 
    def update_status(self, message):
         self.master.after(0, self._update_status_on_main_thread, message)
 
    def _update_status_on_main_thread(self, message):
        self.status_label.config(text=message)
 
    def update_progress(self, current, total, action="Scanning"):
         self.master.after(0, self._update_progress_on_main_thread, current, total, action)
 
    def _update_progress_on_main_thread(self, current, total, action):
        if total > 0:
            progress = min(100,(current / total) * 100)
            self.progress_var.set(progress)
            self._update_status_on_main_thread(f"{action}: {current}/{total}")
        elif total == 0:
             self.progress_var.set(0)
             self._update_status_on_main_thread(f"{action}: 0 items found/analyzed")
        else:
             self.progress_var.set(0)
             self._update_status_on_main_thread(f"{action}: Discovered {current} items...")
 
    def run_organizer(self):
        self.clear_status_log()
        self.log("Starting analysis...")
        try:
            for item in self.log_display_tree.get_children():
                self.log_display_tree.delete(item)
        except tk.TclError as e:
            print(f"Debug: Error clearing tree: {e}")
        self.treeview_item_states.clear()
        self.commit_button.config(state="disabled")
        self.run_button.config(state="disabled")
        self.progress_var.set(0)
        self.update_status("Initializing...")
        self.proposed_moves = []
 
        search_directory = self.search_dir.get()
        output_directory = self.output_dir.get()
 
        if not search_directory or not os.path.isdir(search_directory):
            messagebox.showerror("Error", "Please select a valid search directory.")
            self.log("Error: Invalid search directory selected."); self.run_button.config(state="normal"); return
        if not output_directory:
             messagebox.showerror("Error", "Please select an output directory.")
             self.log("Error: No output directory selected."); self.run_button.config(state="normal"); return
        if not os.path.isdir(output_directory):
            if messagebox.askyesno("Create Directory?", f"Output directory '{output_directory}' does not exist. Create it?"):
                try: os.makedirs(output_directory, exist_ok=True); self.log(f"Created output directory: {output_directory}")
                except Exception as e: messagebox.showerror("Error", f"Failed to create output directory:\n{e}"); self.log(f"Error: Failed to create output directory: {e}"); self.run_button.config(state="normal"); return
            else: messagebox.showerror("Error", "Output directory does not exist."); self.log("Error: Output directory does not exist."); self.run_button.config(state="normal"); return
        try:
            abs_search = os.path.abspath(search_directory); abs_output = os.path.abspath(output_directory)
            if os.path.exists(abs_search) and os.path.exists(abs_output) and os.path.samefile(abs_search, abs_output):
                 messagebox.showerror("Error", "Output directory cannot be the same as the search directory."); self.log("Error: Output/Search paths are identical."); self.run_button.config(state="normal"); return
            common_path = os.path.commonpath([abs_search, abs_output])
            if common_path == abs_search and abs_output != abs_search and abs_output.startswith(abs_search + os.sep):
                 messagebox.showerror("Error", "Output directory cannot be inside the search directory."); self.log("Error: Output directory is inside the search directory."); self.run_button.config(state="normal"); return
        except FileNotFoundError: messagebox.showerror("Error", "Search or Output path does not exist for comparison."); self.log("Error: Path check failed (file not found)."); self.run_button.config(state="normal"); return
        except ValueError: pass
        except Exception as e: print(f"Debug: Path comparison failed: {e}"); self.log(f"Warning: Path comparison check failed: {e}")
 
        threading.Thread(target=self._run_organizer_thread, daemon=True).start()
 
    def analyze_folder_contents(self, folder_path, threshold):
        category_counter = Counter()
        direct_files = 0
        direct_folders = 0
        match_percent = 0
        category = self.misc_category
        reason = "N/A"
        try:
            for entry in os.scandir(folder_path):
                if entry.is_file(follow_symlinks=False):
                    base_name = entry.name.lower()
                    _, ext = os.path.splitext(base_name)
                    if ext in ['.ds_store', '.localized', '.ini', '.tmp', '.bak',