Endpoint: Full Backup (racks/api/export/backup.py)
Propósito
Endpoint GET que descarga un ZIP con toda la configuración de una organización (datos + uploads), scoped solo a los ficheros referenciados.
Ruta: /api/racks/backup/full/ (GET)
Acceso: Requiere ser admin de la org (check is_admin())
Comportamiento
Entrada: Request HTTP GET
Salida: FileResponse con ZIP (streaming, no carga en RAM)
Operación:
- Verifica que el usuario sea admin de su org
- Recolecta toda la configuración de la org (via serialización interna)
- Empaqueta uploads: solo los ficheros referenciados por stencils/blueprints
- Via
collect_org_upload_files(org_upload_image_paths(backup_data)) - Dedup + anti-path-escape
- Via
- Escribe ZIP en streaming
- Retorna ZIP como descarga con header
Content-Disposition: attachment
Seguridad
Scoping por organización:
- Solo la org actual (sacada de
get_current_org(request)) - Upload filtering: solo ficheros referenciados (cierra fuga cross-tenant s107)
- Path escape guarding:
collect_org_upload_files()rechaza../../../...
Permisos:
- Admin check:
is_admin(request)debe ser True - Levanta 403 si no es admin
Payload de datos
El ZIP contiene:
backup_data.json
{
"organization": { ... },
"users": [ ... ],
"stencils": [
{
"id": 1,
"name": "Stencil 1",
"image_path": "uploads/stencils/s1.png",
...
}
],
"blueprints": [
{
"id": 1,
"name": "Blueprint 1",
"image_path": "uploads/blueprints/bp1.png",
...
}
],
"devices": [ ... ],
"racks": [ ... ],
...
}
Uploads (solo referenciados)
uploads/
stencils/
s1.png (referenciado por stencil.id=1)
s2.png (referenciado por stencil.id=2)
blueprints/
bp1.png (referenciado por blueprint.id=1)
⚠️ Antes del fix s107: empaquetaba TODA la carpeta uploads/ (todos los tenants)
Implementación
Archivo: racks/api/export/backup.py
Función: backup_full(request)
Locación: línea ~35
Cambio s107:
- Antes:
os.walk(MEDIA_ROOT/uploads)→ todos los ficheros - Después:
collect_org_upload_files(org_upload_image_paths(backup_data))→ solo referenciados
Streaming: Usa FileResponse con archivo ZIP generado en el filesystem (no carga todo en RAM)
- Evita el guard
PayloadBudgetMiddleware(hard_limit 5 MB para responses)
Flujo completo
GET /api/racks/backup/full/is_admin(request)→ True- Serializa org + modelos →
backup_data - Llama
generate_backup(org)que:- Escribe
backup_data.json - Colecta uploads via
collect_org_upload_files() - Escribe ZIP
- Escribe
- Retorna ZIP como
FileResponse
Lado complementario
Restauración: racks/api/export/restore.py::restore_full(request, file)
- Acepta ZIP generado por
backup_full() - Restaura JSON + uploads (espera formato
uploads/...) - Scoped a la org del usuario que restaura
Dependencias
Servicios:
core.services.backup_service::generate_backup()— generación del ZIPcore.services.backup_service::collect_org_upload_files()— scoping de uploadscore.services.backup_service::org_upload_image_paths()— extracción de paths
Middlewares:
get_current_org(request)— org del usuariois_admin(request)— check de permisos
Utilidades:
FileResponse(Django) — streamingzipfile— escritura de ZIP
Tests
Archivo: tests/api/test_backup.py
Cubre:
- Normalización de rutas de uploads
- Dedup de uploads
- Path escape blocking
- Cross-tenant scoping (el upload de orgB NO está en backup de orgA)
Véase también
- [[feature—racks—full-backup-tenant-scoping]]
- [[entity—core—service—backup-service]]
- [[entity—racks—api—restore-full]]
- [[entity—racks—model—stencil]]
- [[entity—racks—model—blueprint]]
- [[entity—core—api—is-admin]]