Downloads folder সাজাতে Python file organiser বানান
pathlib, shutil ও dry-run দিয়ে বাস্তব automation প্রজেক্ট, যাতে কিছু সরানোর আগে দেখে নিতে পারেন। প্রকাশের আগে পরীক্ষা করা হয়েছে।

প্রায় সবার Downloads folder এলোমেলো। একটি ছোট script সেটা সাজিয়ে দিতে পারে, আর এটি বানাতে গিয়ে শেখা যায় pathlib module, shutil দিয়ে ফাইল সরানো, dry-run দিয়ে নিরাপদ নকশা, এবং ফাইল সিস্টেম ছোঁয় এমন কোড test করা। শুধু standard library লাগে। কোড ও আউটপুট প্রকাশের আগে Python 3-এ চালানো হয়েছে।
আমরা কী বানাচ্ছি
একটি folder নিয়ে প্রতিটি ফাইলকে তার ধরনের নামের sub-folder-এ সরানোর কমান্ড:
Images(jpg, png, gif, webp),Documents(pdf, docx, txt, md),Archives(zip, tar, gz),Code(py, js, html, css, json)- বাকি সব
Other-এ যায় --dry-runকিছু না সরিয়ে কী হতো তা দেখায়
কোড
organise.py নামে সংরক্ষণ করুন:
#!/usr/bin/env python3
"""Sort the files in a folder into sub-folders by type. Use --dry-run to preview."""
import argparse
import shutil
from pathlib import Path
CATEGORIES = {
"Images": {".jpg", ".jpeg", ".png", ".gif", ".webp"},
"Documents": {".pdf", ".doc", ".docx", ".txt", ".md"},
"Archives": {".zip", ".tar", ".gz", ".rar"},
"Code": {".py", ".js", ".html", ".css", ".json"},
}
def category_for(name):
suffix = Path(name).suffix.lower()
for category, extensions in CATEGORIES.items():
if suffix in extensions:
return category
return "Other"
def unique_target(folder, name):
target = folder / name
counter = 1
while target.exists():
target = folder / f"{Path(name).stem}_{counter}{Path(name).suffix}"
counter += 1
return target
def organise(root, dry_run=False):
moved = []
for item in sorted(root.iterdir()):
if not item.is_file() or item.name.startswith("."):
continue
folder = root / category_for(item.name)
target = unique_target(folder, item.name)
if not dry_run:
folder.mkdir(exist_ok=True)
shutil.move(str(item), str(target))
moved.append((item.name, target.relative_to(root)))
return moved
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("folder", type=Path)
parser.add_argument("--dry-run", action="store_true", help="show what would move")
args = parser.parse_args(argv)
if not args.folder.is_dir():
parser.error(f"{args.folder} is not a folder")
for name, target in organise(args.folder, args.dry_run):
print(f"{name} -> {target}")
if __name__ == "__main__":
main()
চালিয়ে দেখুন
$ ls
LICENSE backup.zip holiday.JPG notes.txt report.pdf script.py
$ python3 organise.py . --dry-run
LICENSE -> Other/LICENSE
backup.zip -> Archives/backup.zip
holiday.JPG -> Images/holiday.JPG
notes.txt -> Documents/notes.txt
report.pdf -> Documents/report.pdf
script.py -> Code/script.py
$ python3 organise.py .
(same lines, and the files are really moved)
$ python3 organise.py nope
usage: organise.py [-h] [--dry-run] folder
organise.py: error: nope is not a folder
আসল folder-এ আগে সবসময় --dry-run চালান। আমার পরীক্ষায় লুকানো ফাইল .hidden ও বিদ্যমান sub-folder ছোঁয়া হয়নি।
এটি কীভাবে কাজ করে
Category-র dictionary। CATEGORIES folder-এর নামকে extension-এর set-এর সঙ্গে মেলায়। নতুন ধরন যোগ করতে এক লাইন লাগে, যুক্তি বদলাতে হয় না।
category_for suffix ছোট হাতের করে, তাই holiday.JPG-ও ছবি গণ্য হয়। এটি pure function, তাই test করা সহজ।
pathlib.Path path-কে object হিসেবে দেখে: root / "Images" path জোড়ে, .suffix extension দেয়, .stem extension ছাড়া নাম, আর .is_file() folder বাদ দেয়। string জোড়ার চেয়ে পরিষ্কার।
unique_target আপনার data বাঁচায়। Images/a.png থাকলে নতুন ফাইল হয় a_1.png, তারপর a_2.png, এভাবে চলে। এটি না থাকলে কিছু সিস্টেমে shutil.move নীরবে ফাইল বদলে দিতে পারে।
Dry run। একই function সরানোর তালিকা বানায়, কিন্তু dry_run সত্য হলে mkdir ও move বাদ দেয়। preview ও আসল চালানো একই কোডপথে, তাই preview নির্ভরযোগ্য।
বাদ দেওয়া জিনিস। sub-folder ও ডট দিয়ে শুরু ফাইল এড়াই, তাই লুকানো settings ফাইল বা আগে বানানো folder ছুঁই না।
Test যোগ করুন
Test-এ tempfile.TemporaryDirectory ব্যবহার করি, যা পরে মুছে যাওয়া সাময়িক folder, তাই আপনার আসল ফাইল ঝুঁকিতে পড়ে না। test_organise.py নামে সংরক্ষণ করুন:
import tempfile
import unittest
from pathlib import Path
import organise
class OrganiseTest(unittest.TestCase):
def test_category_for(self):
self.assertEqual(organise.category_for("Photo.JPG"), "Images")
self.assertEqual(organise.category_for("notes.txt"), "Documents")
self.assertEqual(organise.category_for("README"), "Other")
def test_moves_files_and_avoids_overwrite(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "a.png").write_text("new")
(root / "Images").mkdir()
(root / "Images" / "a.png").write_text("old")
organise.organise(root)
self.assertEqual((root / "Images" / "a.png").read_text(), "old")
self.assertEqual((root / "Images" / "a_1.png").read_text(), "new")
def test_dry_run_changes_nothing(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "x.zip").write_text("z")
organise.organise(root, dry_run=True)
self.assertTrue((root / "x.zip").exists())
self.assertFalse((root / "Archives").exists())
if __name__ == "__main__":
unittest.main()
$ python3 -m unittest -v
test_category_for ... ok
test_dry_run_changes_nothing ... ok
test_moves_files_and_avoids_overwrite ... ok
Ran 3 tests in 0.005s
OK
দ্বিতীয় test-টি জরুরি: এটি প্রমাণ করে বিদ্যমান ফাইল overwrite হয় না।
জানা সীমা
- এটি recursive নয়: sub-folder-এর ভেতরের ফাইল সাজানো হয় না।
- Undo নেই। ছাপা তালিকাই আপনার রেকর্ড; চাইলে log ফাইলে রাখতে পারেন।
- একই চালানোয় একই নামের দুটি ফাইল একটার পর একটা সরানো হয় বলেই আলাদা নাম পায়। Dry run এই ক্ষেত্র আগে বলতে পারে না।
- Extension আর বিষয়বস্তু এক নয়।
.txtনাম দেওয়া যেকোনো ফাইল document গণ্য হয়। - খুব বড় folder-এ অনুমতি এবং অন্য প্রোগ্রামে খোলা ফাইলের কথা ভাবুন।
চ্যালেঞ্জ
১. ফাইলের পরিবর্তনের সময় (item.stat().st_mtime) দিয়ে 2026/10 folder-এ সাজাতে --by-date যোগ করুন।
২. প্রতিটি সরানো log ফাইলে লিখুন যাতে --undo বানাতে পারেন।
৩. JSON config ফাইল থেকে category পড়ুন।
৪. --recursive option যোগ করুন।
৫. cron বা Windows Task Scheduler দিয়ে নির্ধারিত সময়ে চালান।
মূল কথা: automation script ডিফল্টভাবে নিরাপদ হওয়া উচিত। dry run, overwrite না করা ও কয়েকটি test একটি ঝুঁকিপূর্ণ script-কে নিজের ফাইলে ভরসা করার মতো বানায়।
হাতে-কলমে প্রজেক্ট সিরিজ, পর্ব ১ (ছোট পরীক্ষিত প্রজেক্ট): 3 নম্বর (মোট 5)। আগের লেখা: Framework ছাড়া Node.js-এ REST API বানান (পরীক্ষিত).


