tuspyserver 4.2.3__tar.gz → 4.2.4__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.2.3 → tuspyserver-4.2.4}/PKG-INFO +1 -1
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/pyproject.toml +1 -1
- tuspyserver-4.2.4/src/tuspyserver/lock.py +230 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/request.py +5 -1
- tuspyserver-4.2.3/src/tuspyserver/lock.py +0 -185
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/README.md +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/__init__.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/file.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/info.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/params.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/py.typed +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/router.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/routes/__init__.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/routes/core.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/routes/creation.py +0 -0
- {tuspyserver-4.2.3 → tuspyserver-4.2.4}/src/tuspyserver/routes/termination.py +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File locking utilities for tus uploads.
|
|
3
|
+
|
|
4
|
+
Provides advisory file locking using fcntl (Unix) to prevent race conditions
|
|
5
|
+
during concurrent upload operations. This implementation matches tusd's approach
|
|
6
|
+
by using separate lock files stored in a .locks directory, where each lock file
|
|
7
|
+
contains the PID of the process holding the lock.
|
|
8
|
+
|
|
9
|
+
The lock acquisition is bounded by a timeout so a wedged shared filesystem
|
|
10
|
+
(e.g. flapping Azure Files / SMB) cannot hang request workers indefinitely.
|
|
11
|
+
If the lock directory is unwritable (EACCES / EROFS), we fall back to a
|
|
12
|
+
best-effort no-op lock so a single request fails fast instead of dragging
|
|
13
|
+
all workers down.
|
|
14
|
+
"""
|
|
15
|
+
import errno
|
|
16
|
+
import fcntl
|
|
17
|
+
import logging
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from contextlib import contextmanager
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Bounded waits for filesystem operations. Tuned for shared SMB mounts where
|
|
27
|
+
# operations can briefly stall during reconnects, but must never block forever.
|
|
28
|
+
DEFAULT_LOCK_TIMEOUT = 30.0
|
|
29
|
+
LOCK_POLL_INTERVAL = 0.1
|
|
30
|
+
|
|
31
|
+
_FS_UNWRITABLE_ERRNOS = {errno.EACCES, errno.EPERM, errno.EROFS, errno.ENOSPC}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class LockTimeoutError(IOError):
|
|
35
|
+
"""Raised when a lock cannot be acquired within the configured timeout."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FileLock:
|
|
39
|
+
"""
|
|
40
|
+
Advisory file lock using fcntl (Unix) with separate lock files.
|
|
41
|
+
|
|
42
|
+
Matches tusd's filelocker approach by creating separate .lock files
|
|
43
|
+
in a .locks directory. Each lock file contains the PID of the process
|
|
44
|
+
holding the lock, allowing for lock cleanup if a process crashes.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, file_path: str, locks_dir: Optional[str] = None):
|
|
48
|
+
"""
|
|
49
|
+
Initialize a file lock.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
file_path: Path to the upload file to lock
|
|
53
|
+
locks_dir: Directory to store lock files (defaults to {files_dir}/.locks)
|
|
54
|
+
"""
|
|
55
|
+
self.file_path = file_path
|
|
56
|
+
self.locks_dir = locks_dir
|
|
57
|
+
|
|
58
|
+
# Derive lock file path from upload file path
|
|
59
|
+
if locks_dir:
|
|
60
|
+
lock_filename = os.path.basename(file_path) + ".lock"
|
|
61
|
+
self.lock_file_path = os.path.join(locks_dir, lock_filename)
|
|
62
|
+
else:
|
|
63
|
+
# Default: create .locks directory next to upload file
|
|
64
|
+
upload_dir = os.path.dirname(file_path)
|
|
65
|
+
self.locks_dir = os.path.join(upload_dir, ".locks")
|
|
66
|
+
lock_filename = os.path.basename(file_path) + ".lock"
|
|
67
|
+
self.lock_file_path = os.path.join(self.locks_dir, lock_filename)
|
|
68
|
+
|
|
69
|
+
self._lock_fd: Optional[int] = None
|
|
70
|
+
# Set when the underlying filesystem rejected lock-file creation.
|
|
71
|
+
# Release becomes a no-op so we don't crash on cleanup.
|
|
72
|
+
self._best_effort: bool = False
|
|
73
|
+
|
|
74
|
+
def acquire(
|
|
75
|
+
self,
|
|
76
|
+
blocking: bool = True,
|
|
77
|
+
timeout: float = DEFAULT_LOCK_TIMEOUT,
|
|
78
|
+
) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Acquire an exclusive lock on the upload file.
|
|
81
|
+
|
|
82
|
+
Creates a separate lock file and applies an exclusive lock on it.
|
|
83
|
+
The lock file contains the PID of the process holding the lock.
|
|
84
|
+
This matches tusd's filelocker approach.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
blocking: If True, wait up to ``timeout`` seconds for the lock.
|
|
88
|
+
If False, return immediately when the lock is contended.
|
|
89
|
+
timeout: Max seconds to wait for the exclusive lock when
|
|
90
|
+
``blocking=True``. Bounded so a wedged shared filesystem
|
|
91
|
+
cannot hang request workers.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if lock was acquired (or running in best-effort mode),
|
|
95
|
+
False only when ``blocking=False`` and the lock was held
|
|
96
|
+
elsewhere.
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
LockTimeoutError: ``blocking=True`` and the lock could not be
|
|
100
|
+
acquired within ``timeout`` seconds.
|
|
101
|
+
IOError / OSError: unexpected filesystem failure.
|
|
102
|
+
"""
|
|
103
|
+
# Open the lock file. If the lock directory is unwritable (EACCES on
|
|
104
|
+
# a flapping SMB share, read-only filesystem, ...), degrade to
|
|
105
|
+
# best-effort: do not block the request, just proceed without a
|
|
106
|
+
# cross-process lock. Single-pod correctness still holds via the
|
|
107
|
+
# per-process Python state above this layer; multi-pod contention on
|
|
108
|
+
# the same upload UUID is extremely rare in practice.
|
|
109
|
+
try:
|
|
110
|
+
os.makedirs(self.locks_dir, exist_ok=True)
|
|
111
|
+
self._lock_fd = os.open(
|
|
112
|
+
self.lock_file_path, os.O_RDWR | os.O_CREAT, 0o644
|
|
113
|
+
)
|
|
114
|
+
except OSError as e:
|
|
115
|
+
if e.errno in _FS_UNWRITABLE_ERRNOS:
|
|
116
|
+
logger.warning(
|
|
117
|
+
"Lock dir %s unwritable (%s); proceeding without lock",
|
|
118
|
+
self.locks_dir, e,
|
|
119
|
+
)
|
|
120
|
+
self._best_effort = True
|
|
121
|
+
self._lock_fd = None
|
|
122
|
+
return True
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
# Best-effort PID write (purely informational, like tusd).
|
|
126
|
+
try:
|
|
127
|
+
os.ftruncate(self._lock_fd, 0)
|
|
128
|
+
os.lseek(self._lock_fd, 0, os.SEEK_SET)
|
|
129
|
+
os.write(self._lock_fd, str(os.getpid()).encode("utf-8"))
|
|
130
|
+
os.lseek(self._lock_fd, 0, os.SEEK_SET)
|
|
131
|
+
except OSError as e:
|
|
132
|
+
logger.warning(
|
|
133
|
+
"Failed to write PID to lock file %s: %s",
|
|
134
|
+
self.lock_file_path, e,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Acquire exclusive lock. Use non-blocking + polling so we always have
|
|
138
|
+
# a hard upper bound, even on a misbehaving network filesystem.
|
|
139
|
+
deadline = time.monotonic() + max(timeout, 0.0) if blocking else None
|
|
140
|
+
while True:
|
|
141
|
+
try:
|
|
142
|
+
fcntl.flock(self._lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
143
|
+
return True
|
|
144
|
+
except OSError as e:
|
|
145
|
+
if e.errno not in (errno.EAGAIN, errno.EWOULDBLOCK):
|
|
146
|
+
self._close_fd()
|
|
147
|
+
raise
|
|
148
|
+
if not blocking:
|
|
149
|
+
self._close_fd()
|
|
150
|
+
return False
|
|
151
|
+
if time.monotonic() >= deadline:
|
|
152
|
+
self._close_fd()
|
|
153
|
+
raise LockTimeoutError(
|
|
154
|
+
f"Timed out after {timeout:.1f}s waiting for lock "
|
|
155
|
+
f"{self.lock_file_path}"
|
|
156
|
+
)
|
|
157
|
+
time.sleep(LOCK_POLL_INTERVAL)
|
|
158
|
+
|
|
159
|
+
def _close_fd(self) -> None:
|
|
160
|
+
if self._lock_fd is not None:
|
|
161
|
+
try:
|
|
162
|
+
os.close(self._lock_fd)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
self._lock_fd = None
|
|
166
|
+
|
|
167
|
+
def release(self) -> None:
|
|
168
|
+
"""Release the lock and close the file descriptor."""
|
|
169
|
+
if self._best_effort:
|
|
170
|
+
self._best_effort = False
|
|
171
|
+
return
|
|
172
|
+
if self._lock_fd is not None:
|
|
173
|
+
try:
|
|
174
|
+
fcntl.flock(self._lock_fd, fcntl.LOCK_UN)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.warning("Error unlocking %s: %s", self.lock_file_path, e)
|
|
177
|
+
self._close_fd()
|
|
178
|
+
try:
|
|
179
|
+
if os.path.exists(self.lock_file_path):
|
|
180
|
+
os.remove(self.lock_file_path)
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.warning(
|
|
183
|
+
"Error removing lock file %s: %s",
|
|
184
|
+
self.lock_file_path, e,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def get_fd(self) -> Optional[int]:
|
|
188
|
+
"""File descriptor for the lock file, or None when unlocked / best-effort."""
|
|
189
|
+
return self._lock_fd
|
|
190
|
+
|
|
191
|
+
def __enter__(self):
|
|
192
|
+
self.acquire(blocking=True)
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
196
|
+
self.release()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@contextmanager
|
|
200
|
+
def acquire_upload_lock(
|
|
201
|
+
upload_path: str,
|
|
202
|
+
locks_dir: Optional[str] = None,
|
|
203
|
+
blocking: bool = True,
|
|
204
|
+
timeout: float = DEFAULT_LOCK_TIMEOUT,
|
|
205
|
+
):
|
|
206
|
+
"""
|
|
207
|
+
Context manager for acquiring an upload lock.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
upload_path: Path to the upload file to lock.
|
|
211
|
+
locks_dir: Directory to store lock files (defaults to ``{upload_dir}/.locks``).
|
|
212
|
+
blocking: If True, wait up to ``timeout`` seconds for the lock.
|
|
213
|
+
timeout: Max seconds to wait when blocking. Prevents indefinite
|
|
214
|
+
hangs on misbehaving shared filesystems.
|
|
215
|
+
|
|
216
|
+
Yields:
|
|
217
|
+
FileLock instance with the locked file descriptor.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
LockTimeoutError: Could not acquire the lock within ``timeout``.
|
|
221
|
+
IOError: ``blocking=False`` and the lock is held elsewhere.
|
|
222
|
+
"""
|
|
223
|
+
lock = FileLock(upload_path, locks_dir=locks_dir)
|
|
224
|
+
try:
|
|
225
|
+
acquired = lock.acquire(blocking=blocking, timeout=timeout)
|
|
226
|
+
if not acquired:
|
|
227
|
+
raise IOError("Could not acquire lock")
|
|
228
|
+
yield lock
|
|
229
|
+
finally:
|
|
230
|
+
lock.release()
|
|
@@ -15,7 +15,7 @@ from fastapi import Depends, HTTPException, Path, Request
|
|
|
15
15
|
from starlette.requests import ClientDisconnect
|
|
16
16
|
|
|
17
17
|
from tuspyserver.file import TusUploadFile
|
|
18
|
-
from tuspyserver.lock import acquire_upload_lock
|
|
18
|
+
from tuspyserver.lock import LockTimeoutError, acquire_upload_lock
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def make_request_chunks_dep(options: TusRouterOptions):
|
|
@@ -178,6 +178,10 @@ def make_request_chunks_dep(options: TusRouterOptions):
|
|
|
178
178
|
new_params.upload_part += 1
|
|
179
179
|
# save updated params
|
|
180
180
|
file.info = new_params
|
|
181
|
+
except LockTimeoutError as e:
|
|
182
|
+
# Shared lock dir is contended or wedged. Fail fast so the worker
|
|
183
|
+
# is freed instead of piling up behind a stuck filesystem.
|
|
184
|
+
raise HTTPException(status_code=503, detail=str(e))
|
|
181
185
|
except (IOError, OSError) as e:
|
|
182
186
|
# Lock acquisition or file operation failed
|
|
183
187
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
File locking utilities for tus uploads.
|
|
3
|
-
|
|
4
|
-
Provides advisory file locking using fcntl (Unix) to prevent race conditions
|
|
5
|
-
during concurrent upload operations. This implementation matches tusd's approach
|
|
6
|
-
by using separate lock files stored in a .locks directory, where each lock file
|
|
7
|
-
contains the PID of the process holding the lock.
|
|
8
|
-
"""
|
|
9
|
-
import fcntl
|
|
10
|
-
import logging
|
|
11
|
-
import os
|
|
12
|
-
from contextlib import contextmanager
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Optional
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class FileLock:
|
|
20
|
-
"""
|
|
21
|
-
Advisory file lock using fcntl (Unix) with separate lock files.
|
|
22
|
-
|
|
23
|
-
Matches tusd's filelocker approach by creating separate .lock files
|
|
24
|
-
in a .locks directory. Each lock file contains the PID of the process
|
|
25
|
-
holding the lock, allowing for lock cleanup if a process crashes.
|
|
26
|
-
"""
|
|
27
|
-
|
|
28
|
-
def __init__(self, file_path: str, locks_dir: Optional[str] = None):
|
|
29
|
-
"""
|
|
30
|
-
Initialize a file lock.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
file_path: Path to the upload file to lock
|
|
34
|
-
locks_dir: Directory to store lock files (defaults to {files_dir}/.locks)
|
|
35
|
-
"""
|
|
36
|
-
self.file_path = file_path
|
|
37
|
-
self.locks_dir = locks_dir
|
|
38
|
-
|
|
39
|
-
# Derive lock file path from upload file path
|
|
40
|
-
if locks_dir:
|
|
41
|
-
# Ensure locks directory exists
|
|
42
|
-
os.makedirs(locks_dir, exist_ok=True)
|
|
43
|
-
lock_filename = os.path.basename(file_path) + ".lock"
|
|
44
|
-
self.lock_file_path = os.path.join(locks_dir, lock_filename)
|
|
45
|
-
else:
|
|
46
|
-
# Default: create .locks directory next to upload file
|
|
47
|
-
upload_dir = os.path.dirname(file_path)
|
|
48
|
-
locks_dir = os.path.join(upload_dir, ".locks")
|
|
49
|
-
os.makedirs(locks_dir, exist_ok=True)
|
|
50
|
-
lock_filename = os.path.basename(file_path) + ".lock"
|
|
51
|
-
self.lock_file_path = os.path.join(locks_dir, lock_filename)
|
|
52
|
-
|
|
53
|
-
self._lock_fd: Optional[int] = None
|
|
54
|
-
|
|
55
|
-
def acquire(self, blocking: bool = True) -> bool:
|
|
56
|
-
"""
|
|
57
|
-
Acquire an exclusive lock on the upload file.
|
|
58
|
-
|
|
59
|
-
Creates a separate lock file and applies an exclusive lock on it.
|
|
60
|
-
The lock file contains the PID of the process holding the lock.
|
|
61
|
-
This matches tusd's filelocker approach.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
blocking: If True, block until lock is acquired. If False, return immediately.
|
|
65
|
-
|
|
66
|
-
Returns:
|
|
67
|
-
True if lock was acquired, False otherwise (only when blocking=False)
|
|
68
|
-
"""
|
|
69
|
-
try:
|
|
70
|
-
# Ensure locks directory exists
|
|
71
|
-
os.makedirs(os.path.dirname(self.lock_file_path), exist_ok=True)
|
|
72
|
-
|
|
73
|
-
# Open the lock file for read-write, create if it doesn't exist
|
|
74
|
-
# Use O_RDWR to allow both reading and writing
|
|
75
|
-
# Use O_CREAT to create the file if it doesn't exist
|
|
76
|
-
self._lock_fd = os.open(self.lock_file_path, os.O_RDWR | os.O_CREAT, 0o644)
|
|
77
|
-
|
|
78
|
-
# Ensure the lock file contains only the current PID before writing
|
|
79
|
-
try:
|
|
80
|
-
os.ftruncate(self._lock_fd, 0)
|
|
81
|
-
os.lseek(self._lock_fd, 0, os.SEEK_SET)
|
|
82
|
-
except OSError as e:
|
|
83
|
-
logger.warning(f"Failed to truncate lock file {self.lock_file_path}: {e}")
|
|
84
|
-
|
|
85
|
-
# Write PID to lock file (like tusd does)
|
|
86
|
-
try:
|
|
87
|
-
pid_str = str(os.getpid()).encode('utf-8')
|
|
88
|
-
os.write(self._lock_fd, pid_str)
|
|
89
|
-
os.fsync(self._lock_fd) # Ensure PID is written to disk
|
|
90
|
-
# Seek back to beginning for reading
|
|
91
|
-
os.lseek(self._lock_fd, 0, os.SEEK_SET)
|
|
92
|
-
except (IOError, OSError) as e:
|
|
93
|
-
# If writing PID fails, continue anyway - lock is still valid
|
|
94
|
-
logger.warning(f"Failed to write PID to lock file {self.lock_file_path}: {e}")
|
|
95
|
-
|
|
96
|
-
# Try to acquire exclusive lock (LOCK_EX) on the lock file
|
|
97
|
-
# If blocking=False, use LOCK_NB (non-blocking)
|
|
98
|
-
flags = fcntl.LOCK_EX
|
|
99
|
-
if not blocking:
|
|
100
|
-
flags |= fcntl.LOCK_NB
|
|
101
|
-
|
|
102
|
-
fcntl.flock(self._lock_fd, flags)
|
|
103
|
-
return True
|
|
104
|
-
except (IOError, OSError) as e:
|
|
105
|
-
# Lock acquisition failed
|
|
106
|
-
if self._lock_fd is not None:
|
|
107
|
-
try:
|
|
108
|
-
os.close(self._lock_fd)
|
|
109
|
-
except Exception:
|
|
110
|
-
pass
|
|
111
|
-
self._lock_fd = None
|
|
112
|
-
|
|
113
|
-
if not blocking and e.errno in (11, 35): # EAGAIN/EWOULDBLOCK
|
|
114
|
-
return False
|
|
115
|
-
raise
|
|
116
|
-
|
|
117
|
-
def release(self) -> None:
|
|
118
|
-
"""Release the lock and close the file descriptor."""
|
|
119
|
-
if self._lock_fd is not None:
|
|
120
|
-
try:
|
|
121
|
-
fcntl.flock(self._lock_fd, fcntl.LOCK_UN)
|
|
122
|
-
os.close(self._lock_fd)
|
|
123
|
-
except Exception as e:
|
|
124
|
-
logger.warning(f"Error releasing lock file descriptor: {e}")
|
|
125
|
-
finally:
|
|
126
|
-
self._lock_fd = None
|
|
127
|
-
|
|
128
|
-
# Remove the lock file (tusd does this)
|
|
129
|
-
# Note: We remove the lock file but keep the .locks directory
|
|
130
|
-
try:
|
|
131
|
-
if os.path.exists(self.lock_file_path):
|
|
132
|
-
os.remove(self.lock_file_path)
|
|
133
|
-
except Exception as e:
|
|
134
|
-
logger.warning(f"Error removing lock file {self.lock_file_path}: {e}")
|
|
135
|
-
|
|
136
|
-
def get_fd(self) -> Optional[int]:
|
|
137
|
-
"""
|
|
138
|
-
Get the file descriptor for the lock file.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
File descriptor if lock is acquired, None otherwise
|
|
142
|
-
"""
|
|
143
|
-
return self._lock_fd
|
|
144
|
-
|
|
145
|
-
def __enter__(self):
|
|
146
|
-
"""Context manager entry."""
|
|
147
|
-
self.acquire(blocking=True)
|
|
148
|
-
return self
|
|
149
|
-
|
|
150
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
151
|
-
"""Context manager exit."""
|
|
152
|
-
self.release()
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
@contextmanager
|
|
156
|
-
def acquire_upload_lock(upload_path: str, locks_dir: Optional[str] = None, blocking: bool = True):
|
|
157
|
-
"""
|
|
158
|
-
Context manager for acquiring an upload lock.
|
|
159
|
-
|
|
160
|
-
This matches tusd's filelocker pattern by creating separate lock files
|
|
161
|
-
in a .locks directory. Each lock file contains the PID of the process
|
|
162
|
-
holding the lock.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
upload_path: Path to the upload file to lock
|
|
166
|
-
locks_dir: Directory to store lock files (defaults to {upload_dir}/.locks)
|
|
167
|
-
blocking: If True, block until lock is acquired
|
|
168
|
-
|
|
169
|
-
Yields:
|
|
170
|
-
FileLock instance with the locked file descriptor
|
|
171
|
-
|
|
172
|
-
Example:
|
|
173
|
-
with acquire_upload_lock("/path/to/upload") as lock:
|
|
174
|
-
# Lock is held via lock file in .locks directory
|
|
175
|
-
# Lock file contains PID of this process
|
|
176
|
-
pass
|
|
177
|
-
"""
|
|
178
|
-
lock = FileLock(upload_path, locks_dir=locks_dir)
|
|
179
|
-
try:
|
|
180
|
-
acquired = lock.acquire(blocking=blocking)
|
|
181
|
-
if not acquired:
|
|
182
|
-
raise IOError("Could not acquire lock")
|
|
183
|
-
yield lock
|
|
184
|
-
finally:
|
|
185
|
-
lock.release()
|
|
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
|