diff --git a/bulk-file-organizer/.gitignore b/bulk-file-organizer/.gitignore new file mode 100644 index 0000000..a650ac9 --- /dev/null +++ b/bulk-file-organizer/.gitignore @@ -0,0 +1,45 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Virtual environment +venv/ +.env/ +.venv/ + +# Pytest cache +.pytest_cache/ + +# VS Code settings +.vscode/ + +# macOS metadata +.DS_Store + +# Coverage reports +htmlcov/ +.coverage +coverage.xml +*.cover + +# Build artifacts +build/ +dist/ +*.egg-info/ + +# Logs and temp files +*.log +*.tmp +*.bak + +# MyPy and Ruff +.mypy_cache/ +.ruff_cache/ + +# Jupyter Notebook checkpoints (if any) +.ipynb_checkpoints/ + +# Test output +test-output/ +test/ \ No newline at end of file diff --git a/bulk-file-organizer/README.md b/bulk-file-organizer/README.md new file mode 100644 index 0000000..3db3a37 --- /dev/null +++ b/bulk-file-organizer/README.md @@ -0,0 +1,124 @@ +# ๐Ÿ—‚๏ธ Bulk File Organizer โ€” Integration Module + +**Bulk File Organizer** is a Python command-line tool that automatically sorts files in a directory based on their extensions. +It transforms digital clutter into clean, structured folders โ€” with vibrant terminal feedback powered by **Rich** โœจ + +--- + +## ๐Ÿ“ฃ Latest Update: Integrated Module + +This release brings integration with the [Bulk File Organizer](https://github.com/Shravan250/bulk-file-organizer) โ€” a CLI utility by [Shravan Bobade](https://github.com/Shravan250) that organizes files by type, supports dry-run simulations, and provides colorful output using the `rich` library. + +### โœจ Highlights + +- ๐Ÿ“ฆ Automatically sorts files into categorized folders (Images, Documents, Archives, etc.) +- ๐Ÿ” Recursive directory scanning +- ๐Ÿงช Dry-run simulation mode (no files moved) +- ๐ŸŒˆ Beautiful progress bars & summary tables +- โš™๏ธ Extensible via custom mappings + +--- + +## โš™๏ธ Installation + +```bash +pip install bulk-file-organizer +``` + +Or install directly from source: + +```bash +git clone https://github.com/Shravan250/bulk-file-organizer.git +cd bulk-file-organizer +pip install . +``` + +--- + +## ๐Ÿ–ฅ๏ธ Usage + +```bash +bulk-organizer /path/to/your/folder +``` + +### Options + +| Flag | Description | +| ------------- | ------------------------------------------ | +| `--dry-run` | Simulate organization without moving files | +| `--summary` | Display a summary table after organization | +| `--recursive` | Recursively organize subdirectories | +| `--version` | Display the current version | + +#### Example + +```bash +bulk-organizer ~/Downloads --dry-run --summary +``` + +--- + +## ๐Ÿง  Example Output + +```text +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Bulk Organizer v0.1.0 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +Target Directory: ~/Downloads + +Dry Run Mode Enabled โ€” no files will be moved. + +โœจ Organizing files... โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ” 100% 0:00:02 + +๐Ÿ“Š Organization Summary + +โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ณโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”“ +โ”ƒ Folder โ”ƒ File Count โ”ƒ Example Files โ”ƒ +โ”กโ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ•‡โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”ฉ +โ”‚ Images โ”‚ 12 โ”‚ photo.png, logo.jpg, โ€ฆ โ”‚ +โ”‚ Documents โ”‚ 8 โ”‚ resume.pdf, notes.txt, โ€ฆ โ”‚ +โ”‚ Archives โ”‚ 2 โ”‚ backup.zip, logs.tar.gz โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Operation Complete โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +``` + +--- + +## ๐Ÿงฉ Project Structure + +``` +bulk-file-organizer/ +โ”‚ +โ”œโ”€โ”€ bulk_organizer/ +โ”‚ โ”œโ”€โ”€ __init__.py +โ”‚ โ”œโ”€โ”€ cli.py +โ”‚ โ”œโ”€โ”€ scanner.py +โ”‚ โ”œโ”€โ”€ mapper.py +โ”‚ โ”œโ”€โ”€ organizer.py +โ”‚ โ””โ”€โ”€ utils.py +โ”‚ +โ”œโ”€โ”€ tests/ +โ”œโ”€โ”€ pyproject.toml +โ”œโ”€โ”€ README.md +โ”œโ”€โ”€ CONTRIBUTING.md +โ””โ”€โ”€ LICENSE +``` + +--- + +## ๐Ÿค Contributing + +We love clean code and clever minds. +Check out the [CONTRIBUTING.md](https://github.com/Shravan250/bulk-file-organizer/blob/main/CONTRIBUTING.md) to learn how you can contribute and make this project even better. + +--- + +## ๐Ÿชช License + +Licensed under the **MIT License**. + +--- + +## โญ Show Some Love + +If this project helped you bring order to your chaos โ€” +please consider giving it a โญ on [GitHub](https://github.com/Shravan250/bulk-file-organizer)! +Every star helps this project grow and keeps the world just a little more organized ๐ŸŒโœจ diff --git a/bulk-file-organizer/bulk_organizer/__init__.py b/bulk-file-organizer/bulk_organizer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO new file mode 100644 index 0000000..7f12eec --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO @@ -0,0 +1,11 @@ +Metadata-Version: 2.4 +Name: bulk-file-organizer +Version: 0.1.0 +Summary: A CLI tool to organize files into subfolders based on file extensions. +Author: Shravan +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +Provides-Extra: dev +Requires-Dist: pytest; extra == "dev" +Requires-Dist: ruff; extra == "dev" +Requires-Dist: black; extra == "dev" diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt new file mode 100644 index 0000000..f482473 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +README.md +pyproject.toml +bulk_organizer/bulk_file_organizer.egg-info/PKG-INFO +bulk_organizer/bulk_file_organizer.egg-info/SOURCES.txt +bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt +bulk_organizer/bulk_file_organizer.egg-info/requires.txt +bulk_organizer/bulk_file_organizer.egg-info/top_level.txt +tests/test_mapper.py \ No newline at end of file diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/requires.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/requires.txt new file mode 100644 index 0000000..5e0e4a0 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/requires.txt @@ -0,0 +1,5 @@ + +[dev] +pytest +ruff +black diff --git a/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/top_level.txt b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/top_level.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/bulk_file_organizer.egg-info/top_level.txt @@ -0,0 +1 @@ + diff --git a/bulk-file-organizer/bulk_organizer/cli.py b/bulk-file-organizer/bulk_organizer/cli.py new file mode 100644 index 0000000..afb250f --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/cli.py @@ -0,0 +1,85 @@ +import argparse +import tomllib +from pathlib import Path +from rich.console import Console +from rich.table import Table +from rich.progress import track +from bulk_organizer.scanner import scanner +from bulk_organizer.mapper import map_extension_to_folder, DEFAULT_MAP +from bulk_organizer.organizer import organize_file + +console = Console() + +def get_version(): + toml_path = Path(__file__).resolve().parent.parent / "pyproject.toml" + with open(toml_path, "rb") as f: + pyproject = tomllib.load(f) + return pyproject["project"]["version"] + + +def main(): + parser = argparse.ArgumentParser() + + organized = {} + CATEGORIZED_FOLDERS = set(DEFAULT_MAP.keys()) | {"Others"} + + parser.add_argument("--version", action="version", version=f"bulk-organizer {get_version()}") + parser.add_argument("directory", type=str, help="Path to the directory to organize") + parser.add_argument("--dry-run", action="store_true", help="Simulate the organization without making changes") + parser.add_argument("--recursive", action="store_true", default=True, help="Recursively scan subdirectories") + parser.add_argument("--summary", action="store_true",default=True, help="Show compact summary instead of full paths") + + + args = parser.parse_args() + directory_path = Path(args.directory) + console.rule(f"[bold cyan]Bulk Organizer v{get_version()}[/bold cyan]") + console.print(f"Target Directory: [bold yellow]{directory_path}[/bold yellow]\n") + + + if args.dry_run: + console.print("[bold magenta]Dry Run Mode Enabled[/bold magenta] โ€” no files will be moved.\n") + + # Scan and print file paths + try: + files = list(scanner(directory_path, recursive=args.recursive)) + first = True + for file_path in track(files, description="โœจ Organizing files..."): + if first: + console.print() + console.print() + first = False + + folder = map_extension_to_folder(file_path) + + # Skip already-organized files + if file_path.parent.name in CATEGORIZED_FOLDERS: + continue + + # Always track for summary + organized.setdefault(folder, []).append(file_path) + + # Perform move or simulate + organize_file(file_path, directory_path, folder, dry_run=args.dry_run) + + console.print() + + if args.summary: + console.print("\n๐Ÿ“Š [bold underline]Organization Summary[/bold underline]\n") + table = Table(show_header=True, header_style="bold blue") + table.add_column("Folder", style="cyan") + table.add_column("File Count", style="green") + table.add_column("Example Files", style="yellow") + + for folder, files in organized.items(): + examples = ", ".join(f.name for f in files[:3]) + table.add_row(folder, str(len(files)), examples + (" ..." if len(files) > 3 else "")) + + console.print(table) + + console.rule("[bold green]Operation Complete[/bold green]") + + except ValueError as e: + console.print(f"[bold red]Error:[/bold red] {e}") + +if __name__ == "__main__": + main() diff --git a/bulk-file-organizer/bulk_organizer/mapper.py b/bulk-file-organizer/bulk_organizer/mapper.py new file mode 100644 index 0000000..91e15a8 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/mapper.py @@ -0,0 +1,27 @@ +from pathlib import Path +from bulk_organizer.utils import normalize_extension + + +DEFAULT_MAP = { + "Images": [".jpg", ".jpeg", ".png", ".gif"], + "Documents": [".pdf", ".docx", ".txt"], + "Archives": [".zip", ".tar", ".gz"], + "Audio": [".mp3", ".wav"], +} + +def map_extension_to_folder(file_path: Path, mapping: dict = DEFAULT_MAP) -> str: + """Map a file extension to its corresponding folder name. + + Args: + file_path (Path): The file path to map. + mapping (dict): A dictionary mapping folder names to lists of extensions. + + Returns: + str: The folder name corresponding to the file's extension, or "Others" if not found. + """ + extension = normalize_extension(file_path.suffix) + + for folder , extensions in mapping.items(): + if extension in extensions: + return folder + return "Others" \ No newline at end of file diff --git a/bulk-file-organizer/bulk_organizer/organizer.py b/bulk-file-organizer/bulk_organizer/organizer.py new file mode 100644 index 0000000..1f413c0 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/organizer.py @@ -0,0 +1,38 @@ +import shutil +from pathlib import Path +from rich.console import Console + +console = Console() + +def organize_file(file_path: Path, base_dir: Path, folder_name: str, dry_run: bool = False): + """Move a file into its categorized folder, handling collisions and dry-run mode. + + Args: + file_path (Path): The original file path. + base_dir (Path): The base directory where folders are created. + folder_name (str): The target folder name. + dry_run (bool): If True, simulate the move without performing it. + """ + target_dir = base_dir / folder_name + target_dir.mkdir(parents=True, exist_ok=True) + target_path = target_dir / file_path.name + + # handle name collisions + counter = 1 + while target_path.exists(): + target_path = target_dir / f"{file_path.stem} {counter}{file_path.suffix}" + counter += 1 + + if dry_run: + console.print( + f"[dim yellow][Dry Run][/dim yellow] {file_path.relative_to(base_dir)} โ†’ " + f"[bold cyan]{target_path.relative_to(base_dir)}[/bold cyan]" + ) + else: + shutil.move(str(file_path), str(target_path)) + console.print( + f"[green]โœ” Moved[/green] [bold white]{file_path.relative_to(base_dir)}[/bold white] โ†’ " + f"[bold cyan]{target_path.relative_to(base_dir)}[/bold cyan]" + ) + + diff --git a/bulk-file-organizer/bulk_organizer/scanner.py b/bulk-file-organizer/bulk_organizer/scanner.py new file mode 100644 index 0000000..02c38c9 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/scanner.py @@ -0,0 +1,25 @@ +from pathlib import Path +from typing import Iterator + +def scanner(directory: Path, recursive: bool = True) -> Iterator[Path]: + """Scan the given directory and yield file paths. + + Args: + directory (Path): The directory to scan. + recursive (bool): Whether to scan subdirectories recursively. + + Yields: + Iterator[Path]: An iterator of file paths. + """ + + if not directory.exists() or not directory.is_dir(): + raise ValueError(f"{directory} is not a valid directory") + + if recursive: + yield from (p for p in directory.rglob("*") if p.is_file()) + else: + yield from (p for p in directory.iterdir() if p.is_file()) + + + + diff --git a/bulk-file-organizer/bulk_organizer/utils.py b/bulk-file-organizer/bulk_organizer/utils.py new file mode 100644 index 0000000..1a1cb73 --- /dev/null +++ b/bulk-file-organizer/bulk_organizer/utils.py @@ -0,0 +1,2 @@ +def normalize_extension(ext: str) -> str: + return ext.lower().strip() diff --git a/bulk-file-organizer/pyproject.toml b/bulk-file-organizer/pyproject.toml new file mode 100644 index 0000000..9eebad3 --- /dev/null +++ b/bulk-file-organizer/pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "bulk-file-organizer" +version = "0.1.0" +description = "A CLI tool to organize files into subfolders based on file extensions." +authors = [{ name = "Shravan" }] +readme = "README.md" +requires-python = ">=3.10" +dependencies = [] + +[project.optional-dependencies] +dev = [ + "pytest", + "ruff", # optional linter + "black", # formatter +] + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["bulk_organizer"] diff --git a/bulk-file-organizer/tests/test_mapper.py b/bulk-file-organizer/tests/test_mapper.py new file mode 100644 index 0000000..e17b908 --- /dev/null +++ b/bulk-file-organizer/tests/test_mapper.py @@ -0,0 +1,50 @@ +import pytest +from pathlib import Path +from bulk_organizer.scanner import scanner +from bulk_organizer.mapper import map_extension_to_folder, DEFAULT_MAP +from bulk_organizer.organizer import organize_file + +def test_scanner_non_recursive(tmp_path): + # Setup: create files and folders + (tmp_path / "file1.txt").write_text("hello") + (tmp_path / "subdir").mkdir() + (tmp_path / "subdir" / "file2.txt").write_text("world") + + # Act + files = list(scanner(tmp_path, recursive=False)) + + # Assert + assert len(files) == 1 + assert files[0].name == "file1.txt" + +def test_scanner_recursive(tmp_path): + (tmp_path / "file1.txt").write_text("hello") + (tmp_path / "subdir").mkdir() + (tmp_path / "subdir" / "file2.txt").write_text("world") + + files = list(scanner(tmp_path, recursive=True)) + + assert len(files) == 2 + assert sorted([f.name for f in files]) == ["file1.txt", "file2.txt"] + + +def test_known_extension(): + assert map_extension_to_folder(Path("photo.JPG"), DEFAULT_MAP) == "Images" + assert map_extension_to_folder(Path("song.wav"), DEFAULT_MAP) == "Audio" + +def test_unknown_extension(): + assert map_extension_to_folder(Path("weird.xyz"), DEFAULT_MAP) == "Others" + + +def test_organize_file_dry_run(tmp_path, capsys): + file = tmp_path / "test.txt" + file.write_text("hello") + + organize_file(file, tmp_path, "Documents", dry_run=True) + + # File should still exist in original location + assert file.exists() + + # Check printed output + captured = capsys.readouterr() + assert "[Dry Run]" in captured.out \ No newline at end of file