# Archivos de los ítems

Un ítem de una colección (ver [Colecciones e ítems](colecciones-e-items.html)) puede llevar archivos adjuntos: la propuesta de un cliente, la factura de un pedido, la foto de un producto. Con `met.files` los adjuntas, los listas, los descargas, los renombras y los borras.

Leer pide `items:read`; adjuntar, renombrar y borrar, `items:write`. Los dos funcionan con una key de test. Las rutas son planas (`/items/{itemId}/files`, `/item-files/{fileId}`): operan sobre el workspace de la key, y un ítem o un archivo de otro workspace responde `404`.

## Adjuntar un archivo

Desde los bytes que ya tienes (multipart, campo `file`):

```ts
import { readFile } from 'node:fs/promises';

const bytes = await readFile('propuesta.pdf');
const archivo = await met.files.upload(41207, bytes, {
  filename: 'Propuesta comercial (v2).pdf',
  contentType: 'application/pdf',
});
console.log(archivo.id, archivo.download_url);
```
```python
with open("propuesta.pdf", "rb") as f:
    archivo = met.files.upload(41207, f.read(), filename="Propuesta comercial (v2).pdf", content_type="application/pdf")
print(archivo["id"], archivo["download_url"])
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/items/41207/files \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -F "file=@propuesta.pdf;type=application/pdf"
```

O desde una URL pública, sin bajar y volver a subir el archivo en tu servidor: Met lo descarga por ti.

```ts
await met.files.attachUrl(41207, 'https://mitienda.co/facturas/10293.pdf', { filename: 'Factura 10293.pdf' });
```
```python
met.files.attach_url(41207, "https://mitienda.co/facturas/10293.pdf", filename="Factura 10293.pdf")
```
```bash
curl -X POST https://api.met.meteor.com.co/api/v1/items/41207/files \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://mitienda.co/facturas/10293.pdf","filename":"Factura 10293.pdf"}'
```

- El tope es **25 MB** por archivo, por los dos caminos.
- La URL tiene que ser pública: Met no descarga direcciones internas ni privadas (protección contra SSRF). Tiene 20 segundos para responder; si responde un error o no trae contenido, el adjunto no se crea. El tipo del archivo sale de su `Content-Type`.
- Sin `filename`, el nombre sale de la URL.
- El archivo también aparece en la Mediateca del workspace (`asset_id`), y Met extrae su texto en segundo plano para que un Met pueda leerlo en una conversación.

## Qué devuelve

```json
{
  "id": 8123,
  "item_id": 41207,
  "original_name": "Propuesta comercial (v2).pdf",
  "filename": "1753848291233-propuesta-comercial.pdf",
  "mime_type": "application/pdf",
  "size": 184320,
  "storage_path": "items/41207/1753848291233-propuesta-comercial.pdf",
  "created_by": 42,
  "created_at": "2026-07-30T04:44:51.233Z",
  "asset_id": null,
  "download_url": "https://…/object/sign/…?token=…"
}
```

| Campo | Qué es |
|---|---|
| `original_name` | El nombre que ve la gente y con el que se descarga. |
| `filename` | El nombre saneado con el que se guardó. No lo muestres. |
| `storage_path` | **Lo que conviene guardar** si guardas una referencia: es lo único estable. |
| `download_url` | Solo al adjuntar: un enlace **firmado que vence a los 7 días**. Sirve para usarlo enseguida, no para guardarlo. |

Los archivos viven en un almacenamiento privado: no hay URL pública que se abra sin credenciales.

## Listar y descargar

```ts
import { writeFile } from 'node:fs/promises';

const archivos = await met.files.list(41207); // del más reciente al más viejo

for await (const f of met.files.iterate(41207)) {
  console.log(f.id, f.original_name, f.size);
}

const { data, contentType, filename } = await met.files.download(8123);
await writeFile(filename ?? 'archivo', data);
```
```python
archivos = met.files.list(41207)
for f in met.files.iterate(41207):
    print(f["id"], f["original_name"], f["size"])

descarga = met.files.download(8123)
with open(descarga["filename"] or "archivo", "wb") as out:
    out.write(descarga["data"])
```
```bash
curl -L https://api.met.meteor.com.co/api/v1/item-files/8123/download \
  -H "Authorization: Bearer $METEOR_API_KEY" -o propuesta.pdf
```

- `list()` sin parámetros trae los últimos 100. `iterate()` recorre **todos**, página por página, con cursor.
- `download()` devuelve **bytes, no JSON**, con el tipo y el nombre del archivo. A diferencia de `download_url`, esta ruta no vence: valida la key en cada llamada.

## Renombrar y borrar

```ts
await met.files.update(8123, { name: 'Propuesta aprobada.pdf' });
await met.files.delete(8123); // { message: 'File deleted' }
```
```python
met.files.update(8123, name="Propuesta aprobada.pdf")
met.files.delete(8123)
```

Renombrar cambia solo `original_name`: el contenido y `storage_path` no se tocan. Borrar elimina el archivo y su registro; el ítem sigue existiendo.

## Desde un asistente (MCP)

El servidor MCP lista los archivos de un ítem, pero no sube ni descarga bytes: un archivo no viaja por la conversación de un asistente. Para eso, usa el SDK o la API.

## Otros archivos del workspace

La **Mediateca** —imágenes, videos y documentos del workspace, organizados en carpetas— es otro recurso: `met.assets`, con los scopes `files:read` y `files:write`.

## Errores comunes

| Código | Qué pasó |
|---|---|
| `400` `validation_error` | No mandaste ni `file` ni `url`; la URL no está permitida, respondió un error o no trajo contenido; o el archivo de la URL pasa de 25 MB. |
| `413` `FILE_TOO_LARGE` | El archivo subido por multipart pasa de 25 MB. |
| `404` `not_found` | El ítem o el archivo no existe en este workspace. |
| `403` `missing_scope` | La key no trae `items:write` (o `items:read` para leer). |
