# File System Operations (FSO) API

FSO implementa los servicios de un *Resources Directory* (según el RAAM de Induxsoft) a través de dos interfaces:

- **API pública** (servicios web): endpoints con extensión `.fso` para listar, consultar, modificar, subir y operar sobre carpetas y archivos del sitio.
- **API privada** (biblioteca de funciones): `fso.dk`, para usar las mismas capacidades desde el código de back-end.

Contenido:

1. [Conceptos básicos](#conceptos-básicos)
2. [Servicios web](#servicios-web): [lst](#listar-carpetas-y-archivos-lstfso) · [get](#obtener-metadatos-getfso) · [set](#establecer-metadatos-y-contenido-setfso) · [upl](#subir-archivos-uplfso) · [do](#acciones-sobre-el-sistema-de-archivos-dofso)
3. [Metadatos y privilegios](#metadatos-y-manifiesto-de-privilegios)
4. [Privilegios por operación](#privilegios-por-operación)
5. [Funciones de back-end (fso.dk)](#funciones-de-back-end-fsodk)
6. [Configuración (fso.config.dk)](#configuración-fsoconfigdk)
7. [Referencia de mensajes de error](#referencia-de-mensajes-de-error)

---

## Conceptos básicos

### Enrutamiento

La extensión `.fso` está dirigida a los programas de la API FSO. El mapa de extensiones (`ext.map` de la BWL) contiene:

```
.fso >  $/fso/fso.*.dkl
```

Cada servicio corresponde a un programa: `lst.fso` → `fso.lst.dkl`, `get.fso` → `fso.get.dkl`, `set.fso` → `fso.set.dkl`, `upl.fso` → `fso.upl.dkl` y `do.fso` → `fso.do.dkl`.

### Cómo se determina el recurso sobre el que se opera

El recurso es la **URL desde la que se invoca el servicio, sin el nombre del endpoint**:

| URL invocada | Recurso (URI) |
|---|---|
| `https://sitio.tld/lst.fso` | `/` (raíz del sitio) |
| `https://sitio.tld/documentos/lst.fso` | `/documentos` |
| `https://sitio.tld/documentos/contrato.docx/get.fso` | `/documentos/contrato.docx` |

Todos los servicios aceptan estos parámetros en la **URL de la solicitud** (query string), incluso los que reciben un cuerpo (`set`, `do`):

| Parámetro | Descripción |
|---|---|
| `uri` | Ruta del recurso, relativa a la carpeta base del sitio. **Reemplaza** a la URI deducida de la URL. Las barras `\` se convierten en `/`. |
| `base_path` | Ruta del sistema local (en el servidor) que se usa como carpeta base. Solo se admite con `@dklfso_multisites=@true`. |
| `host` | Nombre de un host del servidor; su carpeta (`<@dklfso_websites_path>/<host>`) se usa como base. Solo se admite con `@dklfso_multisites=@true`. Si se envían `host` y `base_path`, prevalece `host`. |

Si `@dklfso_multisites` es `@false` (valor predeterminado), `base_path` y `host` se ignoran y la carpeta base es la raíz del sitio que atiende la solicitud.

### Autenticación de las solicitudes

FSO usa la sesión del usuario que establece el proveedor de identidades del sitio (`_protected/idp/default.dk`). El módulo `webauth.dkl` busca el token de sesión, en este orden, en:

| Ubicación | Nombre |
|---|---|
| Parámetro GET | `ids` o `token` |
| Cookie | `__induxsoft_token` |
| Campo de formulario | `session_id` |
| Cuerpo JSON | `ids` o `token` |
| Encabezado HTTP | `Authorization: Bearer <token>` (o el valor completo del encabezado `Authorization`) |

La lista puede modificarse en `@http_token_locations`. Ejemplo:

```bash
curl -H "Authorization: Bearer MI_TOKEN" "https://sitio.tld/documentos/lst.fso"
```

Una solicitud **sin sesión** se evalúa como el usuario anónimo (`uid` igual a `*`); tendrá acceso solo si el manifiesto de privilegios del recurso lo concede (ver [Manifiesto de privilegios](#manifiesto-de-privilegios)).

### Formato de las respuestas

Todos los servicios responden `Content-Type: application/json;charset=utf-8`. El código de estado HTTP **no** indica el resultado de la operación; revise siempre `success` y, en los servicios que operan sobre varios recursos, el resultado de cada uno.

```json
{ "success": true,  "data": { } }
{ "success": false, "message": "descripción del error" }
```

- `success: false` con `message` indica un error general (por ejemplo, un JSON de entrada inválido).
- En `set`, `upl` y `do`, `data` contiene un resultado **por recurso**; un error en un recurso no impide procesar los demás. Si un recurso devuelve `message`, hubo un error en ese recurso: revise el mensaje antes de confiar en los indicadores `*_done`.

### Claves de URI en los cuerpos JSON

En `set.fso` y `do.fso` el cuerpo es un objeto cuyas claves identifican los recursos a afectar:

| Clave | Recurso afectado (invocando `https://sitio.tld/carpeta/…`) |
|---|---|
| `.` | La carpeta desde la que se invoca el servicio: `/carpeta` |
| `notas.txt` | Relativa a la URI: `/carpeta/notas.txt` |
| `sub/archivo.txt` | Relativa a la URI: `/carpeta/sub/archivo.txt` |
| `/otra/texto.txt` | Si inicia con `/`, es relativa a la raíz del sitio: `/otra/texto.txt` |

---

## Servicios web

### Listar carpetas y archivos (`lst.fso`)

Devuelve el contenido de una carpeta, con la posibilidad de recorrer subcarpetas, filtrar por nombre de archivo y buscar texto dentro de los archivos.

- **End point:** `uri/lst.fso`
- **Método HTTP:** `GET`

#### Parámetros (todos opcionales)

| Parámetro | Descripción | Predeterminado |
|---|---|---|
| `filter` | Filtro de nombres de archivo, p. ej. `*.txt`. No afecta a las carpetas. | `*.*` |
| `deep` | Niveles de subcarpetas a recorrer. Con `0` solo se devuelve el contenido de la carpeta indicada (las subcarpetas aparecen sin contenido). | `999` |
| `search` | Texto a buscar en el contenido de los archivos. Solo se consideran archivos de hasta `@dklfso_textfile_maxsize_r4search` bytes (262 144 de forma predeterminada); los de mayor tamaño no se incluyen cuando se busca. | vacío |
| `mode` | `0` carpetas y archivos · `1` solo carpetas · `2` solo archivos · `3` solo archivos que coinciden con `search` | `0` |
| `uri`, `base_path`, `host` | Ver [Conceptos básicos](#cómo-se-determina-el-recurso-sobre-el-que-se-opera). | |

Los elementos para los que el usuario no tiene el privilegio de listado se omiten sin error, igual que los archivos de metadatos (`._m_`) y de privilegios (`._p_`) y, si `@dklfso_protected_dir_restricted` es `@true`, la carpeta protegida del sitio.

#### Privilegios requeridos

El usuario debe contar con alguno de los siguientes **sobre cada carpeta y archivo listado**:

- `dkl_fso_list`
- `write`
- `read` + `plus`
- `admin`

#### Respuesta

Con `mode` igual a `0`, `1` o `2`, `data` es un árbol:

```json
{
  "success": true,
  "data": {
    "folders": [
      {
        "type": "folder",
        "name": "informes",
        "path": "/documentos",
        "fullpath": "/documentos/informes",
        "creation": "2026-03-04T09:15:00",
        "lastaccess": "2026-10-15T08:00:00",
        "lastwrite": "2026-10-10T17:42:10",
        "folders": [ ],
        "files": [ ]
      }
    ],
    "files": [
      {
        "type": "file",
        "name": "contrato.docx",
        "path": "/documentos",
        "fullpath": "/documentos/contrato.docx",
        "length": 48213,
        "creation": "2026-03-04T09:15:00",
        "lastaccess": "2026-10-15T08:00:00",
        "lastwrite": "2026-10-10T17:42:10",
        "properties": { "autor": "ana" },
        "privileges": { "read": [ { "user": "+" } ] },
        "priv_inherited": 1
      }
    ]
  }
}
```

Con `mode=3`, `data` es una **lista plana** de los archivos encontrados (con los mismos campos), sin estructura de carpetas.

Campos de cada elemento:

| Campo | Descripción |
|---|---|
| `type` | `folder` o `file` |
| `name` | Nombre, incluyendo la extensión |
| `path` | URI de la carpeta que lo contiene |
| `fullpath` | URI completa del elemento |
| `length` | Tamaño en bytes (solo archivos) |
| `creation`, `lastaccess`, `lastwrite` | Fechas en UTC con formato `yyyy-MM-ddTHH:mm:ss` |
| `properties` | [Propiedades extendidas](#propiedades-extendidas), si existen |
| `privileges` | [Manifiesto de privilegios](#manifiesto-de-privilegios) aplicable (propio o heredado), si existe |
| `priv_inherited` | Niveles hacia arriba a los que se encontró el manifiesto: `0` es propio, `1` es el de la carpeta padre, etc. |
| `folders`, `files` | Contenido de la carpeta (solo en carpetas; nulos o ausentes si está vacía) |

Si el usuario no está autorizado a listar la URI indicada, `data` es nulo.

#### Ejemplos

```
https://sitio.tld/lst.fso
https://sitio.tld/documentos/lst.fso?filter=*.pdf&deep=0
https://sitio.tld/lst.fso?search=guanabanas&filter=*.txt&mode=3
https://sitio.tld/lst.fso?uri=/documentos/informes&mode=1
```

```bash
# Solo carpetas de primer nivel
curl -H "Authorization: Bearer MI_TOKEN" "https://sitio.tld/lst.fso?mode=1&deep=0"
```

---

### Obtener metadatos (`get.fso`)

Devuelve los metadatos de un archivo o carpeta.

- **End point:** `uri/get.fso`
- **Método HTTP:** `GET`
- **Parámetros (opcionales):** `uri`, `base_path`, `host` ([ver](#cómo-se-determina-el-recurso-sobre-el-que-se-opera)).

#### Privilegios requeridos

Para obtener el recurso, alguno de:

- `dkl_fso_getprops`
- `write`
- `read` + `plus`
- `admin`

El campo `privileges` (el manifiesto de privilegios) solo se incluye si además el usuario cuenta con alguno de: `dkl_fso_getprivs`, `write`, `read` + `plus` o `admin`.

#### Respuesta

`data` es el elemento, con los mismos campos descritos en [`lst.fso`](#respuesta) (sin `folders` ni `files`):

```json
{
  "success": true,
  "data": {
    "type": "file",
    "name": "contrato.docx",
    "path": "/documentos",
    "fullpath": "/documentos/contrato.docx",
    "length": 48213,
    "creation": "2026-03-04T09:15:00",
    "lastaccess": "2026-10-15T08:00:00",
    "lastwrite": "2026-10-10T17:42:10",
    "properties": { "autor": "ana" }
  }
}
```

`data` es nulo si el recurso no existe, si el usuario no está autorizado o si está en la carpeta protegida (con `@dklfso_protected_dir_restricted` en `@true`); el servicio no distingue entre estos casos.

#### Ejemplo

```
https://sitio.tld/documentos/contrato.docx/get.fso
https://sitio.tld/get.fso?uri=/documentos/contrato.docx
```

---

### Establecer metadatos y contenido (`set.fso`)

Permite, para uno o varios recursos en una sola llamada: establecer sus propiedades extendidas, establecer su manifiesto de privilegios y crear o reemplazar el contenido de archivos de texto.

- **End point:** `uri/set.fso`
- **Método HTTP:** `POST` o `PUT`
- **Parámetros de URL (opcionales):** `uri`, `base_path`, `host` ([ver](#cómo-se-determina-el-recurso-sobre-el-que-se-opera)).
- **Cuerpo:** `application/json;charset=utf-8`

#### Cuerpo de la solicitud

Un objeto cuyas claves son [URI de recursos](#claves-de-uri-en-los-cuerpos-json). Cada valor puede contener:

| Campo | Descripción |
|---|---|
| `properties` | Objeto JSON con las propiedades extendidas. **Reemplaza por completo** las propiedades existentes (no se fusionan). |
| `privileges` | Objeto JSON con el [manifiesto de privilegios](#manifiesto-de-privilegios). **Reemplaza por completo** el manifiesto existente. |
| `text` | Cadena con el contenido de texto del archivo. Si el archivo no existe, se crea. Solo aplica a archivos. |
| `text_encode` | Codificación con la que se escribe `text`. Predeterminado: `utf8`. |

Los campos de cada clave se procesan en este orden: `properties`, `privileges`, `text`. Si uno falla, no se procesan los siguientes de esa misma clave.

```json
{
  ".": {
    "properties": { "titulo": "Carpeta de ejemplo" }
  },
  "subcarpeta/archivo.txt": {
    "properties": { "autor": "ana", "etiquetas": ["a", "b"] },
    "privileges": {
      "read":  [ { "user": "+" } ],
      "write": [ { "user": "admin" } ]
    }
  },
  "notas.txt": {
    "text": "Primera línea\nSegunda línea",
    "text_encode": "utf8"
  },
  "/otra_carpeta/texto.txt": {
    "properties": { "revisado": true }
  }
}
```

Asumiendo la URL `https://sitio.tld/carpeta_ejemplo/set.fso`, las claves anteriores afectan, respectivamente, a `/carpeta_ejemplo`, `/carpeta_ejemplo/subcarpeta/archivo.txt`, `/carpeta_ejemplo/notas.txt` y `/otra_carpeta/texto.txt`.

#### Respuesta

```json
{
  "success": true,
  "data": {
    ".":                      { "props_done": true,  "privs_done": false },
    "subcarpeta/archivo.txt": { "props_done": true,  "privs_done": false, "message": "No está autorizado" },
    "notas.txt":              { "props_done": false, "privs_done": false, "txt_done": true }
  }
}
```

| Campo | Descripción |
|---|---|
| `props_done` | `true` si se escribieron las propiedades |
| `privs_done` | `true` si se escribió el manifiesto de privilegios |
| `txt_done` | Presente cuando se envió `text`; `true` si se escribió el contenido |
| `message` | Descripción del error de ese recurso, si lo hubo |

Si la clave no existe y no incluye `text`, el resultado es `message: "Resource not found"`.

#### Privilegios requeridos

Todos los privilegios se evalúan sobre el recurso afectado, salvo la creación de archivos nuevos (ver más abajo).

| Campo | Privilegio requerido (alguno de) |
|---|---|
| `properties` | `dkl_fso_setprops`, `write`, `admin` |
| `privileges` | `dkl_fso_setprivs`, `write` + `plus`, `admin` |
| `text` (archivo existente) | `dkl_fso_overwrite`, `write`, `admin` |
| `text` (archivo nuevo) | Para crearlo: `dkl_fso_upload`, `write` o `admin` **sobre la carpeta desde la que se invoca el servicio**; después, para escribir el contenido: `dkl_fso_overwrite`, `write` o `admin` sobre el nuevo archivo |

Además, escribir `text` en un archivo cuya extensión esté en `@dklfso_danger_files_ext` requiere `dkl_fso_danger`, `write` + `plus` o `admin`.

> Para crear los primeros manifiestos de privilegios de un sitio, vea [Primer manifiesto](#primer-manifiesto-de-privilegios-del-sitio).

---

### Subir archivos (`upl.fso`)

Sube uno o varios archivos codificados como `multipart/form-data` a la carpeta indicada. Puede crear subcarpetas, descomprimir archivos `.zip` y procesar archivos `.jsnwm`.

- **End point:** `uri/upl.fso`
- **Método HTTP:** `POST` o `PUT`
- **Content-Type:** `multipart/form-data`
- **Parámetros de URL (opcionales):** `uri`, `base_path`, `host` ([ver](#cómo-se-determina-el-recurso-sobre-el-que-se-opera)).

#### Comportamiento

- La **carpeta de destino** es la URI. Si no existe, se crea (incluidas las carpetas intermedias) siempre que el usuario cuente con el privilegio de crear carpetas según el manifiesto aplicable y la carpeta no esté restringida como carpeta protegida. Para que esto sea posible debe existir un manifiesto de privilegios en la carpeta o en alguna de sus carpetas superiores.
- Cada archivo se guarda con el nombre indicado en el atributo `filename` de su parte del formulario. Si el destino ya contiene un archivo con ese nombre, ese archivo devuelve `done: false` con un mensaje de error y el archivo existente se conserva.
- Cada archivo se procesa de forma independiente; el resultado se identifica por su `filename`.

#### Subir carpetas completas

Si el **nombre del campo** (`name`) de la parte del formulario contiene `/`, la parte de directorio de ese nombre se interpreta como una subcarpeta de la carpeta de destino y se crea si no existe:

| `name` del campo | `filename` | Se guarda en |
|---|---|---|
| `archivo1` | `a.txt` | `<uri>/a.txt` |
| `docs/2026/informe` | `a.txt` | `<uri>/docs/2026/a.txt` |

#### Descomprimir un `.zip` al subirlo

Si el nombre del campo termina en `.zip` seguido del marcador de procesamiento (`@dklfso_id_process_file`, por defecto `*`), es decir, `.zip*`, el archivo se descomprime en la carpeta de destino y el `.zip` se elimina. Requiere, además del privilegio de subida, `dkl_fso_zip`, `write`, `read` + `plus` o `admin`.

#### Archivos `.jsnwm`

Si el nombre del campo termina en `.jsnwm*` (con el mismo marcador), el archivo se interpreta como un archivo de [Websencia](https://websencia.dev): se envía al servicio de generación de websencia.dev, se descarga el resultado y se descomprime en la carpeta de destino. El archivo `.jsnwm` se elimina. El servidor debe tener acceso a internet.

#### Privilegios requeridos

Sobre la carpeta de destino, alguno de:

- `dkl_fso_upload`
- `write`
- `admin`

Adicionalmente:

| Situación | Privilegio requerido (alguno de) |
|---|---|
| Archivos con extensión en `@dklfso_danger_files_ext` | `dkl_fso_danger`, `write` + `plus`, `admin` |
| Crear la carpeta de destino (si no existe) | `dkl_fso_mkdir`, `write`, `admin` |
| Campo `.zip*` | `dkl_fso_zip`, `write`, `read` + `plus`, `admin` |

#### Ejemplos

Formulario HTML de un solo archivo:

```html
<form action="upl.fso" enctype="multipart/form-data" method="POST">
  <input type="file" name="archivo">
  <input type="submit">
</form>
```

Subir un `.zip` y descomprimirlo:

```html
<form action="upl.fso" enctype="multipart/form-data" method="POST">
  <input type="file" name="paquete.zip*">
  <input type="submit">
</form>
```

Subir una carpeta completa, conservando su estructura (JavaScript):

```html
<input type="file" id="dir" webkitdirectory multiple>
<script>
async function subirCarpeta(urlDestino) {
  const fd = new FormData();
  for (const f of document.getElementById("dir").files) {
    // webkitRelativePath: "carpeta/sub/archivo.txt"
    fd.append(f.webkitRelativePath, f, f.name);
  }
  const r = await fetch(urlDestino + "/upl.fso", { method: "POST", body: fd });
  console.log(await r.json());
}
</script>
```

Con `curl`:

```bash
# Un archivo
curl -H "Authorization: Bearer MI_TOKEN" -F "archivo=@informe.pdf" \
     "https://sitio.tld/documentos/upl.fso"

# Un archivo dentro de una subcarpeta (se crea si no existe)
curl -H "Authorization: Bearer MI_TOKEN" -F "2026/q3/informe=@informe.pdf" \
     "https://sitio.tld/documentos/upl.fso"

# Un .zip que se descomprime en la carpeta
curl -H "Authorization: Bearer MI_TOKEN" -F "paquete.zip*=@paquete.zip" \
     "https://sitio.tld/documentos/upl.fso"
```

#### Respuesta

```json
{
  "success": true,
  "data": {
    "informe.pdf":  { "done": true },
    "script.dkl":   { "done": false, "message": "No autorizado" }
  }
}
```

Si no se envía ningún archivo, o el destino existe pero no es una carpeta, la respuesta es `success: false` con el mensaje correspondiente.

---

### Acciones sobre el sistema de archivos (`do.fso`)

Permite, para uno o varios recursos en una sola llamada:

- Crear una subcarpeta (`mkdir`)
- Eliminar un archivo o una carpeta con todo su contenido (`delete`)
- Copiar un archivo (`copy`)
- Mover un archivo (`move`)
- Renombrar un archivo (`rename`)

- **End point:** `uri/do.fso`
- **Método HTTP:** `POST` o `PUT`
- **Parámetros de URL (opcionales):** `uri`, `base_path`, `host` ([ver](#cómo-se-determina-el-recurso-sobre-el-que-se-opera)).
- **Cuerpo:** `application/json;charset=utf-8`

#### Cuerpo de la solicitud

Un objeto cuyas claves son [URI de recursos](#claves-de-uri-en-los-cuerpos-json). Cada valor describe una acción (`action`, sin distinguir mayúsculas de minúsculas):

| `action` | Campos adicionales | Efecto |
|---|---|---|
| `mkdir` | `path` | La clave debe ser una **carpeta existente**; se crea en ella la subcarpeta `path` (puede ser de varios niveles, p. ej. `a/b/c`). |
| `delete` | | Elimina el archivo o la carpeta. Si es un archivo, también elimina sus archivos de metadatos y privilegios (`._m_`, `._p_`). Una carpeta se elimina con **todo su contenido**. |
| `copy` | `to` | Copia el archivo, con el mismo nombre, a la carpeta `to`. Se copian también sus archivos `._m_` y `._p_`. |
| `move` | `to` | Igual que `copy` y después elimina el original y sus archivos `._m_` y `._p_`. |
| `rename` | `as` | Cambia el nombre del archivo (dentro de su misma carpeta) a `as`. Los archivos `._m_` y `._p_` se conservan con el nuevo nombre. |

Notas:

- **El recurso indicado por cada clave debe existir**; de lo contrario el resultado es `Resource not found` (incluso para `mkdir`: la clave es la carpeta donde se crea, no la que se va a crear).
- `to` es la **carpeta de destino**, expresada desde la raíz del sitio (por ejemplo `/archivo/2026`); a diferencia de las claves, no es relativa a la URL desde la que se invoca el servicio. Si la carpeta de destino no existe, se crea (si el manifiesto aplicable lo permite).
- `copy`, `move` y `rename` operan solo sobre archivos; con una carpeta devuelven `Not implemented`.
- Si ya existe un archivo con el mismo nombre en el destino, la acción de esa clave falla (`done: false`) y el archivo existente se conserva.

```json
{
  ".":                 { "action": "mkdir",  "path": "reportes/2026" },
  "borrador.txt":      { "action": "delete" },
  "datos.csv":         { "action": "copy",   "to": "/respaldos" },
  "viejo.csv":         { "action": "move",   "to": "/archivo/2025" },
  "informe_final.pdf": { "action": "rename", "as": "informe_2026.pdf" },
  "temporal":          { "action": "delete" }
}
```

Con la URL `https://sitio.tld/documentos/do.fso`, la primera clave crea `/documentos/reportes/2026`.

```bash
curl -X POST -H "Authorization: Bearer MI_TOKEN" \
     -H "Content-Type: application/json;charset=utf-8" \
     -d '{ ".": { "action": "mkdir", "path": "reportes/2026" } }' \
     "https://sitio.tld/documentos/do.fso"
```

#### Respuesta

```json
{
  "success": true,
  "data": {
    ".":             { "done": true },
    "borrador.txt":  { "done": true },
    "datos.csv":     { "done": false, "message": "No autorizado" }
  }
}
```

#### Privilegios requeridos

Cada acción requiere, **sobre el recurso indicado por la clave**, alguno de los siguientes:

| Acción | Privilegio específico | También lo concede |
|---|---|---|
| `mkdir` | `dkl_fso_mkdir` | `write`, `admin` |
| `delete` | `dkl_fso_delete` | `write`, `admin` |
| `copy` | `dkl_fso_copy` | `write`, `admin` |
| `move` | `dkl_fso_move` | `write`, `admin` |
| `rename` | `dkl_fso_rename` | `write`, `admin` |

Adicionalmente, `copy`, `move` y `rename` requieren sobre la **carpeta de destino** (en `rename`, la carpeta del archivo):

| Condición | Privilegio requerido (alguno de) |
|---|---|
| Siempre | `dkl_fso_upload`, `write`, `admin` |
| El origen o el nombre final tienen una extensión de `@dklfso_danger_files_ext` | `dkl_fso_danger`, `write` + `plus`, `admin` |
| La carpeta de destino no existe y debe crearse | `dkl_fso_mkdir`, `write`, `admin` (según el manifiesto aplicable a esa ruta) |

---

## Metadatos y manifiesto de privilegios

Los metadatos asociados a una carpeta o archivo son sus **propiedades extendidas** (clave/valor) y su **manifiesto de privilegios**. Ambos se guardan en archivos JSON junto al recurso. Estos archivos **no existen de forma predeterminada**: se crean con [`set.fso`](#establecer-metadatos-y-contenido-setfso) o manualmente.

Ninguno de estos archivos se muestra en los listados (`lst.fso`).

### Propiedades extendidas

Se guardan en archivos con extensión `._m_` (configurable con `@dklfso_metadata_ext`). Contienen un objeto JSON cualquiera.

| Recurso | Archivo de propiedades |
|---|---|
| Carpeta `/documentos` | `/documentos/._m_` (archivo sin nombre, solo extensión, dentro de la carpeta) |
| Archivo `notas.docx` | `notas.docx._m_`, junto al archivo |

Si no existe el archivo, el recurso solo tiene las propiedades del sistema de archivos: nombre (con extensión), fecha de creación, de último acceso y de última escritura (en UTC) y, para archivos, el tamaño.

### Manifiesto de privilegios

Se guarda en archivos con extensión `._p_` (configurable con `@dklfso_privs_ext`), con la misma convención de nombres que las propiedades extendidas: `._p_` dentro de la carpeta, o `<nombre-del-archivo>._p_` junto al archivo.

#### Formato

Un objeto JSON cuyas **claves son nombres de privilegio** y cuyos valores son listas de **condiciones**. El usuario tiene un privilegio si cumple **alguna** de las condiciones de su lista.

```json
{
  "read":  [ { "user": "+" } ],
  "write": [ { "role": "editores" }, { "user": "ana" } ],
  "plus":  [ { "team": "ti" } ],
  "admin": [ { "user": "emmanuel" } ],
  "dkl_fso_upload": [ { "workspace": "ventas" } ]
}
```

Cada condición es un objeto con **un solo par** `tipo: valor` (si se incluyen más, solo se evalúa el primero):

| Condición | Se cumple cuando |
|---|---|
| `{"user": "id"}` | El `uid` del usuario es igual a `id` |
| `{"user": "+"}` | Siempre: cualquier usuario, autenticado o anónimo |
| `{"user": "*"}` | El usuario es anónimo (sin sesión) |
| `{"role": "nombre"}` | El usuario pertenece al rol (`memberships/roles`) |
| `{"team": "nombre"}` | El usuario pertenece al equipo (`memberships/teams`) |
| `{"workspace": "nombre"}` | El usuario pertenece al espacio de trabajo (`memberships/workspaces`) |
| `{"campo": "valor"}` | El campo indicado del perfil del usuario es igual a `valor` (p. ej. `{"email": "ana@sitio.tld"}`) |

Los nombres de privilegio pueden ser los básicos (`read`, `write`, `plus`, `admin`) o los de operaciones específicas (`dkl_fso_*`); ver [Privilegios por operación](#privilegios-por-operación).

#### Herencia

Para evaluar un recurso, FSO busca su manifiesto así:

1. El del propio recurso.
2. Si no existe, el de su carpeta padre, luego el del padre del padre, y así hasta la raíz del sitio (máximo `@dklfso_privs_revdeep` niveles, 50 de forma predeterminada).

El primer manifiesto que se encuentre es el que se aplica **completo**; no se combina con los de niveles superiores. El campo `priv_inherited` de las respuestas indica a cuántos niveles se encontró.

**Si no existe ningún manifiesto en la ruta, el acceso se deniega para todos los usuarios, incluidos los administradores.** Esto aplica a todos los servicios.

#### Primer manifiesto de privilegios del sitio

Como todos los servicios requieren un manifiesto, el primero debe crearse manualmente en el servidor. Por ejemplo, un archivo `._p_` en la raíz del sitio que concede todo a un administrador y lectura a todos:

```json
{
  "admin": [ { "user": "emmanuel" } ],
  "read":  [ { "user": "+" } ]
}
```

A partir de ahí, los manifiestos de las demás carpetas y archivos pueden crearse con `set.fso`:

```bash
curl -X POST -H "Authorization: Bearer MI_TOKEN" \
     -H "Content-Type: application/json;charset=utf-8" \
     -d '{ ".": { "privileges": { "read": [ { "role": "clientes" } ], "write": [ { "role": "editores" } ] } } }' \
     "https://sitio.tld/documentos/set.fso"
```

#### Protección de lectura de los recursos

Si el sitio usa la plantilla estándar (`_protected/auth.dk`), el privilegio `read` del manifiesto también protege la descarga directa de los recursos: cuando el manifiesto aplicable tiene un `read` explícito, se exige sesión iniciada (401 si no la hay) y que el usuario cuente con `read` (403 si no). Los manifiestos sin `read` no restringen la lectura.

---

## Privilegios por operación

FSO asume los privilegios básicos de la BWL: `read`, `write`, `plus` y `admin` (sus nombres se definen en `@privid_*`). Además define un privilegio por cada operación (`@op_id_*`), con su equivalencia en privilegios básicos. Un usuario puede realizar una operación si:

1. tiene el privilegio específico de la operación, o
2. tiene `admin`, o
3. cumple la equivalencia de la tabla.

| Privilegio de operación | Equivalencia (además de `admin`) | Se usa en |
|---|---|---|
| `dkl_fso_list` | `write` · `read` + `plus` | `lst.fso` |
| `dkl_fso_getprops` | `write` · `read` + `plus` | `get.fso` |
| `dkl_fso_getprivs` | `write` · `read` + `plus` | `get.fso` (incluir `privileges`) |
| `dkl_fso_setprops` | `write` | `set.fso` (`properties`) |
| `dkl_fso_setprivs` | `write` + `plus` | `set.fso` (`privileges`) |
| `dkl_fso_upload` | `write` | `upl.fso`; crear archivos en `set.fso`; destino de `copy`, `move` y `rename` |
| `dkl_fso_overwrite` | `write` | `set.fso` (`text`) |
| `dkl_fso_mkdir` | `write` | `do.fso` (`mkdir`); crear carpetas de destino |
| `dkl_fso_copy` | `write` | `do.fso` (`copy`) |
| `dkl_fso_move` | `write` | `do.fso` (`move`) |
| `dkl_fso_rename` | `write` | `do.fso` (`rename`) |
| `dkl_fso_delete` | `write` | `do.fso` (`delete`) |
| `dkl_fso_danger` | `write` + `plus` | Subir, copiar, mover, renombrar o escribir archivos con extensión de `@dklfso_danger_files_ext` |
| `dkl_fso_zip` | `write` · `read` + `plus` | `upl.fso` (campos `.zip*`) |
| `dkl_fso_unzip` | `write` + `plus` | Reservado |
| `dkl_fso_lock`, `dkl_fso_recover` | `write` | Reservados (sin servicio asociado) |

La resolución de equivalencias está en `dklfso.authOp` (`fso.config.dk`) y puede personalizarse.

---

## Funciones de back-end (`fso.dk`)

`fso.dk` requiere que se haya incluido antes `fso.config.dk` y depende de `functions.dkh`, `serialize.dkh` y `webauth.dkl`. Al incluirse, deja asignadas las funciones predeterminadas de autorización y búsqueda (ver [Puntos de extensión](#puntos-de-extensión)).

```dkl
#include "dkli.dkh"
#!
program "ejemplo"
{
  #include "functions.dkh"
  #include "serialize.dkh"
  #include "webauth.dkl"

  #include "_protected/fso/fso.config.dk"
  #include "_protected/fso/fso.dk"

  //..su código
}
```

### `dklfso.list::&params`

Devuelve la información de carpetas y archivos con la misma estructura que `data` en `lst.fso` (modos 0, 1 y 2): un registro con `folders` y `files`. Devuelve `@null` si el usuario no está autorizado sobre la URI indicada.

`params` es una referencia a un registro con:

| Campo | Descripción |
|---|---|
| `base_path` | **Requerido.** Ruta del sistema local (en el servidor) usada como base. |
| `uri` | **Requerido.** URI del recurso, relativa a `base_path`. |
| `filter` | Filtro de nombres de archivo, p. ej. `*.txt` (use `*.*` para todos). |
| `deep` | Niveles de subcarpetas a recorrer. |
| `mode` | `0` carpetas y archivos · `1` solo carpetas · `2` solo archivos · `3` solo archivos que coinciden con `search`. |
| `search` | Texto a buscar en el contenido de los archivos. |

Notas de uso:

- Establezca siempre `filter`, `deep` y `mode`. Los valores predeterminados de `lst.fso` (`*.*`, `999` y `0`) los aplica el servicio, no esta función.
- La función **modifica** `params` mientras recorre las carpetas; cree un registro nuevo para cada llamada.
- Con `mode=3`, el valor devuelto es el árbol de carpetas y la lista plana de archivos encontrados queda en `params.found` (una lista de registros).
- Si `base_path` está vacío, lanza el error `Invalid base_path`.

```dkl
base = field.dstr(@http_context,"request/headers/document_root","")

new p
{
    @"base_path": base
    @"uri":       "/documentos"
    @"filter":    "*.pdf"
    @"deep":      2
    @"mode":      2
}
ref r = dklfso.list(p)

// Búsqueda de texto: los archivos encontrados quedan en p.found
new q
{
    @"base_path": base
    @"uri":       "/documentos"
    @"filter":    "*.txt"
    @"deep":      999
    @"mode":      3
    @"search":    "guanabanas"
}
do dklfso.list(q)
ref encontrados = @@(q,"&found")
```

### `dklfso.createObjectInfo::base_path,uri`

Devuelve una referencia a un registro con los metadatos de un archivo o carpeta del sistema local (`type`, `name`, `path`, `fullpath`, `length`, `creation`, `lastaccess`, `lastwrite`, `properties`, `privileges`, `priv_inherited`; ver [lst.fso](#respuesta)), o `@null` si el recurso no existe.

| Parámetro | Descripción |
|---|---|
| `base_path` | Ruta de base del sitio en el sistema local |
| `uri` | Ruta del recurso (archivo o carpeta), relativa a `base_path` |

### `dklfso.isprotected::uri`

Devuelve `@true` si `uri` es la carpeta protegida del sitio (`@dklfso_protected_dir`) o un elemento dentro de ella. **Solo** devuelve `@true` si `@dklfso_protected_dir_restricted` es `@true`; en caso contrario siempre devuelve `@false`. La comparación de `uri` no distingue mayúsculas de minúsculas; escriba `@dklfso_protected_dir` en minúsculas.

### `dklfso.auth::&fi,op`

Devuelve `@true` si el usuario actual puede realizar la operación `op` (uno de los `@op_id_*`) sobre el elemento `fi` (un registro obtenido con `createObjectInfo`). Usa la función de autorización configurada (ver [Puntos de extensión](#puntos-de-extensión)).

### `dklfso.authOp::op,&privs`

Aplica el [mapa de resolución de privilegios](#privilegios-por-operación): devuelve `@true` si la lista de privilegios `privs` (cadenas) permite la operación `op`. Se define en `fso.config.dk`.

### `dklfso.getPrivs::&physicalPath,isfolder`

Busca el manifiesto de privilegios aplicable (propio o heredado) a la ruta física `physicalPath`; `isfolder` indica si es una carpeta. Devuelve un registro con `privileges` y `priv_inherited`, o `@null` si no hay ninguno en la ruta.

### `dklfso.parseRequest::`

Devuelve un registro con `base_path` y `uri` para la solicitud HTTP en curso, aplicando las reglas de [Conceptos básicos](#cómo-se-determina-el-recurso-sobre-el-que-se-opera).

### Puntos de extensión

El comportamiento de la autorización y de la búsqueda de texto se delega en dos punteros a función, que puede reemplazar con `point` **después** de incluir `fso.dk`:

| Puntero | Firma | Predeterminada | Propósito |
|---|---|---|---|
| `@dklfso.funcAuthObject` | `f(&objeto, op)` → booleano | `dklfso.defaultAuthObject` | Decide si el usuario puede realizar `op` sobre `objeto`. La predeterminada evalúa el manifiesto de privilegios y deniega si no hay uno. |
| `@dklfso.funcSearchInFile` | `f(texto, archivo_local)` → booleano | `dklfso.defaultSearchInFile` | Decide si el archivo contiene el texto buscado. La predeterminada busca la cadena en el contenido del archivo como texto. |

Ejemplo: permitir borrar solo al usuario `ops` y delegar todo lo demás en la regla estándar:

```dkl
mi.autorizar::&fi, op
{
    if op==@op_id_dkl_fso_delete
    {
        return @@(@http_context,"session/user/uid")=="ops"
    }
    return dklfso.defaultAuthObject(fi,op)
}

point @dklfso.funcAuthObject to mi.autorizar
```

---

## Configuración (`fso.config.dk`)

| Variable | Valor predeterminado | Descripción |
|---|---|---|
| `@dklfso_websites_path` | `"web"` | Carpeta que contiene los sitios cuando se usa el parámetro `host`. |
| `@dklfso_multisites` | `@false` | Si es `@true`, habilita los parámetros `base_path` y `host`, que permiten acceder al sistema de archivos de otros sitios de la instancia. |
| `@dklfso_metadata_ext` | `"._m_"` | Extensión de los archivos de propiedades extendidas. |
| `@dklfso_privs_ext` | `"._p_"` | Extensión de los manifiestos de privilegios. |
| `@dklfso_danger_files_ext` | `".dkl,.dkh"` | Extensiones de riesgo, separadas por comas. Operar con ellas requiere `dkl_fso_danger`. |
| `@dklfso_id_process_file` | `"*"` | Marcador que, agregado a la extensión del nombre de un campo de `upl.fso`, indica que el archivo debe procesarse (`.zip*`, `.jsnwm*`). |
| `@dklfso_protected_dir` | `"_protected"` | Carpeta protegida del sitio. |
| `@dklfso_protected_dir_restricted` | `@false` | Si es `@true`, la carpeta protegida se oculta de `lst.fso` y `get.fso`. |
| `@dklfso_textfile_maxsize_r4search` | `262144` | Tamaño máximo, en bytes, de los archivos en los que se busca texto. |
| `@dklfso_privs_revdeep` | `50` | Máximo de niveles superiores en los que se busca un manifiesto heredado. |
| `@privid_admin`, `@privid_write`, `@privid_read`, `@privid_plus` | `"admin"`, `"write"`, `"read"`, `"plus"` | Nombres de los privilegios básicos. |
| `@op_id_dkl_fso_*` | `"dkl_fso_*"` | Nombres de los privilegios de operación (ver [Privilegios por operación](#privilegios-por-operación)). |

---

## Referencia de mensajes de error

Mensajes que devuelven los servicios en el campo `message`.

| Mensaje | Significado |
|---|---|
| `Resource not found` | La URI indicada en la clave no existe (y no se envió `text` en `set.fso`). |
| `No autorizado` / `No está autorizado` | El usuario no tiene el privilegio requerido sobre el recurso. |
| `No está autorizado, extensión restringida` / `No autorizado, extensión peligrosa` | La extensión del archivo está en `@dklfso_danger_files_ext` y el usuario no tiene `dkl_fso_danger`. |
| `No está autorizado para subir archivos en esta ubicación` | Falta `dkl_fso_upload` sobre la carpeta de destino. |
| `No está autorizado para crear la estructura de directorios` | Hay que crear la carpeta de destino y el usuario no tiene `dkl_fso_mkdir` o la ruta está restringida como carpeta protegida. |
| `No se pudo crear la estructura de directorios` | Falló la creación de la carpeta en el sistema de archivos. |
| `No existe el folder indicado y no hay manifiesto de privilegios disponible` | La carpeta de destino no existe y no hay ningún manifiesto de privilegios en su ruta. |
| `El destino no es un directorio` | En `upl.fso`, la URI existe pero es un archivo. |
| `No se han enviado archivos` | `upl.fso` recibió una solicitud sin archivos. |
| `Not implemented` | `copy`, `move` o `rename` sobre una carpeta. |
| `Command invalid` | El valor de `action` no es válido. |
| `Action invalid` | `mkdir` sobre un recurso que no es una carpeta. |
| `Invalid base_path` | (`dklfso.list`) `base_path` vacío. |