uchill/backend/apps/materials/services.py

234 lines
9.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Сервисы для работы с материалами и хранилищем.
"""
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),
}