Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Upload di file

In questo capitolo parleremo di:

  • gestire l’upload di file tramite form HTML
  • validare il tipo e la dimensione del file
  • salvare i file su disco in modo sicuro
  • servire i file caricati agli utenti

Upload di base

Per permettere l’upload di file, il form HTML deve avere due accorgimenti rispetto a un form normale:

  • enctype="multipart/form-data" — indica al browser di inviare il file come dati binari
  • <input type="file"> — il campo per selezionare il file
<form action="/upload" method="POST" enctype="multipart/form-data">
    <label>Scegli un file:</label><br>
    <input type="file" name="file"><br><br>
    <button type="submit">Carica</button>
</form>

Senza enctype="multipart/form-data" il file non viene inviato correttamente — è l’errore più comune.

Ricevere il file in Flask

Il file caricato si legge con request.files, in modo analogo a request.form per i campi di testo:

from flask import Flask, request, flash, redirect, url_for

app = Flask(__name__)
app.secret_key = 'chiave_segreta'

@app.route('/upload', methods=['GET', 'POST'])
def upload():
    if request.method == 'POST':
        file = request.files.get('file')

        if not file or file.filename == '':
            flash('Nessun file selezionato.', 'errore')
            return redirect(url_for('upload'))

        file.save('uploads/' + file.filename)
        flash(f'File "{file.filename}" caricato con successo.', 'successo')
        return redirect(url_for('upload'))

    return render_template('upload.html')

file.filename contiene il nome originale del file. file.save(percorso) lo salva su disco.

Creare la cartella uploads

La cartella uploads/ deve esistere prima di salvare i file. Puoi crearla automaticamente all’avvio dell’app:

import os

UPLOAD_FOLDER = 'uploads'
os.makedirs(UPLOAD_FOLDER, exist_ok=True)

app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER

os.makedirs(..., exist_ok=True) crea la cartella se non esiste, senza errori se esiste già.

La route di upload diventa:

file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))

Esercizi

Esercizio f431

Crea una app con una route /upload che permetta di caricare un file qualsiasi. Dopo il caricamento, mostra un flash message di conferma con il nome del file. Verifica che il file sia presente nella cartella uploads/.


Validazione

Accettare qualsiasi file è pericoloso. È buona pratica limitare i tipi di file consentiti verificando l’estensione:

ESTENSIONI_CONSENTITE = {'png', 'jpg', 'jpeg', 'gif', 'pdf'}

def estensione_consentita(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ESTENSIONI_CONSENTITE

rsplit('.', 1) divide il nome del file sull’ultimo punto, restituendo nome ed estensione. Il controllo '.' in filename evita file senza estensione.

La route aggiornata:

if not estensione_consentita(file.filename):
    flash('Tipo di file non consentito.', 'errore')
    return redirect(url_for('upload'))

Nomi sicuri con secure_filename

Il nome del file fornito dall’utente non è affidabile — potrebbe contenere caratteri speciali o percorsi come ../../etc/passwd che potrebbero sovrascrivere file di sistema.

Werkzeug mette a disposizione secure_filename che pulisce il nome del file:

from werkzeug.utils import secure_filename

filename = secure_filename(file.filename)
# 'My File (1).jpg'  →  'My_File_1_.jpg'
# '../../etc/passwd' →  'etc_passwd'

Il salvataggio sicuro diventa:

filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

Evitare sovrascritture

Se due utenti caricano un file con lo stesso nome, il secondo sovrascrive il primo. Una soluzione semplice è aggiungere un prefisso univoco al nome del file usando uuid:

import uuid

def nome_univoco(filename):
    estensione = filename.rsplit('.', 1)[1].lower()
    return f"{uuid.uuid4().hex}.{estensione}"

uuid.uuid4().hex genera una stringa esadecimale casuale di 32 caratteri — praticamente impossibile da indovinare o ripetere.

filename = nome_univoco(secure_filename(file.filename))
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))

Limitare la dimensione del file

Flask permette di impostare una dimensione massima per le richieste in ingresso:

app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024  # 2 MB

Se il file supera il limite, Flask solleva automaticamente un errore 413 Request Entity Too Large. Puoi intercettarlo con un handler personalizzato:

from flask import abort
from werkzeug.exceptions import RequestEntityTooLarge

@app.errorhandler(RequestEntityTooLarge)
def file_troppo_grande(e):
    flash('Il file supera la dimensione massima consentita (2 MB).', 'errore')
    return redirect(url_for('upload'))

Esercizi

Esercizio f441

Aggiorna l’app dell’esercizio precedente aggiungendo:

  • validazione dell’estensione (solo immagini: png, jpg, jpeg, gif)
  • nome del file reso sicuro con secure_filename
  • dimensione massima di 1 MB

Esercizio f442

Modifica l’app in modo che ogni file venga salvato con un nome univoco generato con uuid. Salva in uploads.json un registro dei file caricati con il nome originale, il nome salvato su disco e la data di caricamento.


Servire i file e galleria

I file nella cartella uploads/ non sono automaticamente accessibili dal browser. Per servirli bisogna creare una route apposita con send_from_directory:

from flask import send_from_directory

@app.route('/uploads/<filename>')
def file_caricato(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

Ora il file uploads/immagine.jpg è raggiungibile all’URL /uploads/immagine.jpg.

Una galleria immagini completa

Mettiamo tutto insieme: un’app che permette di caricare immagini e le mostra in una galleria.

import os
import uuid
import json
from datetime import date
from flask import Flask, render_template, request, flash, redirect, url_for, send_from_directory
from werkzeug.utils import secure_filename
from werkzeug.exceptions import RequestEntityTooLarge

app = Flask(__name__)
app.secret_key = 'chiave_segreta'
app.config['UPLOAD_FOLDER'] = 'uploads'
app.config['MAX_CONTENT_LENGTH'] = 2 * 1024 * 1024

ESTENSIONI_CONSENTITE = {'png', 'jpg', 'jpeg', 'gif'}
REGISTRO = 'uploads.json'

os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

def estensione_consentita(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ESTENSIONI_CONSENTITE

def nome_univoco(filename):
    estensione = filename.rsplit('.', 1)[1].lower()
    return f"{uuid.uuid4().hex}.{estensione}"

def leggi_registro():
    try:
        with open(REGISTRO, 'r', encoding='utf-8') as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError):
        return []

def salva_registro(dati):
    with open(REGISTRO, 'w', encoding='utf-8') as f:
        json.dump(dati, f, indent=2, ensure_ascii=False)

@app.errorhandler(RequestEntityTooLarge)
def file_troppo_grande(e):
    flash('Il file supera la dimensione massima consentita (2 MB).', 'errore')
    return redirect(url_for('galleria'))

@app.route('/', methods=['GET', 'POST'])
def galleria():
    if request.method == 'POST':
        file = request.files.get('file')

        if not file or file.filename == '':
            flash('Nessun file selezionato.', 'errore')
            return redirect(url_for('galleria'))

        if not estensione_consentita(file.filename):
            flash('Tipo di file non consentito.', 'errore')
            return redirect(url_for('galleria'))

        filename_sicuro = secure_filename(file.filename)
        filename_salvato = nome_univoco(filename_sicuro)
        file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename_salvato))

        registro = leggi_registro()
        registro.append({
            'nome_originale': filename_sicuro,
            'nome_salvato': filename_salvato,
            'data': date.today().isoformat()
        })
        salva_registro(registro)

        flash(f'"{filename_sicuro}" caricato con successo.', 'successo')
        return redirect(url_for('galleria'))

    immagini = leggi_registro()
    return render_template('galleria.html', immagini=immagini)

@app.route('/uploads/<filename>')
def file_caricato(filename):
    return send_from_directory(app.config['UPLOAD_FOLDER'], filename)

Il template galleria.html:

{% extends 'base.html' %}

{% block titolo %}Galleria{% endblock %}

{% block contenuto %}
<h1>Galleria immagini</h1>

<form action="/" method="POST" enctype="multipart/form-data">
    <input type="file" name="file">
    <button type="submit">Carica</button>
</form>

<div style="display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px;">
{% for img in immagini %}
    <div>
        <img src="/uploads/{{ img.nome_salvato }}" style="width: 200px; height: 150px; object-fit: cover;">
        <p style="font-size: 0.85em;">{{ img.nome_originale }}<br>{{ img.data }}</p>
    </div>
{% endfor %}
</div>
{% endblock %}

Esercizi

Esercizio f451

Aggiungi alla galleria la possibilità di eliminare un’immagine. La cancellazione deve rimuovere sia il file dalla cartella uploads/ che la voce corrispondente in uploads.json.

Tip

usa os.remove(percorso) per eliminare un file dal disco.


Esercizio f452 (più impegnativo)

Proteggi la route di upload con @login_required usando Flask-Login (vedi Capitolo 1). Solo gli utenti loggati possono caricare immagini, ma tutti possono vedere la galleria.