Удаление фото по расписанию из папки temp
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
"""
|
||||
Celery задачи для асинхронной обработки фото товаров.
|
||||
"""Celery задачи для асинхронной обработки фото товаров.
|
||||
|
||||
ВАЖНО: django-tenants мультитенантность!
|
||||
Все задачи получают schema_name и активируют нужную схему для изоляции данных.
|
||||
@@ -9,6 +8,8 @@ Celery задачи для асинхронной обработки фото т
|
||||
TenantAwareFileSystemStorage добавляет tenant_id при сохранении/удалении файлов.
|
||||
Безопасность: Каждый Celery worker активирует правильную схему БД через connection.set_schema().
|
||||
"""
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
from celery import shared_task
|
||||
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)
|
||||
except self.MaxRetriesExceededError:
|
||||
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 {
|
||||
'status': 'error',
|
||||
'reason': 'max_retries_exceeded',
|
||||
@@ -189,3 +203,110 @@ def process_multiple_photos_async(photo_ids, photo_model_class, schema_name):
|
||||
'group_id': result.id,
|
||||
'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)}
|
||||
|
||||
Reference in New Issue
Block a user