Spaces:
Sleeping
Sleeping
Download scripts/migrate_anticheat_storage.py from Kurtgitgit/mission17-ai: direct link, hf CLI and curl.
- Browser
- Download file 5.55 kB
-
https://huggingface.co/spaces/Kurtgitgit/mission17-ai/resolve/main/scripts/migrate_anticheat_storage.py
- Command line
-
hf download hf://spaces/Kurtgitgit/mission17-ai/scripts/migrate_anticheat_storage.py
-
curl -L -o migrate_anticheat_storage.py https://huggingface.co/spaces/Kurtgitgit/mission17-ai/resolve/main/scripts/migrate_anticheat_storage.py
5.55 kB
| """Validate legacy hashes and backfill MongoDB anti-cheat records safely. | |
| The command is dry-run only unless ``--apply`` is supplied. It never removes | |
| the SQLite or JSON sources. | |
| """ | |
| import argparse | |
| import json | |
| import sqlite3 | |
| import sys | |
| from collections import Counter | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from pymongo import MongoClient | |
| from pymongo.errors import PyMongoError | |
| AI_ROOT = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(AI_ROOT)) | |
| from utils.anticheat import AntiCheatEngine, AntiCheatUnavailable # noqa: E402 | |
| def read_json_hashes(path): | |
| if not path.exists(): | |
| return [] | |
| with path.open('r', encoding='utf-8') as source: | |
| payload = json.load(source) | |
| values = payload.get('hashes', []) if isinstance(payload, dict) else [] | |
| return values if isinstance(values, list) else [] | |
| def read_sqlite_hashes(path): | |
| if not path.exists(): | |
| return [] | |
| connection = None | |
| try: | |
| connection = sqlite3.connect(f'file:{path.as_posix()}?mode=ro', uri=True) | |
| return [row[0] for row in connection.execute('SELECT hash_str FROM hashes')] | |
| except sqlite3.Error as error: | |
| raise RuntimeError(f'Could not read legacy SQLite database: {error}') from error | |
| finally: | |
| if connection is not None: | |
| connection.close() | |
| def validated_legacy_hashes(sqlite_path, json_path): | |
| raw_by_source = { | |
| 'sqlite': read_sqlite_hashes(sqlite_path), | |
| 'json': read_json_hashes(json_path), | |
| } | |
| valid = set() | |
| counts = Counter() | |
| for source_name, values in raw_by_source.items(): | |
| counts[f'{source_name}_read'] = len(values) | |
| for value in values: | |
| normalized = AntiCheatEngine.normalize_hash(value) | |
| if normalized: | |
| valid.add(normalized) | |
| counts[f'{source_name}_valid'] += 1 | |
| else: | |
| counts[f'{source_name}_invalid'] += 1 | |
| counts['unique_valid'] = len(valid) | |
| return sorted(valid), counts | |
| def backfill_buckets(collection, apply_changes): | |
| scanned = updated = invalid = 0 | |
| projection = {'hashValue': 1, 'buckets': 1} | |
| for record in collection.find({}, projection): | |
| scanned += 1 | |
| normalized = AntiCheatEngine.normalize_hash(record.get('hashValue')) | |
| if not normalized: | |
| invalid += 1 | |
| continue | |
| expected = AntiCheatEngine._buckets(normalized) | |
| if record.get('hashValue') != normalized or record.get('buckets') != expected: | |
| updated += 1 | |
| if apply_changes: | |
| collection.update_one( | |
| {'_id': record['_id']}, | |
| {'$set': {'hashValue': normalized, 'buckets': expected}}, | |
| ) | |
| return {'scanned': scanned, 'updates_needed': updated, 'invalid': invalid} | |
| def migrate_legacy(collection, hashes, apply_changes): | |
| inserted = existing = 0 | |
| now = datetime.now(timezone.utc) | |
| for hash_value in hashes: | |
| selector = { | |
| 'hashType': AntiCheatEngine.LEGACY_HASH_TYPE, | |
| 'algorithmVersion': AntiCheatEngine.LEGACY_ALGORITHM_VERSION, | |
| 'hashValue': hash_value, | |
| } | |
| if collection.find_one(selector, {'_id': 1}): | |
| existing += 1 | |
| continue | |
| inserted += 1 | |
| if apply_changes: | |
| collection.update_one( | |
| selector, | |
| {'$setOnInsert': { | |
| **selector, | |
| 'buckets': AntiCheatEngine._buckets(hash_value), | |
| 'createdAt': now, | |
| 'migrationSource': 'legacy-local-stores', | |
| }}, | |
| upsert=True, | |
| ) | |
| return {'inserts_needed': inserted, 'already_present': existing} | |
| def parse_args(): | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument('--apply', action='store_true', help='Write validated changes to MongoDB.') | |
| parser.add_argument('--sqlite', type=Path, default=AI_ROOT / 'anticheat.db') | |
| parser.add_argument('--json', type=Path, default=AI_ROOT / 'anticheat_hashes.json') | |
| return parser.parse_args() | |
| def get_collection(engine, apply_changes): | |
| if apply_changes: | |
| return engine._get_collection() | |
| if not engine.mongo_uri: | |
| raise AntiCheatUnavailable('ANTICHEAT_MONGO_URI is not configured.') | |
| try: | |
| client = MongoClient( | |
| engine.mongo_uri, | |
| serverSelectionTimeoutMS=5_000, | |
| connectTimeoutMS=5_000, | |
| socketTimeoutMS=5_000, | |
| ) | |
| client.admin.command('ping') | |
| return client[engine.db_name][engine.collection_name] | |
| except PyMongoError as error: | |
| raise AntiCheatUnavailable('Durable anti-cheat storage is unavailable.') from error | |
| def main(): | |
| args = parse_args() | |
| hashes, source_counts = validated_legacy_hashes(args.sqlite, args.json) | |
| engine = AntiCheatEngine() | |
| try: | |
| collection = get_collection(engine, args.apply) | |
| except AntiCheatUnavailable as error: | |
| print(json.dumps({'status': 'error', 'message': str(error)}, indent=2)) | |
| return 2 | |
| report = { | |
| 'mode': 'apply' if args.apply else 'dry-run', | |
| 'legacy_sources': dict(source_counts), | |
| 'legacy_import': migrate_legacy(collection, hashes, args.apply), | |
| 'bucket_backfill': backfill_buckets(collection, args.apply), | |
| 'source_files_removed': False, | |
| } | |
| print(json.dumps(report, indent=2, sort_keys=True)) | |
| return 0 | |
| if __name__ == '__main__': | |
| raise SystemExit(main()) | |