Project 3 ยท Week 3

Desktop Automated File & System Organizer

~2 hours

1. Project Overview & Architecture

Welcome to Major Project 3: Desktop Automated File & System Organizer! In this major project, you will build an automated operating system utility that interacts directly with your computer's file system. You will combine file system operations (using pathlib and shutil), safe path resolution, dry-run simulation modes, and system logging to create a production-grade utility for decluttering directories like Downloads or Desktop.

Project Core Objectives

  • Automated Directory Classification: Inspect file extensions dynamically and map files to dedicated category folders (Images, Documents, Archives, Audio, etc.).
  • Dry-Run Safety Mode: Allow users to preview actions before making any permanent file system modifications on disk.
  • Collision Resolution: Prevent file overwrites by appending timestamps or numbers when destination files already exist.
  • System Activity Logging: Maintain an audit trail of all file movements using Python's standard logging module.

2. Functional Requirements & Feature Specifications

Your File Organizer must fulfill the following technical standards:

Feature Component Functional Specification Python Mechanism
Path Resolution Handles relative and absolute pathing cross-platform (Mac, Windows, Linux). pathlib.Path with .resolve().
File Operations Creates category folders on demand and transfers files cleanly. Path.mkdir(exist_ok=True), shutil.move().
Duplicate Protection Renames files dynamically if a file with the same name exists at destination. Custom path collision checker with datetime suffixes.
Audit Logging Records date, time, and move details to organizer.log. logging.basicConfig() and logging.info().

3. Complete Reference Implementation

Examine the full, modular Python script below to understand how file system operations, logging, and collision safety combine:

import shutil
import logging
from pathlib import Path
from datetime import datetime

# Set up audit logging
logging.basicConfig(
    filename="organizer.log",
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s"
)

# Extension mapping dictionary
CATEGORY_MAP = {
    "Images": [".jpg", ".jpeg", ".png", ".gif", ".svg", ".bmp", ".webp"],
    "Documents": [".pdf", ".docx", ".doc", ".txt", ".xlsx", ".pptx", ".csv"],
    "Audio": [".mp3", ".wav", ".aac", ".flac", ".ogg"],
    "Videos": [".mp4", ".mkv", ".mov", ".avi"],
    "Archives": [".zip", ".tar", ".gz", ".7z", ".rar"],
    "Code": [".py", ".js", ".html", ".css", ".json", ".sql"]
}

def get_category(file_path):
    """Returns the matching category string for a file based on its extension."""
    ext = file_path.suffix.lower()
    for category, extensions in CATEGORY_MAP.items():
        if ext in extensions:
            return category
    return "Others"

def resolve_duplicate(destination_path):
    """If destination file exists, appends a timestamp to prevent overwriting."""
    if not destination_path.exists():
        return destination_path
    
    stem = destination_path.stem
    suffix = destination_path.suffix
    parent = destination_path.parent
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    
    return parent / f"{stem}_{timestamp}{suffix}"

def organize_directory(target_path_str, dry_run=True):
    """Scans and organizes files in the target directory."""
    target = Path(target_path_str).resolve()
    
    if not target.exists() or not target.is_dir():
        print(f"โŒ Error: '{target}' is not a valid directory.")
        return

    mode_label = "๐Ÿ” [DRY RUN PREVIEW]" if dry_run else "๐Ÿš€ [EXECUTING ORGANIZATION]"
    print(f"\n{mode_label} Target: {target}")
    
    # Reserved category folders to avoid moving newly created category directories
    protected_folders = set(CATEGORY_MAP.keys()) | {"Others"}
    moved_count = 0

    for item in target.iterdir():
        # Skip subdirectories, protected folders, and the log file
        if item.is_dir() or item.name in protected_folders or item.name == "organizer.log":
            continue

        category = get_category(item)
        dest_folder = target / category
        dest_file_path = resolve_duplicate(dest_folder / item.name)

        if dry_run:
            print(f"  โ€ข [PREVIEW] Would move: '{item.name}' โž” '{category}/'")
        else:
            dest_folder.mkdir(exist_ok=True)
            shutil.move(str(item), str(dest_file_path))
            log_msg = f"Moved: '{item.name}' โž” '{category}/{dest_file_path.name}'"
            print(f"  โœ… {log_msg}")
            logging.info(log_msg)
            moved_count += 1

    if not dry_run:
        print(f"\n๐ŸŽ‰ Finished! Successfully organized {moved_count} files.")
        logging.info(f"Directory organization completed for {target}. Total files moved: {moved_count}")
    else:
        print("\nโ„น๏ธ Preview complete. Choose option 2 from the main menu to perform actual changes.")

def main():
    """Main terminal driver menu."""
    print("==================================================")
    print("   ๐Ÿ“ AUTOMATED FILE & SYSTEM ORGANIZER          ")
    print("==================================================")
    
    target_dir = input("Enter directory path to organize (Press Enter for current directory): ").strip()
    if not target_dir:
        target_dir = "."

    while True:
        print("\nORGANIZER MAIN MENU:")
        print("1) Preview Changes (Dry-Run)")
        print("2) Execute File Organization")
        print("3) View Activity Log")
        print("4) Exit Program")

        choice = input("\nSelect choice (1-4): ").strip()

        if choice == "1":
            organize_directory(target_dir, dry_run=True)
        elif choice == "2":
            confirm = input("โš ๏ธ Are you sure you want to organize files on disk? (y/N): ").strip().lower()
            if confirm == "y":
                organize_directory(target_dir, dry_run=False)
            else:
                print("Operation cancelled.")
        elif choice == "3":
            log_file = Path("organizer.log")
            if log_file.exists():
                print("\n--- ๐Ÿ“‹ Recent Activity Log ---")
                print(log_file.read_text(encoding="utf-8"))
            else:
                print("\nNo log file found yet.")
        elif choice == "4":
            print("\nExiting File Organizer. Stay organized!")
            break
        else:
            print("โš ๏ธ Invalid selection. Please choose 1, 2, 3, or 4.")

if __name__ == "__main__":
    main()

4. Common Project Pitfalls

โš ๏ธ Project Watchlist:

  • Infinite Subfolder Traversal: If you do not skip existing category subdirectories during iteration (item.is_dir()), your script might try to move subfolders into themselves.
  • Silent Overwriting: Using shutil.move() without checking if the destination file exists will overwrite files with matching names. Always resolve duplicate names first.
  • Hardcoded Slash Separators: Avoid string concatenations like folder + "/" + filename. Use pathlib.Path slash operators (folder / filename) for cross-platform compatibility.

5. Interactive Project Workspace

Project Workspace

Build & Test: File & System Organizer

Use the workspace below to customize your organizer script. Try extending the category mapping with custom file extensions or adding an undo option!

0% complete
Project Feature Checklist

6. Key Takeaways & Milestone Achievement

  • The pathlib library simplifies cross-platform file path management and metadata inspection.
  • Always implement a dry-run or preview mode when writing scripts that modify or move local files.
  • System logging produces audit trails essential for troubleshooting and verifying automation runs.

๐Ÿ† Milestone 3 Complete

Congratulations on completing Major Project 3! Up next: **Major Project 4: Web Scraper & Data Pipeline** where you will gather and process real-time data from the web!