ForgeFix и новые фичи в интерфейсе
This commit is contained in:
+105
-5
@@ -1,9 +1,10 @@
|
||||
import hashlib
|
||||
import os
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import json
|
||||
from typing import Optional, Dict
|
||||
from typing import Optional, Dict, List
|
||||
import structlog
|
||||
import asyncio
|
||||
import aiofiles
|
||||
@@ -19,6 +20,46 @@ DATA_DIR.mkdir(exist_ok=True)
|
||||
# Cache for loaded manifests
|
||||
_manifest_cache: Dict[str, PackMeta] = {}
|
||||
|
||||
# Pack config cache
|
||||
_packs_config: Optional[Dict] = None
|
||||
|
||||
def load_packs_config() -> dict:
|
||||
global _packs_config
|
||||
if _packs_config is not None:
|
||||
return _packs_config
|
||||
config_path = DATA_DIR / "packs_config.json"
|
||||
if config_path.exists():
|
||||
try:
|
||||
_packs_config = json.loads(config_path.read_text())
|
||||
except Exception:
|
||||
_packs_config = {}
|
||||
else:
|
||||
_packs_config = {}
|
||||
return _packs_config
|
||||
|
||||
def save_packs_config(config: dict):
|
||||
global _packs_config
|
||||
_packs_config = config
|
||||
config_path = DATA_DIR / "packs_config.json"
|
||||
config_path.write_text(json.dumps(config, indent=2))
|
||||
|
||||
def is_pack_disabled(pack_name: str) -> bool:
|
||||
config = load_packs_config()
|
||||
return config.get(pack_name, {}).get("disabled", False)
|
||||
|
||||
def toggle_pack_disabled(pack_name: str) -> bool:
|
||||
config = load_packs_config()
|
||||
entry = config.setdefault(pack_name, {})
|
||||
entry["disabled"] = not entry.get("disabled", False)
|
||||
save_packs_config(config)
|
||||
return entry["disabled"]
|
||||
|
||||
def get_pack_presets(pack_name: str) -> List[str]:
|
||||
presets_dir = PACKS_DIR / pack_name / "presets"
|
||||
if not presets_dir.exists() or not presets_dir.is_dir():
|
||||
return []
|
||||
return sorted([d.name for d in presets_dir.iterdir() if d.is_dir()])
|
||||
|
||||
def get_cached_manifest(pack_name: str) -> Optional[PackMeta]:
|
||||
"""Get manifest from memory cache if available"""
|
||||
return _manifest_cache.get(pack_name)
|
||||
@@ -66,10 +107,11 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
||||
new_files: Dict[str, FileEntry] = {}
|
||||
changed = False
|
||||
|
||||
# Get ignored directories
|
||||
ignored_dirs = current_meta.ignored_dirs if current_meta else [
|
||||
# Get ignored directories (always use current list, not cached from meta)
|
||||
ignored_dirs = [
|
||||
"resourcepacks", "shaderpacks", "saves", "logs",
|
||||
"crash-reports", "screenshots", "journeymap", "config"
|
||||
"crash-reports", "screenshots", "journeymap",
|
||||
"presets"
|
||||
]
|
||||
|
||||
# Walk through pack directory
|
||||
@@ -81,10 +123,19 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
||||
file_path = Path(root) / file
|
||||
rel_path = file_path.relative_to(pack_path).as_posix()
|
||||
|
||||
# Skip metadata files that should not be part of the pack
|
||||
if file in ("description.txt", "instance.json"):
|
||||
continue
|
||||
|
||||
# Skip if in ignored directory
|
||||
if any(ignored_dir in rel_path.split('/') for ignored_dir in ignored_dirs):
|
||||
continue
|
||||
|
||||
# Validate .jar files — skip corrupted ones
|
||||
if file.endswith(".jar") and not _is_valid_zip(file_path):
|
||||
logger.warning(f"Skipping corrupt jar in pack {pack_name}: {rel_path}")
|
||||
continue
|
||||
|
||||
stat = file_path.stat()
|
||||
file_hash = await calculate_sha256(file_path)
|
||||
|
||||
@@ -153,4 +204,53 @@ async def scan_pack(pack_name: str, force_rescan: bool = False) -> PackMeta:
|
||||
return current_meta
|
||||
|
||||
# Should not happen
|
||||
raise Exception(f"Failed to scan pack {pack_name}")
|
||||
raise Exception(f"Failed to scan pack {pack_name}")
|
||||
|
||||
def _is_valid_zip(path: Path) -> bool:
|
||||
"""Check if a file is a valid ZIP archive"""
|
||||
try:
|
||||
with zipfile.ZipFile(path, 'r') as zf:
|
||||
return zf.testzip() is None
|
||||
except zipfile.BadZipFile:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def get_merged_manifest(pack_name: str, preset: str) -> PackMeta:
|
||||
"""Get manifest merged with preset files. Preset files override base files."""
|
||||
base_meta = get_cached_manifest(pack_name)
|
||||
if not base_meta:
|
||||
raise FileNotFoundError(f"Pack {pack_name} not found")
|
||||
|
||||
preset_dir = PACKS_DIR / pack_name / "presets" / preset
|
||||
if not preset_dir.exists() or not preset_dir.is_dir():
|
||||
raise FileNotFoundError(f"Preset {preset} not found for pack {pack_name}")
|
||||
|
||||
merged_files = dict(base_meta.files)
|
||||
|
||||
for root, dirs, files in os.walk(preset_dir):
|
||||
for file in files:
|
||||
file_path = Path(root) / file
|
||||
rel_path = file_path.relative_to(preset_dir).as_posix()
|
||||
stat = file_path.stat()
|
||||
file_hash = calculate_sha256_sync(file_path)
|
||||
merged_files[rel_path] = FileEntry(
|
||||
path=rel_path,
|
||||
hash=file_hash,
|
||||
size=stat.st_size,
|
||||
added_at=datetime.utcfromtimestamp(stat.st_ctime),
|
||||
modified_at=datetime.utcfromtimestamp(stat.st_mtime)
|
||||
)
|
||||
|
||||
return PackMeta(
|
||||
pack_name=base_meta.pack_name,
|
||||
version=base_meta.version,
|
||||
files=merged_files,
|
||||
updated_at=base_meta.updated_at,
|
||||
ignored_dirs=base_meta.ignored_dirs,
|
||||
minecraft_version=base_meta.minecraft_version,
|
||||
loader_type=base_meta.loader_type,
|
||||
loader_version=base_meta.loader_version,
|
||||
asset_index=base_meta.asset_index
|
||||
)
|
||||
Reference in New Issue
Block a user