Skip to content

Commit 2b3077f

Browse files
Create FileOrganizer.py
1 parent dea3af9 commit 2b3077f

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

FileOrganizer.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""
2+
Simple file organizer.
3+
- Scans a folder
4+
- Moves files into subfolders by extension (e.g., .jpg -> Images)
5+
- Skips conflicts by auto-renaming
6+
- Creates folders if missing
7+
"""
8+
9+
from pathlib import Path
10+
import shutil
11+
12+
# Map extensions to target folder names
13+
EXT_MAP = {
14+
# Images
15+
".jpg": "Images", ".jpeg": "Images", ".png": "Images", ".gif": "Images", ".webp": "Images", ".svg": "Images",
16+
# Videos
17+
".mp4": "Videos", ".mkv": "Videos", ".mov": "Videos", ".avi": "Videos",
18+
# Audio
19+
".mp3": "Audio", ".wav": "Audio", ".m4a": "Audio", ".flac": "Audio",
20+
# Documents
21+
".pdf": "Documents", ".doc": "Documents", ".docx": "Documents",
22+
".xls": "Documents", ".xlsx": "Documents", ".ppt": "Documents", ".pptx": "Documents",
23+
".txt": "Documents", ".md": "Documents", ".rtf": "Documents",
24+
# Code
25+
".py": "Code", ".js": "Code", ".ts": "Code", ".html": "Code", ".css": "Code", ".json": "Code", ".yml": "Code", ".yaml": "Code",
26+
# Archives
27+
".zip": "Archives", ".rar": "Archives", ".7z": "Archives", ".tar": "Archives", ".gz": "Archives",
28+
# Installers
29+
".exe": "Installers", ".msi": "Installers", ".dmg": "Installers", ".apk": "Installers"
30+
}
31+
32+
33+
def safe_move(src: Path, dest_dir: Path) -> Path:
34+
"""
35+
Move src into dest_dir, avoiding name collisions by appending a counter.
36+
Returns the final destination path.
37+
"""
38+
dest_dir.mkdir(parents=True, exist_ok=True) # ensure target exists
39+
dest = dest_dir / src.name # candidate destination path
40+
41+
if not dest.exists(): # if no conflict, move directly
42+
return src.rename(dest)
43+
44+
# split name into base and extension
45+
stem, suffix = dest.stem, dest.suffix
46+
counter = 1 # start counter for duplicates
47+
while True: # loop until a free name is found
48+
candidate = dest_dir / f"{stem} ({counter}){suffix}"
49+
if not candidate.exists(): # if candidate is free, move and break
50+
return src.rename(candidate)
51+
counter += 1
52+
53+
54+
def organize_folder(root: str) -> None:
55+
"""
56+
Organize files in 'root' directory by extension using EXT_MAP.
57+
Files with unknown extensions go to 'Others'.
58+
Folders are left in place.
59+
"""
60+
base = Path(root).expanduser().resolve(
61+
) # normalize user path (handles ~)
62+
if not base.is_dir(): # ensure the path is a directory
63+
raise NotADirectoryError(f"Not a directory: {base}")
64+
65+
for item in base.iterdir(): # iterate over items in the folder
66+
if item.is_dir(): # skip subfolders
67+
continue
68+
69+
ext = item.suffix.lower() # get file extension in lowercase
70+
# pick target folder or Others
71+
target_name = EXT_MAP.get(ext, "Others")
72+
target_dir = base / target_name # compute full target folder path
73+
74+
try:
75+
# move safely with collision handling
76+
safe_move(item, target_dir)
77+
except PermissionError:
78+
# handle locked files gracefully
79+
print(f"Skipped (locked): {item}")
80+
except OSError:
81+
# handle cross-device move by copying then deleting if needed
82+
temp_dest = target_dir / item.name
83+
target_dir.mkdir(parents=True, exist_ok=True)
84+
shutil.copy2(item, temp_dest)
85+
item.unlink(missing_ok=True)
86+
87+
88+
if __name__ == "__main__":
89+
# Change this to the folder you want to organize, e.g., r"D:\Downloads"
90+
# uses a relative path by default
91+
organize_folder(r"./Downloads")

0 commit comments

Comments
 (0)