mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-22 03:49:18 +02:00
Adds an archive counterpart to the library trash sweeper shipped in the
previous commit. Unlike the library flow, archives are hard-deleted —
print history is a decaying timeline, so there is no trash intermediate;
download or favourite anything you want to keep first.
Backend
- New ArchivePurgeService (backend/app/services/archive_purge.py) with
its own 15-minute scheduler loop and a 24h throttle on actual purge
runs. Delegates every delete to the existing safety-checked
ArchiveService.delete_archive so the 3MF, thumbnail, timelapse, source
3MF, F3D, and photo folder all get cleaned up together with the DB
row. Per-row session via async_session() avoids commit-per-row churn
on any caller-passed session.
- New /archives/purge/{preview,settings} + POST /archives/purge routes
gated on a dedicated archives:purge permission (not archives:delete_all)
so admins can delegate bulk-delete to a role without granting
per-archive delete on other users' rows.
- seed_default_groups() now backfills both library:purge and
archives:purge on the Administrators group for upgraded installs —
the original library:purge was added after Administrators was first
seeded so the "create if not exists" path skipped existing DBs and
left admins without the permission.
- 8 new integration tests (defaults, settings roundtrip, bound
validation, preview, manual purge, auto-purge enabled path, 24h
throttle, disabled skip).
Frontend
- Settings → Archives card gains an auto-purge toggle + age input (7d
floor, 10y ceiling, 365d default), with a save-toast on every change.
The bulk "Purge old" button lives on the Archives page header
(rightmost, after Upload 3MF) to match the File Manager pattern —
configuration in Settings, one-shot action on the page.
- New PurgeArchivesModal mirrors PurgeOldFilesModal: live preview (count
+ total size freed + sample filenames) debounced at 300ms, amber
"hard-delete, no undo" warning.
- Admin-only UI gates on archives:purge via the standard hasPermission
hook; Permission TS union updated.
- i18n blocks across all 8 locales (en/de full, other 6 English
fallback per project convention).
Docs
- CHANGELOG entry under 0.2.4b1 following the existing library-trash
entry.
- bambuddy-wiki archiving.md gains a new "Auto-Purge" section.
- bambuddy-website features.html gets a matching bullet.
Verification: python -m ruff check backend/app/ clean; 25 integration
tests pass (8 archive_purge + 17 library_trash regression); npm run
build clean.
82 lines
3.0 KiB
Python
82 lines
3.0 KiB
Python
"""Archive auto-purge endpoints (#1008 follow-up).
|
|
|
|
Admin-only (``ARCHIVES_PURGE``). Provides:
|
|
|
|
* ``GET /archives/purge/preview`` — live count for the admin slider
|
|
* ``POST /archives/purge`` — one-shot manual bulk delete
|
|
* ``GET/PUT /archives/purge/settings`` — auto-purge toggle + threshold
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.core.auth import require_permission_if_auth_enabled
|
|
from backend.app.core.database import get_db
|
|
from backend.app.core.permissions import Permission
|
|
from backend.app.models.user import User
|
|
from backend.app.schemas.archive_purge import (
|
|
ArchivePurgePreviewResponse,
|
|
ArchivePurgeRequest,
|
|
ArchivePurgeResponse,
|
|
ArchivePurgeSettings,
|
|
)
|
|
from backend.app.services.archive_purge import (
|
|
MAX_AUTO_PURGE_DAYS,
|
|
MIN_AUTO_PURGE_DAYS,
|
|
archive_purge_service,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/archives", tags=["archives-purge"])
|
|
|
|
|
|
@router.get("/purge/preview", response_model=ArchivePurgePreviewResponse)
|
|
async def preview_archive_purge(
|
|
older_than_days: int = Query(ge=1, le=3650),
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
"""Count + size of archives eligible for purge. Read-only."""
|
|
result = await archive_purge_service.preview_purge(db, older_than_days=older_than_days)
|
|
return ArchivePurgePreviewResponse(**result)
|
|
|
|
|
|
@router.post("/purge", response_model=ArchivePurgeResponse)
|
|
async def execute_archive_purge(
|
|
body: ArchivePurgeRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
"""Hard-delete archives older than the threshold. Irreversible."""
|
|
deleted = await archive_purge_service.purge_older_than(db, older_than_days=body.older_than_days)
|
|
return ArchivePurgeResponse(deleted=deleted)
|
|
|
|
|
|
@router.get("/purge/settings", response_model=ArchivePurgeSettings)
|
|
async def get_archive_purge_settings(
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
cfg = await archive_purge_service.get_settings(db)
|
|
return ArchivePurgeSettings(enabled=cfg["enabled"], days=cfg["days"])
|
|
|
|
|
|
@router.put("/purge/settings", response_model=ArchivePurgeSettings)
|
|
async def update_archive_purge_settings(
|
|
body: ArchivePurgeSettings,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
if body.days < MIN_AUTO_PURGE_DAYS or body.days > MAX_AUTO_PURGE_DAYS:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"days must be between {MIN_AUTO_PURGE_DAYS} and {MAX_AUTO_PURGE_DAYS}",
|
|
)
|
|
saved = await archive_purge_service.set_settings(db, enabled=body.enabled, days=body.days)
|
|
return ArchivePurgeSettings(enabled=saved["enabled"], days=saved["days"])
|