LoginSignup
126
126

More than 5 years have passed since last update.

Django の ImageField

Last updated at Posted at 2015-12-18

前準備

ImageField を扱うには PIL (Pillow) が必要となるので Python 実行環境に入っていなかったら以下で導入する:

pip install pillow
pip3 install pillow # Python 3.x

あと settings.pyMEDIA_ROOT の定義が必要なので追加しておく:

settings.py
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'

urls.py にも以下を追加しておく:

urls.py
urlpatterns = [
    url(r'^media/(?P<path>.*)$','django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
]

ImageField とは

Django の Model にはさまざまな Field が定義できるが、画像ファイルを簡単に扱うための Field として ImageField がある。
以下が一番単純な例だ:

class Image(models.Model):
    image = models.ImageField(upload_to='images/')

これで画像ファイル保存の際は MEDIA_ROOT + 'images/(元の画像ファイル名) で保存されることになる。
「元の画像ファイル名」が被っている場合は Django によって勝手に変な suffix が付けられ上書きされない。

元のファイル名を使いたくない

まず初めに画像保存時に primary key を使って保存したいと考えた。例えば元のファイル名は car.png だが保存する際の primary key が id=123 (pk=123) ならば 123.png にリネームされる、といったものが理想だ。

ImageField は upload_to 引数に関数を与えることで画像の保存先ディレクトリだけでなくファイル名も自由に変更できるのだが、例えば以下のようにしても関数がコールされた段階ではまだ id が取れていないのでうまくいかない:

def get_image_path(self, filename):
    prefix = 'images/'
    name = self.id  # これだと新規作成時にまだ ID が決まっていないので None になる!! ダメ
    extension = os.path.splitext(filename)[-1]
    return prefix + name + extension

では self.idNone の際は DB から max(id) + 1 を取ってそれを使うというのも一つの手だが、DB にもよるが必ずしも次の id が max(id) + 1 になるわけではないので厳密性に欠ける。

StackOverFlow によると「とりあえず適当な名前で保存しておいて保存後に ID が決まったら正しいファイル名にリネームすればいいのでは」と書いてあってそれも試してみたが、実ファイルの方は確かにそれでいけたのだが DB に入っている方のパスが古いままなので Django admin などで見た時におかしなことになる。

後、例えば 123.png で保存したとしてその画像を更新した時に 123.png を上書きすることができずに 123なんたら.png という訳の分からないファイルができてしまう。Django が上書きを許さないからだ。これを対処しようとすると古いファイルを前もって削除しておいてから保存して...とする必要がある。

などと考えていたらもう面倒くさくなったので primary key で画像ファイル名を保存するのは諦めた。

UUID とかにしておけばいいだろう

普通に一意なファイル名を作成したいのであれば UUID を使えばいいだろうということで以下のようにしてみた:

def get_image_path(self, filename):
    """カスタマイズした画像パスを取得する.

    :param self: インスタンス (models.Model)
    :param filename: 元ファイル名
    :return: カスタマイズしたファイル名を含む画像パス
    """
    prefix = 'images/'
    name = str(uuid.uuid4()).replace('-', '')
    extension = os.path.splitext(filename)[-1]
    return prefix + name + extension

これで images/7f9a9970cc8645a99a2191c114856426.jpg などという名前で登録されるようになった。だが ImageField はこのまま管理サイトなどで更新したり削除しても DB のレコードが消えるが元のファイルが残ってしまうという問題がある。これをなんとかしたい。

更新若しくは削除時に古い画像ファイルを削除する

表題の通り save() 若しくは delete() の際に古いファイルを削除してすっきりさせたい。更新・削除の前に古いファイルパスを保存しておき、実処理が行われてから古いファイルを削除するのが良いだろう。これはデコレータの出番だ。

def delete_previous_file(function):
    """不要となる古いファイルを削除する為のデコレータ実装.

    :param function: メイン関数
    :return: wrapper
    """
    def wrapper(*args, **kwargs):
        """Wrapper 関数.

        :param args: 任意の引数
        :param kwargs: 任意のキーワード引数
        :return: メイン関数実行結果
        """
        self = args[0]

        # 保存前のファイル名を取得
        result = Image.objects.filter(pk=self.pk)
        previous = result[0] if len(result) else None
        super(Image, self).save()

        # 関数実行
        result = function(*args, **kwargs)

        # 保存前のファイルがあったら削除
        if previous:
            os.remove(MEDIA_ROOT + '/' + previous.image.name)
        return result
    return wrapper

こんなデコレータ用の関数を用意し、

class Image(models.Model):
    @delete_previous_file
    def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
        super(Image, self).save()

    @delete_previous_file
    def delete(self, using=None, keep_parents=False):
        super(Image, self).delete()

    image = models.ImageField('画像', upload_to=get_image_path)

とすることで、更新・削除時に古いファイルが削除されるようになった。

126
126
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
126
126