PixelLauncher 2.1.0 — стабильная сборка
Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров (Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
commit
ec6f13ddcc
33 files changed
+10445
No files matched your search
@@ -0,0 +1,145 @@
|
||||
# -*- 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
|
||||
Reference in new issue
Block a user