読者です 読者をやめる 読者になる 読者になる

管理画面でファイル名に日本語を含むファイルをアップロードしてみる

Python Django

前提とする環境は http://d.hatena.ne.jp/re_guzy/20070424/p1 と同じ。
Windows 2003 Server、Python 2.4.4、 Django 0.96(SQLite使用) で動作確認済み。

※oldformsを使っているので、Django 0.96 より後のリリースでは動かないはず。

困ったこと

FileFieldを使うと、アップロードしたファイルのファイル名のうち、日本語はすべて除去される。
(「新規テキスト文書.txt」→「.txt」)

参考URL

無理やりなんとかしてみる

http://d.hatena.ne.jp/re_guzy/20070330/p1 と同じように メタクラスを利用する。
(ManyToManyの対応が不要ならdispatcher周りはなくてもいいかな。)

仕組みとしてはこんな感じ。

  1. Manipulator.saveをデコレートする。
  2. オリジナルのファイル名がManipulator.saveの引数にあるので、それを使う。
  3. アップロードしたファイルが保存されてから、リネームする。

やってることは単純なんだけど、文字コード周りで苦労した。
DBにSQLiteを使っている関係で、オリジナルのファイル名を保存するときにUTF-8エンコードしている。他のDBならいらないかも。

#models.py
# -*- encoding: utf-8 -*-
from django.db import models
from django.db.models.base import ModelBase
from django.db.models.manipulators import *
from django.db.models import *
from django.dispatch import dispatcher
import re, sys, os

CODE_MOD = 'utf-8' #モジュールの文字コード
CODE_SYS = sys.getfilesystemencoding()

def save_decorator(func):
   def save_decorator_main(self, new_data):
       saved = func(self, new_data)
       for key in new_data.keys():
           m = re.search(r'^(\w+)_file$', key)
           if not m or not 'filename' in new_data[key]:
               continue
 
           field = m.groups(1)[0]
           original_name = new_data[key]['filename'].decode(CODE_MOD)
           path = getattr(saved, "get_%s_filename" % (field))().decode(CODE_MOD)
           dirname, filename = os.path.split(path)
           #パスはOSの文字コードにエンコードしておく(os.path.existsなどが意図したとおりに動かないので)
           newpath = os.path.join(dirname, original_name).encode(CODE_SYS)
           #すでにファイルが存在する場合は、ファイル名先頭に「_」をつける
           while os.path.exists(newpath):
                original_name = '_' + original_name
                newpath = os.path.join(dirname, original_name).encode(CODE_SYS)
           #SQLiteはUTF-8しか対応していないのでエンコードしておく
           newfield = getattr(saved, field).decode(CODE_MOD).replace(filename, original_name).encode(CODE_MOD)
           os.rename(path, newpath)
           setattr(saved, field, newfield)
           saved.save() #ファイルパスとDB上のデータの不整合を防ぐためにここで保存する

       dispatcher.send(signal=post_manipulator_save, sender=saved.__class__, instance=saved)
       return saved
   return save_decorator_main

def get_filename_decorator(func):
    def get_filename_decorator_main(self):
        return func(self).decode(CODE_MOD)
    return get_filename_decorator_main

class CustomModelBase(ModelBase):
   def __new__(cls, name, bases, attrs):
       meta = super(CustomModelBase, cls).__new__(cls, name, bases, attrs)
       for manipulator in [getattr(meta, 'AddManipulator'), getattr(meta, 'ChangeManipulator')]:
           # 複数回デコレートを防いでおく
           if not "save_decorator_main" in str(manipulator.save):
               manipulator.save = save_decorator(manipulator.save)
           
       for attr in dir(meta):
           m = re.search('^get_\w+_filename$', attr)
           if m:
               funcname = m.group()
               func = getattr(meta, funcname)
               # 複数回デコレートを防いでおく
               if not "get_filename_decorator_main" in str(func):
                   setattr(meta, funcname, get_filename_decorator(func))
           
       return meta

post_manipulator_save = object()

class Upload(models.Model):
    __metaclass__ = CustomModelBase
    content = models.FileField(upload_to='uploaded/%Y/%m', blank=True, null=True)
    def __str__(self):
        return self.content
    class Admin:pass
#settings.py
MEDIA_ROOT = 'd:/www/site_media/'
MEDIA_URL = 'http://servername/site_media/'

残課題

  1. apacheで静的ファイルを配信している場合は、管理画面からアップロードしたファイルを開くことができるけど、開発サーバの場合はできない。
  2. 一度アップロードしたファイルを変更できない。

上は開発時に不便なだけなのでまあいいけど、下は結構痛いかな。