""" Сервисы для работы с материалами и хранилищем. """ import logging from django.db import transaction from django.utils import timezone from datetime import timedelta from .models import Material, StorageQuota from apps.subscriptions.models import Subscription from apps.notifications.services import NotificationService logger = logging.getLogger(__name__) class StorageService: """Сервис для работы с хранилищем.""" @staticmethod def get_storage_limit_bytes(user): """ Получить лимит хранилища в байтах для пользователя. Args: user: Пользователь Returns: int: Лимит в байтах """ # Получаем активную подписку subscription = Subscription.objects.filter( user=user, status='active' ).select_related('plan').first() if subscription and subscription.plan: # Лимит из подписки (в МБ, переводим в байты) return subscription.plan.max_storage_mb * 1024 * 1024 else: # Базовый лимит для пользователей без подписки (100 МБ) return 100 * 1024 * 1024 @staticmethod def sync_quota_with_subscription(user): """ Синхронизировать квоту хранилища с подпиской пользователя. Args: user: Пользователь """ quota, created = StorageQuota.objects.get_or_create(user=user) new_limit = StorageService.get_storage_limit_bytes(user) if quota.total_quota != new_limit: quota.total_quota = new_limit quota.save(update_fields=['total_quota']) logger.info(f'Storage quota synced for user {user.id}: {new_limit / (1024*1024):.2f} MB') @staticmethod def check_storage_limit(user, file_size): """ Проверить, можно ли загрузить файл указанного размера. Args: user: Пользователь file_size: Размер файла в байтах Returns: tuple: (can_upload: bool, error_message: str or None, warning_message: str or None) """ # Синхронизируем квоту с подпиской StorageService.sync_quota_with_subscription(user) quota, created = StorageQuota.objects.get_or_create(user=user) # Проверяем достаточно ли места if not quota.has_space(file_size): available_mb = quota.get_available_space() / (1024 * 1024) needed_mb = file_size / (1024 * 1024) total_mb = quota.total_quota / (1024 * 1024) used_mb = quota.used_space / (1024 * 1024) return False, f'Недостаточно места для загрузки файла ({needed_mb:.2f} МБ). Доступно: {available_mb:.2f} МБ из {total_mb:.2f} МБ (использовано: {used_mb:.2f} МБ)', None # Проверяем предупреждение о приближении к лимиту (80% и выше) used_percentage = quota.get_used_percentage() if used_percentage >= 80: available_mb = quota.get_available_space() / (1024 * 1024) total_mb = quota.total_quota / (1024 * 1024) warning = f'Внимание: используется {used_percentage:.1f}% хранилища. Осталось {available_mb:.2f} МБ из {total_mb:.2f} МБ' return True, None, warning return True, None, None @staticmethod def add_file_usage(user, file_size): """ Добавить использование хранилища. Args: user: Пользователь file_size: Размер файла в байтах """ quota, created = StorageQuota.objects.get_or_create(user=user) quota.add_usage(file_size) # Проверяем и отправляем уведомления о приближении к лимиту StorageService._check_and_notify_quota_warnings(user, quota) @staticmethod def remove_file_usage(user, file_size): """ Убрать использование хранилища. Args: user: Пользователь file_size: Размер файла в байтах """ quota, created = StorageQuota.objects.get_or_create(user=user) quota.remove_usage(file_size) @staticmethod def _check_and_notify_quota_warnings(user, quota): """ Проверить квоту и отправить уведомления при необходимости. Args: user: Пользователь quota: Объект StorageQuota """ used_percentage = quota.get_used_percentage() # Уведомление при 90% использования if used_percentage >= 90: NotificationService.create_notification_with_telegram( recipient=user, notification_type='system', title='⚠️ Хранилище почти заполнено', message=f'Использовано {used_percentage:.1f}% хранилища ({quota.used_space / (1024*1024):.2f} МБ из {quota.total_quota / (1024*1024):.2f} МБ). Рекомендуется освободить место.', priority='high', action_url='/materials' ) # Уведомление при 80% использования (только если еще не отправляли) elif used_percentage >= 80: # Проверяем, не отправляли ли уже уведомление за последний час from apps.notifications.models import Notification recent_notification = Notification.objects.filter( recipient=user, notification_type='system', title__icontains='Хранилище', created_at__gte=timezone.now() - timedelta(hours=1) ).exists() if not recent_notification: NotificationService.create_notification_with_telegram( recipient=user, notification_type='system', title='📦 Хранилище заполняется', message=f'Использовано {used_percentage:.1f}% хранилища ({quota.used_space / (1024*1024):.2f} МБ из {quota.total_quota / (1024*1024):.2f} МБ).', priority='normal', action_url='/materials' ) @staticmethod def get_storage_stats(user): """ Получить статистику использования хранилища. Args: user: Пользователь Returns: dict: Статистика хранилища """ StorageService.sync_quota_with_subscription(user) quota, created = StorageQuota.objects.get_or_create(user=user) # Получаем информацию о подписке subscription = Subscription.objects.filter( user=user, status='active' ).select_related('plan').first() plan_name = subscription.plan.name if subscription and subscription.plan else 'Без подписки' return { 'total_quota_mb': quota.total_quota / (1024 * 1024), 'used_space_mb': quota.used_space / (1024 * 1024), 'available_space_mb': quota.get_available_space() / (1024 * 1024), 'used_percentage': quota.get_used_percentage(), 'plan_name': plan_name, 'materials_count': Material.objects.filter(owner=user, is_deleted=False).count(), } @staticmethod def cleanup_old_unused_files(user, days_old=90): """ Автоматическая очистка старых неиспользуемых файлов. Args: user: Пользователь days_old: Возраст файлов в днях для удаления (по умолчанию 90) Returns: dict: Статистика очистки """ cutoff_date = timezone.now() - timedelta(days=days_old) # Находим старые материалы, которые не просматривались и не скачивались old_materials = Material.objects.filter( owner=user, is_deleted=False, created_at__lt=cutoff_date, views_count=0, downloads_count=0 ) total_size = 0 deleted_count = 0 for material in old_materials: if material.file: file_size = material.file_size or 0 material.soft_delete() StorageService.remove_file_usage(user, file_size) total_size += file_size deleted_count += 1 return { 'deleted_count': deleted_count, 'freed_space_mb': total_size / (1024 * 1024), }