добавил удаление таймлапсов
All checks were successful
Deploy timelaps / deploy (push) Successful in 5s
All checks were successful
Deploy timelaps / deploy (push) Successful in 5s
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
"""Сервисы для работы с камерами и storage: превью, автообнаружение, утилиты."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
@@ -11,6 +13,7 @@ logger = logging.getLogger('camlaps')
|
||||
|
||||
|
||||
def _storage_root() -> Path:
|
||||
"""Возвращает корень storage, в который смонтированы фото камер."""
|
||||
storage_path = getattr(settings, 'STORAGE_PATH', None)
|
||||
if storage_path is None:
|
||||
return Path('/app/storage')
|
||||
@@ -18,6 +21,7 @@ def _storage_root() -> Path:
|
||||
|
||||
|
||||
def generate_unique_camera_slug(directory_name: str, storage_path: str) -> str:
|
||||
"""Генерирует уникальный slug камеры для новых записей, избегая конфликтов."""
|
||||
base_slug = (slugify(directory_name) or f'camera-{directory_name.lower()}')[:70]
|
||||
slug = base_slug
|
||||
suffix = 1
|
||||
@@ -31,6 +35,7 @@ def generate_unique_camera_slug(directory_name: str, storage_path: str) -> str:
|
||||
|
||||
|
||||
def is_storage_available() -> bool:
|
||||
"""Проверяет, что storage примонтирован и доступен как директория."""
|
||||
logger.info('cameras:storage_available:start')
|
||||
try:
|
||||
root = _storage_root()
|
||||
@@ -43,6 +48,7 @@ def is_storage_available() -> bool:
|
||||
|
||||
|
||||
def list_active_cameras() -> list[Camera]:
|
||||
"""Возвращает список активных камер для отображения в UI."""
|
||||
logger.info('cameras:list_active:start')
|
||||
try:
|
||||
cameras = list(Camera.objects.filter(is_active=True).order_by('name'))
|
||||
@@ -54,6 +60,7 @@ def list_active_cameras() -> list[Camera]:
|
||||
|
||||
|
||||
def get_camera_lastsnap_path(camera: Camera) -> Path | None:
|
||||
"""Возвращает путь к lastsnap.jpg для камеры или None, если файла нет/путь небезопасен."""
|
||||
logger.info('cameras:lastsnap_path:start camera_id=%s', camera.id)
|
||||
try:
|
||||
root = _storage_root().resolve()
|
||||
@@ -75,6 +82,7 @@ def get_camera_lastsnap_path(camera: Camera) -> Path | None:
|
||||
|
||||
|
||||
def discover_camera_candidates() -> list[dict[str, str]]:
|
||||
"""Сканирует подпапки storage и возвращает кандидатов камер без записи в БД."""
|
||||
logger.info('cameras:discover_candidates:start')
|
||||
try:
|
||||
root = _storage_root()
|
||||
@@ -100,6 +108,7 @@ def discover_camera_candidates() -> list[dict[str, str]]:
|
||||
|
||||
|
||||
def create_cameras_from_candidates(selected_storage_paths: list[str]) -> int:
|
||||
"""Создает записи Camera в БД только для выбранных storage_path, возвращает количество созданных."""
|
||||
logger.info('cameras:create_from_candidates:start count=%s', len(selected_storage_paths))
|
||||
try:
|
||||
created = 0
|
||||
|
||||
52
camlaps/services/jobs.py
Normal file
52
camlaps/services/jobs.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Сервисы для операций над заданиями: удаление и чистка артефактов."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from ..models import TimelapseJob
|
||||
|
||||
logger = logging.getLogger('camlaps')
|
||||
|
||||
|
||||
def _resolve_output_video_path(job: TimelapseJob) -> Path | None:
|
||||
"""Возвращает абсолютный путь к mp4 из output_rel_path или None, если видео не задано/путь небезопасен."""
|
||||
rel = (job.output_rel_path or '').strip().lstrip('/')
|
||||
if not rel:
|
||||
return None
|
||||
|
||||
filename = Path(rel).name
|
||||
export_dir = Path(settings.TIMELAPS_EXPORT_DIR).resolve()
|
||||
candidate = (export_dir / filename).resolve()
|
||||
|
||||
if candidate != export_dir and export_dir not in candidate.parents:
|
||||
return None
|
||||
|
||||
return candidate
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def delete_job_and_artifacts(job_id: int) -> bool:
|
||||
"""Удаляет задачу и её видеофайл (если существует). Возвращает True, если файл видео был удалён."""
|
||||
logger.info('jobs:delete:start job_id=%s', job_id)
|
||||
|
||||
job = TimelapseJob.objects.select_for_update().filter(pk=job_id).first()
|
||||
if not job:
|
||||
logger.info('jobs:delete:done job_not_found=true job_id=%s', job_id)
|
||||
return False
|
||||
|
||||
video_path = _resolve_output_video_path(job)
|
||||
video_deleted = False
|
||||
|
||||
if video_path and video_path.exists():
|
||||
try:
|
||||
video_path.unlink()
|
||||
video_deleted = True
|
||||
except Exception:
|
||||
logger.exception('jobs:delete:video_unlink_error job_id=%s', job_id)
|
||||
|
||||
job.delete()
|
||||
logger.info('jobs:delete:done job_id=%s video_deleted=%s', job_id, video_deleted)
|
||||
return video_deleted
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Фоновый воркер сборки таймлапса: выбор кадров из storage и сборка видео через ffmpeg."""
|
||||
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
Reference in New Issue
Block a user