"""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()