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_runmodunda test edin. Yanlış bir path silme felaket olabilir.
- pathlib.rename vs shutil.move:
renamesadece aynı filesystem içinde çalışır. Farklı partition’lar veya mount noktaları arasında taşıma içinshutil.movekullanın.
- copytree ve symlink’ler:
shutil.copytreevarsayılan olarak symlink’leri takip eder.symlinks=Trueparametresi ile sembolik linkleri koruyabilirsiniz.
- Büyük dosyaları kopyalarken:
shutil.copy2bü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:
pathlibcross-platform çalışır.Path("/etc/nginx")Linux’ta beklendiği gibi çalışırken Windows’taWindowsPathnesnesi döner. Portable script yazıyorsanız bu farka dikkat edin.
- Dosya izinleri:
shutil.copyveshutil.copy2izinleri kopyalar ama sahipliği (ownership) kopyalamaz. Root olarak çalışıyor ve sahipliği korumak istiyorsanızshutil.copystatile birlikteos.chownkullanmanı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.