DjangoのモデルにはFileField
やImageField
など、バイナリファイルを扱う属性があります。
これらに格納されるファイルの保存先を動的に変更する方法を確認しました。
FileField
やImageField
の動き
これらのフィールドにはファイルを格納することができますが、ファイルがバイナリ化されてバックエンドのDBに格納されるわけではありません。パフォーマンスの観点からsettings.MEDIA_ROOT
に指定されたディレクトリ配下にファイルを保存し、MEDIA_ROOT
を起点とした相対パスの情報のみをDBに格納するように動きます。
なお、保存されるパスは各Fieldのupload_to
も考慮されますので実際には以下のようなディレクトリになります。
MEDIA_ROOT/upload_to/file_name
通常はMEDIA_ROOT
はローカルファイルパスを指しており、Djangoが稼働するノードのファイルシステム上に保存されます。
S3BotoStorageの場合
S3BotoStorage
はFileField
などで格納されるファイルをS3のBucket上に保存してくれるサードパーティライブラリです。保存先はsettings.AWS_STORAGE_BUCKET_NAME
で指定したバケットになり、MEDIA_ROOT
は使用されなくなります(たしか…)
FileFieldの保存先が確定するタイミング
class FileField(Field): # The class to wrap instance attributes in. Accessing the file object off # the instance will always return an instance of attr_class. attr_class = FieldFile # The descriptor to use for accessing the attribute off of the class. descriptor_class = FileDescriptor description = _("File") def __init__(self, verbose_name=None, name=None, upload_to='', storage=None, **kwargs): self._primary_key_set_explicitly = 'primary_key' in kwargs self.storage = storage or default_storage self.upload_to = upload_to kwargs['max_length'] = kwargs.get('max_length', 100) super(FileField, self).__init__(verbose_name, name, **kwargs)
FileField
のソースはこんな感じになっていて、格納先に関わるのはself.storage
とself.upload_to
です。いずれもクラスが初期化された時点で確定します。storage
を明示していない場合はsettings
で設定したものがdefault_storage
として利用されます。
FileField
がどのようなバックエンドで(S3なのかローカルファイルシステムなのか)どのような場所(バケット名やディレクトリ名)を使用するかが確定し、settings
から読み込まれるのはdjango.setup()
が実行されて該当のモデルが初期化されたタイミングです。(厳密には、初期化されてから最初にアクセスされたタイミングで確定しますが面倒なので無視します)
これがとういうことかは以下のS3BotoStorage
を使用しているコードを例に説明します。モデルのcontent
はStorage
を明示していないものとします。
from django.conf import settings settings.AWS_STORAGE_BUCKET_NAME = 'bucket_1' # 既存のモデルを取得 my_model = MyModel.objects.get(pk=1) # デフォルトのバケットからファイルを取得 my_model.content.open() data = my_model.content.read() my_model.content.close() # 移植用に仮想のファイルオブジェクトを生成 tmp_file = BytesIO(data) tmp_file.seek(0) tmp_file = ContentFile(tmp_file.getvalue()) tmp_file.name = my_model.filename # バケット名を変更する settings.AWS_STORAGE_BUCKET_NAME = 'bucket_2' # 別のモデルに移植 my_other_model = MyModel( content=tmp_file, ) my_other_model.save()
コードだけ見ればmy_other_model.content
はbucket_2
に保存されてほしいですが、残念ながらbucket_1
に保存されます。bucket_2
への変更をしてもすでにMyModel.content
のstorage
は確定しているので変更が読み込まれることはありません。
もちろん、移行先のModel定義自体が違うのであればFileField.storage
を明示して他のバケットを書けばできますが、今回はやんごとなき事情で同じモデルのまま別のバケットに移行する必要があったのでこれはできませんでした。
ということでどうすればいいのかです。
from django.conf import settings # 既存のモデルを取得 my_model = MyModel.objects.get(pk=1) # これはデフォルトと同じなので書かなくてもいいけど、Storageを明示 my_model.content.storage = S3BotoStorage(bucket='bucket_1') # デフォルトのバケットからファイルを取得 my_model.content.open() data = my_model.content.read() my_model.content.close() # 移植用に仮想のファイルオブジェクトを生成 tmp_file = BytesIO(data) tmp_file.seek(0) tmp_file = ContentFile(tmp_file.getvalue()) tmp_file.name = my_model.filename # 別のモデルに移植 my_other_model = MyModel( content=tmp_file, ) # ここでStorageごと差し替える my_other_model.content.storage = S3BotoStorage(bucket='bucket_2') my_other_model.save()
結局、実ファイルへのアクセスが発生するタイミング(read,write)時点でのStorageをもとにファイルアクセスが行われるので、上述の例のようにFileField.storage
をまるっと差し替えてやれば意図した結果を得ることができます。
まとめ
利用ケースがマニアック過ぎて誰の役にたつんだって内容をまとめました。FileFieldのデータをバックエンドのファイル含めて別に移行しなければいけなくなったケースくらいでしか生きない知識ですが、どこかで役に立つことを祈っています。