Peupler un FileField Django par programmation

Ce billet date de plusieurs années, ses informations peuvent être devenues obsolètes.

Dans beaucoup de cas les fichiers media sont téléversés par les utilisateurs et Django fait le job presque tout seul.

Mais il m'arrive d'avoir à déplacer des fichiers par l'intermédiaire d'une commande ou de devoir récupérer des ressources distantes en provenance d'une API et d'avoir à les enregistrer dans un FileField par programmation.

Le code Py3k

Un modèle d'exemple :

from django.db import models
from django.utils.translation import ugettext_lazy as _

class StoredMedia(models.Model):
    name = models.CharField(verbose_name=_("Nom"), max_length=255)
    file = models.FileField(verbose_name=_("Fichier"), max_length=255)

    def __str__(self):
        return self.name

Le code pour enregistrer un fichier distant :

import os
import tempfile
import urllib
from django.core.files import File
from my.app.models import StoredMedia

# Retrieve the file.
response = urllib.request.urlopen('https://marcarea.com/media/blog/djangocong.2012.png')

# Get the file name.
split_url = urllib.parse.urlsplit(response.url).path
file_name = os.path.basename(split_url)

tmp_file = '{tmp_dir}{file_name}'.format(
    tmp_dir=tempfile.gettempdir(),
    file_name=file_name
)

# Create a temporary file locally.
with open(tmp_file, 'wb') as f:
    f.write(response.read())

# Open the temporary file as a normal Python File object.
with open(tmp_file, 'rb') as f:
    # Convert it to a Django File.
    django_file = File(f)
    # Create a StoredMedia instance.
    stored_media = StoredMedia()
    stored_media.name = "Beautiful picture"
    # Attach the temporary file to the StoredMedia instance.
    # 'save=True' means that we want to save the StoredMedia instance after the image is saved.
    stored_media.file.save(file_name, django_file, save=True)

os.remove(tmp_file)

Plus concis avec django.core.files.temp.NamedTemporaryFile ?

C'est possible :

import os
import urllib
from django.core.files import File
from django.core.files.temp import NamedTemporaryFile
from my.app.models import StoredMedia

response = urllib.request.urlopen('https://marcarea.com/media/blog/djangocong.2012.png')

split_url = urllib.parse.urlsplit(response.url).path
file_name = os.path.basename(split_url)

tmp_file = NamedTemporaryFile(delete=True)
tmp_file.write(response.read())
tmp_file.flush()

stored_media = StoredMedia()
stored_media.name = "Beautiful picture"
stored_media.file.save(file_name, File(tmp_file), save=True)

tmp_file.close()

C'est plus beau, mais c'est à vos risques et périls !

D'une part parce que cette classe est marquée for Django's internal use et certains suggèrent même de la supprimer, d'autre part parce qu'elle est basée sur la fonction Python du même nom qualifiée de not particularly useful on Windows !

À propos de FileField

Une solution quick-and-dirty pour prévisualiser les images dans l'admin

En premier lieu j'ajoute une méthode à mon modèle (avec un style en ligne bien sale) :

class StoredMedia(models.Model):

    [...]

    def file_preview(self):
        return '<img src="{0}" style="max-width: 150px;">'.format(self.file.url)
    file_preview.short_description = _("Image")
    file_preview.allow_tags = True

Et puis je me fais plaisir dans l'admin :

from django.contrib import admin
from my.app.models import StoredMedia

class StoredMediaAdmin(admin.ModelAdmin):
    list_display = ('id', 'name', 'file', 'file_preview',)

admin.site.register(StoredMedia, StoredMediaAdmin)

Les enfants, ne faites pas ça chez vous avec un FileField ! Faites le avec un ImageField si vous avez des images de taille raisonnable.

L'instruction with en Python avec open

D'après la documentation Python :

It is good practice to use the with keyword when dealing with file objects. This has the advantage that the file is properly closed after its suite finishes, even if an exception is raised on the way. It is also much shorter than writing equivalent try-finally blocks

Concrètement, l'objet File créé et retourné par la fonction open() expose ses propres méthodes __enter__ et __exit__ et se comporte comme un context manager avec l'instruction with. Le fichier est toujours fermé à la fin.

Ressources

#1 Romuald Brunet

09/02/2015 08:58

Concernant le with, les objets retournés par file/open seront (presque) toujours fermés dans tous les cas, même sans le with.

En effet, les fichiers sont "fermés" lors de la destruction de l'objet file. Donc dans la majorité des cas lorsque l'ont sort de la fonction.

On peut éventuellement leaker une référence (et donc garder un fichier ouvert) si une exception référençant le fichier est levée

#2 Kemar

09/02/2015 15:23

@Romuald Si tu penses que c'est le garbage collector qui ferme le fichier, tu as raison en ce qui concerne l'interpréteur CPython. Ça n'est pas garanti avec les autres implémentations.

Inférer qu'on va toujours être sur du CPython est probablement une mauvaise pratique.

Si tu penses que c'est os.remove qui va supprimer le fichier, ça fonctionnera sous *nix même si le fichier n'est pas fermé au préalable, mais pas sous Windows :

On Windows, attempting to remove a file that is in use causes an exception to be raised; on Unix, the directory entry is removed but the storage allocated to the file is not made available until the original file is no longer in use.

Par conséquent je pense qu'il est préférable de toujours fermer un fichier de manière explicite pour maximiser la portabilité du code.

Avant Django et le pouvoir du Q Après Django avec CloudFront et S3, PEP 476

Tag Kemar Joint