Bienvenue sur IndexError.

Ici vous pouvez poser des questions sur Python et le Framework Django.

Mais aussi sur les technos front comme React, Angular, Typescript et Javascript en général.

Consultez la FAQ pour améliorer vos chances d'avoir des réponses à vos questions.

(Flask) Comment servir des fichiers pdf ?

+4 votes

Je développe une application avec Flask, dans laquelle j'ai besoin d'afficher des liens vers des fichiers pdf stockés sur le serveur.

Dans une vue /annuaire/annee, je dois afficher autant de liens (une cinquantaine) que de publications (en pdf) dans l'année. Les fichiers pdf sont stockés dans "/static/data/yearbooks/" (pas de base de données).

Je voudrais qu'en cliquant sur le lien "publication1", le pdf s'affiche dans le navigateur (en utilisant le moteur du navigateur), à l'adresse /annuaire/<annee>/publication1.pdf.

J'ai essayé d'utiliser url_for(...), qui me permet d'afficher le pdf à l'écran mais l'url n'est plus la bonne, forcément (elle devient /static/data/yearbooks/<year>/publication1.pdf)...

Je suppose qu'il existe un moyen (simple?) d'afficher les pdf comme je le souhaite; quel est-il?

demandé 28-Sep-2015 par meta (208 points)
edité 30-Sep-2015 par meta

2 Réponses

+6 votes
 
Meilleure réponse

Réponse basée sur les commentaires de @jc (merci à lui).

Le dossier /static/data/yearbooks/<year> contient toutes les publications de l'année year.

Renvoyer une publication se fait en créant la route suivante:

@app.route('/annuaire/<int:year>/<publi_name>.pdf')
def publication(year, publi_name):
    directory = os.path.join('static', 'data', 'yearbooks', str(year))
    fname = "{}.pdf".format(publi_name)
    return send_from_directory(directory, fname)

Idéalement, le chemin vers le répertoire data sera spécifié dans un fichier de configuration.

Le fait que year soit forcément un int empêche de faire n'importe quoi avec os.path().

répondu 30-Sep-2015 par meta (208 points)
sélectionné 30-Sep-2015 par meta
+4 votes

il suffit de créér un route qui va lire le fichier et envoyer la réponse:

@app.route('/annuaire/annee/publication<int:num>.pdf')
def publish(num):
    file_path = "../../static/data/yearbooks/publication{}.pdf".format(num)
    raw_bytes = ""
    with open(file_path, 'rb') as r:
        for line in r:
            raw_bytes = raw_bytes + line
    response = make_response(raw_bytes)
    response.headers['Content-Type'] = "application/pdf"
    return response

plus d'info sur response
http://flask.pocoo.org/docs/0.10/api/#flask.make_response

répondu 28-Sep-2015 par yohann (312 points)

Et encore mieux tu peux utiliser sendfromdirectory.

Ca fait appel a la méthode send_file (source), qui gère entre autres les e-tags et un timeout pour le cache du navigateur.

J'y suis arrivé en faisant raw_bytes = tab.read() plutôt qu'en lisant le fichier ligne par ligne.

@jc j'ai essayé d'utiliser send_from_directory() mais j'obtiens systématiquement une erreur 404 (j'utilise os.path.join() pour trouver mon fichier). Question complémentaire: en quoi send_from_directory() est-il meilleur que la solution proposée par @yohann?

A priori ca s'utiliserait de la forme

# repertoire dans lequel 
directory = "../../static/data/yearbooks/"
fname = "publication{}.pdf".format(num)
send_from_directory(directory, fname)

en quoi send_from_directory() est-il meilleur que la solution proposée par @yohann?

Parce que c'est toujours mieux de faire confiance au framework que t’utilise plutôt que de réinventer la roue ;)

La je vois 2 principaux avantages :

Déjà pour le cache : send_file va gérer les etags : tu informes le browser client que le fichier ne vas pas changer tout de suite, il ne reviendra pas bloquer ton app en lisant un PDF qui n'a pas changé. Ça évite de bloquer le worker de ton app sur des lectures de fichier inutiles.

Pour la securité : send_from_directory() fait un appel a safe_join() (code) qui évite de faire des os.path.join("../static", "../../../../../../../etc/passwd") par exemple (ici si tu caste en int ton id tu n'aura pas ce probleme, mais c'est bien de le savoir)

Après, idéalement, l'app Flask ne devrait pas gérer les fichiers statiques et laisser ce job a celui qui fait çà beaucoup mieux que nous : le serveur web en front de ton app (nginx ou apache). Dans ton cas la transformation /annuaire/annee/publication1.pdf vers /static/data/yearbooks/publication1.pdf se ferait avec un rewrite dans ta conf nginx, mais c'est moins simple qu'un send_from_directory)

Ok, je viens de réussir à utiliser correctement send_from_directory(). Merci pour les précisions. Je verrai comment gérer ça avec nginx quand mon app sera prête à être déployée.

@jc t'aurais dû mettre ton 1° commentaire en reponse. là pour le coup la reponse ne sera pas validée puisque @meta a trouvé un autre moyen :)
Sinon au pire @meta, fais toi ta reponse qui marche et valide là
Ainsi chacun saura que la question est bouclée ;)

Tu as raison.

Si @meta peut poser sa solution en utilisant send_from_directory(), avec le code complet (parsing des arguments, transformation du path et utilisation de send_from_directory()), ça me parait mieux.

Sinon je posterais mon commentaire en réponse.

...