mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-20 17:50:12 +02:00
feat(#1008): archive auto-purge + dedicated archives:purge permission
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.
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""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"])
|
||||
Reference in New Issue
Block a user