PixelLauncher 2.1.0 — стабильная сборка

Офлайн-лаунчер Minecraft: аккаунты без Microsoft, установка версий и лоадеров
(Fabric, Quilt, Forge, NeoForge), каталог модпаков Modrinth, поиск и обновление
модов, разбор вылетов, системный трей, автозагрузка, плагины, прокси и зеркала.
This commit is contained in:
kosta2014p committed 2026-10-18 22:44:19 +07:00
commit ec6f13ddcc
33 files changed
+10445

No files matched your search

+145
View File
@@ -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