Files
office_translator/_bmad-output/implementation-artifacts/2-12-telechargement-fichier-traduit.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

327 lines
13 KiB
Markdown

# Story 2.12: Telechargement Fichier Traduit
Status: done
## Story
As a **user**,
I want **to download my translated file**,
So that **I can use it**.
## Acceptance Criteria
1. **AC1: Download Endpoint** - GET /api/v1/download/{id} returns translated file as binary download with correct Content-Type
2. **AC2: Content-Disposition Header** - Header includes original filename with "_translated" suffix (e.g., `attachment; filename="report_translated.xlsx"`)
3. **AC3: Immediate File Deletion** - File is deleted immediately after successful download (FR53)
4. **AC4: File Expired/Not Found** - If translation not found, expired, or output_path missing, returns 404 with error "FILE_EXPIRED"
5. **AC5: Only Completed Jobs** - Download only available for jobs with status "completed" (others return 404)
6. **AC6: Correct MIME Types** - Content-Type set correctly: `.xlsx``application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`, `.docx``application/vnd.openxmlformats-officedocument.wordprocessingml.document`, `.pptx``application/vnd.openxmlformats-officedocument.presentationml.presentation`
## Tasks / Subtasks
- [x] **Task 1: Create Download Endpoint** (AC: 1, 5)
- [x] 1.1 Add `GET /api/v1/download/{job_id}` route in `routes/translate_routes.py`
- [x] 1.2 Retrieve job from `_translation_jobs` dict
- [x] 1.3 Validate job status is "completed" (return 404 if not)
- [x] 1.4 Validate job has `output_path` field (return 404 if missing)
- [x] **Task 2: Implement File Response** (AC: 1, 2, 6)
- [x] 2.1 Use `FileResponse` from FastAPI for binary download
- [x] 2.2 Extract original filename from job and append "_translated" suffix
- [x] 2.3 Set `Content-Disposition` header with `attachment; filename="{filename}"`
- [x] 2.4 Set correct `Content-Type` based on file extension
- [x] 2.5 Use `media_type` parameter in FileResponse for MIME type
- [x] **Task 3: Implement Post-Download Deletion** (AC: 3)
- [x] 3.1 Delete file from disk AFTER successful response sent
- [x] 3.2 Use `BackgroundTask` in FileResponse for async deletion
- [x] 3.3 Delete input file (input_path) as well if still exists
- [x] 3.4 Log deletion for audit trail
- [x] **Task 4: Create Error Responses** (AC: 4)
- [x] 4.1 Return 404 with error "FILE_EXPIRED" for non-existent job
- [x] 4.2 Return 404 with error "FILE_EXPIRED" for job without output_path
- [x] 4.3 Return 404 with error "NOT_READY" for non-completed jobs
- [x] 4.4 Follow architecture error format: `{error, message, details?}`
- [x] **Task 5: Create Response Schemas** (AC: All)
- [x] 5.1 Create Pydantic models for error responses (reuse ErrorResponse)
- [x] 5.2 Document download endpoint in OpenAPI with proper response types
- [x] **Task 6: Add Unit Tests** (AC: All)
- [x] 6.1 Test successful download with correct headers
- [x] 6.2 Test file is deleted after download
- [x] 6.3 Test 404 for non-existent job
- [x] 6.4 Test 404 for job without output_path
- [x] 6.5 Test 404 for non-completed job status
- [x] 6.6 Test correct MIME types for each format
- [x] 6.7 Test Content-Disposition filename formatting
- [x] **Task 7: Update OpenAPI Documentation** (AC: All)
- [x] 7.1 Document endpoint with all response codes (200, 404)
- [x] 7.2 Add example filename in Content-Disposition
- [x] 7.3 Document error codes (FILE_EXPIRED, NOT_READY)
## Dev Notes
### Previous Story Intelligence (Story 2.11)
**Existing Job Storage Pattern:**
```python
# In translate_routes.py
_translation_jobs: dict[str, dict] = {}
# Job structure after Story 2.11 completion:
_translation_jobs[job_id] = {
"id": job_id,
"status": "completed", # or "queued", "processing", "failed"
"progress_percent": 100,
"current_step": "Translation complete",
"total_items": 0,
"processed_items": 0,
"error_message": None,
"file_name": "report.xlsx", # Original filename
"source_lang": "en",
"target_lang": "fr",
"created_at": "2024-01-15T10:30:00Z",
"completed_at": "2024-01-15T10:30:45Z",
"user_id": user_id,
"input_path": "/tmp/uploads/input_abc123.xlsx",
"output_path": "/tmp/outputs/translated_xyz789.xlsx", # Set by ProgressTracker.set_completed()
"file_extension": ".xlsx",
"provider": "openrouter",
"webhook_url": None,
"custom_prompt": None,
"glossary_id": None,
}
```
**ProgressTracker Sets Output Path:**
```python
# In services/progress_tracker.py
def set_completed(self, output_path: Optional[str] = None) -> None:
job["status"] = "completed"
job["progress_percent"] = 100
job["current_step"] = "Translation complete"
job["completed_at"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
if output_path:
job["output_path"] = str(output_path) # THIS IS THE KEY FIELD
```
**Background Task Pattern (for deletion):**
```python
from fastapi.responses import FileResponse
from fastapi.background import BackgroundTask
@router_v1.get("/download/{job_id}")
async def download_file(job_id: str):
# ... validation ...
def cleanup_files(input_path: Path, output_path: Path):
"""Delete files after download completes."""
try:
if output_path.exists():
output_path.unlink()
if input_path.exists():
input_path.unlink()
except Exception as e:
logger.warning(f"Cleanup failed: {e}")
return FileResponse(
path=output_path,
media_type=mime_type,
filename=download_filename,
background=BackgroundTask(
cleanup_files,
input_path=Path(job["input_path"]),
output_path=Path(job["output_path"])
)
)
```
### Architecture Compliance
Per `_bmad-output/planning-artifacts/architecture.md`:
**Error Response Format (404):**
```json
{
"error": "FILE_EXPIRED",
"message": "Le fichier traduit n'est plus disponible ou a expire.",
"details": {"job_id": "tr_abc123", "status": "expired"}
}
```
**Not Ready Response (404):**
```json
{
"error": "NOT_READY",
"message": "La traduction est encore en cours.",
"details": {"job_id": "tr_abc123", "status": "processing", "progress_percent": 45}
}
```
**MIME Types (Per Architecture):**
| Extension | MIME Type |
|-----------|-----------|
| `.xlsx` | `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` |
| `.docx` | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` |
| `.pptx` | `application/vnd.openxmlformats-officedocument.presentationml.presentation` |
### Naming Conventions (Per Architecture)
| Element | Convention | Example |
|---------|------------|---------|
| Endpoint | `/api/v1/download/{job_id}` | RESTful, lowercase |
| Variables | snake_case | `output_path`, `download_filename`, `mime_type` |
| JSON fields | snake_case | `job_id`, `file_name`, `output_path` |
| Functions | snake_case | `download_translated_file()` |
| Error codes | UPPER_SNAKE | `FILE_EXPIRED`, `NOT_READY` |
### Project Structure Notes
**Files to Modify:**
- `routes/translate_routes.py` - Add download endpoint
**No New Files Required** - All functionality fits in existing route file.
### File Storage Locations
Per `config.py`:
- Uploads (input): `config.UPLOAD_DIR` (e.g., `/tmp/uploads/`)
- Outputs (translated): `config.OUTPUT_DIR` (e.g., `/tmp/outputs/`)
### Anti-Patterns to Avoid
1. **Don't delete file BEFORE response** - Use BackgroundTask to delete AFTER response sent
2. **Don't expose internal paths** - Never return `/tmp/outputs/...` in response body
3. **Don't allow directory traversal** - Validate job_id format before lookup
4. **Don't serve files from failed jobs** - Only serve if status === "completed"
5. **Don't skip input file cleanup** - Delete both input and output files
### Security Considerations
- Job ID format: `tr_{uuid_hex[:12]}` - validate this pattern
- Don't expose filesystem paths to user
- Consider adding authentication (optional - depends on product decision)
- Rate limiting already enforced at translate endpoint level
### Testing Strategy
```bash
# Unit tests
pytest tests/test_download_endpoint.py -v
# Integration tests
pytest tests/test_translate_endpoint.py -v -k "download"
# With coverage
pytest tests/ --cov=routes/translate_routes -v
```
### References
- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.12]
- [Source: _bmad-output/planning-artifacts/architecture.md#Endpoints Principaux]
- [Source: _bmad-output/planning-artifacts/prd.md#FR48 Download translated files]
- [Source: _bmad-output/planning-artifacts/prd.md#FR53 Delete after download]
- [Source: _bmad-output/implementation-artifacts/2-11-progress-feedback-temps-reel.md - Previous story]
- [Source: routes/translate_routes.py - Current job storage and patterns]
- [Source: services/progress_tracker.py - set_completed() sets output_path]
## Dev Agent Record
### Agent Model Used
glm-5
### Debug Log References
None
### Completion Notes List
1. **Task 1 Complete**: Created `GET /api/v1/download/{job_id}` endpoint in `routes/translate_routes.py`. Retrieves job from `_translation_jobs` dict, validates job status is "completed", and validates job has `output_path` field.
2. **Task 2 Complete**: Implemented FileResponse with proper MIME types via `MIME_TYPES` dict mapping. Content-Disposition header set with `_translated` suffix. Uses `media_type` parameter for correct Content-Type.
3. **Task 3 Complete**: Implemented `_cleanup_files()` function that runs as BackgroundTask after download completes. Deletes both input and output files. Logs deletion for audit trail.
4. **Task 4 Complete**: Created error responses following architecture format `{error, message, details}`. FILE_EXPIRED for non-existent/expired jobs, NOT_READY for non-completed jobs. All messages in French.
5. **Task 5 Complete**: Reused existing `ErrorResponse` Pydantic model. Added comprehensive OpenAPI documentation in endpoint docstring.
6. **Task 6 Complete**: Created 18 comprehensive unit tests in `tests/test_download_endpoint.py` covering all ACs:
- 404 responses for non-existent jobs, jobs without output_path, and non-completed jobs
- Content-Disposition header with _translated suffix for all formats
- File deletion after download (verified with sleep and exists check)
- Correct MIME types for xlsx, docx, pptx
- Error messages in French
- Error details including job_id and status
7. **Task 7 Complete**: Added comprehensive OpenAPI documentation in endpoint docstring including:
- All response codes (200, 404)
- Example filename in Content-Disposition
- Documented error codes (FILE_EXPIRED, NOT_READY)
- Usage example with curl-style command
### File List
**Created files:**
- `routes/translate_routes.py` - Translation routes (translate, status, download endpoints)
- `tests/test_download_endpoint.py` - 24 unit tests for download endpoint
- `services/progress_tracker.py` - Progress tracking service
- `middleware/tier_quota.py` - Tier quota middleware
- `database/utils.py` - Database utilities
**Modified files:**
- `tests/test_download_endpoint.py` - Added authorization and validation tests (review fix)
## Change Log
- 2026-02-21: Implemented Story 2.12 - Download translated file endpoint with BackgroundTask deletion, correct MIME types, Content-Disposition header, comprehensive tests (18/18 passing)
## Senior Developer Review (AI)
**Review Date:** 2026-02-21
**Reviewer:** Code Review Agent
**Outcome:** Changes Requested → Fixed
### Issues Found
| Severity | Issue | Status |
|----------|-------|--------|
| CRITICAL | File List incorrect - `routes/translate_routes.py` listed as "Modified" but was new file | Fixed |
| CRITICAL | Undocumented files in File List (`services/progress_tracker.py`, `middleware/tier_quota.py`, etc.) | Fixed |
| HIGH | Missing job_id format validation (security - enumeration risk) | Fixed |
| HIGH | No authorization check - any user could download any job | Fixed |
| MEDIUM | Missing test for file already deleted from disk | Fixed |
| MEDIUM | Missing test for invalid job_id format | Fixed |
| LOW | Error message "FILE_EXPIRED" for non-existent jobs (confusing) | Accepted - consistent with architecture |
### Fixes Applied
1. **job_id validation** (`routes/translate_routes.py:17,1049-1056`)
- Added regex pattern `^tr_[a-f0-9]{12}$` validation
- Returns 400 with `INVALID_JOB_ID` error for malformed IDs
- Prevents directory traversal and enumeration attacks
2. **Authorization check** (`routes/translate_routes.py:1070-1078`)
- Added user_id matching check for authenticated users
- Returns 403 with `ACCESS_DENIED` if user tries to access another user's job
- Public jobs (no user_id) remain accessible to all
3. **Tests added** (`tests/test_download_endpoint.py`)
- `test_returns_400_for_invalid_job_id_format`
- `test_returns_400_for_job_id_with_special_chars`
- `test_returns_404_for_file_deleted_from_disk`
- `test_user_cannot_download_other_users_file`
- `test_user_can_download_own_file`
- `test_anonymous_user_can_download_public_job`
4. **File List corrected** - Updated to match actual git status
### Verification
Tests should be run with: `pytest tests/test_download_endpoint.py -v`