Gestion des fichiers temporaires avec Python

Publié par Pierre le 1 mars 2024 14:53

Il est commun pour des développeurs de créer des fichiers temporaires pour y écrire de la donnée qui ne pourrait tenir en mémoire ou bien qui doit être partagée entre des threads (ou avec des programmes tiers). Il peut sembler trivial de créer un fichier dans le répertoire /tmp/, c’est d’ailleurs plutôt simple à faire en Python, mais la gestion correcte des fichiers temporaires est en fait un peu plus complexe qu’il n’y parait et les fichiers temporaires devraient se conformer aux règles suivantes:

  • Le nom d’un fichier doit être unique et non prévisible: des noms de fichiers prévisibles ouvrent une brèche de sécurité grâce à laquelle des utilisateurs mal avisés pourraient abuser de votre logiciel en écrivant dans un fichier qu’ils savent lu par votre application. Aussi, l’utilisation de noms uniques limite le risque de bugs qui pourraient survenir si plusieurs processus de votre logiciel essaient d’écrire en même temps dans un même fichier.
  • De manière générale, les fichiers temporaires devraient être créés sur le système de fichier local de votre système. Certains systèmes de fichiers distants ne supportent en effet pas les options (flags) d’ouverture requises pour créer des fichiers temporaires (cf: file status flags on GNU linux).
  • Les fichiers temporaires doivent être supprimés même si le programme rencontre une erreur: en ne le faisant pas, votre application risque de consommer de plus en plus d’espace disque au cours de son exécution. Pour une application cliente, ce n’est pas très respectueux pour l’utilisateur et, particulièrement pour les applications serveur, vous pouvez atteindre la limite de stockage disponible. Même si les répertoires de fichiers temporaires (tels que /tmp/) sont, généralement, vidés au moment de l’arrêt du système, un serveur peut ne pas redémarrer pendant un long moment.

Aussi, n’oubliez pas que:

  • Les répertoires de fichiers temporaires sont, généralement, partagés entre plusieurs utilisateurs et applications.
  • Un répertoire peut être considéré comme sécurisé si seul sont propriétaire (et éventuellement l’administrateur du système) ont l’autorisation de créer, déplacer ou bien supprimer des fichiers à l’intérieur de celui ci.

Les tests décrits dans cet article ont été réalisés sous Python 3.12.

Sommaire

Le module tempfile

Afin de nous aider à faire les choses correctement, Python contient un module nommé tempfile.

Ce module expose des interfaces de haut et de bas niveaux qui fonctionnent sur toutes les plateformes supportées par Python et que nous pouvons utiliser pour manipuler des fichiers temporaires de manière appropriée. Les interfaces de haut niveau (TemporaryFile, NamedTemporaryFile, TemporaryDirectory, SpooledTemporaryFile) peut être utilisées en tant que context managers et fournissent des aides de suppression automatique des fichiers, tandis que les interfaces de plus bas niveau (mkstemp() et mkdtemp()) nécessiteront que vous gériez la problématique de suppression des fichiers temporaires par vous même.

Dans cet article, nous allons nous focaliser sur les interfaces de haut niveau.

Créer des fichiers temporaires

Le module tempfile fourni deux interfaces pour vous aider à manipuler des fichiers temporaires:

  • TemporaryFile: il s’agit de l’interface la plus simple à manipuler. En fonction de votre OS, elle ne vous assure pas que les fichiers créés soient visible dans le système de fichier.
  • NameTemporaryFile: fonctionne de manière similaire à TemporaryFile mais s’assure que les fichiers crées aient un nom visible dans le système de fichier (vous permettant de ré-ouvrir des fichiers en utilisant leur nom).

Tout fichier temporaire créé en utilisant le module tempfile ne sera lisible et éditable que par l’utilisateur qui l’a créé.

L’interface TemporaryFile

TemporaryFile retourne un objet représentant un fichier. En l’utilisant en temps que context manager, nous nous assurons que le fichier est supprimé à la fin du bloc with:

import tempfile

with tempfile.TemporaryFile() as temporary_file:
    temporary_file.write('whatever')

Le fonctionnement de cette méthode peut varier d’un système d’exploitation à un autre. Ainsi, sur un système Unix l’entrée de répertoire pour le fichier:

  • n’est pas créé du tout
  • ou bien supprimée immédiatement après la création du fichier

Les autres plate formes ne supportent pas ce fonctionnement, votre code ne devrait jamais partir du principe qu’un fichier temporaire créé à l’aide de cette fonction aura un nom visible dans le système de fichier.

Il est à noter que TemporaryFile accepte un argument optionnel dir. Si la valeur de dir est différente de None (valeur par défaut), alors le fichier sera créé dans le répertoire spécifié. Dans le cas contraire, le répertoire est choisi automatiquement en fonction de votre système d’exploitation (/tmp/ par exemple sur un système Linux).

L’interface NamedTemporaryFile

NamedTemporaryFile à un comportement similaire à TemporaryFile à l’exception des deux différences suivantes:

  • La fonction vous garantie de retourner un fichier qui aura un nom visible dans le système de fichier.
  • La fonction expose deux arguments optionnels: delete (True par défaut) et delete_on_close (True par défaut) qui vous permettent de ré-ouvrir des fichiers temporaires fermés en vous servant de leur nom.

La manière la plus simple de manipuler NamedTemporaryFile est de l’utiliser en tant que que context manager (de la même manière que TemporaryFile):

import os
import tempfile

# Create a temporary file with a visible name in file system.
with tempfile.NamedTemporaryFile() as temporary_file:
   print(temporary_file.name)
   print(os.path.isfile(temporary_file.name))
   temporary_file.write(b'whatever')

print('--- Context manager exit ---')

# File has been removed at this point.
print(os.path.isfile(temporary_file.name))
/tmp/tmp87yk7rwz
True
--- Context manager exit ---
False

Nous pouvons constater que le nom du fichier inclus une chaîne de caractères aléatoires assurant son unicité. Vous pouvez faire des manipulations sur le fichier jusqu’à sa fermeture ou bien jusqu’à la fin de l’exécution du context manager.

Vous pouvez aussi choisir de gérer la fermeture du fichier par vous même:

import tempfile

temporary_file = tempfile.NamedTemporaryFile()

print(temporary_file.name)
print(os.path.isfile(temporary_file.name))

try:
    temporary_file.write(b'whatever')
finally:
    # File is deleted on close.
    temporary_file.close()

print(os.path.isfile(temporary_file.name))

(la sortie de la console devrait être similaire à celle de l’exemple précédent).

Le fichier sera ici supprimé à sa fermeture. Fermer le fichier dans une clause finally est important afin de s’assurer que le fichier est bien supprimé même si votre application rencontre une erreur.

Ré-ouvrir un fichier temporaire fermé

Si vous souhaitez un peu grand controle, si vous avez besoin d’utiliser le nom du fichier temporaire afin de ré-ouvrir ce fichier après sa fermeture, vous pouvez utiliser les arguments delete et delete_on_close:

  • delete à False désactivera la suppression automatique du fichier (aussi bien lors de sa fermeture qu’à la sortie d’un context manager). Quand sa valeur est à True, la valeur de delete_on_close est ignorée.
  • delete_on_close à False désactivera la suppression automatique du fichier à sa fermeture mais continuera de fournir une assistance à la suppression automatique du fichier lors de la fin de l’exécution d’un bloc with (si la fonction est utilisée en tant que context manager).

L’utilisation de delete_on_close et d’un context manager vous fournira toujours une assistance à la suppression automatique du fichier temporaire alors que l’utilisation de delete vous demandera de gérer la suppression du fichier par vous même.

En utilisant delete à False

import os
import tempfile

temporary_file = tempfile.NamedTemporaryFile(delete=False)

print(temporary_file.name)
print(os.path.isfile(temporary_file.name))

# File is not deleted on close.
temporary_file.close()

print(os.path.isfile(temporary_file.name))

Produit le résultat suivant:

/tmp/tmpq2wkbxbv
True
True

C’est alors de votre responsabilité de manuellement supprimer le fichier car vous ne bénéficiez d’aucune assistance à la suppression automatique du fichier.

En utilisant delete_on_close à False

import os
import tempfile

with tempfile.NamedTemporaryFile(delete_on_close=False) as temporary_file:
    print(temporary_file.name)
    temporary_file.close()
    # File is not deleted on close.
    print(os.path.isfile(temporary_file.name))

print('--- Context manager exit ---')

# File is deleted at context manager exit.
print(os.path.isfile(temporary_file.name))

Produit le résultat suivant:

/tmp/tmparbrvy0h
True
--- Context manager exit ---
False

Cette fois ci, le fichier n’est pas supprimé lorsque nous le fermons (nous autorisant à le ré-ouvrir plus tard) mais nous bénéficions d’une assistance au nettoyage automatique du fichier par sa suppression à la sortie du context manager. Cette manière de faire est plus sure que d’utiliser delete et est recommandée par la documentation.

Le cas particulier de Windows

Sur Windows, ré-ouvrir un fichier temporaire en utilisant son nom requiers le respect d’un certains nombre de conditions qui sont décrite dans la documentation de NamedTemporaryFile. Je ne creuserais pas ce sujet mais il est intéressant de noter que Django fourni sa propre implémentation de NamedTemporaryFile (cf: le module django.core.files.temp). Cela vous permet de ré-ouvrir un fichier temporaire, sur Windows, sans avoir à vous préoccuper des conditions listées dans la documentation ci-dessus.

Créer des dossiers temporaires

Pour terminer, le module tempfile fourni l’interface TemporaryDirectory qui vous permet de créer des dossiers temporaires. Cette fois, la valeur de retour est une chaîne de caractères qui représente le chemin du dossier:

import os
import tempfile

with tempfile.TemporaryDirectory() as temporary_directory_path:
    print(f"Directory path: {temporary_directory_path}")
    print(f"Directory exists: {os.path.exists(temporary_directory_path)}")

    temporary_file_path = os.path.join(temporary_directory_path, 'file.txt')
    temporary_file = open(temporary_file_path, 'w')

    print(f"Temporary file exists: {os.path.exists(temporary_file_path)}")

print('--- Context manager exit ---')

print(f"Directory exists: {os.path.exists(temporary_directory_path)}")
print(f"Temporary file exists: {os.path.exists(temporary_file_path)}")

Produit le résutlat suivant:

Directory path: /tmp/tmpxgamn3z7
Directory exists: True
Temporary file exists: True
--- Context manager exit ---
Directory exists: False
Temporary file exists: False

Comme nous pouvons le voir, le répertoire et les fichiers qu’il contient sont supprimés à la sortie du context manager.

Vous pouvez contrôler l’assistance de nettoyage automatique du dossier en vous donnant la valeur False au paramètre delete:

import os
import tempfile

# Create the temporary directory with `delete` to `False`.
with tempfile.TemporaryDirectory(delete=False) as temporary_directory_path:
    print(f"Directory path: {temporary_directory_path}")
    print(f"Directory exists: {os.path.exists(temporary_directory_path)}")

    temporary_file_path = os.path.join(temporary_directory_path, 'file.txt')
    temporary_file = open(temporary_file_path, 'w')

    print(f"Temporary file exists: {os.path.exists(temporary_file_path)}")

print('--- Context manager exit ---')

# Directory and files are not deleted on context manager exit.
print(f"Directory exists: {os.path.exists(temporary_directory_path)}")
print(f"Temporary file exists: {os.path.exists(temporary_file_path)}")

Qui produira cette fois le résultat suivant:

Directory path: /tmp/tmpd_n9eso5
Directory exists: True
Temporary file exists: True
--- Context manager exit ---
Directory exists: True
Temporary file exists: True

Définir la valeur de delete à False désactive l’assistance au nettoyage automatique. Utilisez le avec précaution car, lors de son utilisation, la responsabilité de suppression du répertoire vous revient.

Conclusion

Comme nous avons pu le voir, créer, utiliser et supprimer des fichiers temporaires de manière sur requiert un peu d’attention. En utilisant, de manière correct, les interfaces de haut niveau fournies par le module tempfile vous vous assurez de créer des fichiers temporaires:

  • avec les options (eg: flags) ainsi que les permissions appropriées (lecture / écriture pour le créateur du fichier seulement)
  • dans un dossier approprié en fonction du système d’exploitation sur lequel votre programme est executé
  • nommés de manière unique et non prévisible
  • avec une assistance au nettoyage automatique

En conclusion, le module tempfile devrait être utilisé à chaque fois que votre programme à besoin de manipuler des fichiers temporaires.

Si vous souhaitez limiter le risque d’introduire une gestion incorrecte des fichiers temporaires (par exemple en créant manuellement des fichiers dans un répertoire /tmp/) dans votre programme, vous devriez considérer l’utilisation de bandit. Bandit est un linter spécialisé dans la détection de problèmes de sécurité dans du code Python. Le check B108 est particulièrement efficace pour trouver des usages non sécurisés de fichiers et dossiers temporaires en cherchant dans votre code des occurrences correspondant aux chemins les plus couramment utilisés pour le stockage ce type de fichiers (/tmp, /var/tmp, etc …). Vous pouvez l’utiliser en tant que pre-commit et en tant qu’étape de vérification de votre CI.