plain 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/README.md +33 -0
- plain/__main__.py +5 -0
- plain/assets/README.md +56 -0
- plain/assets/__init__.py +6 -0
- plain/assets/finders.py +233 -0
- plain/assets/preflight.py +14 -0
- plain/assets/storage.py +916 -0
- plain/assets/utils.py +52 -0
- plain/assets/whitenoise/__init__.py +5 -0
- plain/assets/whitenoise/base.py +259 -0
- plain/assets/whitenoise/compress.py +189 -0
- plain/assets/whitenoise/media_types.py +137 -0
- plain/assets/whitenoise/middleware.py +197 -0
- plain/assets/whitenoise/responders.py +286 -0
- plain/assets/whitenoise/storage.py +178 -0
- plain/assets/whitenoise/string_utils.py +13 -0
- plain/cli/README.md +123 -0
- plain/cli/__init__.py +3 -0
- plain/cli/cli.py +439 -0
- plain/cli/formatting.py +61 -0
- plain/cli/packages.py +73 -0
- plain/cli/print.py +9 -0
- plain/cli/startup.py +33 -0
- plain/csrf/README.md +3 -0
- plain/csrf/middleware.py +466 -0
- plain/csrf/views.py +10 -0
- plain/debug.py +23 -0
- plain/exceptions.py +242 -0
- plain/forms/README.md +14 -0
- plain/forms/__init__.py +8 -0
- plain/forms/boundfield.py +58 -0
- plain/forms/exceptions.py +11 -0
- plain/forms/fields.py +1030 -0
- plain/forms/forms.py +297 -0
- plain/http/README.md +1 -0
- plain/http/__init__.py +51 -0
- plain/http/cookie.py +20 -0
- plain/http/multipartparser.py +743 -0
- plain/http/request.py +754 -0
- plain/http/response.py +719 -0
- plain/internal/__init__.py +0 -0
- plain/internal/files/README.md +3 -0
- plain/internal/files/__init__.py +3 -0
- plain/internal/files/base.py +161 -0
- plain/internal/files/locks.py +127 -0
- plain/internal/files/move.py +102 -0
- plain/internal/files/temp.py +79 -0
- plain/internal/files/uploadedfile.py +150 -0
- plain/internal/files/uploadhandler.py +254 -0
- plain/internal/files/utils.py +78 -0
- plain/internal/handlers/__init__.py +0 -0
- plain/internal/handlers/base.py +133 -0
- plain/internal/handlers/exception.py +145 -0
- plain/internal/handlers/wsgi.py +216 -0
- plain/internal/legacy/__init__.py +0 -0
- plain/internal/legacy/__main__.py +12 -0
- plain/internal/legacy/management/__init__.py +414 -0
- plain/internal/legacy/management/base.py +692 -0
- plain/internal/legacy/management/color.py +113 -0
- plain/internal/legacy/management/commands/__init__.py +0 -0
- plain/internal/legacy/management/commands/collectstatic.py +297 -0
- plain/internal/legacy/management/sql.py +67 -0
- plain/internal/legacy/management/utils.py +175 -0
- plain/json.py +40 -0
- plain/logs/README.md +24 -0
- plain/logs/__init__.py +5 -0
- plain/logs/configure.py +39 -0
- plain/logs/loggers.py +74 -0
- plain/logs/utils.py +46 -0
- plain/middleware/README.md +3 -0
- plain/middleware/__init__.py +0 -0
- plain/middleware/clickjacking.py +52 -0
- plain/middleware/common.py +87 -0
- plain/middleware/gzip.py +64 -0
- plain/middleware/security.py +64 -0
- plain/packages/README.md +41 -0
- plain/packages/__init__.py +4 -0
- plain/packages/config.py +259 -0
- plain/packages/registry.py +438 -0
- plain/paginator.py +187 -0
- plain/preflight/README.md +3 -0
- plain/preflight/__init__.py +38 -0
- plain/preflight/compatibility/__init__.py +0 -0
- plain/preflight/compatibility/django_4_0.py +20 -0
- plain/preflight/files.py +19 -0
- plain/preflight/messages.py +88 -0
- plain/preflight/registry.py +72 -0
- plain/preflight/security/__init__.py +0 -0
- plain/preflight/security/base.py +268 -0
- plain/preflight/security/csrf.py +40 -0
- plain/preflight/urls.py +117 -0
- plain/runtime/README.md +75 -0
- plain/runtime/__init__.py +61 -0
- plain/runtime/global_settings.py +199 -0
- plain/runtime/user_settings.py +353 -0
- plain/signals/README.md +14 -0
- plain/signals/__init__.py +5 -0
- plain/signals/dispatch/__init__.py +9 -0
- plain/signals/dispatch/dispatcher.py +320 -0
- plain/signals/dispatch/license.txt +35 -0
- plain/signing.py +299 -0
- plain/templates/README.md +20 -0
- plain/templates/__init__.py +6 -0
- plain/templates/core.py +24 -0
- plain/templates/jinja/README.md +227 -0
- plain/templates/jinja/__init__.py +22 -0
- plain/templates/jinja/defaults.py +119 -0
- plain/templates/jinja/extensions.py +39 -0
- plain/templates/jinja/filters.py +28 -0
- plain/templates/jinja/globals.py +19 -0
- plain/test/README.md +3 -0
- plain/test/__init__.py +16 -0
- plain/test/client.py +985 -0
- plain/test/utils.py +255 -0
- plain/urls/README.md +3 -0
- plain/urls/__init__.py +40 -0
- plain/urls/base.py +118 -0
- plain/urls/conf.py +94 -0
- plain/urls/converters.py +66 -0
- plain/urls/exceptions.py +9 -0
- plain/urls/resolvers.py +731 -0
- plain/utils/README.md +3 -0
- plain/utils/__init__.py +0 -0
- plain/utils/_os.py +52 -0
- plain/utils/cache.py +327 -0
- plain/utils/connection.py +84 -0
- plain/utils/crypto.py +76 -0
- plain/utils/datastructures.py +345 -0
- plain/utils/dateformat.py +329 -0
- plain/utils/dateparse.py +154 -0
- plain/utils/dates.py +76 -0
- plain/utils/deconstruct.py +54 -0
- plain/utils/decorators.py +90 -0
- plain/utils/deprecation.py +6 -0
- plain/utils/duration.py +44 -0
- plain/utils/email.py +12 -0
- plain/utils/encoding.py +235 -0
- plain/utils/functional.py +456 -0
- plain/utils/hashable.py +26 -0
- plain/utils/html.py +401 -0
- plain/utils/http.py +374 -0
- plain/utils/inspect.py +73 -0
- plain/utils/ipv6.py +46 -0
- plain/utils/itercompat.py +8 -0
- plain/utils/module_loading.py +69 -0
- plain/utils/regex_helper.py +353 -0
- plain/utils/safestring.py +72 -0
- plain/utils/termcolors.py +221 -0
- plain/utils/text.py +518 -0
- plain/utils/timesince.py +138 -0
- plain/utils/timezone.py +244 -0
- plain/utils/tree.py +126 -0
- plain/validators.py +603 -0
- plain/views/README.md +268 -0
- plain/views/__init__.py +18 -0
- plain/views/base.py +107 -0
- plain/views/csrf.py +24 -0
- plain/views/errors.py +25 -0
- plain/views/exceptions.py +4 -0
- plain/views/forms.py +76 -0
- plain/views/objects.py +229 -0
- plain/views/redirect.py +72 -0
- plain/views/templates.py +66 -0
- plain/wsgi.py +11 -0
- plain-0.1.0.dist-info/LICENSE +85 -0
- plain-0.1.0.dist-info/METADATA +51 -0
- plain-0.1.0.dist-info/RECORD +169 -0
- plain-0.1.0.dist-info/WHEEL +4 -0
- plain-0.1.0.dist-info/entry_points.txt +3 -0
|
File without changes
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from io import BytesIO, StringIO, UnsupportedOperation
|
|
3
|
+
|
|
4
|
+
from plain.internal.files.utils import FileProxyMixin
|
|
5
|
+
from plain.utils.functional import cached_property
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class File(FileProxyMixin):
|
|
9
|
+
DEFAULT_CHUNK_SIZE = 64 * 2**10
|
|
10
|
+
|
|
11
|
+
def __init__(self, file, name=None):
|
|
12
|
+
self.file = file
|
|
13
|
+
if name is None:
|
|
14
|
+
name = getattr(file, "name", None)
|
|
15
|
+
self.name = name
|
|
16
|
+
if hasattr(file, "mode"):
|
|
17
|
+
self.mode = file.mode
|
|
18
|
+
|
|
19
|
+
def __str__(self):
|
|
20
|
+
return self.name or ""
|
|
21
|
+
|
|
22
|
+
def __repr__(self):
|
|
23
|
+
return "<{}: {}>".format(self.__class__.__name__, self or "None")
|
|
24
|
+
|
|
25
|
+
def __bool__(self):
|
|
26
|
+
return bool(self.name)
|
|
27
|
+
|
|
28
|
+
def __len__(self):
|
|
29
|
+
return self.size
|
|
30
|
+
|
|
31
|
+
@cached_property
|
|
32
|
+
def size(self):
|
|
33
|
+
if hasattr(self.file, "size"):
|
|
34
|
+
return self.file.size
|
|
35
|
+
if hasattr(self.file, "name"):
|
|
36
|
+
try:
|
|
37
|
+
return os.path.getsize(self.file.name)
|
|
38
|
+
except (OSError, TypeError):
|
|
39
|
+
pass
|
|
40
|
+
if hasattr(self.file, "tell") and hasattr(self.file, "seek"):
|
|
41
|
+
pos = self.file.tell()
|
|
42
|
+
self.file.seek(0, os.SEEK_END)
|
|
43
|
+
size = self.file.tell()
|
|
44
|
+
self.file.seek(pos)
|
|
45
|
+
return size
|
|
46
|
+
raise AttributeError("Unable to determine the file's size.")
|
|
47
|
+
|
|
48
|
+
def chunks(self, chunk_size=None):
|
|
49
|
+
"""
|
|
50
|
+
Read the file and yield chunks of ``chunk_size`` bytes (defaults to
|
|
51
|
+
``File.DEFAULT_CHUNK_SIZE``).
|
|
52
|
+
"""
|
|
53
|
+
chunk_size = chunk_size or self.DEFAULT_CHUNK_SIZE
|
|
54
|
+
try:
|
|
55
|
+
self.seek(0)
|
|
56
|
+
except (AttributeError, UnsupportedOperation):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
while True:
|
|
60
|
+
data = self.read(chunk_size)
|
|
61
|
+
if not data:
|
|
62
|
+
break
|
|
63
|
+
yield data
|
|
64
|
+
|
|
65
|
+
def multiple_chunks(self, chunk_size=None):
|
|
66
|
+
"""
|
|
67
|
+
Return ``True`` if you can expect multiple chunks.
|
|
68
|
+
|
|
69
|
+
NB: If a particular file representation is in memory, subclasses should
|
|
70
|
+
always return ``False`` -- there's no good reason to read from memory in
|
|
71
|
+
chunks.
|
|
72
|
+
"""
|
|
73
|
+
return self.size > (chunk_size or self.DEFAULT_CHUNK_SIZE)
|
|
74
|
+
|
|
75
|
+
def __iter__(self):
|
|
76
|
+
# Iterate over this file-like object by newlines
|
|
77
|
+
buffer_ = None
|
|
78
|
+
for chunk in self.chunks():
|
|
79
|
+
for line in chunk.splitlines(True):
|
|
80
|
+
if buffer_:
|
|
81
|
+
if endswith_cr(buffer_) and not equals_lf(line):
|
|
82
|
+
# Line split after a \r newline; yield buffer_.
|
|
83
|
+
yield buffer_
|
|
84
|
+
# Continue with line.
|
|
85
|
+
else:
|
|
86
|
+
# Line either split without a newline (line
|
|
87
|
+
# continues after buffer_) or with \r\n
|
|
88
|
+
# newline (line == b'\n').
|
|
89
|
+
line = buffer_ + line
|
|
90
|
+
# buffer_ handled, clear it.
|
|
91
|
+
buffer_ = None
|
|
92
|
+
|
|
93
|
+
# If this is the end of a \n or \r\n line, yield.
|
|
94
|
+
if endswith_lf(line):
|
|
95
|
+
yield line
|
|
96
|
+
else:
|
|
97
|
+
buffer_ = line
|
|
98
|
+
|
|
99
|
+
if buffer_ is not None:
|
|
100
|
+
yield buffer_
|
|
101
|
+
|
|
102
|
+
def __enter__(self):
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
def __exit__(self, exc_type, exc_value, tb):
|
|
106
|
+
self.close()
|
|
107
|
+
|
|
108
|
+
def open(self, mode=None):
|
|
109
|
+
if not self.closed:
|
|
110
|
+
self.seek(0)
|
|
111
|
+
elif self.name and os.path.exists(self.name):
|
|
112
|
+
self.file = open(self.name, mode or self.mode)
|
|
113
|
+
else:
|
|
114
|
+
raise ValueError("The file cannot be reopened.")
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
def close(self):
|
|
118
|
+
self.file.close()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ContentFile(File):
|
|
122
|
+
"""
|
|
123
|
+
A File-like object that takes just raw content, rather than an actual file.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
def __init__(self, content, name=None):
|
|
127
|
+
stream_class = StringIO if isinstance(content, str) else BytesIO
|
|
128
|
+
super().__init__(stream_class(content), name=name)
|
|
129
|
+
self.size = len(content)
|
|
130
|
+
|
|
131
|
+
def __str__(self):
|
|
132
|
+
return "Raw content"
|
|
133
|
+
|
|
134
|
+
def __bool__(self):
|
|
135
|
+
return True
|
|
136
|
+
|
|
137
|
+
def open(self, mode=None):
|
|
138
|
+
self.seek(0)
|
|
139
|
+
return self
|
|
140
|
+
|
|
141
|
+
def close(self):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
def write(self, data):
|
|
145
|
+
self.__dict__.pop("size", None) # Clear the computed size.
|
|
146
|
+
return self.file.write(data)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def endswith_cr(line):
|
|
150
|
+
"""Return True if line (a text or bytestring) ends with '\r'."""
|
|
151
|
+
return line.endswith("\r" if isinstance(line, str) else b"\r")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def endswith_lf(line):
|
|
155
|
+
"""Return True if line (a text or bytestring) ends with '\n'."""
|
|
156
|
+
return line.endswith("\n" if isinstance(line, str) else b"\n")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def equals_lf(line):
|
|
160
|
+
"""Return True if line (a text or bytestring) equals '\n'."""
|
|
161
|
+
return line == ("\n" if isinstance(line, str) else b"\n")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Portable file locking utilities.
|
|
3
|
+
|
|
4
|
+
Based partially on an example by Jonathan Feignberg in the Python
|
|
5
|
+
Cookbook [1] (licensed under the Python Software License) and a ctypes port by
|
|
6
|
+
Anatoly Techtonik for Roundup [2] (license [3]).
|
|
7
|
+
|
|
8
|
+
[1] https://code.activestate.com/recipes/65203/
|
|
9
|
+
[2] https://sourceforge.net/p/roundup/code/ci/default/tree/roundup/backends/portalocker.py # NOQA
|
|
10
|
+
[3] https://sourceforge.net/p/roundup/code/ci/default/tree/COPYING.txt
|
|
11
|
+
|
|
12
|
+
Example Usage::
|
|
13
|
+
|
|
14
|
+
>>> from plain.internal.files import locks
|
|
15
|
+
>>> with open('./file', 'wb') as f:
|
|
16
|
+
... locks.lock(f, locks.LOCK_EX)
|
|
17
|
+
... f.write('Plain')
|
|
18
|
+
"""
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
__all__ = ("LOCK_EX", "LOCK_SH", "LOCK_NB", "lock", "unlock")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _fd(f):
|
|
25
|
+
"""Get a filedescriptor from something which could be a file or an fd."""
|
|
26
|
+
return f.fileno() if hasattr(f, "fileno") else f
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if os.name == "nt":
|
|
30
|
+
import msvcrt
|
|
31
|
+
from ctypes import (
|
|
32
|
+
POINTER,
|
|
33
|
+
Structure,
|
|
34
|
+
Union,
|
|
35
|
+
WinDLL,
|
|
36
|
+
byref,
|
|
37
|
+
c_int64,
|
|
38
|
+
c_ulong,
|
|
39
|
+
c_void_p,
|
|
40
|
+
sizeof,
|
|
41
|
+
)
|
|
42
|
+
from ctypes.wintypes import BOOL, DWORD, HANDLE
|
|
43
|
+
|
|
44
|
+
LOCK_SH = 0 # the default
|
|
45
|
+
LOCK_NB = 0x1 # LOCKFILE_FAIL_IMMEDIATELY
|
|
46
|
+
LOCK_EX = 0x2 # LOCKFILE_EXCLUSIVE_LOCK
|
|
47
|
+
|
|
48
|
+
# --- Adapted from the pyserial project ---
|
|
49
|
+
# detect size of ULONG_PTR
|
|
50
|
+
if sizeof(c_ulong) != sizeof(c_void_p):
|
|
51
|
+
ULONG_PTR = c_int64
|
|
52
|
+
else:
|
|
53
|
+
ULONG_PTR = c_ulong
|
|
54
|
+
PVOID = c_void_p
|
|
55
|
+
|
|
56
|
+
# --- Union inside Structure by stackoverflow:3480240 ---
|
|
57
|
+
class _OFFSET(Structure):
|
|
58
|
+
_fields_ = [("Offset", DWORD), ("OffsetHigh", DWORD)]
|
|
59
|
+
|
|
60
|
+
class _OFFSET_UNION(Union):
|
|
61
|
+
_anonymous_ = ["_offset"]
|
|
62
|
+
_fields_ = [("_offset", _OFFSET), ("Pointer", PVOID)]
|
|
63
|
+
|
|
64
|
+
class OVERLAPPED(Structure):
|
|
65
|
+
_anonymous_ = ["_offset_union"]
|
|
66
|
+
_fields_ = [
|
|
67
|
+
("Internal", ULONG_PTR),
|
|
68
|
+
("InternalHigh", ULONG_PTR),
|
|
69
|
+
("_offset_union", _OFFSET_UNION),
|
|
70
|
+
("hEvent", HANDLE),
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
LPOVERLAPPED = POINTER(OVERLAPPED)
|
|
74
|
+
|
|
75
|
+
# --- Define function prototypes for extra safety ---
|
|
76
|
+
kernel32 = WinDLL("kernel32")
|
|
77
|
+
LockFileEx = kernel32.LockFileEx
|
|
78
|
+
LockFileEx.restype = BOOL
|
|
79
|
+
LockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, DWORD, LPOVERLAPPED]
|
|
80
|
+
UnlockFileEx = kernel32.UnlockFileEx
|
|
81
|
+
UnlockFileEx.restype = BOOL
|
|
82
|
+
UnlockFileEx.argtypes = [HANDLE, DWORD, DWORD, DWORD, LPOVERLAPPED]
|
|
83
|
+
|
|
84
|
+
def lock(f, flags):
|
|
85
|
+
hfile = msvcrt.get_osfhandle(_fd(f))
|
|
86
|
+
overlapped = OVERLAPPED()
|
|
87
|
+
ret = LockFileEx(hfile, flags, 0, 0, 0xFFFF0000, byref(overlapped))
|
|
88
|
+
return bool(ret)
|
|
89
|
+
|
|
90
|
+
def unlock(f):
|
|
91
|
+
hfile = msvcrt.get_osfhandle(_fd(f))
|
|
92
|
+
overlapped = OVERLAPPED()
|
|
93
|
+
ret = UnlockFileEx(hfile, 0, 0, 0xFFFF0000, byref(overlapped))
|
|
94
|
+
return bool(ret)
|
|
95
|
+
|
|
96
|
+
else:
|
|
97
|
+
try:
|
|
98
|
+
import fcntl
|
|
99
|
+
|
|
100
|
+
LOCK_SH = fcntl.LOCK_SH # shared lock
|
|
101
|
+
LOCK_NB = fcntl.LOCK_NB # non-blocking
|
|
102
|
+
LOCK_EX = fcntl.LOCK_EX
|
|
103
|
+
except (ImportError, AttributeError):
|
|
104
|
+
# File locking is not supported.
|
|
105
|
+
LOCK_EX = LOCK_SH = LOCK_NB = 0
|
|
106
|
+
|
|
107
|
+
# Dummy functions that don't do anything.
|
|
108
|
+
def lock(f, flags):
|
|
109
|
+
# File is not locked
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
def unlock(f):
|
|
113
|
+
# File is unlocked
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
else:
|
|
117
|
+
|
|
118
|
+
def lock(f, flags):
|
|
119
|
+
try:
|
|
120
|
+
fcntl.flock(_fd(f), flags)
|
|
121
|
+
return True
|
|
122
|
+
except BlockingIOError:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def unlock(f):
|
|
126
|
+
fcntl.flock(_fd(f), fcntl.LOCK_UN)
|
|
127
|
+
return True
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Move a file in the safest way possible::
|
|
3
|
+
|
|
4
|
+
>>> from plain.internal.files.move import file_move_safe
|
|
5
|
+
>>> file_move_safe("/tmp/old_file", "/tmp/new_file")
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from shutil import copymode, copystat
|
|
10
|
+
|
|
11
|
+
from plain.internal.files import locks
|
|
12
|
+
|
|
13
|
+
__all__ = ["file_move_safe"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _samefile(src, dst):
|
|
17
|
+
# Macintosh, Unix.
|
|
18
|
+
if hasattr(os.path, "samefile"):
|
|
19
|
+
try:
|
|
20
|
+
return os.path.samefile(src, dst)
|
|
21
|
+
except OSError:
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
# All other platforms: check for same pathname.
|
|
25
|
+
return os.path.normcase(os.path.abspath(src)) == os.path.normcase(
|
|
26
|
+
os.path.abspath(dst)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def file_move_safe(
|
|
31
|
+
old_file_name, new_file_name, chunk_size=1024 * 64, allow_overwrite=False
|
|
32
|
+
):
|
|
33
|
+
"""
|
|
34
|
+
Move a file from one location to another in the safest way possible.
|
|
35
|
+
|
|
36
|
+
First, try ``os.rename``, which is simple but will break across filesystems.
|
|
37
|
+
If that fails, stream manually from one file to another in pure Python.
|
|
38
|
+
|
|
39
|
+
If the destination file exists and ``allow_overwrite`` is ``False``, raise
|
|
40
|
+
``FileExistsError``.
|
|
41
|
+
"""
|
|
42
|
+
# There's no reason to move if we don't have to.
|
|
43
|
+
if _samefile(old_file_name, new_file_name):
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
if not allow_overwrite and os.access(new_file_name, os.F_OK):
|
|
48
|
+
raise FileExistsError(
|
|
49
|
+
"Destination file %s exists and allow_overwrite is False."
|
|
50
|
+
% new_file_name
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
os.rename(old_file_name, new_file_name)
|
|
54
|
+
return
|
|
55
|
+
except OSError:
|
|
56
|
+
# OSError happens with os.rename() if moving to another filesystem or
|
|
57
|
+
# when moving opened files on certain operating systems.
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
# first open the old file, so that it won't go away
|
|
61
|
+
with open(old_file_name, "rb") as old_file:
|
|
62
|
+
# now open the new file, not forgetting allow_overwrite
|
|
63
|
+
fd = os.open(
|
|
64
|
+
new_file_name,
|
|
65
|
+
(
|
|
66
|
+
os.O_WRONLY
|
|
67
|
+
| os.O_CREAT
|
|
68
|
+
| getattr(os, "O_BINARY", 0)
|
|
69
|
+
| (os.O_EXCL if not allow_overwrite else 0)
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
try:
|
|
73
|
+
locks.lock(fd, locks.LOCK_EX)
|
|
74
|
+
current_chunk = None
|
|
75
|
+
while current_chunk != b"":
|
|
76
|
+
current_chunk = old_file.read(chunk_size)
|
|
77
|
+
os.write(fd, current_chunk)
|
|
78
|
+
finally:
|
|
79
|
+
locks.unlock(fd)
|
|
80
|
+
os.close(fd)
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
copystat(old_file_name, new_file_name)
|
|
84
|
+
except PermissionError:
|
|
85
|
+
# Certain filesystems (e.g. CIFS) fail to copy the file's metadata if
|
|
86
|
+
# the type of the destination filesystem isn't the same as the source
|
|
87
|
+
# filesystem. This also happens with some SELinux-enabled systems.
|
|
88
|
+
# Ignore that, but try to set basic permissions.
|
|
89
|
+
try:
|
|
90
|
+
copymode(old_file_name, new_file_name)
|
|
91
|
+
except PermissionError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
os.remove(old_file_name)
|
|
96
|
+
except PermissionError as e:
|
|
97
|
+
# Certain operating systems (Cygwin and Windows)
|
|
98
|
+
# fail when deleting opened files, ignore it. (For the
|
|
99
|
+
# systems where this happens, temporary files will be auto-deleted
|
|
100
|
+
# on close anyway.)
|
|
101
|
+
if getattr(e, "winerror", 0) != 32:
|
|
102
|
+
raise
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The temp module provides a NamedTemporaryFile that can be reopened in the same
|
|
3
|
+
process on any platform. Most platforms use the standard Python
|
|
4
|
+
tempfile.NamedTemporaryFile class, but Windows users are given a custom class.
|
|
5
|
+
|
|
6
|
+
This is needed because the Python implementation of NamedTemporaryFile uses the
|
|
7
|
+
O_TEMPORARY flag under Windows, which prevents the file from being reopened
|
|
8
|
+
if the same flag is not provided [1][2]. Note that this does not address the
|
|
9
|
+
more general issue of opening a file for writing and reading in multiple
|
|
10
|
+
processes in a manner that works across platforms.
|
|
11
|
+
|
|
12
|
+
The custom version of NamedTemporaryFile doesn't support the same keyword
|
|
13
|
+
arguments available in tempfile.NamedTemporaryFile.
|
|
14
|
+
|
|
15
|
+
1: https://mail.python.org/pipermail/python-list/2005-December/336957.html
|
|
16
|
+
2: https://bugs.python.org/issue14243
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import os
|
|
20
|
+
import tempfile
|
|
21
|
+
|
|
22
|
+
from plain.internal.files.utils import FileProxyMixin
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
"NamedTemporaryFile",
|
|
26
|
+
"gettempdir",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if os.name == "nt":
|
|
31
|
+
|
|
32
|
+
class TemporaryFile(FileProxyMixin):
|
|
33
|
+
"""
|
|
34
|
+
Temporary file object constructor that supports reopening of the
|
|
35
|
+
temporary file in Windows.
|
|
36
|
+
|
|
37
|
+
Unlike tempfile.NamedTemporaryFile from the standard library,
|
|
38
|
+
__init__() doesn't support the 'delete', 'buffering', 'encoding', or
|
|
39
|
+
'newline' keyword arguments.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, mode="w+b", bufsize=-1, suffix="", prefix="", dir=None):
|
|
43
|
+
fd, name = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir)
|
|
44
|
+
self.name = name
|
|
45
|
+
self.file = os.fdopen(fd, mode, bufsize)
|
|
46
|
+
self.close_called = False
|
|
47
|
+
|
|
48
|
+
# Because close can be called during shutdown
|
|
49
|
+
# we need to cache os.unlink and access it
|
|
50
|
+
# as self.unlink only
|
|
51
|
+
unlink = os.unlink
|
|
52
|
+
|
|
53
|
+
def close(self):
|
|
54
|
+
if not self.close_called:
|
|
55
|
+
self.close_called = True
|
|
56
|
+
try:
|
|
57
|
+
self.file.close()
|
|
58
|
+
except OSError:
|
|
59
|
+
pass
|
|
60
|
+
try:
|
|
61
|
+
self.unlink(self.name)
|
|
62
|
+
except OSError:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def __del__(self):
|
|
66
|
+
self.close()
|
|
67
|
+
|
|
68
|
+
def __enter__(self):
|
|
69
|
+
self.file.__enter__()
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, exc, value, tb):
|
|
73
|
+
self.file.__exit__(exc, value, tb)
|
|
74
|
+
|
|
75
|
+
NamedTemporaryFile = TemporaryFile
|
|
76
|
+
else:
|
|
77
|
+
NamedTemporaryFile = tempfile.NamedTemporaryFile
|
|
78
|
+
|
|
79
|
+
gettempdir = tempfile.gettempdir
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Classes representing uploaded files.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from io import BytesIO
|
|
7
|
+
|
|
8
|
+
from plain.internal.files import temp as tempfile
|
|
9
|
+
from plain.internal.files.base import File
|
|
10
|
+
from plain.internal.files.utils import validate_file_name
|
|
11
|
+
from plain.runtime import settings
|
|
12
|
+
|
|
13
|
+
__all__ = (
|
|
14
|
+
"UploadedFile",
|
|
15
|
+
"TemporaryUploadedFile",
|
|
16
|
+
"InMemoryUploadedFile",
|
|
17
|
+
"SimpleUploadedFile",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class UploadedFile(File):
|
|
22
|
+
"""
|
|
23
|
+
An abstract uploaded file (``TemporaryUploadedFile`` and
|
|
24
|
+
``InMemoryUploadedFile`` are the built-in concrete subclasses).
|
|
25
|
+
|
|
26
|
+
An ``UploadedFile`` object behaves somewhat like a file object and
|
|
27
|
+
represents some file data that the user submitted with a form.
|
|
28
|
+
"""
|
|
29
|
+
|
|
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
|
|
44
|
+
|
|
45
|
+
def __repr__(self):
|
|
46
|
+
return f"<{self.__class__.__name__}: {self.name} ({self.content_type})>"
|
|
47
|
+
|
|
48
|
+
def _get_name(self):
|
|
49
|
+
return self._name
|
|
50
|
+
|
|
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)
|
|
56
|
+
|
|
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
|
|
62
|
+
|
|
63
|
+
name = validate_file_name(name)
|
|
64
|
+
|
|
65
|
+
self._name = name
|
|
66
|
+
|
|
67
|
+
name = property(_get_name, _set_name)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TemporaryUploadedFile(UploadedFile):
|
|
71
|
+
"""
|
|
72
|
+
A file uploaded to a temporary location (i.e. stream-to-disk).
|
|
73
|
+
"""
|
|
74
|
+
|
|
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)
|
|
81
|
+
|
|
82
|
+
def temporary_file_path(self):
|
|
83
|
+
"""Return the full path of this file."""
|
|
84
|
+
return self.file.name
|
|
85
|
+
|
|
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
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class InMemoryUploadedFile(UploadedFile):
|
|
97
|
+
"""
|
|
98
|
+
A file uploaded into memory (i.e. stream-to-memory).
|
|
99
|
+
"""
|
|
100
|
+
|
|
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
|
|
113
|
+
|
|
114
|
+
def open(self, mode=None):
|
|
115
|
+
self.file.seek(0)
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
def chunks(self, chunk_size=None):
|
|
119
|
+
self.file.seek(0)
|
|
120
|
+
yield self.read()
|
|
121
|
+
|
|
122
|
+
def multiple_chunks(self, chunk_size=None):
|
|
123
|
+
# Since it's in memory, we'll never have multiple chunks.
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SimpleUploadedFile(InMemoryUploadedFile):
|
|
128
|
+
"""
|
|
129
|
+
A simple representation of a file, which just has content, size, and a name.
|
|
130
|
+
"""
|
|
131
|
+
|
|
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
|
+
)
|
|
137
|
+
|
|
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
|
+
)
|