Downloads folder সাজাতে Python file organiser বানান

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

Images, Documents ও Code folder-এ ফাইল সাজানোর একটি terminal

প্রায় সবার 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 বানান (পরীক্ষিত).

আরও পড়ুন

Python দিয়ে এলোমেলো CSV পরিষ্কার: নাম, তারিখ, ফোন ও ডুপ্লিকেট

আসল data এলোমেলো। standard library দিয়ে contacts পরিষ্কার করা, প্রতিটি বাতিল row ব্যাখ্যা করা ও test-সহ একটি script বানান।

প্রজেক্ট•৫ মিনিটে পড়া