IlmHamroh
Python kursi/Standart kutubxona13/16-dars22 daqiqa
Mundarija (21)

15.13-dars: shutil va fayl amallari

15-QISM — STANDART KUTUBXONA · 13-dars


1. Kirish va motivatsiya

pathlib yo'llar va bitta fayl bilan ishlashni yaxshi qiladi. Lekin real vazifalar yuqori darajadagi amallarni talab qiladi: butun katalogni ma'lum fayllarsiz nusxalash, daraxtni o'chirish, arxiv yaratish va ochish, diskda joy borligini tekshirish, tizimda dastur o'rnatilganini aniqlash. Bular — shutil (shell utilities) moduli.

Real vaziyat. Zaxira skripti har kecha loyihani arxivlab, eski arxivlarni o'chirardi:

python
shutil.rmtree(os.path.join(ZAXIRA_KATALOGI, eski_nom))

Bir kuni eski_nom bo'sh satr bo'lib qoldi. os.path.join(ZAXIRA_KATALOGI, "") — bu butun zaxira katalogi. Barcha zaxiralar o'chdi. Keyingi haftada disk to'lib, yangi zaxira yarim yozilgan holda qoldi — skript diskda joy borligini tekshirmagan edi. Boshqa jamoada esa foydalanuvchi yuklagan arxivni ochish serverdagi sozlama faylini ustidan yozib yubordi.

Bu darsda shutil ni to'liq o'rganamiz — va har bir "kuchli" amalni xavfsiz qilish usullarini.

Bu darsda:

  • copyfile, copy, copy2 — nima nusxalanadi, nima yo'q
  • copytree: ignore, dirs_exist_ok
  • move — qachon nomlash, qachon nusxa + o'chirish
  • rmtree: faqat o'qiladigan fayllar, onexc 3.12-bob, himoya tekshiruvlari
  • Arxivlar: make_archive, unpack_archive, root_dir va base_dir
  • Arxiv xavfsizligi: zip slip va tar filtrlari (3.14 sukuti)
  • disk_usage, which, get_terminal_size
  • Amaliy: aylanma zaxira nusxa vositasi

2. Nazariya — chuqur tushuntirish

2.1. Fayl nusxalash: uch daraja

Funksiya Mazmun Ruxsatlar (mode) Vaqtlar va metama'lumot dst katalog bo'lsa
copyfile(src, dst) Xato
copy(src, dst) Ichiga, shu nom bilan
copy2(src, dst) (mtime, atime, imkon qadar kengaytirilgan atributlar)
Path.copy(dst) (3.14) (sukut) Fayl yo'li kerak

copyfile ga katalog berilsa: Linux'da IsADirectoryError, Windows'da PermissionError.

Zaxira va sinxronlash uchun — copy2: aks holda "fayl o'zgarganmi?" tekshiruvi (vaqt bo'yicha) buziladi.

Boshqa funksiyalar: copymode (faqat ruxsatlar), copystat (ruxsatlar va vaqtlar), copyfileobj(fsrc, fdst, length) — ochiq fayl obyektlari orasida bo'laklab.

2.2. copytree

python
shutil.copytree(
    "loyiha", "zaxira/loyiha",
    ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".git"),
    dirs_exist_ok=True,        # maqsad mavjud bo'lsa ham
    copy_function=shutil.copy2,  # sukut
)
Parametr Ma'nosi
ignore (katalog, nomlar) -> tashlanadigan_nomlar funksiyasi; ignore_patterns(...) tayyor
dirs_exist_ok=False Maqsad mavjud bo'lsa FileExistsError
symlinks=False Havolalar o'rniga ular ko'rsatgan fayllar nusxalanadi
copy_function Har fayl uchun: copy2 (sukut), copy, o'z funksiyangiz

2.3. move

Holat shutil.move(src, dst)
dst mavjud katalog src uning ichiga ko'chiriladi
Bir fayl tizimida os.rename — tez, atomar
Turli disklar / bo'limlar Nusxa + o'chirish — sekin, atomar emas
dst mavjud fayl Ustiga yozadi (platformaga bog'liq xulqlar bor)

Natija sifatida yangi yo'l qaytariladi.

2.4. rmtree

shutil.rmtree(yol) — katalogni butun ichidagisi bilan o'chiradi. Qaytarib bo'lmaydi.

Holat Xulq
Faqat o'qiladigan fayl (Windows, .git obyektlari) PermissionError
onexc=funksiya (3.12+) Xatoda chaqiriladi: (funksiya, yol, istisno) — ruxsatni o'zgartirib qayta urinish
onerror Eski nom, 3.12 dan eskirgan
ignore_errors=True Xatolarni jim yutadi — yarim o'chirilgan katalog qolishi mumkin

Himoya tekshiruvlari (rmtree oldidan):

  1. Yo'l bo'sh emas va resolve() qilingan
  2. Ildiz (/, C:\), uy katalogi yoki loyiha ildizi emas
  3. Ruxsat berilgan ota katalog ichida (is_relative_to)
  4. Kutilgan nom shakliga mos (masalan, zaxira-2026-...)

2.5. Arxivlar

python
shutil.make_archive(base_name, format, root_dir=None, base_dir=None)
shutil.unpack_archive(fayl, maqsad, format=None, *, filter=None)
Format Kengaytma
zip .zip
tar, gztar, bztar, xztar .tar, .tar.gz, .tar.bz2, .tar.xz
zstdtar .tar.zst — 3.14+
Parametr Ma'nosi
base_name Kengaytmasiz nom — format kengaytmasi qo'shiladi
root_dir Arxiv ichidagi yo'llar shu katalogga nisbatan
base_dir root_dir ichidan qaysi qism arxivlanadi — arxivda shu nom bilan saqlanadi
python
make_archive("zaxira", "zip", root_dir="loyiha")                  # ichida: src/..., README.md
make_archive("zaxira", "gztar", root_dir=".", base_dir="loyiha")  # ichida: loyiha/src/...

2.6. Arxiv xavfsizligi

Arxiv ichidagi nom ../../etc/cron.d/x yoki absolyut yo'l bo'lishi mumkin (zip slip). Ochganda fayl maqsad katalogdan tashqariga yoziladi.

Vosita Xulq
shutil.unpack_archive + zip .. yoki / bilan boshlanadigan nomlarni jim o'tkazib yuboradi
tarfile / unpack_archive + tar (3.14) Sukut filtri "data": tashqariga yo'l, xavfli ruxsatlar, qurilma fayllari — OutsideDestinationError va boshqa xatolar
filter="fully_trusted" Filtrsiz — faqat o'zingiz yaratgan arxivlar
Hajm portlashi (zip bomb) Hech bir vosita cheklamaydi — ochishdan oldin infolist() bilan umumiy hajmni tekshiring

Qoida: ishonchsiz arxivni alohida vaqtinchalik katalogga oching, hajm va fayllar sonini oldindan tekshiring, keyin kerakli fayllarni ko'chiring.

2.7. Tizim yordamchilari

Funksiya Qaytaradi
disk_usage(yol) usage(total, used, free) — baytlarda
which("git") Dastur yo'li yoki None (PATH bo'yicha; Windows'da PATHEXT ham)
get_terminal_size((80, 24)) Terminal o'lchami; aniqlanmasa — berilgan sukut
chown(yol, user, group) Egasini o'zgartirish (Unix)

Katta yozuvdan oldin disk_usage(...).free ni tekshiring — yarim yozilgan arxiv yaroqsiz arxivdan ham yomonroq.


3. Tez ma'lumotnoma

python
import shutil

shutil.copy2(manba, maqsad)                           # metama'lumot bilan
shutil.copytree(a, b, ignore=shutil.ignore_patterns("__pycache__", ".git"), dirs_exist_ok=True)
shutil.move(a, b)
shutil.rmtree(yol, onexc=ruxsatni_tuzat)              # xavfsizlik tekshiruvidan keyin!
arxiv = shutil.make_archive("zaxira-2026-09-17", "gztar", root_dir=ildiz)
shutil.unpack_archive(arxiv, vaqtinchalik_katalog)
shutil.disk_usage("/").free; shutil.which("git")

Qoidalar

zaxira — copy2, copy emas
copytree maqsad mavjud — dirs_exist_ok=True
rmtree oldidan — yo'l tekshiruvi (bo'sh, ildiz, ruxsat etilgan katalog)
Windows faqat o'qiladigan fayllar — onexc
arxiv yo'llari — root_dir va base_dir
ishonchsiz arxiv — alohida katalog, hajm tekshiruvi
katta yozuv oldidan — disk_usage

4. Batafsil misollar

Misol 1 — Nusxalash va ko'chirish

python
"""copyfile, copy, copy2 va metama'lumot; katalogga nusxalash; copytree ignore va dirs_exist_ok; o'z ignore funksiyasi; move xulqi."""

import os
import shutil
import tempfile
from pathlib import Path

ESKI_VAQT = 1_700_000_000


def main() -> None:
    with tempfile.TemporaryDirectory() as vaqtinchalik:
        r = Path(vaqtinchalik)
        manba = r / "hisobot.txt"
        manba.write_text("2026-yil hisoboti", encoding="utf-8")
        os.utime(manba, (ESKI_VAQT, ESKI_VAQT))

        print("=== 1. Uch daraja ===")
        shutil.copyfile(manba, r / "copyfile.txt")
        shutil.copy(manba, r / "copy.txt")
        shutil.copy2(manba, r / "copy2.txt")
        for nom in ("copyfile.txt", "copy.txt", "copy2.txt"):
            yol = r / nom
            print(f"  {nom:13} mazmun bir xil: {yol.read_text(encoding='utf-8') == manba.read_text(encoding='utf-8')}, "
                  f"vaqt saqlandi: {yol.stat().st_mtime == ESKI_VAQT}")

        print("\n=== 2. Katalogga nusxalash ===")
        (r / "arxiv").mkdir()
        natija = shutil.copy2(manba, r / "arxiv")
        print(f"  copy2(fayl, katalog) → {Path(natija).relative_to(r).as_posix()}")
        try:
            shutil.copyfile(manba, r / "arxiv")
        except (IsADirectoryError, PermissionError) as xato:
            print(f"  copyfile(fayl, katalog) → {type(xato).__name__} (Linux'da IsADirectoryError)")

        print("\n=== 3. copytree ===")
        loyiha = r / "loyiha"
        for nisbiy in ("src/app.py", "src/__pycache__/app.cpython-314.pyc", ".git/HEAD", "README.md",
                       "data/katta.csv", "logs/bugun.log"):
            yol = loyiha / nisbiy
            yol.parent.mkdir(parents=True, exist_ok=True)
            yol.write_text("x" * (2000 if nisbiy.endswith(".csv") else 10), encoding="utf-8")

        shutil.copytree(loyiha, r / "zaxira", ignore=shutil.ignore_patterns("__pycache__", ".git", "*.log"))
        print(f"  ignore_patterns: {sorted(p.relative_to(r / 'zaxira').as_posix() for p in (r / 'zaxira').rglob('*') if p.is_file())}")

        def katta_fayllarsiz(katalog: str, nomlar: list[str]) -> set[str]:
            return {n for n in nomlar if os.path.isfile(os.path.join(katalog, n)) and os.path.getsize(os.path.join(katalog, n)) > 1000}

        shutil.copytree(loyiha, r / "yengil", ignore=katta_fayllarsiz)
        print(f"  o'z ignore funksiyasi (>1000 bayt): 'data/katta.csv' yo'q: {not (r / 'yengil' / 'data' / 'katta.csv').exists()}")
        try:
            shutil.copytree(loyiha, r / "zaxira")
        except FileExistsError:
            print("  qayta copytree → FileExistsError")
        shutil.copytree(loyiha, r / "zaxira", dirs_exist_ok=True, ignore=shutil.ignore_patterns(".git"))
        print(f"  dirs_exist_ok=True bilan yangilandi, logs qo'shildi: {(r / 'zaxira' / 'logs' / 'bugun.log').exists()}")

        print("\n=== 4. move ===")
        (r / "qabul").mkdir()
        yangi = shutil.move(r / "copy.txt", r / "qabul")
        print(f"  mavjud katalogga: {Path(yangi).relative_to(r).as_posix()}")
        yangi = shutil.move(r / "copyfile.txt", r / "qabul" / "nomi_ozgardi.txt")
        print(f"  yangi nom bilan:  {Path(yangi).relative_to(r).as_posix()}")
        yangi = shutil.move(r / "yengil", r / "qabul")
        print(f"  katalog:          {Path(yangi).relative_to(r).as_posix()}, ichida README: {(Path(yangi) / 'README.md').exists()}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Uch daraja ===
  copyfile.txt  mazmun bir xil: True, vaqt saqlandi: False
  copy.txt      mazmun bir xil: True, vaqt saqlandi: False
  copy2.txt     mazmun bir xil: True, vaqt saqlandi: True

=== 2. Katalogga nusxalash ===
  copy2(fayl, katalog) → arxiv/hisobot.txt
  copyfile(fayl, katalog) → PermissionError (Linux'da IsADirectoryError)

=== 3. copytree ===
  ignore_patterns: ['README.md', 'data/katta.csv', 'src/app.py']
  o'z ignore funksiyasi (>1000 bayt): 'data/katta.csv' yo'q: True
  qayta copytree → FileExistsError
  dirs_exist_ok=True bilan yangilandi, logs qo'shildi: True

=== 4. move ===
  mavjud katalogga: qabul/copy.txt
  yangi nom bilan:  qabul/nomi_ozgardi.txt
  katalog:          qabul/yengil, ichida README: True

Nima ko'rsatdi: 2.1, 2.2, 2.3-bo'limlar.

Misol 2 — Xavfsiz o'chirish

python
"""Bo'sh yo'l bilan join tuzog'i; rmtree himoya funksiyasi; faqat o'qiladigan fayllar va onexc; ignore_errors xavfi."""

import os
import shutil
import stat
import tempfile
from pathlib import Path


class XavfliOchirish(Exception):
    pass


def xavfsiz_rmtree(yol: Path, ruxsat_ildizi: Path, nom_prefiksi: str) -> None:
    yol, ruxsat_ildizi = yol.resolve(), ruxsat_ildizi.resolve()
    if yol == ruxsat_ildizi:
        raise XavfliOchirish("ruxsat ildizining o'zini o'chirib bo'lmaydi")
    if yol == Path(yol.anchor) or yol == Path.home().resolve():
        raise XavfliOchirish(f"tizim katalogi: {yol}")
    if not yol.is_relative_to(ruxsat_ildizi):
        raise XavfliOchirish("ruxsat etilgan katalogdan tashqarida")
    if not yol.name.startswith(nom_prefiksi):
        raise XavfliOchirish(f"nom {nom_prefiksi!r} bilan boshlanmaydi: {yol.name!r}")

    def onexc(funksiya, xato_yoli, istisno):
        if isinstance(istisno, PermissionError):
            os.chmod(xato_yoli, stat.S_IWRITE)
            funksiya(xato_yoli)
        else:
            raise istisno

    shutil.rmtree(yol, onexc=onexc)


def main() -> None:
    with tempfile.TemporaryDirectory() as vaqtinchalik:
        zaxiralar = Path(vaqtinchalik) / "zaxiralar"
        for nom in ("zaxira-2026-09-15", "zaxira-2026-09-16", "muhim-hujjatlar"):
            (zaxiralar / nom).mkdir(parents=True)
            (zaxiralar / nom / "fayl.txt").write_text(nom, encoding="utf-8")

        print("=== 1. ⚠️ Bo'sh nom bilan join ===")
        eski_nom = ""
        print(f"  os.path.join(zaxiralar, '') ildizning o'zi: {Path(os.path.join(zaxiralar, eski_nom)).resolve() == zaxiralar.resolve()}")
        print(f"  Path(zaxiralar) / '' ham: {(zaxiralar / eski_nom).resolve() == zaxiralar.resolve()}")

        print("\n=== 2. Himoya tekshiruvlari ===")
        sinovlar = [("bo'sh nom", zaxiralar / ""), ("boshqa katalog", zaxiralar / "muhim-hujjatlar"),
                    ("tashqariga ..", zaxiralar / ".." / ".."), ("to'g'ri", zaxiralar / "zaxira-2026-09-15")]
        for tavsif, yol in sinovlar:
            try:
                xavfsiz_rmtree(yol, zaxiralar, "zaxira-")
                print(f"  {tavsif:14} → o'chirildi")
            except XavfliOchirish as xato:
                print(f"  {tavsif:14} → rad: {xato}")
        print(f"  qolganlar: {sorted(p.name for p in zaxiralar.iterdir())}")

        print("\n=== 3. ⚠️ Faqat o'qiladigan fayllar ===")
        git = Path(vaqtinchalik) / "klon" / ".git" / "objects"
        git.mkdir(parents=True)
        obyekt = git / "ab12"
        obyekt.write_bytes(b"blob")
        obyekt.chmod(stat.S_IREAD)
        try:
            shutil.rmtree(Path(vaqtinchalik) / "klon")
            print("  oddiy rmtree o'chirdi (Linux/macOS'da ota katalog ruxsati yetarli)")
        except PermissionError:
            print("  oddiy rmtree → PermissionError (Windows)")
        shutil.rmtree(Path(vaqtinchalik) / "klon", ignore_errors=True)
        print(f"  ignore_errors=True dan keyin katalog hali ham bor: {(Path(vaqtinchalik) / 'klon').exists()}  ← jim yarim natija")
        if (Path(vaqtinchalik) / "klon").exists():
            (Path(vaqtinchalik) / "klon").rename(zaxiralar / "zaxira-klon")
            xavfsiz_rmtree(zaxiralar / "zaxira-klon", zaxiralar, "zaxira-")
        print(f"  onexc bilan to'liq o'chirildi: {not (zaxiralar / 'zaxira-klon').exists()}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. ⚠️ Bo'sh nom bilan join ===
  os.path.join(zaxiralar, '') ildizning o'zi: True
  Path(zaxiralar) / '' ham: True

=== 2. Himoya tekshiruvlari ===
  bo'sh nom      → rad: ruxsat ildizining o'zini o'chirib bo'lmaydi
  boshqa katalog → rad: nom 'zaxira-' bilan boshlanmaydi: 'muhim-hujjatlar'
  tashqariga ..  → rad: ruxsat etilgan katalogdan tashqarida
  to'g'ri        → o'chirildi
  qolganlar: ['muhim-hujjatlar', 'zaxira-2026-09-16']

=== 3. ⚠️ Faqat o'qiladigan fayllar ===
  oddiy rmtree → PermissionError (Windows)
  ignore_errors=True dan keyin katalog hali ham bor: True  ← jim yarim natija
  onexc bilan to'liq o'chirildi: True

Nima ko'rsatdi: 2.4-bo'lim.

Misol 3 — Arxivlar va xavfsizlik

python
"""make_archive: root_dir va base_dir farqi; formatlar; unpack_archive; zip slip — jim o'tkazish; tar filtri 3.14; hajm tekshiruvi; disk_usage, which, terminal o'lchami."""

import io
import shutil
import tarfile
import tempfile
import zipfile
from pathlib import Path


def ichidagilar(arxiv: str) -> list[str]:
    if arxiv.endswith(".zip"):
        with zipfile.ZipFile(arxiv) as z:
            return sorted(z.namelist())
    with tarfile.open(arxiv) as t:
        return sorted(t.getnames())


def main() -> None:
    with tempfile.TemporaryDirectory() as vaqtinchalik:
        r = Path(vaqtinchalik)
        loyiha = r / "loyiha"
        (loyiha / "src").mkdir(parents=True)
        (loyiha / "src" / "app.py").write_text("print(1)\n", encoding="utf-8")
        (loyiha / "README.md").write_text("# Loyiha\n", encoding="utf-8")

        print("=== 1. root_dir va base_dir ===")
        a = shutil.make_archive(str(r / "faqat_ichi"), "zip", root_dir=loyiha)
        b = shutil.make_archive(str(r / "katalog_bilan"), "gztar", root_dir=r, base_dir="loyiha")
        print(f"  {Path(a).name:24} {ichidagilar(a)}")
        print(f"  {Path(b).name:24} {[n for n in ichidagilar(b) if n.endswith(('.py', '.md'))]}")
        print(f"  formatlar: {sorted(nom for nom, _ in shutil.get_archive_formats())}")

        print("\n=== 2. Ochish ===")
        shutil.unpack_archive(a, r / "ochildi")
        print(f"  {sorted(p.relative_to(r / 'ochildi').as_posix() for p in (r / 'ochildi').rglob('*') if p.is_file())}")

        print("\n=== 3. ⚠️ Zip slip ===")
        yomon_zip = r / "yomon.zip"
        with zipfile.ZipFile(yomon_zip, "w") as z:
            z.writestr("rasm.png", b"png")
            z.writestr("../../tashqari.txt", b"hujum")
        print(f"  arxiv ichida: {ichidagilar(str(yomon_zip))}")
        shutil.unpack_archive(yomon_zip, r / "zip_ochildi")
        print(f"  unpack_archive: xavfsiz fayl ochildi: {(r / 'zip_ochildi' / 'rasm.png').exists()}, "
              f"tashqariga yozilmadi: {not (r.parent / 'tashqari.txt').exists() and not (r / 'tashqari.txt').exists()}")
        print("  ⚠️ xavfli nom jim o'tkazib yuborildi — xabar yo'q")

        print("\n=== 4. ⭐ tar filtri (3.14 sukuti 'data') ===")
        yomon_tar = r / "yomon.tar"
        with tarfile.open(yomon_tar, "w") as t:
            malumot = b"hujum"
            info = tarfile.TarInfo("../tashqari2.txt")
            info.size = len(malumot)
            t.addfile(info, io.BytesIO(malumot))
        try:
            shutil.unpack_archive(yomon_tar, r / "tar_ochildi")
        except tarfile.OutsideDestinationError as xato:
            print(f"  {type(xato).__name__}: '{xato.tarinfo.name}' maqsaddan tashqarida")

        print("\n=== 5. Hajm tekshiruvi (zip bomb) ===")
        bomba = r / "bomba.zip"
        with zipfile.ZipFile(bomba, "w", compression=zipfile.ZIP_DEFLATED) as z:
            z.writestr("nol.bin", b"\x00" * 20_000_000)
        with zipfile.ZipFile(bomba) as z:
            ochilgan = sum(i.file_size for i in z.infolist())
        siqilgan = bomba.stat().st_size
        print(f"  arxiv hajmi 100 KB dan kam: {siqilgan < 100_000}, ochilganda 20 MB: {ochilgan == 20_000_000}")
        print(f"  siqish nisbati 200 dan katta: {ochilgan / siqilgan > 200}  → ochishdan oldin rad etish kerak")

        print("\n=== 6. Tizim yordamchilari ===")
        joy = shutil.disk_usage(r)
        print(f"  disk_usage: total > used > 0: {joy.total > joy.used > 0}, free > 0: {joy.free > 0}")
        print(f"  which('python') yoki which('py') topildi: {bool(shutil.which('python') or shutil.which('py'))}")
        print(f"  which('bunday-dastur-yoq'): {shutil.which('bunday-dastur-yoq')}")
        print(f"  get_terminal_size((80, 24)) turi: {type(shutil.get_terminal_size((80, 24))).__name__}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. root_dir va base_dir ===
  faqat_ichi.zip           ['README.md', 'src/', 'src/app.py']
  katalog_bilan.tar.gz     ['loyiha/README.md', 'loyiha/src/app.py']
  formatlar: ['bztar', 'gztar', 'tar', 'xztar', 'zip', 'zstdtar']

=== 2. Ochish ===
  ['README.md', 'src/app.py']

=== 3. ⚠️ Zip slip ===
  arxiv ichida: ['../../tashqari.txt', 'rasm.png']
  unpack_archive: xavfsiz fayl ochildi: True, tashqariga yozilmadi: True
  ⚠️ xavfli nom jim o'tkazib yuborildi — xabar yo'q

=== 4. ⭐ tar filtri (3.14 sukuti 'data') ===
  OutsideDestinationError: '../tashqari2.txt' maqsaddan tashqarida

=== 5. Hajm tekshiruvi (zip bomb) ===
  arxiv hajmi 100 KB dan kam: True, ochilganda 20 MB: True
  siqish nisbati 200 dan katta: True  → ochishdan oldin rad etish kerak

=== 6. Tizim yordamchilari ===
  disk_usage: total > used > 0: True, free > 0: True
  which('python') yoki which('py') topildi: True
  which('bunday-dastur-yoq'): None
  get_terminal_size((80, 24)) turi: terminal_size

Nima ko'rsatdi: 2.5, 2.6, 2.7-bo'limlar.

Misol 4 — Amaliy: aylanma zaxira nusxa vositasi

Kirishdagi vaziyatning to'g'ri yechimi. Vosita: loyihani keraksiz fayllarsiz arxivlaydi, nomga vaqt belgisini qo'yadi (hozir parametridan), yozishdan oldin diskda joyni tekshiradi, arxivni vaqtinchalik nom bilan yaratib tekshirgandan keyin replace qiladi, arxivni ochib mazmunini xesh bilan solishtiradi va faqat oxirgi N ta zaxirani saqlaydi — o'chirish himoyalangan. Bir necha "kun" simulyatsiya qilinadi.

python
"""copytree ignore bilan tayyorlash; make_archive vaqt belgili nom; disk_usage tekshiruvi; vaqtinchalik nom + os.replace; ochib xesh bilan tekshirish; aylanish va himoyalangan o'chirish."""

import hashlib
import os
import shutil
import tempfile
from datetime import UTC, datetime, timedelta
from pathlib import Path

PREFIKS = "zaxira-"
OTKAZ = shutil.ignore_patterns("__pycache__", ".git", "*.log", "node_modules")


def xeshlar(ildiz: Path) -> dict[str, str]:
    return {p.relative_to(ildiz).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
            for p in sorted(ildiz.rglob("*")) if p.is_file()}


def zaxira_ol(loyiha: Path, zaxiralar: Path, hozir: datetime, saqlash: int, min_bosh_joy: int) -> dict[str, object]:
    zaxiralar.mkdir(parents=True, exist_ok=True)
    nom = f"{PREFIKS}{hozir:%Y%m%d-%H%M%S}"
    if shutil.disk_usage(zaxiralar).free < min_bosh_joy:
        raise OSError("diskda joy yetarli emas")

    with tempfile.TemporaryDirectory() as ish:
        tayyor = Path(ish) / "loyiha"
        shutil.copytree(loyiha, tayyor, ignore=OTKAZ)
        asl = xeshlar(tayyor)
        vaqtinchalik = shutil.make_archive(str(Path(ish) / nom), "gztar", root_dir=tayyor)

        tekshiruv = Path(ish) / "tekshiruv"
        shutil.unpack_archive(vaqtinchalik, tekshiruv)
        if xeshlar(tekshiruv) != asl:
            raise RuntimeError("arxiv mazmuni mos kelmadi")

        yakuniy = zaxiralar / Path(vaqtinchalik).name
        shutil.copy2(vaqtinchalik, zaxiralar / (yakuniy.name + ".tmp"))
        os.replace(zaxiralar / (yakuniy.name + ".tmp"), yakuniy)

    mavjud = sorted(p for p in zaxiralar.iterdir() if p.name.startswith(PREFIKS) and p.name.endswith(".tar.gz"))
    ochirilgan = []
    for eski in mavjud[:-saqlash]:
        yol = eski.resolve()
        if yol.parent == zaxiralar.resolve() and yol.name.startswith(PREFIKS):
            yol.unlink()
            ochirilgan.append(yol.name)
    return {"arxiv": yakuniy.name, "fayllar": len(asl), "ochirilgan": ochirilgan}


def main() -> None:
    with tempfile.TemporaryDirectory() as vaqtinchalik:
        r = Path(vaqtinchalik)
        loyiha, zaxiralar = r / "loyiha", r / "zaxiralar"
        for nisbiy, mazmun in {"src/app.py": "v1", "src/__pycache__/app.pyc": "kesh", "README.md": "# L",
                               "server.log": "log", ".git/HEAD": "ref"}.items():
            (loyiha / nisbiy).parent.mkdir(parents=True, exist_ok=True)
            (loyiha / nisbiy).write_text(mazmun, encoding="utf-8")
        (zaxiralar / "muhim.txt").parent.mkdir(parents=True)
        (zaxiralar / "muhim.txt").write_text("boshqa fayl — tegmaslik kerak", encoding="utf-8")

        print("=== 1. Besh kunlik zaxira, oxirgi 3 tasi saqlanadi ===")
        boshlanish = datetime(2026, 9, 13, 2, 0, tzinfo=UTC)
        for kun in range(5):
            (loyiha / "src" / "app.py").write_text(f"v{kun + 1}", encoding="utf-8")
            natija = zaxira_ol(loyiha, zaxiralar, boshlanish + timedelta(days=kun), saqlash=3, min_bosh_joy=1024)
            print(f"  {natija['arxiv']}  fayllar={natija['fayllar']}  o'chirildi={natija['ochirilgan']}")

        print("\n=== 2. Holat ===")
        print(f"  zaxiralar katalogi: {sorted(p.name for p in zaxiralar.iterdir())}")
        print(f"  .tmp qolmadi: {not any(p.suffix == '.tmp' for p in zaxiralar.iterdir())}")
        print(f"  begona fayl saqlandi: {(zaxiralar / 'muhim.txt').exists()}")

        print("\n=== 3. Eng oxirgi zaxirani tiklash ===")
        oxirgi = sorted(zaxiralar.glob(f"{PREFIKS}*.tar.gz"))[-1]
        tiklash = r / "tiklandi"
        shutil.unpack_archive(oxirgi, tiklash)
        print(f"  tiklangan fayllar: {sorted(xeshlar(tiklash))}")
        print(f"  app.py oxirgi versiya: {(tiklash / 'src' / 'app.py').read_text(encoding='utf-8')!r}")
        print(f"  keraksizlar kirmadi (__pycache__, .git, *.log): {not any(k in s for s in xeshlar(tiklash) for k in ('__pycache__', '.git', '.log'))}")

        print("\n=== 4. Diskda joy yetmasa ===")
        try:
            zaxira_ol(loyiha, zaxiralar, boshlanish + timedelta(days=10), saqlash=3, min_bosh_joy=10**18)
        except OSError as xato:
            print(f"  OSError: {xato} — mavjud zaxiralar tegilmadi: {len(list(zaxiralar.glob(PREFIKS + '*')))} ta")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Besh kunlik zaxira, oxirgi 3 tasi saqlanadi ===
  zaxira-20260913-020000.tar.gz  fayllar=2  o'chirildi=[]
  zaxira-20260914-020000.tar.gz  fayllar=2  o'chirildi=[]
  zaxira-20260915-020000.tar.gz  fayllar=2  o'chirildi=[]
  zaxira-20260916-020000.tar.gz  fayllar=2  o'chirildi=['zaxira-20260913-020000.tar.gz']
  zaxira-20260917-020000.tar.gz  fayllar=2  o'chirildi=['zaxira-20260914-020000.tar.gz']

=== 2. Holat ===
  zaxiralar katalogi: ['muhim.txt', 'zaxira-20260915-020000.tar.gz', 'zaxira-20260916-020000.tar.gz', 'zaxira-20260917-020000.tar.gz']
  .tmp qolmadi: True
  begona fayl saqlandi: True

=== 3. Eng oxirgi zaxirani tiklash ===
  tiklangan fayllar: ['README.md', 'src/app.py']
  app.py oxirgi versiya: 'v5'
  keraksizlar kirmadi (__pycache__, .git, *.log): True

=== 4. Diskda joy yetmasa ===
  OSError: diskda joy yetarli emas — mavjud zaxiralar tegilmadi: 3 ta

Nima ko'rsatdi: 2.1, 2.2, 2.4, 2.5, 2.7-bo'limlar.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"shutil.copy hamma narsani nusxalaydi" Vaqtlar saqlanmaydi — copy2
"copytree mavjud katalogga qo'shadi" FileExistsError — dirs_exist_ok=True
"move doim tez va atomar" Turli disklarda nusxa + o'chirish
"rmtree hamma joyda bir xil ishlaydi" Windows'da faqat o'qiladigan fayllar — onexc
"ignore_errors=True xavfsiz" Yarim o'chirilgan katalog jim qoladi
"os.path.join(ildiz, nom) doim ildiz ichidagi narsa" nom="" — ildizning o'zi
"Arxivni ochish xavfsiz" Zip slip, zip bomb — tekshiruv kerak
"base_dir va root_dir bir xil" Arxiv ichidagi yo'llar boshqacha bo'ladi

6. Keng tarqalgan xatolar va yechimlari

1. Himoyasiz rmtree

python
shutil.rmtree(os.path.join(ZAXIRA, nom))    # ❌ nom="" — hammasi
xavfsiz_rmtree(ZAXIRA / nom, ZAXIRA, "zaxira-")   # ✅

2. Zaxirada copy

python
shutil.copy(manba, zaxira)                  # ⚠️ vaqt yo'qoladi
shutil.copy2(manba, zaxira)                 # ✅

3. Joyni tekshirmaslik

python
shutil.make_archive(...)                    # ⚠️ disk to'lsa — yaroqsiz arxiv
if shutil.disk_usage(katalog).free < kerak: raise OSError(...)   # ✅

4. Arxivni tekshirmay yakuniy nomga yozish

python
shutil.make_archive(ZAXIRA / nom, ...)      # ⚠️ uzilsa — buzilgan "yangi" zaxira
# ✅ vaqtinchalik joyda yaratish, ochib tekshirish, os.replace

5. Ishonchsiz arxivni to'g'ridan-to'g'ri ochish

python
shutil.unpack_archive(yuklangan, "/srv/app")   # ❌
# ✅ vaqtinchalik katalog + hajm tekshiruvi + kerakli fayllarni ko'chirish

6. filter="fully_trusted" ni odat qilish

python
tarfile.open(f).extractall(maqsad, filter="fully_trusted")   # ❌ ishonchsiz arxivda

7. which natijasini tekshirmaslik

python
subprocess.run([shutil.which("ffmpeg"), ...])   # ❌ None → TypeError
if (ffmpeg := shutil.which("ffmpeg")) is None: sys.exit("ffmpeg o'rnatilmagan")   # ✅

8. copytree bilan keraksiz fayllar

python
shutil.copytree(loyiha, zaxira)             # ⚠️ node_modules, .git, loglar
shutil.copytree(loyiha, zaxira, ignore=shutil.ignore_patterns(...))   # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 15.11–15.12-darslar (o'tilgan): os.walk, os.replace, pathlib, path traversal
  • 15.14-dars: subprocess — which bilan topilgan dasturlarni ishga tushirish
  • 16-qism: fayl formatlari, zipfile va tarfile tafsilotlari
  • 17-qism: testlarda vaqtinchalik kataloglar
  • 19-qism: paket yig'ish — arxivlar
  • 20.13-dars: fayl yuklash — zip slip, zip bomb
  • 26-qism: avtomatlashtirish — zaxira, joylashtirish skriptlari

8. Eng yaxshi amaliyotlar

  1. Zaxira va sinxronlash — copy2.

  2. copytree — doim ignore bilan.

  3. rmtree — faqat himoya funksiyasi orqali.

  4. Katta yozuv — joy tekshiruvi, vaqtinchalik nom, tekshiruv, os.replace.

  5. Arxiv mazmunini ochib, xesh bilan tekshiring.

  6. Ishonchsiz arxiv — alohida katalog, hajm va nisbat tekshiruvi, 3.14 filtrlari.

  7. Tashqi dastur — which bilan oldindan tekshiring.

  8. Xavfli ommaviy amallarda — quruq ishga tushirish rejimi.


9. Amaliy topshiriq

Vazifa 1: Natijani bashorat qiling

python
import shutil, os
1.  print(os.path.join("zaxira", "") in ("zaxira/", "zaxira\\"))
2.  print(shutil.which("bunday_dastur_yoq_123"))
3.  print(type(shutil.disk_usage(".")).__name__)
4.  print(shutil.get_terminal_size((100, 30)).columns >= 1)
5.  print("zip" in [n for n, _ in shutil.get_archive_formats()])
6.  print("zstdtar" in [n for n, _ in shutil.get_archive_formats()])
7.  print(callable(shutil.ignore_patterns("*.pyc")))
8.  print(shutil.ignore_patterns("*.pyc")(".", ["a.py", "b.pyc", "c.pyc"]))
9.  print(hasattr(shutil, "copy2"))
10. print(shutil.disk_usage(".").free <= shutil.disk_usage(".").total)
11. print(issubclass(FileExistsError, OSError))
12. print(sorted(shutil.ignore_patterns("__pycache__", "*.log")(".", ["__pycache__", "x.log", "y.txt"])))
Javoblar
  1. True
  2. None
  3. usage
  4. True
  5. True
  6. True — 3.14
  7. True
  8. {'b.pyc', 'c.pyc'}
  9. True
  10. True
  11. True
  12. ['__pycache__', 'x.log']

Vazifa 2: Xatolarni tuzating

python
1.  def tozala(eski_nom):
        shutil.rmtree(os.path.join("/srv/zaxiralar", eski_nom))

2.  def sinxronla(manba, maqsad):
        shutil.copytree(manba, maqsad)          # har kuni ishga tushadi

3.  def arxivla(katalog):
        return shutil.make_archive("/srv/zaxira/oxirgi", "zip", katalog)

4.  def ochish(yuklangan_zip):
        shutil.unpack_archive(yuklangan_zip, "/srv/app/plugins")

5.  def video_siq(fayl):
        subprocess.run([shutil.which("ffmpeg"), "-i", fayl, "chiqish.mp4"])
Javoblar
python
1.  ILDIZ = Path("/srv/zaxiralar")
    def tozala(eski_nom):
        if not eski_nom:
            raise ValueError("nom bo'sh")
        xavfsiz_rmtree(ILDIZ / eski_nom, ILDIZ, "zaxira-")

2.  def sinxronla(manba, maqsad):
        shutil.copytree(manba, maqsad, dirs_exist_ok=True,
                        ignore=shutil.ignore_patterns(".git", "__pycache__"))

3.  def arxivla(katalog):
        with tempfile.TemporaryDirectory() as ish:
            vaqtinchalik = shutil.make_archive(os.path.join(ish, "oxirgi"), "zip", katalog)
            # ... ochib tekshirish
            shutil.copy2(vaqtinchalik, "/srv/zaxira/oxirgi.zip.tmp")
            os.replace("/srv/zaxira/oxirgi.zip.tmp", "/srv/zaxira/oxirgi.zip")

4.  def ochish(yuklangan_zip):
        with zipfile.ZipFile(yuklangan_zip) as z:
            if sum(i.file_size for i in z.infolist()) > 50_000_000 or len(z.infolist()) > 1000:
                raise ValueError("arxiv juda katta")
        with tempfile.TemporaryDirectory() as ish:
            shutil.unpack_archive(yuklangan_zip, ish)
            # ... fayllarni tekshirib, kerakligini /srv/app/plugins ga ko'chirish

5.  def video_siq(fayl):
        ffmpeg = shutil.which("ffmpeg")
        if ffmpeg is None:
            raise RuntimeError("ffmpeg o'rnatilmagan")
        subprocess.run([ffmpeg, "-i", fayl, "chiqish.mp4"], check=True)

Vazifa 3: Katalog sinxronizatori

sinxronla(manba, maqsad, oyna=False):

  1. Yangi va o'zgargan fayllarni (st_mtime va hajm) copy2 bilan nusxalang
  2. --ochir bilan manbada yo'q fayllarni maqsaddan o'chiring (himoya bilan)
  3. ignore naqshlari fayldan (.sinxignore) o'qilsin
  4. Quruq rejimda reja chiqarilsin
  5. Ikkinchi ishga tushirishda hech narsa nusxalanmasligini tekshiring

Vazifa 4: Xavfsiz arxiv ochuvchi

xavfsiz_och(arxiv, maqsad, maks_hajm, maks_fayllar, maks_nisbat):

  1. zip va tar ni qo'llasin
  2. Ochishdan oldin: umumiy hajm, fayllar soni, siqish nisbati, absolyut va .. yo'llar
  3. Har rad etish sababini stderr ga yozsin
  4. Symlink va qurilma fayllarini rad etsin
  5. Zip slip, zip bomb va oddiy arxiv bilan testlang

Vazifa 5: Disk hajmi hisoboti

  1. Berilgan katalog uchun eng katta 10 ta ichki katalog (rekursiv hajm)
  2. disk_usage bilan diskning umumiy holati va foizi
  3. Terminal kengligiga moslashgan gistogramma (get_terminal_size)
  4. --min-hajm=100M kabi birliklarni tahlil qilish

Vazifa 6: Joylashtirish (deploy) skripti

  1. Yangi versiyani releases/2026-09-17-1400/ ga nusxalang (copytree, ignore)
  2. current symlink ini atomar almashtiring (vaqtinchalik symlink + os.replace)
  3. Oxirgi 5 ta relizni saqlang
  4. Qaytarish (rollback) buyrug'i
  5. Windows'da symlink ruxsati bo'lmasa — aniq xabar

Vazifa 7: O'ylash

rm -rf / — Unix tarixidagi eng mashhur halokatli buyruqlardan biri. Zamonaviy GNU rm endi --preserve-root ni sukut bo'yicha qo'llaydi. Python'ning shutil.rmtree esa bunday himoyaga ega emas. Nega standart kutubxona "xavfli" funksiyalarni himoyasiz qoldiradi va himoya qayerda bo'lishi kerak: kutubxonadami, ilovadami yoki operatsion tizimdami?

Javob

Qisqa javob: Kutubxona funksiyasi ilovaning kontekstini bilmaydi: /tmp/build ni o'chirish bir dasturda odatiy, boshqasida halokatli. Umumiy himoya (/ ni rad etish) faqat eng aniq holatni qamraydi, qolganlarida esa soxta xavfsizlik hissini beradi. Shuning uchun himoya qatlamli bo'lishi kerak: ilova — kontekstli tekshiruvlar, operatsion tizim — ruxsatlar va izolyatsiya, jarayon — minimal huquqlar va zaxiralar.

1. --preserve-root tarixi

  • Klassik rm -rf / tizimni o'chirardi
  • GNU coreutils 2006-yildan --preserve-root ni sukut qildi
  • Lekin rm -rf /*, rm -rf "$BOSH/" (bo'sh o'zgaruvchi) — baribir xavfli
  • Mashhur hodisa: Steam for Linux o'rnatish skriptida rm -rf "$STEAMROOT/"* — o'zgaruvchi bo'sh bo'lganda foydalanuvchi fayllari o'chgan

2. Nega kutubxona himoyasiz

Sabab Tafsilot
Kontekst yo'q Qaysi katalog "muhim" — faqat ilova biladi
Umumiy qoida yetarli emas / ni bloklash /home/aziz yoki /srv/data ni himoya qilmaydi
Soxta xavfsizlik "Kutubxona himoya qiladi" degan ishonch — tekshiruvlarni kamaytiradi
Qonuniy foydalanish Konteyner ichida /app ni tozalash — odatiy
Oddiylik Standart kutubxona funksiyalari bitta vazifani bajaradi

3. Qatlamli himoya

Qatlam Nima qiladi Misol
Ilova Kontekstli tekshiruvlar xavfsiz_rmtree: ruxsat ildizi, nom prefiksi, bo'sh nom
Kiritish Yo'lni tekshirish va tozalash resolve(), is_relative_to() (15.12)
Jarayon Minimal huquqlar Zaxira skripti faqat zaxira katalogiga yozish huquqiga ega foydalanuvchi
Operatsion tizim Ruxsatlar, faqat o'qish uchun o'rnatish Konteyner fayl tizimi read-only, chattr +i
Ma'lumot Zaxiralar, snapshotlar O'chirilgan narsani qaytarish imkoni
Jarayon (odamlar) Quruq ishga tushirish, tasdiqlash --dry-run, "o'chirilsinmi?"

4. Muhandislik tamoyillari

  1. Xavfli amal — tor interfeys orqali: to'g'ridan-to'g'ri rmtree emas, zaxirani_ochir(sana)
  2. Sukut — xavfsiz: --ochir bayrog'isiz faqat reja
  3. Chuqur himoya: bitta tekshiruvga ishonmaslik
  4. Qaytariluvchanlik: o'chirish o'rniga "savat"ga ko'chirish, keyin muddat bilan tozalash
  5. Kuzatuv: har o'chirishni jurnalga yozish

5. Xulosa

  1. Standart kutubxona mexanizm beradi, siyosatni ilova belgilaydi
  2. Kutubxona darajasidagi umumiy himoya faqat eng aniq holatni qamraydi
  3. To'g'ri himoya — qatlamli: ilova, huquqlar, OS, zaxiralar
  4. Halokatli amallar — tor, tekshirilgan va jurnalga yoziladigan funksiyalar orqali

Nimani mustahkamlaydi: 2.1–2.7-bo'limlar.


Xulosa

Bu darsda fayl tizimidagi yuqori darajadagi amallarni va ularni xavfsiz bajarishni o'rgandik.

Eng muhim uch fikr:

  1. Nusxalash darajalari muhim. copyfile faqat mazmunni, copy ruxsatlarni ham, copy2 vaqtlar va metama'lumotni ham nusxalaydi — zaxira va sinxronlash uchun copy2. copytree doim ignore bilan (__pycache__, .git, loglar) va mavjud katalogga dirs_exist_ok=True bilan ishlatiladi; move bir diskda tez nomlash, turli disklarda esa nusxa va o'chirish.

  2. rmtree — himoya funksiyasi orqali. Bo'sh nom bilan join ildizning o'zini beradi — va butun katalog o'chadi. Himoya: resolve(), ruxsat ildizi ichida (is_relative_to), ildiz yoki uy katalogi emas, kutilgan nom prefiksi. Windows'da faqat o'qiladigan fayllar onexc 3.12-bob bilan hal qilinadi; ignore_errors=True esa yarim natijani jim qoldiradi.

  3. Arxivlar: yaratish ishonchli, ochish ehtiyotkor. make_archive da root_dir va base_dir arxiv ichidagi yo'llarni belgilaydi; 3.14 da zstdtar formati qo'shildi. Ishonchli zaxira — joy tekshiruvi, vaqtinchalik nom, ochib xesh bilan solishtirish va os.replace. Ishonchsiz arxivda zip slip nomlari unpack_archive da jim o'tkazib yuboriladi, tar da esa 3.14 ning data filtri OutsideDestinationError beradi; hajm portlashidan esa hech biri himoya qilmaydi — ochishdan oldin infolist() bilan tekshiring.

Keyingi darsda tashqi dasturlarni ishga tushirishni o'rganamiz: subprocess — run, chiqish kodlari, oqimlarni ushlash, muddatlar va shell injection xavfi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
15.13-dars: shutil va fayl amallari — IlmHamroh