TINET · Documentació tècnica

Python al servidor TINET
via FTP i CGI

Com pujar i executar scripts Python, des de zero fins a peticions Ajax amb resposta JSON.

1 Com funciona: arquitectura

TINET permet executar codi Python al servidor web via el protocol CGI (Common Gateway Interface). El flux és senzill:

  1. El navegador fa una petició HTTP (GET o POST) a una URL que apunta a un script .py.
  2. Apache detecta que és dins de cgi-bin/ i executa el fitxer Python.
  3. El script imprimeix una capçalera HTTP (Content-Type) i el cos de la resposta.
  4. El navegador rep HTML, text pla, o JSON — depenent del que hagi imprès el script.
💡 Per a peticions Ajax (la pàgina no es recarrega), el script retorna JSON en lloc d'HTML. El JavaScript de la pàgina rep el JSON i actualitza el DOM. Funciona exactament igual que qualsevol API REST.

2 Connexió i pujada de fitxers per FTP

Dades de connexió

ParàmetreValorNotes
Servidor (host)usuaris.tinet.catTant per FTP com per SFTP
Nom d'usuariel_teu_usuariEl mateix que fas servir al correu TINET
Contrasenyala_teva_contrasenyaLa mateixa que el correu
Port FTP21FTP estàndard (no xifrat)
Port SFTP22Recomanat: transferència xifrada
🔒 Fes servir SFTP (port 22) sempre que sigui possible. Xifra la connexió i les credencials viatgen protegides. FTP (port 21) envia la contrasenya en text pla.

Estructura de directoris al servidor

Un cop connectat, trobaràs aquesta estructura al servidor remot:

/html/               ← arrel del teu espai web
  ├── index.html       ← pàgines web normals aquí
  ├── la_teva_pagina.html
  └── cgi-bin/         ← tots els scripts Python aquí
      ├── consulta.py
      └── consulta_ajax.py

La teva URL pública serà https://usuaris.tinet.cat/el_teu_usuari/

Opció A — FileZilla (Windows / macOS / Linux)

FileZilla és el client FTP/SFTP més estès. Pots descarregar-lo gratuïtament a filezilla-project.org.

⚠️ Salts de línia: els fitxers .py han de tenir salts de línia Unix (LF), no Windows (CRLF). Si els has editat amb el Bloc de notes de Windows, és probable que tinguin CRLF i el servidor donarà error 500. Fes servir VS Code, Notepad++ (format → Unix), o qualsevol editor modern configurat per defecte a LF.

Opció B — Línia de comandes (Linux / macOS)

Si treballes en un terminal, tens dues opcions ràpides: sftp (interactiu) i scp (còpia directa, com cp però remot).

sftp — interactiu scp — còpia directa
sftp — sessió interactiva
# Connecta't al servidor
sftp el_teu_usuari@usuaris.tinet.cat

# Un cop dins (veuràs el prompt "sftp>")
cd html                       # entra a la carpeta web
mkdir cgi-bin                 # crea la carpeta si no existeix

put servidor.html             # puja el fitxer HTML
put servidor_ajax.html

cd cgi-bin
put consulta.py               # puja els scripts Python
put consulta_ajax.py

# Assigna permisos executables
chmod 755 consulta.py
chmod 755 consulta_ajax.py
chmod 755 .                   # la carpeta cgi-bin també

bye                           # tanca la sessió
scp — còpia directa sense entrar al servidor
# Puja un fitxer HTML
scp servidor.html el_teu_usuari@usuaris.tinet.cat:html/

# Puja un script Python a cgi-bin
scp consulta.py el_teu_usuari@usuaris.tinet.cat:html/cgi-bin/

# Puja tota una carpeta local de cop (-r = recursiu)
scp -r cgi-bin/ el_teu_usuari@usuaris.tinet.cat:html/

# Assigna permisos via ssh (sense haver d'entrar manualment)
ssh el_teu_usuari@usuaris.tinet.cat "chmod 755 html/cgi-bin/*.py html/cgi-bin/"
💡 Si et cansa escriure la contrasenya cada vegada, pots configurar claus SSH: genera un parell de claus amb ssh-keygen i copia la clau pública al servidor amb ssh-copy-id el_teu_usuari@usuaris.tinet.cat. A partir d'aquí, sftp, scp i ssh funcionaran sense contrasenya.
lftp — client avançat (puja carpetes senceres fàcilment)
# Instal·la lftp si no el tens
sudo apt install lftp          # Debian/Ubuntu
sudo dnf install lftp          # Fedora/RHEL

# Connecta i sincronitza la carpeta local "web/" amb /html/ al servidor
lftp -u el_teu_usuari sftp://usuaris.tinet.cat
mirror -R web/ html/           # -R = puja (reverse mirror)
bye

3 Permisos i shebang

Permisos necessaris

ElementPermísPer què
cgi-bin/755Apache ha de poder entrar-hi
consulta.py755El script ha de ser executable
la_pagina.html644Fitxer llegible, no executable

A FileZilla: clic dret sobre el fitxer → Permisos del fitxer → escriu 755 al camp numèric.

Shebang: primera línia obligatòria

Tots els scripts Python han de començar exactament amb aquesta línia:

qualsevol_script.py — primera línia
#!/usr/bin/env python3

Sense aquesta línia, Apache no sap com executar el fitxer i retornarà un error 500.

4 Exemple 1 — CGI clàssic (pàgina es recarrega)

El formulari envia les dades al servidor, Python les processa i retorna una pàgina HTML nova. Senzill i directe.

Fitxer HTML — servidor.html

servidor.html
HTML
<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <title>Demo CGI - TINET</title>
</head>
<body>

<h1>Exemple CGI clàssic</h1>

<!-- Exemple: hora del servidor -->
<form action="cgi-bin/consulta.py" method="get">
    <input type="hidden" name="accio" value="hora">
    <button type="submit">Quina hora és al servidor?</button>
</form>

<!-- Exemple: salutació personalitzada -->
<form action="cgi-bin/consulta.py" method="get">
    <input type="hidden" name="accio" value="saluda">
    <label>Nom: <input type="text" name="nom"></label>
    <button type="submit">Envia</button>
</form>

</body></html>

Script Python — cgi-bin/consulta.py

cgi-bin/consulta.py
Python 3
#!/usr/bin/env python3
import cgi, cgitb, datetime, html

cgitb.enable()  # mostra errors al navegador (treure en producció)

form  = cgi.FieldStorage()
accio = form.getvalue('accio', 'desconeguda')

# Capçalera HTTP — SEMPRE la primera cosa que imprimim
print("Content-Type: text/html; charset=utf-8")
print()  # línia en blanc obligatòria

print("""<!DOCTYPE html><html lang="ca"><head>
<meta charset="UTF-8"><title>Resposta</title>
</head><body>""")

if accio == 'hora':
    ara = datetime.datetime.now()
    print(f"<h2>Hora del servidor: {ara.strftime('%H:%M:%S')}</h2>")
    print(f"<p>Data: {ara.strftime('%A, %d de %B de %Y')}</p>")

elif accio == 'saluda':
    nom = html.escape(form.getvalue('nom', 'desconegut'))
    print(f"<h2>Hola, {nom}!</h2>")

else:
    print("<p>Acció desconeguda.</p>")

print("</body></html>")
La URL del formulari (action="cgi-bin/consulta.py") és relativa al fitxer HTML. Si el teu HTML és a /html/servidor.html, el script ha d'estar a /html/cgi-bin/consulta.py.

5 Exemple 2 — Ajax + JSON (sense recàrrega)

La pàgina no es recarrega mai. JavaScript fa la petició, Python retorna JSON, i el DOM s'actualitza al moment. Exactament el flux que necessites.

💡 L'única diferència respecte al CGI clàssic és la capçalera HTTP del script: Content-Type: application/json en lloc de text/html, i que imprimim JSON en lloc d'HTML.

Fitxer HTML — servidor_ajax.html

servidor_ajax.html
HTML + JS
<!DOCTYPE html>
<html lang="ca">
<head>
    <meta charset="UTF-8">
    <title>Demo Ajax + CGI - TINET</title>
</head>
<body>

<h1>Exemple Ajax — la pàgina no es recarrega</h1>

<!-- Exemple: hora del servidor sense recàrrega -->
<button onclick="demanarHora()">Quina hora és al servidor?</button>
<div id="resultat-hora"></div>

<!-- Exemple: salutació sense recàrrega -->
<input type="text" id="nom" placeholder="El teu nom">
<button onclick="demanarSalutacio()">Saluda'm</button>
<div id="resultat-saluda"></div>

<script>
// Funció genèrica: crida el CGI i actualitza un div amb el resultat
function cridar(params, idDiv, callback) {
    const url = 'cgi-bin/consulta_ajax.py?' + new URLSearchParams(params);
    fetch(url)
        .then(r => r.json())
        .then(data => {
            document.getElementById(idDiv).innerHTML = callback(data);
        })
        .catch(() => {
            document.getElementById(idDiv).innerHTML =
                '<strong style="color:red">Error de connexió</strong>';
        });
}

function demanarHora() {
    cridar({ accio: 'hora' }, 'resultat-hora',
        data => `Hora: <strong>${data.hora}</strong> — ${data.data}`
    );
}

function demanarSalutacio() {
    const nom = document.getElementById('nom').value;
    cridar({ accio: 'saluda', nom }, 'resultat-saluda',
        data => `<strong>${data.missatge}</strong><br>${data.extra}`
    );
}
</script>

</body></html>

Script Python — cgi-bin/consulta_ajax.py

cgi-bin/consulta_ajax.py
Python 3
#!/usr/bin/env python3
import cgi, cgitb, datetime, html, json

cgitb.enable()

form  = cgi.FieldStorage()
accio = form.getvalue('accio', 'desconeguda')

# Ara retornem JSON en lloc d'HTML
print("Content-Type: application/json; charset=utf-8")
print()  # línia en blanc obligatòria

if accio == 'hora':
    ara = datetime.datetime.now()
    resposta = {
        "hora":      ara.strftime('%H:%M:%S'),
        "data":      ara.strftime('%A, %d de %B de %Y'),
        "timestamp": int(ara.timestamp())
    }

elif accio == 'saluda':
    nom  = html.escape(form.getvalue('nom', 'desconegut'))
    edat = form.getvalue('edat', '')
    extra = f"D'aquí a 10 anys en tindràs {int(edat) + 10}." if edat else ''
    resposta = {
        "missatge": f"Hola, {nom}! Resposta generada per Python al servidor TINET.",
        "extra":    extra
    }

elif accio == 'suma':
    try:
        a = float(form.getvalue('a', 0))
        b = float(form.getvalue('b', 0))
        resposta = {
            "a": a, "b": b,
            "suma":      a + b,
            "producte":  a * b,
            "diferencia": a - b,
            "divisio":   round(a / b, 4) if b != 0 else "∞"
        }
    except (ValueError, TypeError):
        resposta = {"error": "Els valors han de ser números"}

else:
    resposta = {"error": f"Acció desconeguda: {html.escape(accio)}"}

print(json.dumps(resposta, ensure_ascii=False))
🔑 Clau: l'únic canvi entre els dos scripts és la capçalera Content-Type: application/json i que s'imprimeix json.dumps(resposta) en comptes d'HTML. Tot el mecanisme CGI és idèntic.

Afegir les teves pròpies accions

Pots ampliar el script afegint nous blocs elif. Per exemple, per llegir un fitxer de dades:

fragment — nova acció personalitzada
elif accio == 'el_meu_calcul':
    valor = form.getvalue('parametre', '')
    # → fes aquí la teva lògica Python
    resultat = valor.upper()  # exemple trivial
    resposta = {
        "ok": True,
        "resultat": resultat
    }

I al JavaScript de la pàgina HTML, crida-ho amb:

fragment JS — cridar la nova acció
cridar(
    { accio: 'el_meu_calcul', parametre: 'hola' },
    'id-del-div-resultat',
    data => data.ok ? `Resultat: ${data.resultat}` : 'Error'
);

6 Demos en directe

Pots provar directament els dos exemples complets al servidor TINET:

Usa el codi de les seccions 4 i 5 com a base. Modifica els blocs elif del script Python per afegir la teva pròpia lògica.

7 Errors habituals i solucions

ErrorCausa més probableSolució
500 Internal Server Error Script no executable, shebang incorrecte, o error de sintaxi Python Comprova permisos (755), shebang (#!/usr/bin/env python3), i que el fitxer tingui salts de línia Unix (LF)
403 Forbidden La carpeta cgi-bin/ no existeix o no té permisos adequats Crea la carpeta i assigna-li permisos 755
404 Not Found Ruta incorrecta a l'atribut action del formulari o a la URL del fetch() Verifica que la ruta sigui relativa: cgi-bin/consulta.py (sense / inicial)
JSON parse error al navegador El script imprimeix text o HTML abans de la capçalera JSON, o la capçalera és incorrecta Assegura't que la primera impressió és exactament Content-Type: application/json; charset=utf-8 seguit d'una línia en blanc
Caràcters estranys (’ etc.) Problema d'encodatge: el fitxer no s'ha desat en UTF-8 Desa el script i l'HTML sempre en UTF-8 sense BOM
🛠️ Durant el desenvolupament, cgitb.enable() al principi del script fa que els errors Python surtin directament al navegador en lloc d'un genèric 500. Recorda treure'l (o desactivar-lo) quan el codi passi a producció.