Удаление фото по расписанию из папки temp
This commit is contained in:
@@ -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',
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user