""" Corrige el mismatch de case entre el campo `archivo` en BD y los nombres reales de los objetos en MinIO. Causa habitual: transferencia de archivos de producción a local lowercaseó los filenames, pero la BD conserva los nombres originales con mayúsculas. Estrategia: para cada Document cuyo `archivo` no exista en MinIO con el nombre exacto, intenta el filename en minúsculas. Si lo encuentra, actualiza el campo en BD. Los archivos que ya coinciden no se tocan. Uso: python manage.py fix_archivo_case --pedimento --dry-run python manage.py fix_archivo_case --pedimento python manage.py fix_archivo_case --organizacion --dry-run python manage.py fix_archivo_case --organizacion """ import posixpath from django.core.management.base import BaseCommand, CommandError from api.customs.models import Pedimento from api.record.models import Document from api.utils.minio_client import minio_client class Command(BaseCommand): help = "Corrige mismatch de case entre campo archivo en BD y MinIO." def add_arguments(self, parser): parser.add_argument( "--pedimento", metavar="UUID", help="UUID del pedimento a corregir.", ) parser.add_argument( "--organizacion", metavar="UUID", help="UUID de la organización.", ) parser.add_argument( "--dry-run", action="store_true", help="Solo diagnóstico, sin aplicar cambios.", ) def handle(self, *args, **options): ped_id = options.get("pedimento") org_id = options.get("organizacion") dry_run = options["dry_run"] if dry_run: self.stdout.write(self.style.WARNING( "=== MODO PRUEBA (--dry-run): Sin cambios en BD ===\n" )) qs = Document.objects.all() if ped_id: try: ped = Pedimento.objects.get(id=ped_id) except Pedimento.DoesNotExist: raise CommandError(f"Pedimento {ped_id!r} no encontrado.") qs = qs.filter(pedimento=ped) self.stdout.write(f"Pedimento: {ped.pedimento_app}\n") elif org_id: qs = qs.filter(organizacion_id=org_id) total = qs.count() self.stdout.write(f"Documentos a revisar: {total}\n") ok = mismatch = not_found = 0 for doc in qs.iterator(chunk_size=500): name = doc.archivo.name if doc.archivo else None if not name: continue if minio_client.file_exists(name): ok += 1 continue lower_name = self._lower_filename(name) if lower_name == name: not_found += 1 continue if minio_client.file_exists(lower_name): mismatch += 1 self.stdout.write( f" {'[DRY]' if dry_run else '[FIX]'} doc {doc.id}:\n" f" BD : {name}\n" f" MinIO : {lower_name}\n" ) if not dry_run: doc.archivo.name = lower_name doc.save(update_fields=["archivo"]) else: not_found += 1 self.stdout.write( f"\n{'─' * 60}\nRESUMEN\n" f" Coinciden exacto : {ok}\n" f" Mismatch de case : {mismatch}\n" f" No encontrados : {not_found}\n" ) if dry_run and mismatch: self.stdout.write(self.style.WARNING( "\nEjecuta sin --dry-run para aplicar los cambios." )) elif not dry_run and mismatch: self.stdout.write(self.style.SUCCESS( f"\n{mismatch} registros actualizados en BD." )) def _lower_filename(self, name): """Lowercase solo el filename, preserva el path del directorio.""" dir_part = posixpath.dirname(name) filename = posixpath.basename(name) return posixpath.join(dir_part, filename.lower())