# -*- coding: utf-8 -*- """Список миров и резервные копии по одному миру. Копии хранятся в .launcher/backups/<мир>/ (не путать со старыми общими saves-*.zip в game_dir/backups — их делает автобэкап). """ import os import shutil import time import zipfile def saves_dir(game_dir): return os.path.join(game_dir, "saves") def _safe(name): return "".join(c for c in str(name) if c not in '\\/:*?"<>|').strip() or "world" def backups_dir(game_dir, world=""): d = os.path.join(game_dir, ".launcher", "backups") if world: d = os.path.join(d, _safe(world)) return d def _dir_size(path): total = 0 for root, _dirs, files in os.walk(path): for f in files: try: total += os.path.getsize(os.path.join(root, f)) except OSError: pass return total def list_worlds(game_dir): """[{name, size, mtime}] — папки в saves/ (свежие первые).""" sd = saves_dir(game_dir) out = [] if not os.path.isdir(sd): return out for name in sorted(os.listdir(sd), key=str.lower): p = os.path.join(sd, name) if not os.path.isdir(p): continue mtime = 0.0 for cand in ("level.dat", None): try: mtime = os.path.getmtime( os.path.join(p, cand) if cand else p) break except OSError: continue out.append({"name": name, "size": _dir_size(p), "mtime": mtime}) out.sort(key=lambda w: w["mtime"], reverse=True) return out def list_backups(game_dir, world): """[{name, size, mtime}] — zip-копии мира (свежие первые).""" d = backups_dir(game_dir, world) out = [] if not os.path.isdir(d): return out for name in sorted(os.listdir(d), reverse=True): p = os.path.join(d, name) if name.lower().endswith(".zip") and os.path.isfile(p): try: out.append({"name": name, "size": os.path.getsize(p), "mtime": os.path.getmtime(p)}) except OSError: pass return out def prune_backups(game_dir, world, cap): """Оставляет cap последних копий. Возвращает число удалённых.""" cap = max(1, int(cap or 1)) d = backups_dir(game_dir, world) removed = 0 for b in list_backups(game_dir, world)[cap:]: try: os.remove(os.path.join(d, b["name"])) removed += 1 except OSError: pass return removed def backup_world(game_dir, world, cap=5, progress=None, log=None): """ZIP-копия одного мира + удаление старых копий сверх cap. Возвращает путь к архиву. """ src = os.path.join(saves_dir(game_dir), world) if not os.path.isdir(src): raise RuntimeError("мир не найден: " + world) d = backups_dir(game_dir, world) os.makedirs(d, exist_ok=True) stamp = time.strftime("%Y%m%d-%H%M%S") path = os.path.join(d, f"{_safe(world)}-{stamp}.zip") n = 1 while os.path.exists(path): # две копии в одну секунду — суффикс n += 1 path = os.path.join(d, f"{_safe(world)}-{stamp}-{n}.zip") files = [] for root, _dirs, fnames in os.walk(src): for f in fnames: fp = os.path.join(root, f) files.append((fp, os.path.relpath(fp, src))) if log: log(f"Копия мира {world}: {len(files)} файлов…") with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z: for i, (fp, rel) in enumerate(files, 1): try: z.write(fp, rel) except OSError: continue if progress and (i % 20 == 0 or i == len(files)): progress(i, len(files)) prune_backups(game_dir, world, cap) return path def remove_backup(game_dir, world, name): """Удаляет одну копию. True при успехе.""" try: os.remove(os.path.join(backups_dir(game_dir, world), _safe(name))) return True except OSError: return False def remove_world(game_dir, world): """Удаляет мир целиком. True при успехе.""" p = os.path.join(saves_dir(game_dir), world) if not os.path.isdir(p): return False shutil.rmtree(p, ignore_errors=True) return True