Files
2026-hypn0-site/hypn0/hypn0_site/management/commands/rehash.py
T

106 lines
5.1 KiB
Python

from django.core.management.base import BaseCommand
from django.conf import settings
from hashids import Hashids
from hypn0_site.models import TbHypn0Item
class Command(BaseCommand):
"""
Проверка и синхронизация s_hash_id с текущими настройками HASHIDS_SALT.
АРХИТЕКТУРНОЕ НАЗНАЧЕНИЕ:
=========================
1. Изоляция сред (Dev / Stage / Prod) и ротация секретов:
В разных окружениях или при переносе базы данных значение HASHIDS_SALT может
отличаться от исходного.
2. Детерминированность и обратимость:
Hashids — это однозначное обратимое кодирование: pk <-> s_hash_id.
Если соль изменилась, старые s_hash_id в базе перестают соответствовать
текущей конфигурации проекта.
3. Назначение команды:
- Сверяет каждый s_hash_id в базе с тем, что должен генерироваться при текущем settings.HASHIDS_SALT.
- Выявляет битые, пустые или сгенерированные со старой солью идентификаторы.
- Безопасно обновляет s_hash_id без изменения первичных ключей (pk) и других полей картины.
"""
help = "Проверка целостности и перегенерация s_hash_id картин под текущий HASHIDS_SALT"
def add_arguments(self, parser):
parser.add_argument(
"--dry-run",
action="store_true",
help="Тестовый прогон: только проверить несоответствия без записи изменений в БД",
)
parser.add_argument(
"--batch-size",
type=int,
default=500,
help="Размер пакета для пакетного обновления (по умолчанию: 500)",
)
def handle(self, *args, **options):
dry_run = options["dry_run"]
batch_size = options["batch_size"]
salt = getattr(settings, "HASHIDS_SALT", "hypn0_default_salt")
min_length = getattr(settings, "HASHIDS_MIN_LENGTH", 6)
hasher = Hashids(salt=salt, min_length=min_length)
self.stdout.write(self.style.NOTICE(f"Старт проверки s_hash_id (соль: '{salt[:4]}***', min_length: {min_length})..."))
items = TbHypn0Item.objects.all().only("id", "s_hash_id")
total_items = items.count()
if total_items == 0:
self.stdout.write(self.style.WARNING("В базе данных нет картин для проверки."))
return
to_update = []
mismatched_count = 0
empty_count = 0
for item in items.iterator(chunk_size=batch_size):
expected_hash = hasher.encode(item.id)
if not item.s_hash_id:
empty_count += 1
item.s_hash_id = expected_hash
to_update.append(item)
elif item.s_hash_id != expected_hash:
mismatched_count += 1
self.stdout.write(
f"Несоответствие [ID={item.id}]: текущий='{item.s_hash_id}' -> ожидаемый='{expected_hash}'"
)
item.s_hash_id = expected_hash
to_update.append(item)
# Пакетное сохранение порциями
if not dry_run and len(to_update) >= batch_size:
TbHypn0Item.objects.bulk_update(to_update, fields=["s_hash_id"])
to_update.clear()
# Сохранение остатка
if not dry_run and to_update:
TbHypn0Item.objects.bulk_update(to_update, fields=["s_hash_id"])
total_issues = empty_count + mismatched_count
self.stdout.write("--------------------------------------------------")
self.stdout.write(f"Всего проверено записей: {total_items}")
self.stdout.write(f"Пустых s_hash_id: {empty_count}")
self.stdout.write(f"Несоответствующих s_hash_id: {mismatched_count}")
if total_issues == 0:
self.stdout.write(self.style.SUCCESS("Все s_hash_id в базе идеально соответствуют текущей соли!"))
else:
if dry_run:
self.stdout.write(
self.style.WARNING(f"[DRY RUN] Найдено {total_issues} несоответствий. Запустите без --dry-run для применения.")
)
else:
self.stdout.write(
self.style.SUCCESS(f"Успешно обновлено {total_issues} записей под текущую конфигурацию.")
)