184 lines
7.2 KiB
Python
184 lines
7.2 KiB
Python
"""
|
||
Management команда для очистки и оптимизации аватаров сотрудников.
|
||
|
||
Функции:
|
||
1. Удаление неиспользуемых файлов аватаров
|
||
2. Сжатие существующих аватаров до оптимального размера
|
||
"""
|
||
|
||
import os
|
||
from django.core.management.base import BaseCommand
|
||
from django.core.files.base import ContentFile
|
||
from django.conf import settings
|
||
from employees.models import Employee
|
||
from common.image_utils import compress_avatar
|
||
|
||
|
||
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 Exception:
|
||
pass
|
||
|
||
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'
|
||
)
|
||
)
|