feat: документация из Nextcloud, график у сотрудника, автоочистка фото, мониторинг, итог мастера, фильтр по местам; доп. сервисы в docker-compose
- docker-compose.yml: профили docs (Nextcloud), office (ONLYOFFICE Document Server), ai (Ollama + скачивание модели) - автонастройка: адреса Nextcloud, ONLYOFFICE, портала и модели подставляются из .env или по именам сервисов во внутренней сети Docker; значения из интерфейса важнее - локальная модель в своей сети (имя сервиса, частный IP) подключается без ключа API - .env.example, docs/DEPLOY.md (раздел 7а), README
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""Самоочистка сервера от старых фото нарядов и заданий.
|
||||
|
||||
Фото, которые нужно хранить дольше (у пункта чек-листа или задания отмечено «хранить более года»), с самого начала
|
||||
кладутся в отдельный каталог keep/ и очисткой не трогаются. Остальные удаляются, когда старше заданного числа дней
|
||||
(настройка photo_keep_days, 0 — не удалять). Проверка — раз в сутки из планировщика, вручную — из настроек сервера."""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import config, media, settings_store as ss
|
||||
from .models import Photo, TaskPhoto
|
||||
|
||||
log = logging.getLogger("photo_cleanup")
|
||||
KEEP = "keep/"
|
||||
EVERY = timedelta(hours=24)
|
||||
|
||||
|
||||
def days(db: Session) -> int:
|
||||
try:
|
||||
return max(0, int(ss.get(db, "photo_keep_days") or 0))
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
def _old(db: Session, model, cut: datetime):
|
||||
return db.scalars(select(model).where(model.created_at < cut, model.path.not_like(KEEP + "%"))).all()
|
||||
|
||||
|
||||
def run(db: Session, dry: bool = False, now: datetime | None = None) -> dict:
|
||||
"""Удалить старые фото (dry — только посчитать). Возвращает число фото и освобождённые байты."""
|
||||
n = days(db)
|
||||
if n <= 0:
|
||||
return {"enabled": False, "days": 0, "photos": 0, "bytes": 0}
|
||||
cut = (now or config.now()) - timedelta(days=n)
|
||||
root = ss.photo_root(db)
|
||||
order_ph, task_ph = _old(db, Photo, cut), _old(db, TaskPhoto, cut)
|
||||
out = {"enabled": True, "days": n, "before": config.iso(cut), "photos": len(order_ph) + len(task_ph),
|
||||
"bytes": sum(p.size or 0 for p in order_ph + task_ph)}
|
||||
if dry:
|
||||
return out
|
||||
for p in order_ph:
|
||||
(root / p.path).unlink(missing_ok=True)
|
||||
db.delete(p)
|
||||
for p in task_ph:
|
||||
media.remove(db, "tasks", p.path)
|
||||
db.delete(p)
|
||||
ss.set_(db, "photo_cleanup_at", config.now().isoformat())
|
||||
db.commit()
|
||||
if out["photos"]:
|
||||
log.warning("Удалено старых фото: %s (%.1f МБ), старше %s дней", out["photos"], out["bytes"] / 1048576, n)
|
||||
return out
|
||||
|
||||
|
||||
def due(db: Session) -> bool:
|
||||
if days(db) <= 0:
|
||||
return False
|
||||
at = ss.get(db, "photo_cleanup_at")
|
||||
try:
|
||||
return not at or config.now() - datetime.fromisoformat(at) >= EVERY
|
||||
except ValueError:
|
||||
return True
|
||||
Reference in New Issue
Block a user