# Documentación técnica — Integra Drive (servidor Flask)

Este documento describe **toda la arquitectura del backend y del frontend web** del proyecto **Integra Drive**, con foco en que una IA (o un equipo) pueda **implementar la app Android** alineada con el comportamiento real del servidor: mismas rutas, mismos cuerpos JSON, mismas cookies de sesión y mismos flujos de subida (simple y multipart).

---

## Tabla de contenidos

0. [Changelog reciente (Android / IA)](#changelog-reciente-android--ia)
1. [Visión general](#1-visión-general)
2. [Estructura de carpetas del repositorio](#2-estructura-de-carpetas-del-repositorio)
3. [Arranque, WSGI y entorno](#3-arranque-wsgi-y-entorno)
4. [Configuración (`app/config.py` y `.env`)](#4-configuración-appconfigpy-y-env)
5. [Base de datos MySQL](#5-base-de-datos-mysql)
6. [Capa de persistencia: `Database`](#6-capa-de-persistencia-database)
7. [Capa de almacenamiento: `Storage`](#7-capa-de-almacenamiento-storage)
8. [Modelos (`app/models/`)](#8-modelos-appmodels)
9. [Seguridad y formato](#9-seguridad-y-formato)
10. [Middleware y errores HTTP](#10-middleware-y-errores-http)
11. [Aplicación Flask (`app/__init__.py`)](#11-aplicación-flask-app__init__py)
12. [Blueprint web — vistas HTML](#12-blueprint-web--vistas-html)
13. [Blueprint `auth` — autenticación](#13-blueprint-auth--autenticación)
14. [Blueprint `files` — API y archivos](#14-blueprint-files--api-y-archivos)
15. [Subida de archivos (detalle para Android)](#15-subida-de-archivos-detalle-para-android)
16. [JSON común de archivos (`_file_item_json`)](#16-json-común-de-archivos-_file_item_json)
17. [Frontend web: HTML (Jinja)](#17-frontend-web-html-jinja)
18. [Frontend web: JavaScript (`main.js`)](#18-frontend-web-javascript-mainjs)
19. [Frontend web: CSS](#19-frontend-web-css)
20. [Despliegue: Nginx (`deploy/`)](#20-despliegue-nginx-deploy)
21. [Guía para la app Android](#21-guía-para-la-app-android)
22. [Paridad producto web ↔ cliente móvil](#22-paridad-producto-web--cliente-móvil)
23. [`GET /api/config` — capacidades del servidor](#23-get-apiconfig--capacidades-del-servidor)
24. [Encaje con `integra_mobile` (estado actual aproximado)](#encaje-integra-mobile)

---

## Changelog reciente (Android / IA)

<a id="changelog-reciente-android--ia"></a>

*Resumen de cambios en servidor y documentación que afectan a la app móvil o a la interpretación del contrato API. Si implementas Android, lee esto primero.*

### API y backend Flask

| Cambio | Qué permite hacer la app / la IA ahora |
|--------|----------------------------------------|
| **`GET /api/config`** (pública, sin cookies) | Conocer `storage_driver`, si existe **`multipart_upload_available`**, y los umbrales **`multipart_client_min_bytes`**, **`multipart_part_size_hint_bytes`**, **`multipart_presign_expires_seconds`** sin hardcodear. Incluye un objeto **`api`** con rutas de referencia (`list_trash_json`, `list_shared_json`, auth). **Acción**: llamar al inicio (o al cambiar de servidor) y decidir monolítico vs multipart como `main.js`. |
| **`GET /files/?trash=1`** (con sesión) | Listar **papelera en JSON** (`folders: []`, archivos borrados lógicamente, clave opcional **`"view": "trash"`**). Acepta `trash=true` / `trash=yes`. **No** usar `?view=trash` (no existe). **Acción**: pantalla papelera = esta URL + cookies, no parsear HTML de `GET /trash`. |
| Documentación **§14.1 / §22 / §23** | Deja explícito: compartidos conmigo = **`GET /files/shared-with-me`**; no hay `?view=shared` en `/files/`. |
| **`POST /files/multipart/list-parts`** y **`POST /files/multipart/abort`** | Reanudar / inspeccionar partes ya subidas a S3 y cancelar una subida multipart huérfana. **Acción**: multipart resumible y limpieza en errores (ver §15.2). |
| **`login_required`** con **401 JSON** | Si el cliente envía **`Accept: application/json`** o **`X-Requested-With: XMLHttpRequest`** y no hay sesión, respuesta **401** + `{"error": "No autenticado..."}` en lugar de redirect HTML. **Acción**: mismas cabeceras que ya recomienda la guía (§13.4, §21.2). |
| **`Config`**: `SESSION_COOKIE_*`, `MULTIPART_*` en `.env` | Producción HTTPS puede fijar cookies seguras; TTL de URLs firmadas de partes configurable. **Acción**: documentar en despliegue, no en la app. |
| **`INTEGRA_LOG_FILE`** + **`INTEGRA_LOG_LEVEL`** (`INFO` por defecto) | Log rotativo del **Flask app.logger**: **`POST /files/upload`** (`upload start` / `upload ok` / `upload failed`); **`POST /files/multipart/init`** (`multipart init start` / `multipart init ok` / `multipart init failed`); **`multipart/complete`** (inicio, éxito, fallo S3). **Acción Android / IA**: en subidas **grandes con S3** no esperes `upload start` (van por multipart); en el servidor **grep** `multipart init` y `multipart complete`. |
| **413 en `error_middleware`** | Se registra **`http_413`** con `path`, `content_length`, IP y user-agent para distinguir límite Flask vs proxy. |

### Cliente web (`main.js`)

| Cambio | Efecto |
|--------|--------|
| **`uploadMultipartResumable`** | Si `storageDriver === "s3"` y el archivo ≥ umbral (`multipartClientMinBytes`), la web sube por **init → sign-part → PUT a S3 → complete**, con reintentos en cada parte, **`list-parts`** para reanudar, **`abort`** si falla antes de `complete` (no aborta si falla solo `complete`). **Acción Android**: replicar este flujo cuando `/api/config` indique multipart y tamaño adecuado. |

### Almacenamiento S3 (`app/core/storage.py`)

| Cambio | Efecto |
|--------|--------|
| Cliente boto3 con **timeouts largos** y reintentos | Subidas multipart servidor→MinIO más estables. |
| **`list_multipart_parts`** / **`abort_multipart_upload`** | Soporte backend para los endpoints JSON anteriores. |
| **`sign_multipart_part`** con **`ExpiresIn`** desde config | Antes ~15 min; ahora configurable (**default 86400 s** vía `MULTIPART_PRESIGN_EXPIRES_SECONDS`). |
| **`TransferConfig`** en **`upload_fileobj`** (subida monolítica a S3) | Multipart interno del SDK (umbral/chunk 8 MiB, concurrencia 8) para que un ISO vaya a MinIO en trozos en lugar de una sola petición gigante al API S3. |

### Modelo de datos

| Cambio | Efecto |
|--------|--------|
| **`MultipartUploadSession.mark_aborted`** | Marca sesiones multipart como `aborted` en BD tras cancelar en S3. |

### Despliegue Nginx (`deploy/drive.romdevs.com.conf`)

| Cambio | Efecto |
|--------|--------|
| **`location /`** y **`location ^~ /cloud-remote/`** | Además de **`client_max_body_size 0`** y **`proxy_request_buffering off`**: **`client_body_timeout`** y **`send_timeout`** en **86400s** (el default ~60 s de nginx corta subidas **lentas** parte a parte hacia MinIO aunque no haya límite de tamaño). **`/cloud-remote/`**: mismos timeouts de cuerpo + **`proxy_read_timeout` / `proxy_send_timeout`** alineados a 86400s (antes el proxy a MinIO podía usar 300 s). **Acción**: `nginx -t` + reload en el servidor; la app Android se beneficia igual que la web (mismos PUT y mismo host). |

### Documentación (este `.md`)

| Cambio | Efecto |
|--------|--------|
| Secciones **§22** (paridad web ↔ móvil) y **§23** (`/api/config`) | Tabla de qué pantalla web corresponde a qué ruta JSON; contrato explícito de config pública. |
| **§15.1**, **§21** ampliados | Riesgo de solo `POST /files/upload` en S3 con archivos enormes; uso de **`/api/config`** alineado con la web. |
| **§21.4** iconos | Diferencia entre **`icon_class`** del servidor (Font Awesome / BD) e iconos locales por MIME. |
| **§20.1** (diagnóstico subidas) | Checklist nginx (`client_body_timeout`, 413, 502), Gunicorn / disco y **`INTEGRA_LOG_FILE`** (incluye multipart **init**). |

### Diagnóstico si “sigue fallando” la subida grande (operación)

El repositorio Git **no contiene** `access.log` ni `error.log` del servidor: hay que mirar el **host** donde corre Nginx + Gunicorn.

1. **`INTEGRA_LOG_FILE`**: con Gunicorn reiniciado: subida **monolítica** → **`upload start`** luego **`upload ok`** / **`upload failed`**; subida **multipart S3** → **`multipart init start`** / **`multipart init ok`** (y al cerrar, logs de **`multipart/complete`**). Si no hay **`multipart init`**, el cliente no llegó a completar `POST /files/multipart/init` (red, 401, etc.).
2. **Sin `upload start` en un archivo grande con `s3`**: puede ser **normal** (la web y OkHttp usan **multipart**, no `POST /files/upload`). Si tampoco hay **`multipart init`**, la petición **no llega a Flask** o falla antes del body JSON. Revisar **`/var/log/nginx/..._error.log`** (413, 408/timeout de cuerpo, 499, 502).
3. **`http_413` en log Flask**: cuerpo rechazado por **`MAX_CONTENT_LENGTH`** de Flask (revisar `.env`) o por proxy si el 413 lo genera otro nivel.
4. **Gunicorn**: arranque típico `gunicorn -w 4 -b 127.0.0.1:7000 --timeout 0 wsgi:app` (o `--timeout 86400`). Timeout bajo mata subidas largas aunque Nginx esté bien.
5. **Disco** `/tmp` o directorio de spool de Werkzeug: sin espacio, fallo al escribir el cuerpo de la petición.
6. **`STORAGE_DRIVER=s3`**: para ISOs muy grandes lo más fiable sigue siendo **multipart orquestado** (§15.2), no depender solo del monolítico.

### Fuera de este repo (referencia)

- Si en otro repositorio existe la **app Android** con **`MemoryCookieJar`**: se corrigió la fusión de cookies (no reemplazar todas las cookies del host en cada `saveFromResponse`) para no perder la sesión tras respuestas con `Set-Cookie` parcial. Eso no está en este monorepo si solo contiene Flask.

---

## 1. Visión general

- **Qué es**: un “drive” personal (carpetas + archivos por usuario), con papelera, compartir por enlace y compartir con otro usuario, vista previa y emulador web para ROMs.
- **Stack servidor**: **Python 3**, **Flask**, **MySQL** (conector `mysql-connector-python`, pool de 10 conexiones), almacenamiento **local en disco** o **S3-compatible (MinIO)** vía **boto3**.
- **Sesión**: **cookie de sesión firmada** de Flask (`session`), clave `user_id`. No hay JWT en el diseño actual.
- **API para clientes JSON**: las mismas rutas que usa el navegador con cabeceras `Content-Type: application/json` / `Accept: application/json` y a menudo `X-Requested-With: XMLHttpRequest` para forzar respuestas JSON en lugar de redirects HTML.

---

## 2. Estructura de carpetas del repositorio

| Ruta | Rol |
|------|-----|
| `app/` | Paquete principal de la aplicación Flask. |
| `app/__init__.py` | Factory `create_app()`, blueprints, filtros Jinja, `context_processor`. |
| `app/config.py` | Variables de entorno → clase `Config` cargada en `app.config`. |
| `app/controllers/` | Blueprints `auth_controller`, `file_controller`. |
| `app/core/` | `database.py`, `storage.py`, `schema.py`, `security.py`, `file_icon_presets.py`. |
| `app/models/` | Entidades de dominio + SQL. |
| `app/middleware/` | Carga de usuario y manejadores de error. |
| `app/templates/` | Plantillas Jinja2 (HTML). |
| `app/static/css/` | Estilos. |
| `app/static/js/` | Lógica del cliente (drive, subidas, búsqueda, etc.). |
| `app/formatting.py` | Tamaños y fechas para plantillas y JSON. |
| `app/views/` | `TemplateView` (envoltorio mínimo de `render_template`). |
| `run.py` | Instancia `app` para `flask run` / debug. |
| `wsgi.py` | `from app import create_app` → `app = create_app()` para Gunicorn. |
| `deploy/` | Ejemplo de Nginx y `.env.example`. |
| `uploads/` | Directorio por defecto de ficheros en modo `local`. |
| `cloud_remote.sql` | Volcado/esquema SQL de referencia (si existe en el repo). |

---

## 3. Arranque, WSGI y entorno

- **`run.py`**: crea `app = create_app()` y si `__main__` ejecuta `app.run(host="0.0.0.0", port=5000, debug=True)`.
- **`wsgi.py`**: expone `app` para **Gunicorn** (`wsgi:app`).
- **`create_app()`** (en `app/__init__.py`):
  1. Carga `app.config.from_object(Config)`.
  2. **`_configure_app_logging`**: si `INTEGRA_LOG_FILE` está definido, añade **RotatingFileHandler** al `app.logger` (subidas, 413, 500).
  3. Opcional: `ProxyFix` si `USE_PROXY_FIX=true` (cabeceras `X-Forwarded-*` de Nginx).
  4. Crea `UPLOAD_DIR` con `Path(...).mkdir(parents=True, exist_ok=True)`.
  5. `Database.initialize(_storage_config())` y `Storage.initialize(_storage_config())`.
  6. Registra middleware de auth y error handlers.
  7. Registra filtros Jinja y `context_processor`.
  8. Registra la ruta pública `GET /api/config` (sin prefijo de blueprint).
  9. Registra blueprints: `web` (sin prefijo), `auth` en `/auth`, `files` en `/files`.

---

## 4. Configuración (`app/config.py` y `.env`)

Variables leídas con `python-dotenv` (`load_dotenv()` al importar `config`).

| Variable | Uso |
|----------|-----|
| `SECRET_KEY` | Firma de cookies de sesión Flask. |
| `SESSION_COOKIE_SECURE` | Cookie solo HTTPS (`true`/`1`/`yes`). |
| `SESSION_COOKIE_SAMESITE` | Vacío = default Flask; o `Lax` / `Strict` / `None` (RFC; con `None` suele exigirse `Secure`). |
| `MAX_CONTENT_LENGTH` | Límite bytes del body en Flask; `""`, `0`, `-1`, `none`, `null` → **sin límite** (`None`). |
| `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` | MySQL. |
| `UPLOAD_DIR` | Ruta carpeta subidas modo local. |
| `STORAGE_DRIVER` | `local` o `s3` (minúsculas tras `.lower()`). |
| `S3_ENDPOINT_URL` | URL del API S3 **interna** (ej. `http://127.0.0.1:9010`). |
| `S3_PUBLIC_BASE_URL` | URL base **pública** para URLs prefirmadas al navegador/app (ej. `https://drive.tudominio.com` vía Nginx `/cloud-remote/`). |
| `S3_REGION`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET`, `S3_USE_SSL` | Cliente boto3. |
| `USE_PROXY_FIX` | Activa `werkzeug.middleware.proxy_fix.ProxyFix`. |
| `MULTIPART_PRESIGN_EXPIRES_SECONDS` | TTL segundos de cada URL prefirmada de parte (default **86400**). |
| `MULTIPART_PART_SIZE_HINT_BYTES` | Hint de tamaño de parte para clientes (default **8 MiB**). |
| `MULTIPART_CLIENT_MIN_BYTES` | Por debajo, el **cliente web** usa subida monolítica `POST /files/upload`; Android puede seguir la misma política o siempre multipart. |
| `INTEGRA_LOG_FILE` | Ruta absoluta de archivo de log rotativo (20 MB × 5). Vacío = solo consola por defecto del proceso. |
| `INTEGRA_LOG_LEVEL` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` (default **INFO**). |

`_storage_config()` en `app/__init__.py` pasa a `Storage.initialize` también `MULTIPART_PRESIGN_EXPIRES_SECONDS` y `MULTIPART_PART_SIZE_HINT_BYTES` (no duplica `MULTIPART_CLIENT_MIN_BYTES` en el dict de storage; ese valor solo está en `Config` y en el `context_processor` para el HTML).

---

## 5. Base de datos MySQL

El esquema se crea/ajusta con **`app/core/schema.py`** → `initialize_schema()` (tablas `CREATE IF NOT EXISTS` y `ALTER` idempotentes).

### 5.1 Tabla `users`

- `id` PK AI, `username` UNIQUE NOT NULL, `email` NULL UNIQUE, `password_hash`, `full_name`, `created_at`.
- Migración histórica: si faltaba `username`, se rellena y se añade UNIQUE.

### 5.2 Tabla `folders`

- `id`, `user_id` FK → `users`, `parent_id` NULL FK → `folders`, `name`, `deleted_at`, `created_at`.
- Soft-delete implícito por `deleted_at IS NULL` en consultas del modelo.

### 5.3 Tabla `files`

- `id`, `user_id` FK, `folder_id` NULL FK → `folders`, `original_name`, `stored_name` (nombre interno único en storage), `mime_type`, `size_bytes`, `deleted_at`, `created_at`, `updated_at`.

### 5.4 Tabla `share_links`

- Enlaces de descarga temporal: `user_id`, `file_id`, `token` UNIQUE, `expires_at`, `created_at`.

### 5.5 Tabla `multipart_uploads`

- Sesiones de subida multipart S3: `user_id`, `folder_id`, `original_name`, `stored_name`, `mime_type`, `upload_id` (UploadId de S3), `status` (`initiated` \| `completed` \| `aborted`), `created_at`, `completed_at`.

### 5.6 Tabla `file_permissions`

- Compartir archivo con usuario: `owner_user_id`, `target_user_id`, `file_id`, `permission` (ej. `viewer`, `editor`), `created_at`.
- Índice único `(file_id, target_user_id)`.

### 5.7 Tabla `file_type_icons`

- Reglas para iconos Font Awesome: `match_kind` (`mime_exact`, `mime_prefix`, `extension`), `match_value`, `icon_class`, `priority`.
- Población inicial desde `file_icon_presets.py`.

---

## 6. Capa de persistencia: `Database`

**Archivo**: `app/core/database.py`

- `MySQLConnectionPool` nombre `cloud_remote_pool`, `pool_size=10`, `autocommit=False`.
- `Database.connection()` es un **context manager** que: obtiene conexión del pool, hace **`commit`** al salir sin excepción, **`rollback`** si hay excepción, y **`close`** (devuelve al pool).

**Importante para la IA Android**: no hay API REST para “consultar pool”; la app solo habla HTTP con Flask.

---

## 7. Capa de almacenamiento: `Storage`

**Archivo**: `app/core/storage.py`

### 7.1 Modo `local` (`STORAGE_DRIVER=local`)

- Archivos en `UPLOAD_DIR` con nombre interno `stored_name` (UUID + extensión).
- `save_fileobj(key, file_obj, content_type)`: usa `werkzeug.FileStorage.save` → path `UPLOAD_DIR/key`.
- Descarga/preview: rutas de fichero local o redirects si en el futuro se unificara (en local, `send_download` devuelve `local_path`).

### 7.2 Modo `s3` (`STORAGE_DRIVER=s3`)

- **`_s3_client`**: habla con **MinIO/S3 interno** (`S3_ENDPOINT_URL`) para operaciones del servidor (`upload_fileobj`, `complete_multipart_upload`, `get_object`, `abort_multipart_upload`, etc.).
- **`_s3_presign_client`**: si `S3_PUBLIC_BASE_URL` difiere del interno, es un **segundo cliente boto3** apuntando a la URL pública (mismo bucket/credenciales típicamente detrás de Nginx) para **generar URLs prefirmadas** que el navegador o la app puedan llamar sin credenciales AWS en el cliente.
- **Multipart**: `create_multipart_upload`, `sign_multipart_part` (URL `upload_part`, expiración `MULTIPART_PRESIGN_EXPIRES_SECONDS`), `complete_multipart_upload`, `list_multipart_parts`, `abort_multipart_upload`.
- **Timeouts boto3**: `connect_timeout=60`, `read_timeout=3600`, reintentos adaptativos.
- **Presign GET**: descargas/previews con `ExpiresIn=60` (URLs cortas).

### 7.3 Regla clave para Android

- Subida **monolítica**: el binario pasa por **Flask** (`POST /files/upload`).
- Subida **multipart**: el binario de cada parte va **directo a S3/MinIO** con **PUT** a la URL devuelta por `sign-part`; Flask solo orquesta (init, sign, list, complete, abort).

---

## 8. Modelos (`app/models/`)

### 8.1 `User` (`user.py`)

| Método | Descripción |
|--------|-------------|
| `create(username, password, full_name, email)` | INSERT, devuelve `User` con `id`. |
| `find_by_username(username)` | SELECT por username. |
| `find_by_id(user_id)` | SELECT por id; `None` si id `None`. |
| `find_public_by_username` | Igual que find_by_username (lectura pública). |
| `verify_password(raw)` | `werkzeug.check_password_hash`. |

### 8.2 `Folder` (`folder.py`)

| Método | Descripción |
|--------|-------------|
| `create(user_id, name, parent_id)` | INSERT carpeta. |
| `list_for_user(user_id, parent_id)` | Carpetas hijas de `parent_id` (NULL = raíz), no borradas. |
| `find_owned(folder_id, user_id)` | Carpeta del usuario o `None`. |

### 8.3 `FileItem` (`file_item.py`)

| Método | Descripción |
|--------|-------------|
| `create(...)` | INSERT archivo activo. |
| `list_for_user(user_id, folder_id, include_deleted=False)` | Archivos en carpeta o raíz. |
| `find_owned`, `find_any_owned` | Por id y dueño. |
| `soft_delete`, `restore`, `move`, `rename` | Mutaciones. |
| `delete_permanently` | DELETE fila; devuelve `stored_name` para borrar blob. |
| `list_deleted_for_user` | Papelera. |
| `find_public(file_id)` | Archivo por id sin filtrar dueño (para enlaces; debe combinarse con permisos en rutas). |
| `search_for_user` | `LIKE` en nombre; `global_search` ignora carpeta. |
| `list_shared_with_user` | JOIN `file_permissions` + `users` → `owner_username`. |

### 8.4 `FilePermission` (`file_permission.py`)

- `grant(owner_user_id, target_user_id, file_id, permission)` — `ON DUPLICATE KEY UPDATE`.
- `has_access(target_user_id, file_id)`.

### 8.5 `ShareLink` (`share_link.py`)

- `create(user_id, file_id, expires_in_hours=24)` — token URL-safe.
- `find_valid(token)` — no expirado.

### 8.6 `FileTypeIcon` (`file_type_icon.py`)

- Carga reglas desde BD con caché en memoria; `resolve_for(mime, original_name)` → clase CSS Font Awesome.

### 8.7 `MultipartUploadSession` (`multipart_upload_session.py`)

- `create(...)` — status `initiated`.
- `find_active_owned(session_id, user_id)` — solo `status = 'initiated'`.
- `mark_completed`, `mark_aborted`.

---

## 9. Seguridad y formato

- **`app/core/security.py`**: `PasswordManager.hash_password` / `verify_password` con `werkzeug.security`.
- **`app/formatting.py`**: `format_file_size`, `format_datetime`, `datetime_iso` (ISO para JSON).

---

## 10. Middleware y errores HTTP

### 10.1 `register_auth_middleware` (`auth_middleware.py`)

- **`before_request`**: `g.current_user = User.find_by_id(session.get("user_id"))` o `None`.

### 10.2 `login_required`

- Si no hay usuario:
  - Si `Accept` contiene `application/json` **o** `X-Requested-With: XMLHttpRequest` → **401** + `{"error": "No autenticado. Vuelve a iniciar sesion."}`.
  - Si no → redirect a `web.login_page`.

### 10.3 `register_error_handlers` (`error_middleware.py`)

| Código | Respuesta JSON |
|--------|----------------|
| 413 | `{"error": "El archivo excede el tamano maximo permitido."}` |
| 404 | `{"error": "Recurso no encontrado."}` |
| 500 | `{"error": "Error interno del servidor."}` |

---

## 11. Aplicación Flask (`app/__init__.py`)

### 11.1 Filtros Jinja

- `format_dt` → `format_datetime`
- `file_size` → `format_file_size`
- `gravatar_hash`, `first`, `file_icon_class`

### 11.2 `context_processor` → variables globales plantillas

- `storage_driver` — valor de `Config.STORAGE_DRIVER`.
- `multipart_part_size_hint_bytes`, `multipart_client_min_bytes`.

### 11.3 Ruta `/`

- Autenticado → redirect `web.dashboard_page`.
- No autenticado → redirect `web.login_page`.

### 11.4 `GET /api/config` (pública, sin login)

- **No requiere cookies** ni `Authorization`.
- **No** expone secretos (`SECRET_KEY`, contraseñas, URLs internas de MinIO con credenciales).
- Sirve para que **Android (o cualquier cliente)** decida política de subida y paridad con `main.js` **antes o después** del login.

Ver [§23](#23-get-apiconfig--capacidades-del-servidor) para el cuerpo JSON exacto.

---

## 12. Blueprint web — vistas HTML

**Blueprint**: `web` (sin `url_prefix`).

| Método | Ruta | Handler | Plantilla / acción |
|--------|------|---------|---------------------|
| GET | `/login` | `login_page` | `login.html` o redirect dashboard |
| GET | `/register` | `register_page` | `register.html` o redirect |
| GET | `/dashboard` | `dashboard_page` | `dashboard.html` — query `folder_id` opcional |
| GET | `/trash` | `trash_page` | `trash.html` |
| GET | `/shared` | `shared_page` | `shared.html` |
| GET | `/profile` | `profile_page` | `profile.html` |

**Dashboard**: valida que `folder_id` pertenezca al usuario; carga `Folder.list_for_user` y `FileItem.list_for_user`.

---

## 13. Blueprint `auth` — autenticación

**Prefijo**: `/auth`  
**Blueprint**: `auth_blueprint` (`auth_controller.py`)

### 13.1 `POST /auth/register`

**Body** (JSON o `application/x-www-form-urlencoded`):

```json
{
  "username": "string",
  "password": "string",
  "full_name": "string",
  "email": "string opcional"
}
```

- Normalización: `username` y `email` en minúsculas y trim; `email` puede ser `null`/vacío.
- **400** si faltan `username`, `password` o `full_name`.
- **409** si usuario existe: `{"error": "El usuario ya existe."}`.
- Crea usuario, pone `session["user_id"] = user.id`.
- Si `request.is_json`: **201** + `{"message": "Usuario creado", "user_id": <int>}`.
- Si no JSON: redirect HTML al dashboard.

### 13.2 `POST /auth/login`

```json
{ "username": "string", "password": "string" }
```

- **401** credenciales inválidas: `{"error": "Credenciales invalidas."}` (nota: texto sin tilde en “invalidas”).
- Éxito: `session["user_id"] = user.id`.
- Si JSON: **200** + `{"message": "Login correcto"}`.
- Si no JSON: redirect dashboard.

### 13.3 `POST /auth/logout`

- `session.clear()`.
- JSON: `{"message": "Sesion cerrada"}`.
- No JSON: redirect login.

### 13.4 Contrato para Android

1. Usar **`POST`** con **`Content-Type: application/json`** y cuerpo JSON.
2. Guardar **cookies** de respuesta (`Set-Cookie`: sesión Flask) y enviarlas en **todas** las peticiones siguientes al mismo host (`Cookie` header). OkHttp: `CookieJar`.
3. Cabeceras recomendadas (como hace el cliente web en muchas rutas):
   - `Accept: application/json`
   - `X-Requested-With: XMLHttpRequest`  
   Así, rutas protegidas devuelven **401 JSON** en lugar de redirect HTML.

---

## 14. Blueprint `files` — API y archivos

**Prefijo**: `/files`  
**Archivo**: `app/controllers/file_controller.py`  
Casi todas las rutas mutadoras aceptan **JSON o form** según `request.get_json(silent=True) or request.form`.

### 14.1 `GET /files/` — listado (JSON)

- **`@login_required`**.
- **No existe** `?view=shared` ni `?view=trash` en esta API. El diseño explícito es:
  - **Drive normal** (carpetas + archivos activos): este endpoint con `folder_id` / `q` / `global`.
  - **Archivos compartidos conmigo**: `GET /files/shared-with-me` (lista distinta, con `owner_username` en cada ítem).
  - **Papelera (solo JSON)**: **`GET /files/?trash=1`** (también acepta `trash=true` / `trash=yes`). Devuelve `folders: []` y solo archivos con `deleted_at` no nulo. Incluye la clave **`"view": "trash"`** en el JSON para que el cliente distinga la vista. La **página web** de papelera sigue siendo HTML en **`GET /trash`** (`web_blueprint`); es otra ruta, no un query de `/files/`.
- Query params (modo drive normal, cuando **no** se usa `trash=1`):
  - `folder_id` (opcional, int): carpeta actual; omitir o vacío = raíz (`NULL`).
  - `q` (opcional): búsqueda por nombre (`LIKE`).
  - `global=1`: búsqueda global (ignora carpeta en la búsqueda).
- **200** JSON — modo drive normal (sin `trash=1`):

```json
{
  "folders": [
    { "id": 1, "name": "Docs", "parent_id": null }
  ],
  "files": [
    {
      "id": 10,
      "original_name": "a.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 1234,
      "size_label": "1,21 KB",
      "folder_id": null,
      "created_at": "ISO8601",
      "updated_at": "ISO8601",
      "created_at_label": "dd/mm/yyyy HH:MM",
      "updated_at_label": "...",
      "icon_class": "fa-regular fa-file-pdf"
    }
  ]
}
```

- **200** JSON — papelera (`GET /files/?trash=1`): mismos campos por archivo, **`folders` siempre `[]`**, y se añade **`"view": "trash"`** en la raíz del objeto.

La clave `view` **solo** aparece cuando `trash=1`. En modo normal no se envía.

Si hay compartidos con `owner_username`, el campo aparece en **`GET /files/shared-with-me`** y en objetos que el servidor enriquezca así.

### 14.2 `POST /files/folders` — crear carpeta

Body JSON o form:

```json
{ "name": "Nueva", "parent_id": null }
```

- **400** sin nombre; **404** padre inexistente.
- Si `request.is_json` o `X-Requested-With: XMLHttpRequest`: **201** + `{"message": "...", "folder_id": <int>}`.
- Si no: redirect al referrer o dashboard.

### 14.3 `POST /files/upload` — subida monolítica

- **Multipart form**: campo archivo `file`, opcional `folder_id` (texto/int).
- Guarda con `Storage.save_fileobj`, crea `FileItem`.
- Respuesta JSON (Ajax): **201** + `message`, `file_id`, `file` (objeto con forma `_file_item_json`).

**Android**: `MultipartBody.Part` + campo `folder_id` como `text/plain` RequestBody o form field.

### 14.4 Multipart S3 (subida grande) — solo si `STORAGE_DRIVER=s3`

#### `POST /files/multipart/init`

Body JSON:

```json
{
  "filename": "Win10.iso",
  "mime_type": "application/octet-stream",
  "folder_id": null
}
```

- **201**: `session_id`, `part_size_hint`, `presign_expires_seconds`, `max_parts` (10000), `message`.
- **Servidor (operación, no contrato de app)**: con **`INTEGRA_LOG_FILE`**, Flask escribe **`multipart init start`**, **`multipart init ok`** o **`multipart init failed`** (útil cuando la app Android sube grande y **no** aparece `upload start` en el log).

#### `POST /files/multipart/sign-part`

```json
{ "session_id": 123, "part_number": 1 }
```

- `part_number` entre 1 y 10000.
- **200**: `{"url": "<URL prefirmada PUT>"}`.

#### `PUT` a `url` (cliente → S3/MinIO, no Flask)

- Cuerpo: bytes **exactos** de esa parte.
- Respuesta S3 incluye cabecera **`ETag`** (a menudo entre comillas). Guardar para `complete`.

#### `POST /files/multipart/list-parts`

```json
{ "session_id": 123 }
```

- **200**: `{"parts": [ {"PartNumber": 1, "ETag": "\"...\"", "Size": 8388608 }, ... ]}`  
  Orden por `PartNumber` en servidor.

#### `POST /files/multipart/complete`

```json
{
  "session_id": 123,
  "parts": [
    { "PartNumber": 1, "ETag": "\"abc\"" },
    { "PartNumber": 2, "ETag": "\"def\"" }
  ]
}
```

- **200**: mensaje + `file_id` + `file` (JSON enriquecido).
- Marca sesión `completed` y crea fila en `files`.

#### `POST /files/multipart/abort`

```json
{ "session_id": 123 }
```

- Intenta `abort_multipart_upload` en S3 (errores ignorados).
- Marca sesión `aborted`.

### 14.5 Papelera y metadatos

| Ruta | Método | Descripción |
|------|--------|-------------|
| `/<file_id>/trash` | POST | Soft delete. |
| `/<file_id>/restore` | POST | Restaurar. |
| `/<file_id>/move` | POST | Body `folder_id` destino (null = raíz). |
| `/<file_id>/rename` | POST | Body `new_name` → `secure_filename`. |
| `/<file_id>/delete` | POST | Borrado permanente + `Storage.delete_object`. |

Respuestas JSON si `request.is_json` o patrón similar según ruta (muchas devuelven `jsonify` con error o message).

### 14.6 Compartir

- **`POST /files/<file_id>/share`**: body `expires_in_hours` (1–168, default 24). Respuesta `url` absoluta de descarga compartida, `expires_at` ISO.
- **`POST /files/<file_id>/share-user`**: body `username`, `permission` (`viewer`|`editor`). Otorga `FilePermission`.

### 14.7 Descarga y preview

- **`GET /files/shared/<token>`**: **sin login**; valida `ShareLink` y sirve archivo.
- **`GET /files/<file_id>/download`**: login; acceso vía `_find_accessible_file` (dueño o permiso).
- **`GET /files/<file_id>/preview`**: política `_preview_access_policy` (imágenes, PDF, texto, extensiones código en `PREVIEW_CODE_EXTENSIONS`).
- Con **S3**, preview/ROM pueden usar **proxy streaming** desde Flask (`_preview_s3_proxy_response`, `_rom_s3_proxy_response`) para evitar CORS/mixed content en iframes.

### 14.8 Emulador

- **`GET /files/<file_id>/rom`**: stream ROM (extensiones en `EMULATOR_EXTENSION_CORE`).
- **`GET /files/<file_id>/emulator`**: plantilla `emulator_frame.html` con core EmulatorJS.

### 14.9 `GET /files/shared-with-me`

- JSON array de archivos compartidos con el usuario actual (misma forma enriquecida que listados).

### 14.10 `_find_accessible_file(file_id, user_id)`

1. Si es **dueño** (`FileItem.find_owned`) → devuelve archivo.
2. Si no, si **`FilePermission.has_access`** → `FileItem.find_public(file_id)`.
3. Si no → `None` (404 en rutas que lo usan).

---

## 15. Subida de archivos (detalle para Android)

### 15.1 Decisión de estrategia

| Condición | Estrategia |
|-----------|------------|
| `STORAGE_DRIVER=local` | Solo **`POST /files/upload`** (Flask recibe todo el body). |
| `STORAGE_DRIVER=s3` y archivo pequeño | Opcional: una sola petición **`POST /files/upload`** (Flask hace `upload_fileobj` a S3). |
| `STORAGE_DRIVER=s3` y archivo grande | **Multipart**: init → bucle (sign-part → PUT S3) → complete; usar **list-parts** para reanudar; **abort** si falla antes de `complete`. |

**Alineación con la web (`main.js`)**: el cliente web usa **`GET /api/config`** vía equivalencia: en HTML los mismos valores vienen en `window.CLOUD_REMOTE_CONFIG` (`storageDriver`, `multipartPartSizeHintBytes`, `multipartClientMinBytes`). La app Android debe llamar **`GET /api/config`** al inicio (o al cambiar de servidor) y aplicar la **misma regla** que `uploadSingleFileWithProgress`: si `multipart_upload_available` es verdadero **y** `file.size >= multipart_client_min_bytes`, usar **multipart S3**; si no, **`POST /files/upload`**.

**Riesgo si solo se usa monolítico en S3**: puede funcionar para ISOs si Nginx/Gunicorn y `MAX_CONTENT_LENGTH` lo permiten; si el proxy o el worker cortan, fallará aunque el móvil esté bien. Por eso en producción **`STORAGE_DRIVER=s3`** se recomienda implementar **multipart** para archivos por encima del umbral.

**Límites S3 por parte**: cada parte excepto la última debe ser **≥ 5 MiB**; la última puede ser menor. Máximo **10000** partes. El hint del servidor (8 MiB por defecto) cumple el mínimo con margen.

### 15.2 Algoritmo multipart (pseudocódigo para Android / Kotlin)

```
PART = max(5MB, server.part_size_hint)
N = ceil(fileSize / PART)
POST /files/multipart/init { filename, mime_type, folder_id }  + cookies
sessionId = response.session_id
existing = POST /files/multipart/list-parts { session_id }
map = { partNumber -> etag } from existing.parts

parts = []
for p in 1..N:
  if map contains p:
    etag = map[p]
  else:
    POST /files/multipart/sign-part { session_id, part_number: p }
    url = response.url
    bytes = file.slice((p-1)*PART, min(p*PART, fileSize))
    responsePut = HTTP PUT url, body = bytes, no cookies needed for AWS sig
    etag = responsePut.header("ETag")  // conservar comillas si vienen
    parts += { PartNumber: p, ETag: etag }

POST /files/multipart/complete { session_id, parts: [...] }  + cookies
```

- **Timeouts**: subidas multi‑GB requieren `readTimeout` / `writeTimeout` muy altos en el cliente **solo para** `POST /files/upload` y para `POST .../complete`. Los **PUT** a S3 pueden usar cliente HTTP separado o mismos timeouts altos.
- **Si falla una parte** (red): reintentar PUT a la misma URL o pedir **nuevo** `sign-part` (la URL expira según `presign_expires_seconds`).
- **Si falla `complete` después de subir todas las partes**: **no** llamar a `abort` (borraría las partes en S3). Reintentar `complete` con la misma lista de `{PartNumber, ETag}`.
- **Si falla antes de `complete`**: llamar **`/files/multipart/abort`** para limpiar.

### 15.3 Infraestructura (Nginx)

- `location /` debe permitir **`client_max_body_size 0`** (o muy alto) para subidas monolíticas grandes.
- **`client_body_timeout`** y **`send_timeout`** altos (p. ej. **86400s**) en **`/`** y en **`/cloud-remote/`**: sin esto, el default de nginx (~60 s) puede cortar **cada PUT** multipart o un monolítico lento aunque no haya tope de megas.
- **`proxy_read_timeout` / `proxy_send_timeout`** altos en ambas locations (el PUT al bucket pasa por **`/cloud-remote/`** si el host de la URL firmada es el mismo que el API).
- **`proxy_request_buffering off`** recomendado para streaming hacia Gunicorn.
- Gunicorn: **`--timeout`** alto o `0` (según versión) para workers en subidas largas.

---

## 16. JSON común de archivos (`_file_item_json`)

Campos típicos devueltos al cliente (listados, upload, complete):

- `id`, `original_name`, `mime_type`, `size_bytes`, `size_label`
- `created_at`, `updated_at` (ISO), `created_at_label`, `updated_at_label`
- `icon_class` (Font Awesome)
- `owner_username` (opcional, vistas compartidas)

---

## 17. Frontend web: HTML (Jinja)

| Plantilla | Uso |
|-----------|-----|
| `base.html` | Layout autenticado vs público, navbar, sidebar, `CLOUD_REMOTE_CONFIG` en `<script>`, carga `main.js` defer. |
| `login.html` / `register.html` | Form POST clásico a `/auth/login` y `/auth/register` (no JSON en formulario HTML por defecto). |
| `dashboard.html` | Tabla/mosaico drive; `data-folder-id`, `data-file-id`, `data-file-name`, etc. |
| `trash.html`, `shared.html`, `profile.html` | Vistas secundarias. |
| `emulator_frame.html` | iframe EmulatorJS. |
| `partials/drive_toolbar.html` | Barra herramientas (subir, vista, etc.). |
| `partials/drive_table_colgroup.html` | Columnas tabla. |

**`CLOUD_REMOTE_CONFIG`** (expuesto en `base.html`):

- `storageDriver`
- `multipartPartSizeHintBytes`, `multipartClientMinBytes`
- `currentFolderId`, `currentUsername`

La lógica de **subida multipart vs XHR simple** está en **`main.js`** según `storageDriver === "s3"` y tamaño mínimo.

---

## 18. Frontend web: JavaScript (`main.js`)

Archivo grande (~1800+ líneas). Áreas funcionales relevantes:

- **Drive UI**: doble clic carpetas, filas archivos, vista tabla/mosaico, menú contextual.
- **Búsqueda** en navbar (debounce, abort controller).
- **Vista previa** de archivos (modal, zoom, tipos MIME/extensiones).
- **EmulatorJS** integración por extensión de ROM.
- **Subidas**:
  - `uploadFileWithToast`, `createUploadToast`, `markToastSuccess` / `markToastError`.
  - `uploadSingleFileWithProgress`: si `storageDriver === "s3"` y tamaño ≥ umbral → **`uploadMultipartResumable`**; si no → **`uploadSinglePostXhr`** a `/files/upload`.
  - Multipart: `parseDriveJsonResponse`, `abortMultipartSession`, `fetchExistingMultipartParts`, `putMultipartPartWithRetries`, bucle de partes, `complete`, limpieza con `abort` solo si falla **antes** de `complete`.
- **Utilidades**: `escapeHtml`, `formatFileSizeClient`, `formatTime`, inserción dinámica de filas en tabla tras subida.

---

## 19. Frontend web: CSS

- **`app/static/css/styles.css`**: estilos globales del producto (navbar, drive table/mosaic, modales, toasts de subida `.upload-toast`, `.upload-progress`, etc.).
- **Fuentes**: Google Fonts “Plus Jakarta Sans” enlazadas en `base.html`.
- **Iconos**: Font Awesome 6 desde CDN en `base.html`.

---

## 20. Despliegue: Nginx (`deploy/`)

- **`drive.romdevs.com.conf`**: ejemplo de `server` HTTPS.
  - **`/cloud-remote/`** → proxy a MinIO (`9010`), `client_max_body_size 0`, **`client_body_timeout` / `send_timeout` largos** (el valor por defecto de nginx ~60 s corta subidas lentas de cada parte PUT aunque el límite de tamaño sea ilimitado), timeouts de proxy alineados con `/`.
  - **`/`** → Gunicorn (`7000`), `client_max_body_size 0`, `client_body_timeout`/`send_timeout` largos, `proxy_request_buffering off`, timeouts largos para subidas.
- **`.env.example`**: plantilla de variables (DB, S3, multipart, `INTEGRA_LOG_*`, etc.).

### 20.1 Diagnóstico subidas grandes (checklist en el servidor)

| Síntoma | Dónde mirar |
|---------|-------------|
| Error 413 en cliente | Nginx `error_log` (cuerpo > límite); Flask log línea **`http_413`** con `content_length`. |
| Toast **“Error subiendo archivo.”** (web) con cuerpo no JSON | Suele ser **502/504 HTML de nginx** o cierre de conexión: revisar `error_log` y **`client_body_timeout`** (subidas lentas por parte o monolíticas). |
| Excel pequeño OK, ZIP/ISO grande mal y **sin** línea **`upload start`** | Con **`STORAGE_DRIVER=s3`**, archivos ≥ umbral usan **multipart** (no pasan por `POST /files/upload`): en log buscar **`multipart init start` / `multipart init ok`**; si no hay ninguna, el fallo es antes (red, sesión, JS). |
| 502 / conexión reset / subida corta a mitad | **Gunicorn `--timeout`**, recursos del host, MinIO caído. |
| 500 JSON “Error al guardar…” en upload | **`INTEGRA_LOG_FILE`**: traza **`upload failed`** (S3, disco, red interna a MinIO). |
| Fallo solo en multipart `complete` | Log **`multipart complete failed`** (ETags incorrectos, orden de parts, subida abortada en S3). |

Tras cambiar Nginx o `.env`, **reiniciar** servicios (`nginx -s reload`, `systemctl restart gunicorn` o equivalente).

### 20.2 Activar logs y reiniciar (ejemplo `integra-drive.service`)

En el servidor **srv.romdevs.com** el unit suele ser **`integra-drive.service`** (usuario `ansro`, puerto `7000`). El agente **no puede** ejecutar `systemctl restart` sin contraseña de `sudo`.

**Ya aplicado en el repo (sin reinicio aún):**

- `.env` con `INTEGRA_LOG_FILE=/home/ansro/integra_drive/var/log/app.log` y `INTEGRA_LOG_LEVEL=INFO`.
- Directorio `var/log/` creado bajo el proyecto (permisos del usuario `ansro`).

**Tú en el servidor (una vez, con sudo):**

```bash
sudo systemctl restart integra-drive.service
sudo systemctl status integra-drive.service
tail -f /home/ansro/integra_drive/var/log/app.log
```

Si el **timeout de Gunicorn** sigue en 300 s, las subidas largas pueden cortarse: en el repo hay **`deploy/integra-drive.service`** con **`--timeout 86400`**. Para instalarlo:

```bash
sudo cp /home/ansro/integra_drive/deploy/integra-drive.service /etc/systemd/system/integra-drive.service
sudo systemctl daemon-reload
sudo systemctl restart integra-drive.service
```

Para **`/var/log/integra_drive/`** (opcional) hace falta root:

```bash
sudo mkdir -p /var/log/integra_drive
sudo chown ansro:ansro /var/log/integra_drive
# Luego en .env: INTEGRA_LOG_FILE=/var/log/integra_drive/app.log
sudo systemctl restart integra-drive.service
```

---

## 21. Guía para la app Android

### 21.1 Base URL

- Una sola **base URL** HTTPS (o HTTP en LAN) que apunte al mismo host que el navegador, **terminando en `/`** para Retrofit, p. ej. `https://drive.ejemplo.com/`.
- Rutas Retrofit relativas: `auth/login`, `files/`, `files/upload`, `files/multipart/init`, etc.

### 21.2 Cliente HTTP

- **Cookie jar** persistente en memoria o en almacenamiento cifrado (la sesión Flask es cookie).
- Interceptor común:
  - `Accept: application/json`
  - `X-Requested-With: XMLHttpRequest`
- **Timeouts**: subidas grandes → `readTimeout` y `writeTimeout` altos (p. ej. horas) en el cliente usado para **`POST /files/upload`**, **`multipart/complete`** y para **cada `PUT`** a la URL prefirmada (una parte puede tardar minutos en redes lentas; OkHttp debe permitirlo). El servidor debe tener **Nginx** con **`client_body_timeout`** alto en **`/`** y **`/cloud-remote/`** (§15.3, §20); si no, fallará igual que en la web aunque la app esté bien.

### 21.3 Pantallas mínimas sugeridas

1. **Config**: `GET /api/config` (sin sesión) para leer `storage_driver`, `multipart_upload_available`, `multipart_client_min_bytes`, `multipart_part_size_hint_bytes`.
2. **Login / registro**: `POST /auth/login`, `POST /auth/register` (el registro existe en servidor; omitirlo en la app es decisión de producto MVP, no de contrato).
3. **Listado drive**: `GET /files/?folder_id=&q=&global=` con cookies.
4. **Papelera (JSON)**: `GET /files/?trash=1` con cookies (equivalente funcional al listado de borrados; la web HTML usa además `GET /trash`).
5. **Compartidos conmigo**: `GET /files/shared-with-me` con cookies (no se modela con `?view=` en `/files/`).
6. **Crear carpeta**: `POST /files/folders`.
7. **Subida**: si `multipart_upload_available` y tamaño ≥ `multipart_client_min_bytes` → flujo **§15.2**; si no → **`POST /files/upload`** multipart form.

### 21.4 Iconos

- El JSON de archivos incluye **`icon_class`** (clase Font Awesome coherente con la tabla `file_type_icons` en MySQL). La app puede **mapear** `icon_class` a recursos locales Material, o replicar reglas MIME/extensión; no es error usar iconos locales, pero **no es el mismo criterio** que el servidor hasta que se respete `icon_class` o se carguen las mismas reglas.

### 21.5 Descarga

- `GET /files/{id}/download` con cookies (o seguir redirect si S3 devuelve URL prefirmada temporal).

### 21.6 Errores

- Cuerpos JSON suelen traer `{"error": "mensaje"}`.
- **401**: re-login.
- **413**: límite body (Nginx/Flask).
- **502 / 504 / HTML en lugar de JSON** en `upload` o en **`PUT`** multipart: suele ser **proxy** (nginx/Gunicorn) o timeout de lectura del cuerpo; mostrar mensaje claro al usuario y revisar logs del servidor (**§20.1**). En multipart, la ausencia de **`upload start`** en `app.log` es **esperada**; debe existir traza **`multipart init`** si `init` llegó a Flask.

### 21.7 Paridad con el servidor

- Mismos nombres de campos JSON que el backend (`full_name`, `folder_id`, `session_id`, `part_number`, `parts` con `PartNumber` y `ETag` en **PascalCase** como exige la API de complete hacia boto3).
- Respetar **`secure_filename`** en el servidor para nombres mostrados almacenados (el cliente envía nombre original; el servidor sanea).

---

## 22. Paridad producto web ↔ cliente móvil

| Funcionalidad (web) | API / recurso JSON para móvil | Notas |
|---------------------|-------------------------------|--------|
| Dashboard por carpeta | `GET /files/?folder_id=` | |
| Búsqueda | `GET /files/?q=&folder_id=` o `?q=&global=1` | |
| Papelera listado | `GET /trash` solo HTML | **`GET /files/?trash=1`** para JSON |
| Compartidos conmigo | `GET /shared` solo HTML | **`GET /files/shared-with-me`** |
| Subida pequeña / local | `POST /files/upload` | Igual móvil |
| Subida grande S3 | `main.js` → multipart | Misma secuencia **§14.4** + **§15.2**; usar **`GET /api/config`** para umbral |
| Soft delete | Acciones web POST | **`POST /files/<id>/trash`** con JSON |
| Restaurar | idem | **`POST /files/<id>/restore`** |
| Mover | idem | **`POST /files/<id>/move`** body `folder_id` |
| Renombrar | idem | **`POST /files/<id>/rename`** body `new_name` |
| Borrar permanente | idem | **`POST /files/<id>/delete`** |
| Enlace compartido | idem | **`POST /files/<id>/share`** |
| Compartir con usuario | idem | **`POST /files/<id>/share-user`** |
| Descarga | enlace `/files/<id>/download` | Cookies; puede ser redirect S3 |
| Preview / emulador | rutas GET | Paridad opcional MVP |

Un MVP móvil “correcto” frente al servidor: cookies + login + **`/api/config`** + listados + carpetas + subida (multipart o monolítico según config) + papelera JSON + compartidos JSON. El resto son extensiones de paridad con el producto web.

---

## 23. `GET /api/config` — capacidades del servidor

- **Método**: `GET`
- **Ruta absoluta**: `/api/config`
- **Autenticación**: ninguna.
- **Cuerpo**: vacío.

**Respuesta 200** (ejemplo):

```json
{
  "storage_driver": "s3",
  "multipart_upload_available": true,
  "multipart_client_min_bytes": 8388608,
  "multipart_part_size_hint_bytes": 8388608,
  "multipart_presign_expires_seconds": 86400,
  "api": {
    "list_files": "/files/",
    "list_trash_json": "/files/?trash=1",
    "list_shared_json": "/files/shared-with-me",
    "auth_register": "/auth/register",
    "auth_login": "/auth/login",
    "auth_logout": "/auth/logout"
  }
}
```

- Con **`storage_driver": "local"`**, `multipart_upload_available` es **`false`** (el servidor rechaza init multipart con 400). El cliente debe usar solo **`POST /files/upload`**.
- El objeto **`api`** es recordatorio de rutas relativas respecto a la misma base URL que Retrofit/OkHttp.

---

## 24. Encaje con `integra_mobile` (estado actual aproximado)

<a id="encaje-integra-mobile"></a>

*Referencia cruzada entre este documento y el cliente Android **`integra_mobile`** (u otro nombre de proyecto móvil) tal como suele estar implementado hoy. Los “no implementado” son orientativos: confirman en el código del repo móvil.*

### 24.1 Tabla documento ↔ app Android (aprox.)

| Lo que define esta guía | Estado típico en app móvil | Notas |
|-------------------------|----------------------------|--------|
| **`GET /api/config`** | A menudo **no implementado aún** | **Siguiente paso lógico** antes de decidir multipart: leer `multipart_upload_available`, umbrales y rutas en `api`. |
| **`GET /files/?trash=1`** (JSON + opcional **`view: "trash"`**) | Pestañas “papelera” a veces **placeholder** | Sustituir placeholder por esta URL con **cookies de sesión**; parsear el mismo JSON que `GET /files/` en modo normal (sin `trash`). |
| **`GET /files/shared-with-me`** | Pestañas “compartidos” a veces **placeholder** | Sustituir por esta API; no usar `?view=shared` en `/files/` (no existe). |
| **Multipart S3 §15.2 / §14.4** | App **solo monolítica** (`POST /files/upload`) en muchos MVPs | Paridad con web = multipart cuando `/api/config` lo indique; riesgo monolítico: proxy, worker, **`client_body_timeout`** nginx (§15.3). Diagnóstico servidor: **`multipart init`** en log, no solo `upload start`. |
| **Cookies + cabeceras JSON** (`Accept`, `X-Requested-With`) | Suele estar **alineado** (OkHttp + interceptores) | Coherente con §13.4 y §21.2. |
| **`POST /files/upload`** (streaming, timeouts altos, no bufferizar todo el archivo en RAM) | Alineado **en espíritu** con §15.3 / §21.2 | Mismo criterio que la web para no duplicar el fichero entero en caché de app si se puede evitar. |

### 24.2 Conclusión

La guía actualizada va en la **dirección correcta** para el cliente móvil: deja **contrato cerrado** donde antes faltaba detalle (**`/api/config`**, papelera y compartidos en **JSON**, sin `?view=` en `/files/`).

### 24.3 Orden razonable de implementación (seguir el documento al pie de la letra)

1. **`GET /api/config`** al arranque o al cambiar servidor/base URL (sin sesión).
2. **Listados** de papelera y compartidos con las URLs del **§14.1** y la tabla del **§22**:
   - Papelera: **`GET /files/?trash=1`**
   - Compartidos conmigo: **`GET /files/shared-with-me`**
3. **Subida**: si `multipart_upload_available == true` **y** `tamaño_archivo >= multipart_client_min_bytes` → flujo **multipart §15.2**; si no → **`POST /files/upload`** con timeouts acordes a **§15.3** y **§21.2** (incluidos los PUT por parte). El despliegue nginx del servidor debe cumplir **§15.3** (`client_body_timeout` en `/` y `/cloud-remote/`) o las subidas grandes fallarán sin cambiar código en la app.

---

## Anexo A: Mapa rápido de rutas HTTP

| Método | Ruta absoluta (prefijo) | Auth |
|--------|-------------------------|------|
| GET | `/` | No |
| GET | `/api/config` | No |
| GET | `/login`, `/register` | No |
| GET | `/dashboard`, `/trash`, `/shared`, `/profile` | Sí |
| POST | `/auth/register`, `/auth/login`, `/auth/logout` | No (logout sí borra sesión) |
| GET | `/files/` | Sí (query `trash=1` = papelera JSON) |
| POST | `/files/folders`, `/files/upload` | Sí |
| POST | `/files/multipart/init`, `sign-part`, `complete`, `list-parts`, `abort` | Sí (s3 multipart) |
| POST | `/files/<id>/trash`, `restore`, `move`, `rename`, `delete`, `share`, `share-user` | Sí |
| GET | `/files/<id>/download`, `preview`, `rom`, `emulator` | Sí (salvo shared token) |
| GET | `/files/shared/<token>` | No |
| GET | `/files/shared-with-me` | Sí |

---

## Anexo B: Dependencias Python relevantes (conceptual)

- Flask, Werkzeug, mysql-connector-python, python-dotenv, boto3/botocore.

---

## Anexo C: Extensiones admitidas para vista previa como texto

El conjunto `PREVIEW_CODE_EXTENSIONS` en `file_controller.py` incluye (minúsculas en código, comparación por sufijo):

`.sql`, `.php`, `.py`, `.html`, `.htm`, `.css`, `.js`, `.mjs`, `.cjs`, `.json`, `.xml`, `.md`, `.yaml`, `.yml`, `.sh`, `.bash`, `.ts`, `.tsx`, `.jsx`, `.vue`, `.scss`, `.less`, `.sass`, `.java`, `.go`, `.rs`, `.rb`, `.c`, `.h`, `.cpp`, `.hpp`, `.cs`, `.swift`, `.kt`, `.gradle`, `.env`, `.ini`, `.cfg`, `.toml`, `.ipynb`, `.txt`, `.log`, `.csv`, `.tsv`.

Además, por **MIME**: `image/*`, `application/pdf`, `text/plain`, cualquier `text/*`, `application/json`, `application/javascript`, `application/xml`, `application/xhtml+xml`.

## Anexo D: Extensiones ROM → núcleo EmulatorJS (`EMULATOR_EXTENSION_CORE`)

| Extensión | Core |
|-----------|------|
| `.nes`, `.fds`, `.unf` | nes |
| `.smc`, `.sfc`, `.fig`, `.swc` | snes |
| `.z64`, `.n64`, `.v64` | n64 |
| `.pbp`, `.chd`, `.iso`, `.img`, `.mdf`, `.bin` | psx |

---

*Documento generado a partir del código del repositorio Integra Drive. Si añades rutas o modelos nuevos, actualiza este archivo para mantener la paridad con la app Android.*
