Durada: ~90 minuts
Prerequisit: Pràctica 1 completada (Nextcloud en marxa)
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
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
compose.yaml per a FileBrowserCreeu ~/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: {}
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": ""
}
}
# 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 aAdmin2025!
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".
alumne1/compartitDes 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.
# 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
# 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\")
"
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.
compose.yaml de NextcloudEditeu ~/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).
# 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"
# 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
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
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
Encadenar els tres serveis: FileBrowser per emmagatzemar, Gotenberg per convertir.
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
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
cd ~/ra4/nextcloud && docker compose stop
cd ~/ra4/filebrowser && docker compose stop
# 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?