前提の気持ち
Djangoへそこそこの大きさのファイル(数10MB)をフォームでアップロード(POST)するときに、ファイルがどういう形式で扱われているのかよくわからないので、調べてみよう。
調べたこと
Djangoでファイルを扱うためのクラスは、ファイルサイズ、保存場所によっていくつかあるんですね。
UploadedFile
DjangoへアップロードされたファイルをDjangoでの使用に便利なオブジェクトとして扱うためのクラス。
フォームでsubmitされたデータはこのインスタンスとして、request.FILES
に格納されている。
class UploadedFile(File):
"""
An abstract uploaded file (``TemporaryUploadedFile`` and
``InMemoryUploadedFile`` are the built-in concrete subclasses).
An ``UploadedFile`` object behaves somewhat like a file object and
represents some file data that the user submitted with a form.
"""
def __init__(
self,
file=None,
name=None,
content_type=None,
size=None,
charset=None,
content_type_extra=None,
):
super().__init__(file, name)
self.size = size
self.content_type = content_type
self.charset = charset
self.content_type_extra = content_type_extra
def __repr__(self):
return "<%s: %s (%s)>" % (self.__class__.__name__, self.name, self.content_type)
def _get_name(self):
return self._name
def _set_name(self, name):
# Sanitize the file name so that it can't be dangerous.
if name is not None:
# Just use the basename of the file -- anything else is dangerous.
name = os.path.basename(name)
# File names longer than 255 characters can cause problems on older OSes.
if len(name) > 255:
name, ext = os.path.splitext(name)
ext = ext[:255]
name = name[: 255 - len(ext)] + ext
name = validate_file_name(name)
self._name = name
name = property(_get_name, _set_name)
TemporaryUploadedFile
UploadedFileを"DISK"上に保存する場合のクラス。
Pythonの組み込みモジュールであるtempfile
を使っている。
2.5MB以上のファイルはこのインスタンスになる。
TemporaryFileUploadedHandler
が生成してくれる。
class TemporaryUploadedFile(UploadedFile):
"""
A file uploaded to a temporary location (i.e. stream-to-disk).
"""
def __init__(self, name, content_type, size, charset, content_type_extra=None):
_, ext = os.path.splitext(name)
file = tempfile.NamedTemporaryFile(
suffix=".upload" + ext, dir=settings.FILE_UPLOAD_TEMP_DIR
)
super().__init__(file, name, content_type, size, charset, content_type_extra)
def temporary_file_path(self):
"""Return the full path of this file."""
return self.file.name
def close(self):
try:
return self.file.close()
except FileNotFoundError:
# The file was moved or deleted before the tempfile could unlink
# it. Still sets self.file.close_called and calls
# self.file.file.close() before the exception.
pass
InMemoryUploadedFile
UploadedFileをメモリ上に保存する場合のクラス。
Pythonの組み込みモジュールであるio.BytesIO
を使っている
2.5MB未満のファイルはこのインスタンスになる。
InMemoryUploadedFileHandeler
が生成してくれる。
class InMemoryUploadedFile(UploadedFile):
"""
A file uploaded into memory (i.e. stream-to-memory).
"""
def __init__(
self,
file,
field_name,
name,
content_type,
size,
charset,
content_type_extra=None,
):
super().__init__(file, name, content_type, size, charset, content_type_extra)
self.field_name = field_name
def open(self, mode=None):
self.file.seek(0)
return self
def chunks(self, chunk_size=None):
self.file.seek(0)
yield self.read()
def multiple_chunks(self, chunk_size=None):
# Since it's in memory, we'll never have multiple chunks.
return False
SimpleUploadedFile
InMemoryUploadedFile
のシンプルなやつ。
データ、サイズ、名前だけのファイルを扱うrasii。
byte列(raw_data)を一時ファイル的に扱えるよね?
class SimpleUploadedFile(InMemoryUploadedFile):
"""
A simple representation of a file, which just has content, size, and a name.
"""
def __init__(self, name, content, content_type="text/plain"):
content = content or b""
super().__init__(
BytesIO(content), None, name, content_type, len(content), None, None
)
@classmethod
def from_dict(cls, file_dict):
"""
Create a SimpleUploadedFile object from a dictionary with keys:
- filename
- content-type
- content
"""
return cls(
file_dict["filename"],
file_dict["content"],
file_dict.get("content-type", "text/plain"),
)
おまけ
FileProxyMixin
DjangoでPython標準のfileオブジェクトを扱うためのミックスインクラス
ソースコード古いかも(Django==3.2.13)
class FileProxyMixin:
"""
A mixin class used to forward file methods to an underlaying file
object. The internal file object has to be called "file"::
class FileProxy(FileProxyMixin):
def __init__(self, file):
self.file = file
"""
encoding = property(lambda self: self.file.encoding)
fileno = property(lambda self: self.file.fileno)
flush = property(lambda self: self.file.flush)
isatty = property(lambda self: self.file.isatty)
newlines = property(lambda self: self.file.newlines)
read = property(lambda self: self.file.read)
readinto = property(lambda self: self.file.readinto)
readline = property(lambda self: self.file.readline)
readlines = property(lambda self: self.file.readlines)
seek = property(lambda self: self.file.seek)
tell = property(lambda self: self.file.tell)
truncate = property(lambda self: self.file.truncate)
write = property(lambda self: self.file.write)
writelines = property(lambda self: self.file.writelines)
@property
def closed(self):
return not self.file or self.file.closed
def readable(self):
if self.closed:
return False
if hasattr(self.file, 'readable'):
return self.file.readable()
return True
def writable(self):
if self.closed:
return False
if hasattr(self.file, 'writable'):
return self.file.writable()
return 'w' in getattr(self.file, 'mode', '')
def seekable(self):
if self.closed:
return False
if hasattr(self.file, 'seekable'):
return self.file.seekable()
return True
def __iter__(self):
return iter(self.file)
File
ミックスインを使ってPython標準のfileオブジェクトにDjangoの機能を追加したもの的な。
class File(FileProxyMixin):
DEFAULT_CHUNK_SIZE = 64 * 2**10
def __init__(self, file, name=None):
self.file = file
if name is None:
name = getattr(file, "name", None)
self.name = name
if hasattr(file, "mode"):
self.mode = file.mode
def __str__(self):
return self.name or ""
def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self or "None")
def __bool__(self):
return bool(self.name)
def __len__(self):
return self.size
@cached_property
def size(self):
if hasattr(self.file, "size"):
return self.file.size
if hasattr(self.file, "name"):
try:
return os.path.getsize(self.file.name)
except (OSError, TypeError):
pass
if hasattr(self.file, "tell") and hasattr(self.file, "seek"):
pos = self.file.tell()
self.file.seek(0, os.SEEK_END)
size = self.file.tell()
self.file.seek(pos)
return size
raise AttributeError("Unable to determine the file's size.")
def chunks(self, chunk_size=None):
"""
Read the file and yield chunks of ``chunk_size`` bytes (defaults to
``File.DEFAULT_CHUNK_SIZE``).
"""
chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZE
try:
self.seek(0)
except (AttributeError, UnsupportedOperation):
pass
while True:
data = self.read(chunk_size)
if not data:
break
yield data
def multiple_chunks(self, chunk_size=None):
"""
Return ``True`` if you can expect multiple chunks.
NB: If a particular file representation is in memory, subclasses should
always return ``False`` -- there's no good reason to read from memory in
chunks.
"""
ContentFile(File)
データコンテントだけを保持するFileオブジェクト。
class ContentFile(File):
"""
A File-like object that takes just raw content, rather than an actual file.
"""
def __init__(self, content, name=None):
stream_class = StringIO if isinstance(content, str) else BytesIO
super().__init__(stream_class(content), name=name)
self.size = len(content)
def __str__(self):
return "Raw content"
def __bool__(self):
return True
def open(self, mode=None):
self.seek(0)
return self
def close(self):
pass
def write(self, data):
self.__dict__.pop("size", None) # Clear the computed size.
return self.file.write(data)
まとめ
なるほど、Python標準のファイルオブジェクトに機能を追加して、Djangoで扱いやすいようにしていたんですね。
アップロード時には、ファイルサイズを見て一時ファイルをどこに保存するか考えてくれていたんですね。
自分の場合は、アップロードしようとしていたファイルが2.5MB以上なのでTemporaryUploadedFile
として扱われていたんですね。
Djangoさんありがとうございます。
間違い、修正、ご意見、アドバイス、etc.あれば何卒よろしくお願い申し上げます。
(参考)
https://docs.djangoproject.com/ja/4.0/_modules/django/core/files/uploadedfile/
https://docs.djangoproject.com/ja/4.0/_modules/django/core/files/uploadhandler/#TemporaryFileUploadHandler
https://docs.djangoproject.com/ja/4.0/_modules/django/core/files/base/