tuspyserver 4.3.0__tar.gz → 4.3.2__tar.gz
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.
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/PKG-INFO +1 -1
- tuspyserver-4.3.2/pyproject.toml +21 -0
- tuspyserver-4.3.0/pyproject.toml → tuspyserver-4.3.2/pyproject.toml.orig +2 -2
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/lock.py +99 -22
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/README.md +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/__init__.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/file.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/info.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/params.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/py.typed +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/request.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/router.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/__init__.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/core.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/creation.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/routes/termination.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage/__init__.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage/local.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage/s3.py +0 -0
- {tuspyserver-4.3.0 → tuspyserver-4.3.2}/src/tuspyserver/storage_file.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tuspyserver"
|
|
3
|
+
version = "4.3.2"
|
|
4
|
+
description = "A Python tus server implementation as a FastAPI router"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.8"
|
|
7
|
+
dependencies = ["fastapi>=0.110.0"]
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "Edi Hasaj"
|
|
11
|
+
email = "edi.hasaj@applifyer.com"
|
|
12
|
+
|
|
13
|
+
[project.optional-dependencies]
|
|
14
|
+
s3 = ["boto3>=1.26"]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.6,<0.13"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[tool.uv.workspace]
|
|
21
|
+
members = ["example"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tuspyserver"
|
|
3
|
-
version = "4.3.
|
|
3
|
+
version = "4.3.2"
|
|
4
4
|
description ="A Python tus server implementation as a FastAPI router"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -16,7 +16,7 @@ dependencies = [
|
|
|
16
16
|
s3 = ["boto3>=1.26"]
|
|
17
17
|
|
|
18
18
|
[build-system]
|
|
19
|
-
requires = ["uv_build>=0.6,<0.
|
|
19
|
+
requires = ["uv_build>=0.6,<0.13"]
|
|
20
20
|
build-backend = "uv_build"
|
|
21
21
|
|
|
22
22
|
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
2
|
File locking utilities for tus uploads.
|
|
3
3
|
|
|
4
|
-
Provides advisory file locking
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Provides advisory file locking to prevent race conditions during concurrent
|
|
5
|
+
upload operations. This implementation matches tusd's approach by using
|
|
6
|
+
separate lock files stored in a .locks directory, where each lock file
|
|
7
7
|
contains the PID of the process holding the lock.
|
|
8
8
|
|
|
9
|
+
Locking is backed by ``fcntl.flock`` on Unix and ``msvcrt.locking`` on
|
|
10
|
+
Windows. Neither module is importable on every platform, so both imports are
|
|
11
|
+
guarded: on an exotic platform that provides neither, the module degrades to
|
|
12
|
+
a best-effort no-op lock instead of failing at import time.
|
|
13
|
+
|
|
9
14
|
The lock acquisition is bounded by a timeout so a wedged shared filesystem
|
|
10
15
|
(e.g. flapping Azure Files / SMB) cannot hang request workers indefinitely.
|
|
11
16
|
If the lock directory is unwritable (EACCES / EROFS), we fall back to a
|
|
@@ -13,13 +18,22 @@ best-effort no-op lock so a single request fails fast instead of dragging
|
|
|
13
18
|
all workers down.
|
|
14
19
|
"""
|
|
15
20
|
import errno
|
|
16
|
-
import fcntl
|
|
17
21
|
import logging
|
|
18
22
|
import os
|
|
19
23
|
import time
|
|
20
24
|
from contextlib import contextmanager
|
|
21
25
|
from typing import Optional
|
|
22
26
|
|
|
27
|
+
try: # Unix
|
|
28
|
+
import fcntl
|
|
29
|
+
except ImportError: # pragma: no cover - platform dependent
|
|
30
|
+
fcntl = None # type: ignore[assignment]
|
|
31
|
+
|
|
32
|
+
try: # Windows
|
|
33
|
+
import msvcrt
|
|
34
|
+
except ImportError: # pragma: no cover - platform dependent
|
|
35
|
+
msvcrt = None # type: ignore[assignment]
|
|
36
|
+
|
|
23
37
|
logger = logging.getLogger(__name__)
|
|
24
38
|
|
|
25
39
|
|
|
@@ -30,18 +44,82 @@ LOCK_POLL_INTERVAL = 0.1
|
|
|
30
44
|
|
|
31
45
|
_FS_UNWRITABLE_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS, errno.ENOSPC}
|
|
32
46
|
|
|
47
|
+
# Errnos meaning "someone else holds this lock" rather than a real failure.
|
|
48
|
+
# flock reports EAGAIN/EWOULDBLOCK; msvcrt.locking reports EACCES or
|
|
49
|
+
# EDEADLOCK depending on the Windows/CRT version.
|
|
50
|
+
_LOCK_CONTENDED_ERRNOS = {
|
|
51
|
+
getattr(errno, name)
|
|
52
|
+
for name in ("EAGAIN", "EWOULDBLOCK", "EACCES", "EDEADLK", "EDEADLOCK")
|
|
53
|
+
if hasattr(errno, name)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Number of bytes locked on Windows. msvcrt locks a byte range from the
|
|
57
|
+
# current file position; one byte at offset 0 is enough for a whole-file
|
|
58
|
+
# advisory lock, and Windows allows locking past EOF.
|
|
59
|
+
_WINDOWS_LOCK_BYTES = 1
|
|
60
|
+
|
|
33
61
|
|
|
34
62
|
class LockTimeoutError(IOError):
|
|
35
63
|
"""Raised when a lock cannot be acquired within the configured timeout."""
|
|
36
64
|
|
|
37
65
|
|
|
66
|
+
def locking_available() -> bool:
|
|
67
|
+
"""True when this platform provides a real cross-process file lock."""
|
|
68
|
+
return fcntl is not None or msvcrt is not None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _try_lock_exclusive(fd: int) -> bool:
|
|
72
|
+
"""
|
|
73
|
+
Try to take an exclusive, non-blocking lock on ``fd``.
|
|
74
|
+
|
|
75
|
+
Returns True when the lock was taken (or no locking primitive exists on
|
|
76
|
+
this platform), False when another process holds it. Unexpected OS
|
|
77
|
+
errors propagate.
|
|
78
|
+
"""
|
|
79
|
+
if fcntl is not None:
|
|
80
|
+
try:
|
|
81
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
82
|
+
return True
|
|
83
|
+
except OSError as e:
|
|
84
|
+
if e.errno in _LOCK_CONTENDED_ERRNOS:
|
|
85
|
+
return False
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
if msvcrt is not None:
|
|
89
|
+
try:
|
|
90
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
91
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, _WINDOWS_LOCK_BYTES)
|
|
92
|
+
return True
|
|
93
|
+
except OSError as e:
|
|
94
|
+
if e.errno in _LOCK_CONTENDED_ERRNOS:
|
|
95
|
+
return False
|
|
96
|
+
raise
|
|
97
|
+
|
|
98
|
+
logger.warning(
|
|
99
|
+
"No file locking primitive available on this platform; "
|
|
100
|
+
"proceeding without a cross-process lock"
|
|
101
|
+
)
|
|
102
|
+
return True
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _unlock(fd: int) -> None:
|
|
106
|
+
"""Release a lock previously taken by :func:`_try_lock_exclusive`."""
|
|
107
|
+
if fcntl is not None:
|
|
108
|
+
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
109
|
+
elif msvcrt is not None:
|
|
110
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
111
|
+
msvcrt.locking(fd, msvcrt.LK_UNLCK, _WINDOWS_LOCK_BYTES)
|
|
112
|
+
|
|
113
|
+
|
|
38
114
|
class FileLock:
|
|
39
115
|
"""
|
|
40
|
-
Advisory file lock
|
|
116
|
+
Advisory file lock with separate lock files.
|
|
41
117
|
|
|
42
118
|
Matches tusd's filelocker approach by creating separate .lock files
|
|
43
119
|
in a .locks directory. Each lock file contains the PID of the process
|
|
44
120
|
holding the lock, allowing for lock cleanup if a process crashes.
|
|
121
|
+
|
|
122
|
+
Uses ``fcntl.flock`` on Unix and ``msvcrt.locking`` on Windows.
|
|
45
123
|
"""
|
|
46
124
|
|
|
47
125
|
def __init__(self, file_path: str, locks_dir: Optional[str] = None):
|
|
@@ -139,22 +217,21 @@ class FileLock:
|
|
|
139
217
|
deadline = time.monotonic() + max(timeout, 0.0) if blocking else None
|
|
140
218
|
while True:
|
|
141
219
|
try:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
except OSError
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
time.sleep(LOCK_POLL_INTERVAL)
|
|
220
|
+
if _try_lock_exclusive(self._lock_fd):
|
|
221
|
+
return True
|
|
222
|
+
except OSError:
|
|
223
|
+
self._close_fd()
|
|
224
|
+
raise
|
|
225
|
+
if not blocking:
|
|
226
|
+
self._close_fd()
|
|
227
|
+
return False
|
|
228
|
+
if time.monotonic() >= deadline:
|
|
229
|
+
self._close_fd()
|
|
230
|
+
raise LockTimeoutError(
|
|
231
|
+
f"Timed out after {timeout:.1f}s waiting for lock "
|
|
232
|
+
f"{self.lock_file_path}"
|
|
233
|
+
)
|
|
234
|
+
time.sleep(LOCK_POLL_INTERVAL)
|
|
158
235
|
|
|
159
236
|
def _close_fd(self) -> None:
|
|
160
237
|
if self._lock_fd is not None:
|
|
@@ -171,7 +248,7 @@ class FileLock:
|
|
|
171
248
|
return
|
|
172
249
|
if self._lock_fd is not None:
|
|
173
250
|
try:
|
|
174
|
-
|
|
251
|
+
_unlock(self._lock_fd)
|
|
175
252
|
except Exception as e:
|
|
176
253
|
logger.warning("Error unlocking %s: %s", self.lock_file_path, e)
|
|
177
254
|
self._close_fd()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|