""" Management команда для очистки и оптимизации аватаров сотрудников. Функции: 1. Удаление неиспользуемых файлов аватаров 2. Сжатие существующих аватаров до оптимального размера """ import os from django.conf import settings from django.core.files.base import ContentFile from django.core.management.base import BaseCommand from common.image_utils import compress_avatar from employees.models import Employee class Command(BaseCommand): help = "Очистка неиспользуемых аватаров и оптимизация существующих" def add_arguments(self, parser): parser.add_argument( "--compress", action="store_true", help="Сжать существующие аватары", ) parser.add_argument( "--cleanup", action="store_true", help="Удалить неиспользуемые файлы", ) parser.add_argument( "--dry-run", action="store_true", help="Показать что будет сделано без реального выполнения", ) def handle(self, *args, **options): dry_run = options["dry_run"] do_compress = options["compress"] do_cleanup = options["cleanup"] # Если не указаны опции, делаем всё if not do_compress and not do_cleanup: do_compress = True do_cleanup = True if dry_run: self.stdout.write( self.style.WARNING("РЕЖИМ ПРОСМОТРА - изменения не применяются") ) # Получаем все используемые файлы аватаров employees = Employee.objects.exclude(avatar="").exclude(avatar=None) used_files = set() for emp in employees: if emp.avatar: try: used_files.add(os.path.basename(emp.avatar.path)) except (OSError, ValueError): continue self.stdout.write(f"Найдено {len(used_files)} используемых аватаров") # Очистка неиспользуемых файлов if do_cleanup: self.stdout.write("\n=== ОЧИСТКА НЕИСПОЛЬЗУЕМЫХ ФАЙЛОВ ===") # Используем абсолютный путь из settings avatar_dir = os.path.join(settings.MEDIA_ROOT, "users", "avatars") if not os.path.exists(avatar_dir): self.stdout.write( self.style.WARNING(f"Директория {avatar_dir} не найдена") ) else: deleted_count = 0 freed_mb = 0 for filename in os.listdir(avatar_dir): filepath = os.path.join(avatar_dir, filename) if not os.path.isfile(filepath): continue if filename not in used_files: size_mb = os.path.getsize(filepath) / (1024 * 1024) freed_mb += size_mb self.stdout.write(f" Удаление: {filename} ({size_mb:.2f} MB)") if not dry_run: try: os.remove(filepath) deleted_count += 1 except Exception as e: self.stdout.write( self.style.ERROR(f" Ошибка удаления: {e}") ) self.stdout.write( self.style.SUCCESS( f"\nУдалено файлов: {deleted_count}, " f"освобождено: {freed_mb:.2f} MB" ) ) # Сжатие существующих аватаров if do_compress: self.stdout.write("\n=== СЖАТИЕ СУЩЕСТВУЮЩИХ АВАТАРОВ ===") compressed_count = 0 saved_mb = 0 for emp in employees: if not emp.avatar: continue try: original_path = emp.avatar.path original_size = os.path.getsize(original_path) # Читаем оригинальный файл with open(original_path, "rb") as f: original_data = f.read() # Сжимаем compressed_data = compress_avatar(original_data) compressed_size = len(compressed_data) # Проверяем, дало ли сжатие выигрыш if compressed_size < original_size * 0.95: # минимум 5% size_diff = original_size - compressed_size saved = size_diff / (1024 * 1024) saved_mb += saved self.stdout.write( f" Сжатие: {emp.get_full_name()} " f"({original_size / 1024:.1f}KB → " f"{compressed_size / 1024:.1f}KB, " f"экономия {saved * 1024:.1f}KB)" ) if not dry_run: # Сохраняем сжатый файл filename = os.path.basename(original_path) if not filename.lower().endswith(".jpg"): filename = filename.rsplit(".", 1)[0] + ".jpg" emp.avatar.save( filename, ContentFile(compressed_data), save=True ) compressed_count += 1 except Exception as e: self.stdout.write( self.style.ERROR( f" Ошибка сжатия для {emp.get_full_name()}: {e}" ) ) self.stdout.write( self.style.SUCCESS( f"\nСжато файлов: {compressed_count}, " f"сэкономлено: {saved_mb:.2f} MB" ) ) if dry_run: self.stdout.write( self.style.WARNING("\nДля применения изменений запустите без --dry-run") )