Files
office_translator/_bmad-output/implementation-artifacts/2-15-job-cleanup-fichiers-ttl-60-min.md
Sepehr Ramezani 26bd096a06 feat: production deployment - full update with providers, admin, glossaries, pricing, tests
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>
2026-04-25 15:01:47 +02:00

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`