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