RA4 — Pràctica 2: FileBrowser, Gotenberg i Integració

Durada: ~90 minuts
Prerequisit: Pràctica 1 completada (Nextcloud en marxa)


Pas 0 — Verificació ràpida de l'entorn

cd ~/ra4/nextcloud
docker compose ps

Heu de veure nc_app i nc_db en estat running. Si no:

docker compose up -d

Comproveu que el port 8181 i 3000 estan lliures:

ss -tlnp | grep -E ':8181|:3000'
# No hauria de mostrar res

Part 1 — FileBrowser: instal·lació i configuració (~30 min)

1.1 Crear la estructura de directoris

mkdir -p ~/ra4/filebrowser/data
mkdir -p ~/ra4/filebrowser/root/compartit
mkdir -p ~/ra4/filebrowser/root/privat_admin
cd ~/ra4/filebrowser

Creeu alguns fitxers de prova:

echo "Fitxer de prova per a la pràctica RA4" > ~/ra4/filebrowser/root/compartit/prova.txt
echo "# Apunts del curs" > ~/ra4/filebrowser/root/compartit/apunts.md
dd if=/dev/urandom bs=1k count=100 2>/dev/null | base64 > ~/ra4/filebrowser/root/compartit/fitxer_gran.txt

1.2 Fitxer compose.yaml per a FileBrowser

Creeu ~/ra4/filebrowser/compose.yaml:

# compose.yaml — FileBrowser per a pràctiques RA4

services:
  filebrowser:
    image: filebrowser/filebrowser:latest
    container_name: ra4_filebrowser
    restart: unless-stopped
    ports:
      - "8181:80"
    volumes:
      # Directori arrel que FileBrowser serveix
      - ./root:/srv
      # Base de dades interna de FileBrowser (usuaris, configuració)
      - ./data/filebrowser.db:/database.db
      # Fitxer de configuració
      - ./data/.filebrowser.json:/.filebrowser.json
    environment:
      - FB_DATABASE=/database.db
      - FB_CONFIG=/.filebrowser.json

volumes: {}

1.3 Fitxer de configuració de FileBrowser

Creeu ~/ra4/filebrowser/data/.filebrowser.json:

{
  "port": 80,
  "baseURL": "",
  "address": "0.0.0.0",
  "log": "stdout",
  "database": "/database.db",
  "root": "/srv",
  "auth": {
    "method": "json",
    "header": "",
    "command": "",
    "shell": ""
  }
}

1.4 Primera arrencada

# Crear la BD buida (necessari per a la primera arrencada)
touch ~/ra4/filebrowser/data/filebrowser.db

docker compose up -d

# Seguir logs fins que estigui llest (uns 5 segons)
docker compose logs -f
# Veureu: "Listening on [::]:80"
# Ctrl+C per sortir

Accediu a http://localhost:8181

Login inicial: usuari admin, contrasenya admin

Important: canvieu la contrasenya de l'admin immediatament: icona d'usuari (dalt dreta) → User Management → editar admin → canviar contrasenya a Admin2025!

1.5 Crear usuaris des de la interfície web

Aneu a Configuració (icona d'engranatge) → User Management → botó +:

Usuari Contrasenya Scope (ruta arrel) Permisos
professor Prof2025! / (arrel completa) Tots
alumne1 Alumne2025! /compartit Lectura, descàrrega
alumne2 Alumne2025! /compartit Lectura, descàrrega

Per a alumne1 i alumne2, desmarqueu: "Create", "Rename", "Modify", "Delete". Deixeu activats: "View" i "Download".

1.6 Verificar els permisos

  1. Tanqueu sessió i entreu com alumne1
  2. Heu de veure només el contingut de la carpeta /compartit
  3. Intenteu crear una carpeta nova — hauria de no aparèixer el botó o donar error

Part 2 — Interacció amb l'API REST de FileBrowser (~25 min)

2.1 Obtenir token JWT

Des de la terminal:

# Autenticar-se i obtenir el token JWT
TOKEN=$(curl -s -X POST http://localhost:8181/api/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"Admin2025!"}' \
  | tr -d '"')   # el token ve entre cometes, les eliminem

echo "Token: $TOKEN"

Si veieu una cadena llarga que comença per eyJ, és correcte. Si veieu {"message":...}, les credencials no són correctes.

2.2 Llistar fitxers via API

# Llistar el directori arrel
curl -s -X GET "http://localhost:8181/api/resources/" \
  -H "X-Auth: $TOKEN" | python3 -m json.tool

# Llistar la carpeta /compartit
curl -s -X GET "http://localhost:8181/api/resources/compartit/" \
  -H "X-Auth: $TOKEN" | python3 -m json.tool

2.3 Pujar un fitxer via API

# Crear un fitxer local de prova
echo "Contingut del fitxer pujat via API" > /tmp/api_test.txt

# Pujar-lo a /compartit/
curl -s -X POST "http://localhost:8181/api/resources/compartit/api_test.txt" \
  -H "X-Auth: $TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @/tmp/api_test.txt

# Verificar que ha aparegut
curl -s "http://localhost:8181/api/resources/compartit/" \
  -H "X-Auth: $TOKEN" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for item in d.get('items', []):
    print(f\"  {item['name']:30} {item.get('size',0):10} bytes\")
"

2.4 Script Python: automatització de la creació d'usuaris

Creeu el fitxer ~/ra4/filebrowser/crear_usuaris_fb.py:

#!/usr/bin/env python3
"""
crear_usuaris_fb.py
Crea múltiples usuaris a FileBrowser via API REST

Requisit: pip3 install requests --break-system-packages
"""

import requests
import json
import sys

BASE_URL = "http://localhost:8181"
ADMIN_USER = "admin"
ADMIN_PASS = "Admin2025!"

# Usuaris a crear: (nom, contrasenya, scope, permisos_admin)
USUARIS = [
    ("coordinador",  "Coord2025!",  "/",           True),
    ("alumne3",      "Alumne2025!", "/compartit",  False),
    ("alumne4",      "Alumne2025!", "/compartit",  False),
]


def obtenir_token(base_url: str, usuari: str, contrasenya: str) -> str | None:
    """Autentica i retorna el token JWT."""
    r = requests.post(
        f"{base_url}/api/login",
        json={"username": usuari, "password": contrasenya}
    )
    if r.status_code == 200:
        return r.text.strip('"')
    print(f"Error d'autenticació: {r.status_code} {r.text}")
    return None


def crear_usuari(base_url: str, token: str, nom: str, contrasenya: str,
                 scope: str, es_admin: bool) -> bool:
    """Crea un usuari nou a FileBrowser."""
    headers = {"X-Auth": token, "Content-Type": "application/json"}

    payload = {
        "what": "user",
        "which": [],
        "data": {
            "username": nom,
            "password": contrasenya,
            "scope": scope,
            "locale": "ca",
            "viewMode": "list",
            "perm": {
                "admin":    es_admin,
                "execute":  False,
                "create":   es_admin,
                "rename":   es_admin,
                "modify":   es_admin,
                "delete":   es_admin,
                "share":    False,
                "download": True,
            },
            "commands": [],
            "hideDotfiles": True,
            "dateFormat": False,
        }
    }

    r = requests.post(f"{base_url}/api/users", headers=headers, json=payload)
    return r.status_code == 201


def main():
    print("=== Creació d'usuaris a FileBrowser ===\n")

    token = obtenir_token(BASE_URL, ADMIN_USER, ADMIN_PASS)
    if not token:
        print("No s'ha pogut autenticar. Comproveu les credencials.")
        sys.exit(1)

    print(f"Token obtingut correctament.\n")

    for nom, contrasenya, scope, es_admin in USUARIS:
        rol = "admin" if es_admin else "usuari"
        print(f"Creant '{nom}' (scope: {scope}, rol: {rol})... ", end="")

        if crear_usuari(BASE_URL, token, nom, contrasenya, scope, es_admin):
            print("OK")
        else:
            print("Error (potser ja existeix?)")

    print("\n=== Fet ===")


if __name__ == "__main__":
    main()

Executeu-lo:

pip3 install requests --break-system-packages 2>/dev/null || true
python3 ~/ra4/filebrowser/crear_usuaris_fb.py

Verifiqueu que els nous usuaris apareixen a la interfície web.


Part 3 — Gotenberg: microservei de conversió a PDF (~25 min)

3.1 Afegir Gotenberg al compose.yaml de Nextcloud

Editeu ~/ra4/nextcloud/compose.yaml i afegiu el servei Gotenberg al final de la secció services:

  gotenberg:
    image: gotenberg/gotenberg:8
    container_name: ra4_gotenberg
    restart: unless-stopped
    ports:
      - "3000:3000"
    networks:
      - nc_net
    command:
      - "gotenberg"
      - "--api-port=3000"
      - "--api-timeout=30s"
      # LibreOffice: temps d'arrencada
      - "--libreoffice-restart-after=10"

Reinicieu els serveis:

cd ~/ra4/nextcloud
docker compose up -d
docker compose ps
# Heu de veure nc_app, nc_db i ra4_gotenberg

Gotenberg pot trigar 20-30 segons a arrencar per primera vegada (descarrega la imatge).

3.2 Conversió bàsica: URL a PDF (Bash)

# Convertir la pàgina principal de Nextcloud a PDF
curl -s -X POST http://localhost:3000/forms/chromium/convert/url \
  -F "url=http://localhost:8080" \
  -o /tmp/nextcloud_captura.pdf

# Verificar que el PDF s'ha creat
ls -lh /tmp/nextcloud_captura.pdf
# Heu de veure un fitxer d'uns quants KB

# Obrir el PDF (si teniu entorn gràfic)
xdg-open /tmp/nextcloud_captura.pdf 2>/dev/null || echo "Fitxer guardat a /tmp/nextcloud_captura.pdf"

3.3 Conversió de DOCX a PDF (Bash)

# Crear un document DOCX de prova (amb LibreOffice si el teniu instal·lat)
# Si no, useu el fitxer de prova inclòs:

cat > /tmp/prova.html << 'EOF'
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Document de prova</title></head>
<body>
  <h1>Document de prova RA4</h1>
  <p>Convertit a PDF via Gotenberg</p>
  <p>Data: $(date)</p>
  <table border="1">
    <tr><th>Alumne</th><th>Nota</th></tr>
    <tr><td>Alice</td><td>8</td></tr>
    <tr><td>Bob</td><td>7</td></tr>
  </table>
</body>
</html>
EOF

# Convertir HTML a PDF
curl -s -X POST http://localhost:3000/forms/chromium/convert/html \
  -F "files=@/tmp/prova.html" \
  -o /tmp/prova_html.pdf

ls -lh /tmp/prova_html.pdf

3.4 Conversió des de PHP

Creeu el fitxer ~/ra4/convertir.php:

<?php
/**
 * convertir.php — Converteix un fitxer a PDF via Gotenberg
 * Ús: php convertir.php <fitxer_entrada> <fitxer_sortida.pdf>
 *
 * Exemple: php convertir.php document.html resultat.pdf
 */

if ($argc < 3) {
    echo "Ús: php convertir.php <entrada> <sortida.pdf>\n";
    exit(1);
}

$fitxer_entrada = $argv[1];
$fitxer_sortida = $argv[2];

if (!file_exists($fitxer_entrada)) {
    echo "Error: el fitxer '$fitxer_entrada' no existeix.\n";
    exit(1);
}

// Detectar el tipus de fitxer per escollir l'endpoint
$extensio = strtolower(pathinfo($fitxer_entrada, PATHINFO_EXTENSION));
$endpoints_libreoffice = ['docx', 'odt', 'xlsx', 'ods', 'pptx', 'odp', 'txt'];

if ($extensio === 'html') {
    $url_gotenberg = 'http://localhost:3000/forms/chromium/convert/html';
} elseif (in_array($extensio, $endpoints_libreoffice)) {
    $url_gotenberg = 'http://localhost:3000/forms/libreoffice/convert';
} else {
    echo "Extensió '$extensio' no suportada. Useu: html, docx, odt, xlsx...\n";
    exit(1);
}

echo "Convertint '$fitxer_entrada' → '$fitxer_sortida'...\n";
echo "Endpoint: $url_gotenberg\n";

// Preparar la petició multipart
$ch = curl_init($url_gotenberg);

curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 60,
    CURLOPT_POSTFIELDS     => [
        'files' => new CURLFile(
            realpath($fitxer_entrada),
            mime_content_type($fitxer_entrada) ?: 'application/octet-stream',
            basename($fitxer_entrada)
        )
    ]
]);

$resposta = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);

if ($error) {
    echo "Error de connexió: $error\n";
    exit(1);
}

if ($http_code !== 200) {
    echo "Error de Gotenberg (HTTP $http_code): $resposta\n";
    exit(1);
}

file_put_contents($fitxer_sortida, $resposta);
$mida = round(filesize($fitxer_sortida) / 1024, 1);
echo "PDF generat: '$fitxer_sortida' ({$mida} KB)\n";

Executeu-lo:

php ~/ra4/convertir.php /tmp/prova.html /tmp/resultat_php.pdf
ls -lh /tmp/resultat_php.pdf

3.5 Script Node.js (alternativa)

Creeu ~/ra4/convertir.js:

#!/usr/bin/env node
/**
 * convertir.js — Converteix HTML a PDF via Gotenberg
 * Ús: node convertir.js <fitxer.html> <sortida.pdf>
 * Requisit: Node.js (inclòs a Debian 13)
 */

const fs = require('fs');
const path = require('path');
const http = require('http');

const args = process.argv.slice(2);
if (args.length < 2) {
    console.error('Ús: node convertir.js <entrada.html> <sortida.pdf>');
    process.exit(1);
}

const [entrada, sortida] = args;

if (!fs.existsSync(entrada)) {
    console.error(`Error: el fitxer '${entrada}' no existeix.`);
    process.exit(1);
}

const contingut = fs.readFileSync(entrada);
const nomFitxer = path.basename(entrada);

// Crear el cos multipart/form-data manualment
const boundary = `----FormBoundary${Date.now()}`;
const parts = [
    `--${boundary}\r\n`,
    `Content-Disposition: form-data; name="files"; filename="${nomFitxer}"\r\n`,
    `Content-Type: text/html\r\n\r\n`,
    contingut,
    `\r\n--${boundary}--\r\n`
];

const cos = Buffer.concat(parts.map(p =>
    Buffer.isBuffer(p) ? p : Buffer.from(p)
));

const opcions = {
    hostname: 'localhost',
    port: 3000,
    path: '/forms/chromium/convert/html',
    method: 'POST',
    headers: {
        'Content-Type': `multipart/form-data; boundary=${boundary}`,
        'Content-Length': cos.length
    }
};

console.log(`Convertint '${entrada}' → '${sortida}'...`);

const req = http.request(opcions, (res) => {
    const chunks = [];
    res.on('data', chunk => chunks.push(chunk));
    res.on('end', () => {
        if (res.statusCode === 200) {
            fs.writeFileSync(sortida, Buffer.concat(chunks));
            const mida = (fs.statSync(sortida).size / 1024).toFixed(1);
            console.log(`PDF generat: '${sortida}' (${mida} KB)`);
        } else {
            console.error(`Error HTTP ${res.statusCode}: ${Buffer.concat(chunks).toString()}`);
            process.exit(1);
        }
    });
});

req.on('error', (e) => {
    console.error(`Error de connexió: ${e.message}`);
    process.exit(1);
});

req.write(cos);
req.end();

Executeu-lo:

node ~/ra4/convertir.js /tmp/prova.html /tmp/resultat_node.pdf
ls -lh /tmp/resultat_node.pdf

Part 4 — Exercici integrador: el flux complet (~10 min)

Objectiu

Encadenar els tres serveis: FileBrowser per emmagatzemar, Gotenberg per convertir.

4.1 Script d'integració Bash

Creeu ~/ra4/flux_complet.sh:

#!/bin/bash
# flux_complet.sh
# Flux: crea un HTML → el puja a FileBrowser → el converteix a PDF via Gotenberg
# → desa el PDF localment

set -e   # atura si hi ha error

FB_URL="http://localhost:8181"
GOTENBERG_URL="http://localhost:3000"
SORTIDA_PDF="/tmp/resultat_integrat.pdf"

echo "=== Flux complet: FileBrowser + Gotenberg ==="
echo ""

# 1. Obtenir token de FileBrowser
echo "1. Autenticant a FileBrowser..."
TOKEN=$(curl -s -X POST "$FB_URL/api/login" \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"Admin2025!"}' | tr -d '"')

if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
  echo "Error: no s'ha pogut obtenir el token"
  exit 1
fi
echo "   Token obtingut."

# 2. Crear un HTML de prova
echo "2. Creant document HTML..."
cat > /tmp/informe.html << HTMLEOF
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Informe</title>
<style>body{font-family:Arial;margin:40px} h1{color:#336} table{border-collapse:collapse;width:100%} td,th{border:1px solid #ccc;padding:8px}</style>
</head><body>
<h1>Informe de pràctiques RA4</h1>
<p>Generat el: $(date '+%d/%m/%Y %H:%M')</p>
<h2>Serveis desplegats</h2>
<table>
  <tr><th>Servei</th><th>Port</th><th>Estat</th></tr>
  <tr><td>Nextcloud</td><td>8080</td><td>✅ Actiu</td></tr>
  <tr><td>FileBrowser</td><td>8181</td><td>✅ Actiu</td></tr>
  <tr><td>Gotenberg</td><td>3000</td><td>✅ Actiu</td></tr>
</table>
</body></html>
HTMLEOF
echo "   HTML creat a /tmp/informe.html"

# 3. Pujar l'HTML a FileBrowser
echo "3. Pujant HTML a FileBrowser (/compartit/informe.html)..."
RESULTAT=$(curl -s -o /dev/null -w "%{http_code}" \
  -X POST "$FB_URL/api/resources/compartit/informe.html?override=true" \
  -H "X-Auth: $TOKEN" \
  -H "Content-Type: text/html" \
  --data-binary @/tmp/informe.html)

if [ "$RESULTAT" = "200" ] || [ "$RESULTAT" = "204" ]; then
  echo "   Fitxer pujat correctament (HTTP $RESULTAT)"
else
  echo "   Error en pujar (HTTP $RESULTAT)"
  exit 1
fi

# 4. Convertir l'HTML a PDF via Gotenberg
echo "4. Convertint a PDF via Gotenberg..."
curl -s -X POST "$GOTENBERG_URL/forms/chromium/convert/html" \
  -F "files=@/tmp/informe.html" \
  -o "$SORTIDA_PDF"

MIDA=$(ls -lh "$SORTIDA_PDF" | awk '{print $5}')
echo "   PDF generat: $SORTIDA_PDF ($MIDA)"

# 5. Resum final
echo ""
echo "=== Resum ==="
echo "  HTML pujat a FileBrowser: http://localhost:8181/compartit/informe.html"
echo "  PDF generat localment:    $SORTIDA_PDF"
echo ""
echo "Verificació de FileBrowser:"
curl -s "$FB_URL/api/resources/compartit/" -H "X-Auth: $TOKEN" | \
  python3 -c "
import sys, json
d = json.load(sys.stdin)
for item in d.get('items', []):
    print(f\"  {item['name']}\")
" 2>/dev/null || echo "  (no s'ha pogut llistar)"

Executeu-lo:

chmod +x ~/ra4/flux_complet.sh
~/ra4/flux_complet.sh

Part 5 — Documentació final (criteri 4.8, ~5 min)

Creeu el fitxer ~/ra4/README.md com a documentació del sistema muntat:

# Plataforma d'ofimàtica web — RA4

## Serveis desplegats

| Servei | URL | Usuari admin | Propòsit |
|---|---|---|---|
| Nextcloud | http://localhost:8080 | admin / [contrasenya] | Suite col·laborativa |
| FileBrowser | http://localhost:8181 | admin / Admin2025! | Gestor de fitxers |
| Gotenberg | http://localhost:3000 | (sense autenticació) | Conversió a PDF |

## Arrencada del sistema

```bash
# Nextcloud + Gotenberg
cd ~/ra4/nextcloud && docker compose up -d

# FileBrowser
cd ~/ra4/filebrowser && docker compose up -d

Aturada del sistema

cd ~/ra4/nextcloud && docker compose stop
cd ~/ra4/filebrowser && docker compose stop

Decisions de seguretat

Microservei Gotenberg: ús

# HTML a PDF
curl -X POST http://localhost:3000/forms/chromium/convert/html \
  -F "files=@fitxer.html" -o sortida.pdf

# URL a PDF
curl -X POST http://localhost:3000/forms/chromium/convert/url \
  -F "url=https://exemple.com" -o sortida.pdf

---

## Preguntes de reflexió final

1. Per quin motiu en un entorn de producció **no** s'hauria d'exposar el port de Gotenberg directament? Quina alternativa proposeu?

2. En el `compose.yaml` de Nextcloud heu afegit Gotenberg a la xarxa `nc_net`. Podria FileBrowser accedir a Gotenberg directament si estigués en un `compose.yaml` diferent? Per què?

3. L'script `flux_complet.sh` fa les operacions de manera **seqüencial**. Si haguéssiu de convertir 50 documents simultàniament, quins problemes podrien aparèixer? (Penseu en recursos del servidor.)

4. FileBrowser guarda els usuaris en un fitxer SQLite (`filebrowser.db`). Quins avantatges i inconvenients té respecte a una base de dades com MariaDB?