101 lines
3.4 KiB
Python
101 lines
3.4 KiB
Python
"""Zip introspection helpers for category-specific version metadata.
|
|
|
|
Parsed once at upload time and stored on the Version row, so the detail page
|
|
never re-reads large archives. All functions take the on-disk path of an
|
|
adopted file (under MEDIA_ROOT) and return plain dicts/values.
|
|
"""
|
|
|
|
import json
|
|
import zipfile
|
|
|
|
|
|
def _open_zip(path):
|
|
"""Return a ZipFile for the path (None when it's not a zip)."""
|
|
try:
|
|
return zipfile.ZipFile(path)
|
|
except (zipfile.BadZipFile, OSError):
|
|
return None
|
|
|
|
|
|
def read_pack_mcmeta(path):
|
|
"""Extract the NoN-relevant data from a datapack's pack.mcmeta.
|
|
|
|
Returns (pack_format, description) or (None, '').
|
|
"""
|
|
zf = _open_zip(path)
|
|
if zf is None:
|
|
return None, ''
|
|
try:
|
|
try:
|
|
data = json.loads(zf.read('pack.mcmeta').decode('utf-8', 'replace'))
|
|
except (KeyError, json.JSONDecodeError):
|
|
return None, ''
|
|
pack = data.get('pack') or {}
|
|
try:
|
|
pack_format = int(pack.get('pack_format'))
|
|
except (TypeError, ValueError):
|
|
pack_format = None
|
|
description = pack.get('description', '') or ''
|
|
if isinstance(description, (dict, list)):
|
|
description = json.dumps(description)
|
|
return pack_format, str(description)
|
|
finally:
|
|
zf.close()
|
|
|
|
|
|
def parse_mods_manifest(path, filename=''):
|
|
"""Inspect a modpack archive and build a manifest for the Mods tab.
|
|
|
|
- Modrinth .mrpack: modrinth.index.json → dependencies
|
|
- Curseforge modpack: manifest.json → files array
|
|
- Classic folder layout: minecraft/mods/*.jar
|
|
Returns a dict: {source, files: [...]}.
|
|
"""
|
|
zf = _open_zip(path)
|
|
if zf is None:
|
|
return {'source': 'unknown', 'files': []}
|
|
|
|
try:
|
|
names = set(zf.namelist())
|
|
|
|
if filename.lower().endswith('.mrpack') or 'modrinth.index.json' in names:
|
|
try:
|
|
index = json.loads(zf.read('modrinth.index.json').decode('utf-8', 'replace'))
|
|
except (KeyError, json.JSONDecodeError):
|
|
index = {}
|
|
deps = []
|
|
for dep in index.get('dependencies') or []:
|
|
deps.append({
|
|
'name': dep.get('file_name') or dep.get('project_id', 'unknown'),
|
|
'version_id': dep.get('version_id'),
|
|
'type': dep.get('dependency_type', ''),
|
|
})
|
|
return {'source': 'modrinth', 'files': deps}
|
|
|
|
if 'manifest.json' in names:
|
|
try:
|
|
manifest = json.loads(zf.read('manifest.json').decode('utf-8', 'replace'))
|
|
except (KeyError, json.JSONDecodeError):
|
|
manifest = {}
|
|
files = []
|
|
for entry in manifest.get('files') or []:
|
|
files.append({
|
|
'name': entry.get('fileName', entry.get('projectID', 'unknown')),
|
|
'project_id': entry.get('projectID'),
|
|
'file_id': entry.get('fileID'),
|
|
})
|
|
if files:
|
|
return {'source': 'curseforge', 'files': files}
|
|
|
|
mods = []
|
|
prefix = 'minecraft/mods/'
|
|
for name in sorted(names):
|
|
if name.startswith(prefix) and not name.endswith('/'):
|
|
mods.append({'name': name[len(prefix):]})
|
|
if mods:
|
|
return {'source': 'folder', 'files': mods}
|
|
|
|
return {'source': 'unknown', 'files': []}
|
|
finally:
|
|
zf.close()
|