Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
190 lines
7.2 KiB
Markdown
190 lines
7.2 KiB
Markdown
# Story 2.15: Job Cleanup Fichiers (TTL 60 min)
|
|
|
|
Status: done
|
|
|
|
## Story
|
|
|
|
As a **system**,
|
|
I want **to automatically delete temporary files after 60 minutes**,
|
|
so that **disk space is managed and user data is not retained (RGPD compliance)**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **TTL Metadata**: Files are stored with TTL metadata (already in place from Story 2.14)
|
|
2. **Cleanup Interval**: The cleanup job runs every 5 minutes (configurable via `CLEANUP_INTERVAL_MINUTES`)
|
|
3. **Age-Based Deletion**: All files older than 60 minutes are hard-deleted from:
|
|
- `config.UPLOAD_DIR` (inputs)
|
|
- `config.OUTPUT_DIR` (outputs)
|
|
- `config.TEMP_DIR` (temporary files)
|
|
4. **Orphaned File Detection**: Files with no corresponding Redis/DB record are also deleted
|
|
5. **Structured Logging**: Cleanup is logged with: `files_deleted`, `bytes_freed_mb`, `cleanup_run_timestamp`
|
|
6. **Resilience**: Job continues even if individual file deletion fails (no crash on error)
|
|
7. **Zero Retention**: This ensures zero data retention (NFR15, NFR17)
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] **Task 1: Update Cleanup Interval Configuration** (AC: #2)
|
|
- [x] 1.1 Change default `CLEANUP_INTERVAL_MINUTES` from 15 to 5 in `config.py`
|
|
- [x] 1.2 Ensure environment variable override works: `CLEANUP_INTERVAL_MINUTES=5`
|
|
|
|
- [x] **Task 2: Add Orphaned File Detection** (AC: #4)
|
|
- [x] 2.1 In `FileCleanupManager.cleanup()`, add logic to detect orphaned files
|
|
- [x] 2.2 An orphaned file = file exists on disk but no Redis key matches it
|
|
- [x] 2.3 Use `StorageTracker` or scan Redis keys with pattern `translation:file:*`
|
|
- [x] 2.4 If file path not found in any tracked metadata, mark as orphan
|
|
- [x] 2.5 Delete orphaned files regardless of age (they're already lost)
|
|
|
|
- [x] **Task 3: Enhance Structured Logging** (AC: #5)
|
|
- [x] 3.1 Use `structlog` for cleanup logs (check if already integrated)
|
|
- [x] 3.2 Log format: `{"event": "cleanup_completed", "files_deleted": N, "bytes_freed_mb": X.XX, "orphaned_deleted": N, "cleanup_run_timestamp": ISO8601}`
|
|
- [x] 3.3 Log individual file deletion errors at warning level, not error (to avoid alert noise)
|
|
|
|
- [x] **Task 4: Verify TTL Configuration** (AC: #3, #7)
|
|
- [x] 4.1 Ensure `FILE_TTL_MINUTES = 60` is correctly used
|
|
- [x] 4.2 Verify `max_file_age_seconds` calculation uses config value
|
|
|
|
- [x] **Task 5: Add Unit & Integration Tests**
|
|
- [x] 5.1 Unit test: orphaned file detection logic
|
|
- [x] 5.2 Integration test: files older than TTL are deleted
|
|
- [x] 5.3 Integration test: cleanup continues after individual failure
|
|
- [x] 5.4 Test: logging output format
|
|
|
|
## Dev Notes
|
|
|
|
### 🔥 CRITICAL: Existing Implementation
|
|
|
|
**IMPORTANT**: A cleanup system already exists in `middleware/cleanup.py`. Your job is to ENHANCE it, not rebuild it.
|
|
|
|
**Current State (`FileCleanupManager`):**
|
|
- ✅ Periodic cleanup loop (`_cleanup_loop`) - runs every `cleanup_interval`
|
|
- ✅ Age-based deletion in `cleanup()` method
|
|
- ✅ TTL tracking via `track_file()` method
|
|
- ✅ Resilience: try/except around each file deletion
|
|
- ✅ Disk usage enforcement (`_enforce_size_limit`)
|
|
- ⚠️ Default interval is 10-15 minutes (needs to be 5)
|
|
- ⚠️ No explicit orphaned file detection
|
|
|
|
### Previous Story Intelligence (2.14)
|
|
|
|
Story 2.14 implemented:
|
|
- `StorageTracker` service in `services/storage_tracker.py` - tracks files in Redis
|
|
- `utils/file_handler.py` - has `cleanup_file()` method
|
|
- `routes/translate_routes.py` - triggers metadata logging after file save
|
|
- Redis key pattern: `translation:file:{job_id}` with 60 min TTL
|
|
|
|
### Architecture Compliance
|
|
|
|
**Naming Conventions:**
|
|
- Files: snake_case (e.g., `cleanup.py`)
|
|
- Classes: PascalCase (e.g., `FileCleanupManager`)
|
|
- Variables: snake_case (e.g., `files_deleted`, `bytes_freed`)
|
|
|
|
**Logging Pattern:**
|
|
```python
|
|
# With structlog (preferred)
|
|
logger.info(
|
|
"cleanup_completed",
|
|
files_deleted=5,
|
|
bytes_freed_mb=12.5,
|
|
orphaned_deleted=2
|
|
)
|
|
|
|
# Without structlog (fallback)
|
|
logger.info(f"Cleanup completed: files_deleted=5, bytes_freed_mb=12.5")
|
|
```
|
|
|
|
**Error Handling:**
|
|
```python
|
|
# NEVER crash on individual file error
|
|
try:
|
|
filepath.unlink()
|
|
except Exception as e:
|
|
logger.warning(f"Failed to delete {filepath}: {e}")
|
|
# Continue to next file
|
|
```
|
|
|
|
### Project Structure Notes
|
|
|
|
**Files to modify:**
|
|
- `config.py` - Update `CLEANUP_INTERVAL_MINUTES` default
|
|
- `middleware/cleanup.py` - Add orphan detection, enhance logging
|
|
- `services/storage_tracker.py` - May need method to list all tracked files
|
|
|
|
**Test location:**
|
|
- `tests/test_cleanup.py` (if exists) or create new
|
|
|
|
### Implementation Approach
|
|
|
|
1. **Start with config change** - simplest, high impact
|
|
2. **Add orphan detection** - requires Redis integration
|
|
3. **Enhance logging** - easy win
|
|
4. **Add tests** - verify everything works
|
|
|
|
### Orphaned File Detection Strategy
|
|
|
|
```python
|
|
async def cleanup(self) -> dict:
|
|
# ... existing age-based cleanup ...
|
|
|
|
# NEW: Orphaned file detection
|
|
redis_client = _get_async_redis()
|
|
if redis_client:
|
|
# Get all tracked file paths from Redis
|
|
tracked_paths = set()
|
|
keys = await redis_client.keys("translation:file:*")
|
|
for key in keys:
|
|
data = await redis_client.get(key)
|
|
if data:
|
|
metadata = json.loads(data)
|
|
if "file_path" in metadata:
|
|
tracked_paths.add(metadata["file_path"])
|
|
|
|
# Delete files not in Redis (orphans)
|
|
for directory in [self.upload_dir, self.output_dir, self.temp_dir]:
|
|
for filepath in directory.iterdir():
|
|
if str(filepath) not in tracked_paths:
|
|
# This is an orphan - delete it
|
|
filepath.unlink()
|
|
```
|
|
|
|
### References
|
|
|
|
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.15]
|
|
- [Source: _bmad-output/planning-artifacts/prd.md#NFR15, NFR17]
|
|
- [Source: _bmad-output/planning-artifacts/architecture.md#File Management]
|
|
- [Source: config.py#CLEANUP_ENABLED, CLEANUP_INTERVAL_MINUTES, FILE_TTL_MINUTES]
|
|
- [Source: middleware/cleanup.py#FileCleanupManager]
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
{{agent_model_name_version}}
|
|
|
|
### Completion Notes List
|
|
|
|
- ✅ Updated `CLEANUP_INTERVAL_MINUTES` to 5 in `config.py` and `middleware/cleanup.py`
|
|
- ✅ Implemented orphaned file detection using Redis scan in `FileCleanupManager.cleanup()`
|
|
- ✅ Added structured logging with `structlog` for cleanup runs
|
|
- ✅ Refactored `FileCleanupManager` to use `max_file_age_minutes` instead of hours for consistency
|
|
- ✅ Fixed a NameError in `services/storage_tracker.py` related to `DEFAULT_TTL`
|
|
- ✅ Verified with 5 tests in `tests/test_cleanup.py` (TTL, orphans, resilience, logging, config)
|
|
|
|
### Code Review Fixes Applied
|
|
|
|
- 🔧 Fixed test imports to avoid middleware/__init__.py dependency (FastAPI)
|
|
- 🔧 Rewrote `test_logging_format` with actual assertions (was fake test)
|
|
- 🔧 Rewrote `test_cleanup_resilience` with proper resilience verification
|
|
- 🔧 Added `test_cleanup_interval_env_override` for AC#2
|
|
- 🔧 Added `test_redis_unavailable_graceful` for fallback behavior
|
|
- 🔧 Added `MAX_TOTAL_SIZE_GB` to `config.py` (was missing)
|
|
- 🔧 Added warning log when Redis unavailable for orphan detection
|
|
- 🔧 Fixed race condition in cleanup by collecting files before iteration
|
|
|
|
### File List
|
|
|
|
- `config.py`
|
|
- `middleware/cleanup.py`
|
|
- `services/storage_tracker.py`
|
|
- `tests/test_cleanup.py`
|