Coverage for /Users/davegaeddert/Development/dropseed/plain/plain/plain/internal/files/uploadedfile.py: 44%
59 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2024-10-16 22:04 -0500
1"""
2Classes representing uploaded files.
3"""
5import os
6from io import BytesIO
8from plain.internal.files import temp as tempfile
9from plain.internal.files.base import File
10from plain.internal.files.utils import validate_file_name
11from plain.runtime import settings
13__all__ = (
14 "UploadedFile",
15 "TemporaryUploadedFile",
16 "InMemoryUploadedFile",
17 "SimpleUploadedFile",
18)
21class UploadedFile(File):
22 """
23 An abstract uploaded file (``TemporaryUploadedFile`` and
24 ``InMemoryUploadedFile`` are the built-in concrete subclasses).
26 An ``UploadedFile`` object behaves somewhat like a file object and
27 represents some file data that the user submitted with a form.
28 """
30 def __init__(
31 self,
32 file=None,
33 name=None,
34 content_type=None,
35 size=None,
36 charset=None,
37 content_type_extra=None,
38 ):
39 super().__init__(file, name)
40 self.size = size
41 self.content_type = content_type
42 self.charset = charset
43 self.content_type_extra = content_type_extra
45 def __repr__(self):
46 return f"<{self.__class__.__name__}: {self.name} ({self.content_type})>"
48 def _get_name(self):
49 return self._name
51 def _set_name(self, name):
52 # Sanitize the file name so that it can't be dangerous.
53 if name is not None:
54 # Just use the basename of the file -- anything else is dangerous.
55 name = os.path.basename(name)
57 # File names longer than 255 characters can cause problems on older OSes.
58 if len(name) > 255:
59 name, ext = os.path.splitext(name)
60 ext = ext[:255]
61 name = name[: 255 - len(ext)] + ext
63 name = validate_file_name(name)
65 self._name = name
67 name = property(_get_name, _set_name)
70class TemporaryUploadedFile(UploadedFile):
71 """
72 A file uploaded to a temporary location (i.e. stream-to-disk).
73 """
75 def __init__(self, name, content_type, size, charset, content_type_extra=None):
76 _, ext = os.path.splitext(name)
77 file = tempfile.NamedTemporaryFile(
78 suffix=".upload" + ext, dir=settings.FILE_UPLOAD_TEMP_DIR
79 )
80 super().__init__(file, name, content_type, size, charset, content_type_extra)
82 def temporary_file_path(self):
83 """Return the full path of this file."""
84 return self.file.name
86 def close(self):
87 try:
88 return self.file.close()
89 except FileNotFoundError:
90 # The file was moved or deleted before the tempfile could unlink
91 # it. Still sets self.file.close_called and calls
92 # self.file.file.close() before the exception.
93 pass
96class InMemoryUploadedFile(UploadedFile):
97 """
98 A file uploaded into memory (i.e. stream-to-memory).
99 """
101 def __init__(
102 self,
103 file,
104 field_name,
105 name,
106 content_type,
107 size,
108 charset,
109 content_type_extra=None,
110 ):
111 super().__init__(file, name, content_type, size, charset, content_type_extra)
112 self.field_name = field_name
114 def open(self, mode=None):
115 self.file.seek(0)
116 return self
118 def chunks(self, chunk_size=None):
119 self.file.seek(0)
120 yield self.read()
122 def multiple_chunks(self, chunk_size=None):
123 # Since it's in memory, we'll never have multiple chunks.
124 return False
127class SimpleUploadedFile(InMemoryUploadedFile):
128 """
129 A simple representation of a file, which just has content, size, and a name.
130 """
132 def __init__(self, name, content, content_type="text/plain"):
133 content = content or b""
134 super().__init__(
135 BytesIO(content), None, name, content_type, len(content), None, None
136 )
138 @classmethod
139 def from_dict(cls, file_dict):
140 """
141 Create a SimpleUploadedFile object from a dictionary with keys:
142 - filename
143 - content-type
144 - content
145 """
146 return cls(
147 file_dict["filename"],
148 file_dict["content"],
149 file_dict.get("content-type", "text/plain"),
150 )