Chapter 12 of 12
Build a file organiser script that scans a directory, categorises files by type, and moves them into organised subfolders โ a genuinely useful automation tool.
#!/usr/bin/env python3
"""
Organise files in a directory into categorised subfolders.
Usage: python organise.py /path/to/directory
"""
import os
import shutil
import sys
import json
from pathlib import Path
from datetime import datetime
# Category definitions
CATEGORIES = {
"Images": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"},
"Documents": {".pdf", ".doc", ".docx", ".txt", ".md", ".csv", ".xlsx"},
"Videos": {".mp4", ".mov", ".avi", ".mkv", ".webm"},
"Audio": {".mp3", ".wav", ".flac", ".aac", ".ogg"},
"Code": {".py", ".js", ".ts", ".html", ".css", ".json", ".yaml"},
"Archives": {".zip", ".tar", ".gz", ".rar", ".7z"},
}
def get_category(extension: str) -> str:
"""Return the category for a file extension."""
ext_lower = extension.lower()
for category, extensions in CATEGORIES.items():
if ext_lower in extensions:
return category
return "Other"
def organise_directory(target: Path, dry_run: bool = False) -> dict:
"""Organise files in target directory. Returns a summary report."""
if not target.is_dir():
raise ValueError(f"Not a directory: {target}")
moved = {}
skipped = []
for file in target.iterdir():
if not file.is_file() or file.name.startswith("."):
continue
category = get_category(file.suffix)
destination = target / category
if not dry_run:
destination.mkdir(exist_ok=True)
# Handle duplicates
dest_file = destination / file.name
if dest_file.exists():
stem = file.stem
new_name = f"{stem}_{datetime.now().strftime('%H%M%S')}{file.suffix}"
dest_file = destination / new_name
shutil.move(str(file), str(dest_file))
moved.setdefault(category, []).append(file.name)
return {"moved": moved, "skipped": skipped, "dry_run": dry_run}
def print_report(report: dict, target: Path) -> None:
"""Print a formatted summary report."""
print(f"
โโ File Organiser Report โโโโโโโโโโโโโโโโโโโโโโโโ")
print(f"Directory: {target}")
print(f"Mode: {'Dry run (no changes made)' if report['dry_run'] else 'Live run'}
")
if not report["moved"]:
print("No files to organise.")
return
total = 0
for category, files in sorted(report["moved"].items()):
print(f" {category}/ ({len(files)} files)")
for f in files[:5]: # show first 5
print(f" โข {f}")
if len(files) > 5:
print(f" ... and {len(files) - 5} more")
total += len(files)
print(f"
Total: {total} files organised.")
def main():
if len(sys.argv) < 2:
print("Usage: python organise.py <directory> [--dry-run]")
sys.exit(1)
target = Path(sys.argv[1]).resolve()
dry_run = "--dry-run" in sys.argv
try:
report = organise_directory(target, dry_run=dry_run)
print_report(report, target)
except ValueError as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()