Python ile Dosya ve Dizin İşlemleri: pathlib ve shutil Kullanımı

Sistem yöneticisi olarak zamanımızın büyük bir kısmını dosya ve dizin işlemlerine harcıyoruz. Log dosyalarını temizlemek, yedek almak, dizin yapılarını taşımak veya belirli kriterlere göre dosyaları bulmak… Bunların hepsini elle yapmak hem zaman alıcı hem de hataya açık. Python’un standart kütüphanesinde yer alan pathlib ve shutil modülleri, bu işlemleri güvenli, okunabilir ve tekrar kullanılabilir scriptlere dönüştürmemizi sağlıyor. Bu yazıda her iki modülü de gerçek dünya senaryolarıyla derinlemesine inceleyeceğiz.

pathlib: Modern Yol Yönetimi

Python 3.4 ile gelen pathlib, dosya yolu işlemlerini nesne yönelimli bir yaklaşımla ele alıyor. Eski os.path yöntemine göre çok daha okunabilir ve sezgisel. Temel fark şu: os.path string üzerinde çalışırken, pathlib Path nesneleri üzerinde çalışıyor.

Temel Path Nesneleri

from pathlib import Path

# Mevcut dizin
current_dir = Path.cwd()
print(current_dir)  # /home/adminuser/scripts

# Ev dizini
home_dir = Path.home()
print(home_dir)  # /home/adminuser

# Sabit bir yol tanımlamak
log_dir = Path("/var/log/nginx")
config_file = Path("/etc/nginx/nginx.conf")

# Yol birleştirme (/ operatörü ile)
backup_dir = home_dir / "backups" / "2024"
print(backup_dir)  # /home/adminuser/backups/2024

Burada dikkat çeken şey / operatörünün yol birleştirme için kullanılması. String concatenation ile uğraşmak yerine bu kadar temiz bir syntax kullanabilmek gerçekten büyük fark yaratıyor.

Path Nesnesinin Temel Özellikleri

Bir Path nesnesinden pek çok bilgiyi kolayca çekebilirsiniz:

from pathlib import Path

p = Path("/var/log/nginx/access.log.gz")

print(p.name)       # access.log.gz
print(p.stem)       # access.log
print(p.suffix)     # .gz
print(p.suffixes)   # ['.log', '.gz']
print(p.parent)     # /var/log/nginx
print(p.parents[0]) # /var/log/nginx
print(p.parents[1]) # /var/log
print(p.parts)      # ('/', 'var', 'log', 'nginx', 'access.log.gz')
print(p.root)       # /

Bu özellikler özellikle log rotation scriptleri yazarken veya dosya adı manipülasyonu gerektiğinde çok işe yarıyor.

Dizin ve Dosya Oluşturma

from pathlib import Path

# Tekli dizin oluşturma
new_dir = Path("/tmp/test_dir")
new_dir.mkdir(exist_ok=True)  # Zaten varsa hata verme

# İç içe dizinler oluşturma
nested_dir = Path("/tmp/app/logs/2024/january")
nested_dir.mkdir(parents=True, exist_ok=True)

# Dosya oluşturma
log_file = nested_dir / "app.log"
log_file.touch()  # Boş dosya oluştur

# İçerik yazma
config_file = Path("/tmp/app/config.ini")
config_file.write_text("[database]nhost=localhostnport=5432n")

# İçerik okuma
content = config_file.read_text()
print(content)

# Binary dosya işleme
binary_file = Path("/tmp/data.bin")
binary_file.write_bytes(b"x00x01x02x03")

Dosya Sisteminde Arama ve Listeleme

pathlib‘in en güçlü özelliklerinden biri glob ve rglob metodları:

from pathlib import Path

log_base = Path("/var/log")

# Belirli bir dizindeki .log dosyaları
for log_file in log_base.glob("*.log"):
    print(log_file)

# Alt dizinler dahil tüm .log dosyaları
for log_file in log_base.rglob("*.log"):
    print(log_file)

# Sadece dizinleri listele
for item in log_base.iterdir():
    if item.is_dir():
        print(f"Dizin: {item.name}")

# Belirli bir pattern ile arama
for config in Path("/etc").rglob("*.conf"):
    print(config)

# Boyutu 100MB'den büyük dosyaları bul
for large_file in Path("/var/log").rglob("*"):
    if large_file.is_file() and large_file.stat().st_size > 100 * 1024 * 1024:
        print(f"{large_file}: {large_file.stat().st_size / 1024 / 1024:.2f} MB")

Dosya Metadata’sına Erişim

from pathlib import Path
from datetime import datetime

p = Path("/var/log/syslog")

if p.exists():
    stats = p.stat()
    
    # Boyut
    size_mb = stats.st_size / 1024 / 1024
    print(f"Boyut: {size_mb:.2f} MB")
    
    # Son değiştirilme zamanı
    mtime = datetime.fromtimestamp(stats.st_mtime)
    print(f"Son değiştirilme: {mtime}")
    
    # İzinleri kontrol et
    print(f"Okunabilir: {p.is_file() and os.access(p, os.R_OK)}")
    
    # Symlink mi?
    print(f"Symlink: {p.is_symlink()}")

shutil: Yüksek Seviyeli Dosya Operasyonları

shutil (shell utilities) modülü, kopyalama, taşıma, silme ve arşivleme gibi yüksek seviyeli dosya operasyonları için tasarlanmış. pathlib yolları nereye gideceğimizi tarif ediyorsa, shutil bizi oraya götüren araçları sağlıyor.

Kopyalama Operasyonları

shutil içinde birden fazla kopyalama fonksiyonu var ve hangisini kullanacağınızı bilmek önemli:

  • shutil.copy(src, dst): Dosyayı ve izinleri kopyalar, metadata kopyalamaz
  • shutil.copy2(src, dst): Dosyayı, izinleri ve metadata’yı kopyalar
  • shutil.copyfile(src, dst): Sadece dosya içeriğini kopyalar
  • shutil.copytree(src, dst): Dizin ağacını tamamıyla kopyalar
import shutil
from pathlib import Path

# Tek dosya kopyalama
shutil.copy2(
    Path("/etc/nginx/nginx.conf"),
    Path("/backup/nginx.conf.bak")
)

# Dizin ağacını kopyalama
shutil.copytree(
    Path("/etc/nginx"),
    Path("/backup/nginx_config"),
    ignore=shutil.ignore_patterns("*.bak", "*.tmp", "__pycache__")
)

# Kopyalama sırasında dosya filtreleme
def ignore_large_files(directory, contents):
    """100MB'den büyük dosyaları kopyalama"""
    ignore_list = []
    for item in contents:
        full_path = Path(directory) / item
        if full_path.is_file() and full_path.stat().st_size > 100 * 1024 * 1024:
            print(f"Atlaniyor (cok buyuk): {full_path}")
            ignore_list.append(item)
    return ignore_list

shutil.copytree(
    Path("/data/source"),
    Path("/data/destination"),
    ignore=ignore_large_files
)

Taşıma ve Yeniden Adlandırma

import shutil
from pathlib import Path

# Dosya taşıma
shutil.move(
    str(Path("/tmp/processed/report.pdf")),
    str(Path("/archive/2024/reports/"))
)

# pathlib ile de taşıma mümkün (rename ile)
old_path = Path("/var/log/app.log.1")
new_path = Path("/archive/logs/app.log.2024-01-15")
old_path.rename(new_path)

# Farklı partition'lar arası taşıma için shutil.move kullanmak gerekir
# pathlib.rename sadece aynı filesystem içinde çalışır
shutil.move(
    str(Path("/mnt/ssd/data/large_file.tar.gz")),
    str(Path("/mnt/hdd/archive/large_file.tar.gz"))
)

Silme Operasyonları

import shutil
from pathlib import Path

# Tek dosya silme (pathlib ile)
file_to_delete = Path("/tmp/temp_file.txt")
if file_to_delete.exists():
    file_to_delete.unlink()

# Boş dizin silme
empty_dir = Path("/tmp/empty_directory")
if empty_dir.is_dir():
    empty_dir.rmdir()  # Sadece boş dizinler için

# Dizin ağacını silme (shutil ile)
full_dir = Path("/tmp/old_deployment")
if full_dir.is_dir():
    shutil.rmtree(full_dir)

# Güvenli silme: Hata olursa devam et
shutil.rmtree(
    Path("/tmp/maybe_exists"),
    ignore_errors=True
)

Arşivleme

shutil ile tar, gz, zip formatlarında arşiv oluşturmak oldukça kolay:

import shutil
from pathlib import Path
from datetime import datetime

# Tarihli arşiv oluşturma
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
archive_name = f"/backup/app_config_{timestamp}"

shutil.make_archive(
    base_name=archive_name,    # Uzantısız arşiv adı
    format="gztar",            # tar.gz formatı
    root_dir="/etc",           # Arşivin kök dizini
    base_dir="nginx"           # Arşivlenecek dizin
)
# Sonuç: /backup/app_config_20240115_143022.tar.gz

# Zip formatında arşiv
shutil.make_archive(
    base_name="/backup/web_files",
    format="zip",
    root_dir="/var/www",
    base_dir="html"
)

# Arşivi açma
shutil.unpack_archive(
    "/backup/app_config_20240115_143022.tar.gz",
    "/restore/nginx_config"
)

Gerçek Dünya Senaryoları

Teorik bilgi güzel ama sysadmin’ler pratik çözümler ister. Gelin birkaç gerçek senaryo üzerinden gidelim.

Senaryo 1: Otomatik Log Temizleme

Belirli bir yaşın üstündeki log dosyalarını arşivleyip silen bir script:

#!/usr/bin/env python3
"""
Log temizleme scripti
Kullanim: python3 clean_logs.py
30 gunluk log dosyalarini arsivler, 90 gunlukleri siler
"""

import shutil
import logging
from pathlib import Path
from datetime import datetime, timedelta

# Logging ayarla
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(levelname)s - %(message)s",
    handlers=[
        logging.FileHandler("/var/log/log_cleaner.log"),
        logging.StreamHandler()
    ]
)

LOG_DIRECTORIES = [
    Path("/var/log/nginx"),
    Path("/var/log/apache2"),
    Path("/var/log/app"),
]

ARCHIVE_DIR = Path("/archive/logs")
ARCHIVE_AGE_DAYS = 30
DELETE_AGE_DAYS = 90

def get_file_age_days(file_path: Path) -> float:
    """Dosyanın kaç günlük olduğunu döndür"""
    mtime = datetime.fromtimestamp(file_path.stat().st_mtime)
    return (datetime.now() - mtime).days

def archive_log(file_path: Path, archive_base: Path) -> bool:
    """Log dosyasını arşiv dizinine taşı"""
    try:
        # Orijinal yapıyı koru
        relative_path = file_path.relative_to("/var/log")
        dest = archive_base / relative_path
        dest.parent.mkdir(parents=True, exist_ok=True)
        shutil.move(str(file_path), str(dest))
        logging.info(f"Arsivlendi: {file_path} -> {dest}")
        return True
    except Exception as e:
        logging.error(f"Arsivleme hatasi ({file_path}): {e}")
        return False

def process_logs():
    ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
    
    archived_count = 0
    deleted_count = 0
    total_freed_bytes = 0
    
    for log_dir in LOG_DIRECTORIES:
        if not log_dir.exists():
            logging.warning(f"Dizin bulunamadi: {log_dir}")
            continue
        
        for log_file in log_dir.rglob("*.log*"):
            if not log_file.is_file():
                continue
            
            age_days = get_file_age_days(log_file)
            file_size = log_file.stat().st_size
            
            if age_days >= DELETE_AGE_DAYS:
                try:
                    log_file.unlink()
                    deleted_count += 1
                    total_freed_bytes += file_size
                    logging.info(f"Silindi ({age_days} gun): {log_file}")
                except Exception as e:
                    logging.error(f"Silme hatasi ({log_file}): {e}")
            
            elif age_days >= ARCHIVE_AGE_DAYS:
                if archive_log(log_file, ARCHIVE_DIR):
                    archived_count += 1
                    total_freed_bytes += file_size
    
    freed_mb = total_freed_bytes / 1024 / 1024
    logging.info(
        f"Tamamlandi: {archived_count} arsivlendi, "
        f"{deleted_count} silindi, "
        f"{freed_mb:.2f} MB kazanildi"
    )

if __name__ == "__main__":
    process_logs()

Senaryo 2: Deployment Yedekleme Sistemi

Yeni bir deployment öncesinde mevcut uygulamayı yedekleyen script:

#!/usr/bin/env python3
"""
Deployment oncesi yedekleme sistemi
Kullanim: python3 backup_before_deploy.py --app myapp --version 2.1.0
"""

import shutil
import argparse
import sys
from pathlib import Path
from datetime import datetime

APP_BASE = Path("/opt/apps")
BACKUP_BASE = Path("/opt/backups")
MAX_BACKUPS_PER_APP = 5  # Her uygulama için en fazla 5 yedek

def get_disk_usage(path: Path) -> dict:
    """Disk kullanim istatistiklerini al"""
    usage = shutil.disk_usage(path)
    return {
        "total_gb": usage.total / 1024**3,
        "used_gb": usage.used / 1024**3,
        "free_gb": usage.free / 1024**3,
        "percent": (usage.used / usage.total) * 100
    }

def cleanup_old_backups(app_backup_dir: Path, max_keep: int):
    """Eski yedekleri temizle, sadece son N tanesini tut"""
    backups = sorted(
        [d for d in app_backup_dir.iterdir() if d.is_dir()],
        key=lambda x: x.stat().st_mtime
    )
    
    while len(backups) >= max_keep:
        oldest = backups.pop(0)
        shutil.rmtree(oldest)
        print(f"Eski yedek silindi: {oldest.name}")

def backup_application(app_name: str, version: str) -> Path:
    app_dir = APP_BASE / app_name
    
    if not app_dir.exists():
        print(f"HATA: Uygulama dizini bulunamadi: {app_dir}")
        sys.exit(1)
    
    # Disk kontrolu
    disk = get_disk_usage(BACKUP_BASE if BACKUP_BASE.exists() else Path("/"))
    if disk["percent"] > 85:
        print(f"UYARI: Disk dolulugu %{disk['percent']:.1f} - devam etmek riskli!")
    
    # Yedek dizinini hazirla
    app_backup_dir = BACKUP_BASE / app_name
    app_backup_dir.mkdir(parents=True, exist_ok=True)
    
    # Eski yedekleri temizle
    cleanup_old_backups(app_backup_dir, MAX_BACKUPS_PER_APP)
    
    # Yedek adini olustur
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_name = f"{app_name}_v{version}_{timestamp}"
    backup_path = app_backup_dir / backup_name
    
    print(f"Yedekleniyor: {app_dir} -> {backup_path}")
    
    # Config dosyalarini ayri yedekle
    config_backup = backup_path / "config"
    for config_file in app_dir.rglob("*.conf"):
        relative = config_file.relative_to(app_dir)
        dest = config_backup / relative
        dest.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(config_file, dest)
    
    # Tam dizin yedeği
    shutil.copytree(
        app_dir,
        backup_path / "full",
        ignore=shutil.ignore_patterns(
            "*.pyc", "__pycache__", "*.log", "tmp", "cache"
        ),
        symlinks=True
    )
    
    # Yedek manifest dosyasi olustur
    manifest = backup_path / "MANIFEST.txt"
    manifest.write_text(
        f"App: {app_name}n"
        f"Version: {version}n"
        f"Timestamp: {datetime.now().isoformat()}n"
        f"Source: {app_dir}n"
    )
    
    print(f"Yedekleme tamamlandi: {backup_path}")
    
    # Yedek boyutunu goster
    total_size = sum(
        f.stat().st_size for f in backup_path.rglob("*") if f.is_file()
    )
    print(f"Yedek boyutu: {total_size / 1024 / 1024:.2f} MB")
    
    return backup_path

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Deployment oncesi yedekleme")
    parser.add_argument("--app", required=True, help="Uygulama adi")
    parser.add_argument("--version", required=True, help="Mevcut versiyon")
    args = parser.parse_args()
    
    backup_path = backup_application(args.app, args.version)
    print(f"Yedek hazir: {backup_path}")

Senaryo 3: Disk Kullanim Raporu

#!/usr/bin/env python3
"""
Dizin bazli disk kullanim raporu
"""

import shutil
from pathlib import Path
from collections import defaultdict

def format_size(size_bytes: int) -> str:
    """Boyutu okunabilir formata cevir"""
    for unit in ["B", "KB", "MB", "GB", "TB"]:
        if size_bytes < 1024:
            return f"{size_bytes:.2f} {unit}"
        size_bytes /= 1024
    return f"{size_bytes:.2f} PB"

def get_dir_size(path: Path) -> int:
    """Dizinin toplam boyutunu hesapla"""
    total = 0
    try:
        for item in path.rglob("*"):
            if item.is_file() and not item.is_symlink():
                try:
                    total += item.stat().st_size
                except PermissionError:
                    pass
    except PermissionError:
        pass
    return total

def disk_usage_report(target_dir: Path, depth: int = 2):
    """Belirtilen derinliğe kadar disk kullanim raporu"""
    
    print(f"nDisk Kullanim Raporu: {target_dir}")
    print("=" * 60)
    
    # Genel disk bilgisi
    disk = shutil.disk_usage(target_dir)
    print(f"Toplam: {format_size(disk.total)}")
    print(f"Kullanilan: {format_size(disk.used)} ({disk.used/disk.total*100:.1f}%)")
    print(f"Bos: {format_size(disk.free)}")
    print("-" * 60)
    
    # Uzantiya gore buyuk dosyalar
    extension_sizes = defaultdict(int)
    extension_counts = defaultdict(int)
    
    for file_path in target_dir.rglob("*"):
        if file_path.is_file() and not file_path.is_symlink():
            try:
                size = file_path.stat().st_size
                ext = file_path.suffix.lower() or "(uzantisiz)"
                extension_sizes[ext] += size
                extension_counts[ext] += 1
            except PermissionError:
                pass
    
    # En cok yer kaplayan uzantilar
    print("nUzantiya Gore Boyut (En Buyuk 10):")
    sorted_exts = sorted(extension_sizes.items(), key=lambda x: x[1], reverse=True)
    for ext, size in sorted_exts[:10]:
        count = extension_counts[ext]
        print(f"  {ext:<15} {format_size(size):<12} ({count} dosya)")

if __name__ == "__main__":
    import sys
    target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
    disk_usage_report(target)

Dikkat Edilmesi Gereken Noktalar

Günlük kullanımda karşılaştığım bazı önemli detayları paylaşmak istiyorum:

  • shutil.rmtree tehlikeli olabilir: Root yetkisiyle çalışıyorsanız her zaman dry_run modunda test edin. Yanlış bir path silme felaket olabilir.
  • pathlib.rename vs shutil.move: rename sadece aynı filesystem içinde çalışır. Farklı partition’lar veya mount noktaları arasında taşıma için shutil.move kullanın.
  • copytree ve symlink’ler: shutil.copytree varsayılan olarak symlink’leri takip eder. symlinks=True parametresi ile sembolik linkleri koruyabilirsiniz.
  • Büyük dosyaları kopyalarken: shutil.copy2 büyük dosyalar için iyi bir seçenek ancak progress takibi yapmak istiyorsanız manuel chunk okuma-yazma yaklaşımı daha iyi sonuç verir.
  • Windows uyumluluğu: pathlib cross-platform çalışır. Path("/etc/nginx") Linux’ta beklendiği gibi çalışırken Windows’ta WindowsPath nesnesi döner. Portable script yazıyorsanız bu farka dikkat edin.
  • Dosya izinleri: shutil.copy ve shutil.copy2 izinleri kopyalar ama sahipliği (ownership) kopyalamaz. Root olarak çalışıyor ve sahipliği korumak istiyorsanız shutil.copystat ile birlikte os.chown kullanmanız gerekir.

Sonuç

pathlib ve shutil ikilisi, Python ile dosya sistemi otomasyonu için gerçekten güçlü bir kombinasyon sunuyor. pathlib ile yolları nesne yönelimli ve okunabilir bir şekilde yönetirken, shutil ile karmaşık kopyalama, taşıma ve arşivleme işlemlerini birkaç satırla halledebiliyorsunuz.

Bu iki modülü öğrendikten sonra os.path ile string manipülasyonu yapan eski scriptleri okumak zorlaşıyor çünkü geri dönmek istemiyorsunuz. Özellikle log temizleme, yedekleme ve deployment scriptleri gibi günlük sysadmin görevlerinde bu araçlar ciddi zaman kazandırıyor.

Bir sonraki adım olarak bu scriptleri cron ile zamanlamayı veya argparse ile daha güçlü CLI araçlarına dönüştürmeyi düşünebilirsiniz. Ayrıca kritik silme ve taşıma işlemleri için her zaman önce --dry-run modunu implement etmenizi öneririm. Production ortamında “undo” yoktur.

Yorum yapın