Удаление фото по расписанию из папки temp

This commit is contained in:
2025-11-30 21:20:45 +03:00
parent e5b37dcd81
commit e723c26e6c
2 changed files with 136 additions and 2 deletions

View File

@@ -211,6 +211,9 @@ MEDIA_ROOT = BASE_DIR / 'media'
# Custom file storage for tenant-aware file organization # Custom file storage for tenant-aware file organization
DEFAULT_FILE_STORAGE = 'products.utils.storage.TenantAwareFileSystemStorage' DEFAULT_FILE_STORAGE = 'products.utils.storage.TenantAwareFileSystemStorage'
# Время жизни временных файлов фото (TTL) до авто-удаления, в часах
TEMP_MEDIA_TTL_HOURS = 24
# ============================================ # ============================================
# IMAGE PROCESSING SETTINGS # IMAGE PROCESSING SETTINGS
@@ -398,6 +401,8 @@ CELERY_TIMEZONE = TIME_ZONE
CELERY_TASK_ROUTES = { CELERY_TASK_ROUTES = {
'products.tasks.process_product_photo_async': {'queue': 'photo_processing'}, 'products.tasks.process_product_photo_async': {'queue': 'photo_processing'},
'products.tasks.process_multiple_photos_async': {'queue': 'photo_processing'}, 'products.tasks.process_multiple_photos_async': {'queue': 'photo_processing'},
'products.tasks.cleanup_temp_media_for_schema': {'queue': 'photo_processing'},
'products.tasks.cleanup_temp_media_all': {'queue': 'photo_processing'},
} }
# Worker настройки для обработки длительных задач # Worker настройки для обработки длительных задач
@@ -430,4 +435,12 @@ CELERY_BEAT_SCHEDULE = {
'expires': 240, # Задача устаревает через 4 минуты (меньше интервала) 'expires': 240, # Задача устаревает через 4 минуты (меньше интервала)
}, },
}, },
# Очистка временных файлов фото каждые 6 часов
'cleanup-temp-media-all-tenants': {
'task': 'products.tasks.cleanup_temp_media_all',
'schedule': crontab(minute=0, hour='*/6'), # Каждые 6 часов
'options': {
'queue': 'photo_processing',
},
},
} }

View File

@@ -1,5 +1,4 @@
""" """Celery задачи для асинхронной обработки фото товаров.
Celery задачи для асинхронной обработки фото товаров.
ВАЖНО: django-tenants мультитенантность! ВАЖНО: django-tenants мультитенантность!
Все задачи получают schema_name и активируют нужную схему для изоляции данных. Все задачи получают schema_name и активируют нужную схему для изоляции данных.
@@ -9,6 +8,8 @@ Celery задачи для асинхронной обработки фото т
TenantAwareFileSystemStorage добавляет tenant_id при сохранении/удалении файлов. TenantAwareFileSystemStorage добавляет tenant_id при сохранении/удалении файлов.
Безопасность: Каждый Celery worker активирует правильную схему БД через connection.set_schema(). Безопасность: Каждый Celery worker активирует правильную схему БД через connection.set_schema().
""" """
import os
import time
import logging import logging
from celery import shared_task from celery import shared_task
from django.db import connection from django.db import connection
@@ -145,6 +146,19 @@ def process_product_photo_async(self, photo_id, photo_model_class, schema_name):
raise self.retry(exc=exc, countdown=60) raise self.retry(exc=exc, countdown=60)
except self.MaxRetriesExceededError: except self.MaxRetriesExceededError:
logger.error(f"[Celery] Max retries exceeded for photo {photo_id}. Task failed permanently.") logger.error(f"[Celery] Max retries exceeded for photo {photo_id}. Task failed permanently.")
# Попытка удалить temp файл при окончательном провале
try:
from .models.photos import PhotoProcessingStatus
status = (PhotoProcessingStatus.objects
.filter(photo_id=photo_id, photo_model=photo_model_class)
.order_by('-created_at')
.first())
temp_path = (status.result_data or {}).get('temp_path') if status else None
if temp_path and default_storage.exists(temp_path):
default_storage.delete(temp_path)
logger.info(f"[Celery] Deleted orphaned temp file (max_retries): {temp_path}")
except Exception as del_exc:
logger.warning(f"[Celery] Could not delete temp file for photo {photo_id} on max_retries: {del_exc}")
return { return {
'status': 'error', 'status': 'error',
'reason': 'max_retries_exceeded', 'reason': 'max_retries_exceeded',
@@ -189,3 +203,110 @@ def process_multiple_photos_async(photo_ids, photo_model_class, schema_name):
'group_id': result.id, 'group_id': result.id,
'schema_name': schema_name 'schema_name': schema_name
} }
@shared_task(name='products.tasks.cleanup_temp_media_for_schema')
def cleanup_temp_media_for_schema(schema_name, ttl_hours=None):
"""
Очистка временных файлов изображений для указанной схемы тенанта.
Удаляет файлы старше TTL из папок: products/temp, kits/temp, categories/temp.
Args:
schema_name: Имя схемы тенанта
ttl_hours: Время жизни файла в часах (по умолчанию из settings.TEMP_MEDIA_TTL_HOURS)
Returns:
dict: Результат очистки с количеством удаленных файлов
"""
from django.conf import settings
try:
# Активируем схему тенанта
connection.set_schema(schema_name)
ttl = int(ttl_hours or getattr(settings, 'TEMP_MEDIA_TTL_HOURS', 24))
cutoff_seconds = ttl * 3600
now = time.time()
temp_dirs = ['products/temp', 'kits/temp', 'categories/temp']
deleted_count = 0
scanned_count = 0
for rel_dir in temp_dirs:
try:
# Получаем полный путь с учётом tenant_id
full_dir = default_storage.path(rel_dir)
if not os.path.isdir(full_dir):
continue
for filename in os.listdir(full_dir):
scanned_count += 1
full_path = os.path.join(full_dir, filename)
# Пропускаем поддиректории, работаем только с файлами
if not os.path.isfile(full_path):
continue
# Проверяем возраст файла
age_seconds = now - os.path.getmtime(full_path)
if age_seconds >= cutoff_seconds:
# Формируем относительный путь для storage.delete
storage_rel_path = os.path.join(rel_dir, filename).replace('\\', '/')
try:
if default_storage.exists(storage_rel_path):
default_storage.delete(storage_rel_path)
deleted_count += 1
logger.info(f"[Cleanup:{schema_name}] Deleted: {storage_rel_path} (age: {age_seconds/3600:.1f}h)")
except Exception as del_exc:
logger.warning(f"[Cleanup:{schema_name}] Could not delete {storage_rel_path}: {del_exc}")
except Exception as dir_exc:
logger.warning(f"[Cleanup:{schema_name}] Error scanning {rel_dir}: {dir_exc}")
logger.info(f"[Cleanup:{schema_name}] Complete. scanned={scanned_count}, deleted={deleted_count}, ttl_hours={ttl}")
return {
'status': 'success',
'schema_name': schema_name,
'deleted': deleted_count,
'scanned': scanned_count,
'ttl_hours': ttl
}
except Exception as exc:
logger.error(f"[Cleanup:{schema_name}] Failed: {exc}", exc_info=True)
return {'status': 'error', 'schema_name': schema_name, 'error': str(exc)}
@shared_task(name='products.tasks.cleanup_temp_media_all')
def cleanup_temp_media_all(ttl_hours=None):
"""
Мастер-задача: перечисляет всех тенантов и запускает очистку temp для каждого.
Запускается периодически через Celery Beat.
Args:
ttl_hours: Время жизни файла в часах (передается в подзадачи)
Returns:
dict: Информация о количестве тенантов и запущенных задачах
"""
from django.conf import settings
try:
# Работаем из public для списка тенантов
connection.set_schema('public')
from tenants.models import Client
schemas = list(Client.objects.values_list('schema_name', flat=True))
ttl = ttl_hours or getattr(settings, 'TEMP_MEDIA_TTL_HOURS', 24)
logger.info(f"[CleanupAll] Scheduling cleanup for {len(schemas)} tenants (TTL: {ttl}h)")
for schema in schemas:
cleanup_temp_media_for_schema.delay(schema, ttl)
return {
'status': 'submitted',
'tenants_count': len(schemas),
'ttl_hours': ttl
}
except Exception as exc:
logger.error(f"[CleanupAll] Failed to schedule: {exc}", exc_info=True)
return {'status': 'error', 'error': str(exc)}