librus-python-api 1.0.0rc1__py3-none-any.whl → 1.0.1__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.
- librus_python_api/__init__.py +1 -1
- librus_python_api/_notification_bootstrap.py +124 -0
- librus_python_api/_notification_codec.py +99 -13
- librus_python_api/_storage.py +42 -3
- librus_python_api/_windows_filesystem.py +390 -0
- librus_python_api/attendance_frequency.py +65 -2
- librus_python_api/config.py +19 -0
- librus_python_api/files.py +108 -17
- librus_python_api/models.py +20 -0
- librus_python_api/notification_models.py +80 -2
- librus_python_api/notification_persistence.py +230 -31
- librus_python_api/notification_workflow.py +41 -7
- librus_python_api/persistence.py +14 -0
- librus_python_api/service.py +53 -3
- {librus_python_api-1.0.0rc1.dist-info → librus_python_api-1.0.1.dist-info}/METADATA +23 -15
- {librus_python_api-1.0.0rc1.dist-info → librus_python_api-1.0.1.dist-info}/RECORD +18 -16
- {librus_python_api-1.0.0rc1.dist-info → librus_python_api-1.0.1.dist-info}/WHEEL +0 -0
- {librus_python_api-1.0.0rc1.dist-info → librus_python_api-1.0.1.dist-info}/licenses/LICENSE +0 -0
librus_python_api/__init__.py
CHANGED
|
@@ -120,7 +120,7 @@ from librus_python_api.models import (
|
|
|
120
120
|
from librus_python_api.sending import SendAttempt
|
|
121
121
|
from librus_python_api.service import AccountClient, LibrusService
|
|
122
122
|
|
|
123
|
-
__version__ = "1.0.
|
|
123
|
+
__version__ = "1.0.1"
|
|
124
124
|
|
|
125
125
|
__all__ = [
|
|
126
126
|
"AccountContext",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Original bounded validation of caller-established, neutral offline mappings."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
|
|
5
|
+
from librus_python_api._notification_codec import (
|
|
6
|
+
HEX,
|
|
7
|
+
canonical_notification_id,
|
|
8
|
+
dump,
|
|
9
|
+
validate_historical_item,
|
|
10
|
+
)
|
|
11
|
+
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
12
|
+
from librus_python_api.models import NotificationCategory, RecentScheduleEvent
|
|
13
|
+
from librus_python_api.notification_models import (
|
|
14
|
+
NotificationBaselineMapping,
|
|
15
|
+
NotificationBootstrap,
|
|
16
|
+
NotificationItem,
|
|
17
|
+
NotificationProvenance,
|
|
18
|
+
NotificationSeen,
|
|
19
|
+
NotificationState,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prepare_bootstrap(
|
|
24
|
+
plan: NotificationBootstrap, per_category: int, batch_items: int, batch_bytes: int
|
|
25
|
+
) -> tuple[
|
|
26
|
+
NotificationState,
|
|
27
|
+
tuple[NotificationItem, ...],
|
|
28
|
+
tuple[NotificationBaselineMapping, ...],
|
|
29
|
+
]:
|
|
30
|
+
if type(plan.mappings) is not tuple or type(plan.pending_events) is not tuple:
|
|
31
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
32
|
+
if (
|
|
33
|
+
len(plan.mappings) > len(NotificationCategory) * per_category
|
|
34
|
+
or len(plan.pending_events) > batch_items
|
|
35
|
+
):
|
|
36
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
37
|
+
seen, unmapped = _mapped_state(plan.mappings, per_category)
|
|
38
|
+
items: list[NotificationItem] = []
|
|
39
|
+
used = 2
|
|
40
|
+
agenda_ids = set(
|
|
41
|
+
next(
|
|
42
|
+
entry.identifiers
|
|
43
|
+
for entry in seen
|
|
44
|
+
if entry.category is NotificationCategory.AGENDA
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
for event in plan.pending_events:
|
|
48
|
+
if type(event) is not RecentScheduleEvent:
|
|
49
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
50
|
+
item = NotificationItem(
|
|
51
|
+
NotificationCategory.AGENDA,
|
|
52
|
+
"0" * 64,
|
|
53
|
+
event,
|
|
54
|
+
None,
|
|
55
|
+
None,
|
|
56
|
+
NotificationProvenance.IMPORTED_HISTORY,
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
validate_historical_item(item)
|
|
60
|
+
except LibrusError:
|
|
61
|
+
raise LibrusError(ErrorKind.INVALID_INPUT) from None
|
|
62
|
+
identifier = canonical_notification_id(NotificationCategory.AGENDA, event)
|
|
63
|
+
if identifier in agenda_ids:
|
|
64
|
+
# A conflict cannot silently consume an externally pending event.
|
|
65
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
66
|
+
agenda_ids.add(identifier)
|
|
67
|
+
final = NotificationItem(
|
|
68
|
+
item.category, identifier, event, None, None, item.provenance
|
|
69
|
+
)
|
|
70
|
+
used += len(
|
|
71
|
+
dump({"kind": "RecentScheduleEvent", "item": asdict(final)}, batch_bytes)
|
|
72
|
+
) + bool(items)
|
|
73
|
+
if used > batch_bytes:
|
|
74
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
75
|
+
items.append(final)
|
|
76
|
+
if len(agenda_ids) > per_category:
|
|
77
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
78
|
+
return NotificationState(True, seen), tuple(items), unmapped
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _mapped_state(
|
|
82
|
+
mappings: tuple[NotificationBaselineMapping, ...], per_category: int
|
|
83
|
+
) -> tuple[tuple[NotificationSeen, ...], tuple[NotificationBaselineMapping, ...]]:
|
|
84
|
+
seen: dict[NotificationCategory, list[str]] = {
|
|
85
|
+
category: [] for category in NotificationCategory
|
|
86
|
+
}
|
|
87
|
+
sources: set[tuple[NotificationCategory, str]] = set()
|
|
88
|
+
native: set[tuple[NotificationCategory, str]] = set()
|
|
89
|
+
unmapped = []
|
|
90
|
+
counts = {category: 0 for category in NotificationCategory}
|
|
91
|
+
for mapping in mappings:
|
|
92
|
+
if (
|
|
93
|
+
type(mapping) is not NotificationBaselineMapping
|
|
94
|
+
or not isinstance(mapping.category, NotificationCategory)
|
|
95
|
+
or type(mapping.source_identifier) is not str
|
|
96
|
+
or not 1 <= len(mapping.source_identifier) <= 256
|
|
97
|
+
):
|
|
98
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
99
|
+
try:
|
|
100
|
+
mapping.source_identifier.encode("utf-8")
|
|
101
|
+
except UnicodeError:
|
|
102
|
+
raise LibrusError(ErrorKind.INVALID_INPUT) from None
|
|
103
|
+
source = (mapping.category, mapping.source_identifier)
|
|
104
|
+
if source in sources:
|
|
105
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
106
|
+
sources.add(source)
|
|
107
|
+
counts[mapping.category] += 1
|
|
108
|
+
if counts[mapping.category] > per_category:
|
|
109
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
110
|
+
identifier = mapping.native_identifier
|
|
111
|
+
if identifier is None:
|
|
112
|
+
unmapped.append(mapping)
|
|
113
|
+
continue
|
|
114
|
+
if type(identifier) is not str or HEX.fullmatch(identifier) is None:
|
|
115
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
116
|
+
key = (mapping.category, identifier)
|
|
117
|
+
if key in native:
|
|
118
|
+
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
119
|
+
native.add(key)
|
|
120
|
+
seen[mapping.category].append(identifier)
|
|
121
|
+
return tuple(
|
|
122
|
+
NotificationSeen(category, tuple(identifiers))
|
|
123
|
+
for category, identifiers in seen.items()
|
|
124
|
+
), tuple(unmapped)
|
|
@@ -10,6 +10,7 @@ from typing import Any, cast
|
|
|
10
10
|
|
|
11
11
|
from pydantic import TypeAdapter, ValidationError
|
|
12
12
|
|
|
13
|
+
from librus_python_api.config import SCHOOL_MAX_CONTENT_LENGTH, SCHOOL_MAX_FIELD_LENGTH
|
|
13
14
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
14
15
|
from librus_python_api.models import (
|
|
15
16
|
Announcement,
|
|
@@ -18,6 +19,8 @@ from librus_python_api.models import (
|
|
|
18
19
|
HomeworkItem,
|
|
19
20
|
Identity,
|
|
20
21
|
MessageSummary,
|
|
22
|
+
MessagingBackend,
|
|
23
|
+
ModernMessageSummary,
|
|
21
24
|
NotificationCategory,
|
|
22
25
|
NumericGrade,
|
|
23
26
|
Observation,
|
|
@@ -28,6 +31,7 @@ from librus_python_api.models import (
|
|
|
28
31
|
from librus_python_api.notification_models import (
|
|
29
32
|
NotificationBatch,
|
|
30
33
|
NotificationItem,
|
|
34
|
+
NotificationProvenance,
|
|
31
35
|
NotificationSeen,
|
|
32
36
|
NotificationState,
|
|
33
37
|
NotificationValue,
|
|
@@ -45,6 +49,7 @@ _VALUE_TYPES: dict[str, type[Any]] = {
|
|
|
45
49
|
DescriptiveGrade,
|
|
46
50
|
AttendanceRecord,
|
|
47
51
|
MessageSummary,
|
|
52
|
+
ModernMessageSummary,
|
|
48
53
|
Announcement,
|
|
49
54
|
HomeworkItem,
|
|
50
55
|
RecentScheduleEvent,
|
|
@@ -53,7 +58,7 @@ _VALUE_TYPES: dict[str, type[Any]] = {
|
|
|
53
58
|
_CATEGORY_TYPES: dict[NotificationCategory, tuple[type[Any], ...]] = {
|
|
54
59
|
NotificationCategory.GRADES: (NumericGrade, DescriptiveGrade),
|
|
55
60
|
NotificationCategory.ATTENDANCE: (AttendanceRecord,),
|
|
56
|
-
NotificationCategory.MESSAGES: (MessageSummary,),
|
|
61
|
+
NotificationCategory.MESSAGES: (MessageSummary, ModernMessageSummary),
|
|
57
62
|
NotificationCategory.ANNOUNCEMENTS: (Announcement,),
|
|
58
63
|
NotificationCategory.HOMEWORK: (HomeworkItem,),
|
|
59
64
|
NotificationCategory.AGENDA: (RecentScheduleEvent,),
|
|
@@ -144,6 +149,14 @@ def canonical_notification_id(
|
|
|
144
149
|
if isinstance(value, RecentScheduleEvent):
|
|
145
150
|
# Independently specified three-field canonical identity, including data.
|
|
146
151
|
canonical: Any = asdict(value)
|
|
152
|
+
elif isinstance(value, ModernMessageSummary):
|
|
153
|
+
canonical = [
|
|
154
|
+
2,
|
|
155
|
+
category.value,
|
|
156
|
+
MessagingBackend.MODERN.value,
|
|
157
|
+
value.reference.folder.value,
|
|
158
|
+
value.reference.identifier,
|
|
159
|
+
]
|
|
147
160
|
elif isinstance(value, MessageSummary):
|
|
148
161
|
canonical = [
|
|
149
162
|
1,
|
|
@@ -248,12 +261,17 @@ def encode_batch(batch: NotificationBatch, maximum: int) -> bytes:
|
|
|
248
261
|
|
|
249
262
|
def restore_batch(payload: bytes, maximum: int) -> NotificationBatch:
|
|
250
263
|
record = load(payload, maximum)
|
|
264
|
+
if type(record) is dict and "messages_backend" not in record:
|
|
265
|
+
# Existing format-3 deliveries could only contain legacy summaries.
|
|
266
|
+
record["messages_backend"] = MessagingBackend.LEGACY.value
|
|
251
267
|
if (
|
|
252
268
|
type(record) is not dict
|
|
253
269
|
or set(record) != {f.name for f in fields(NotificationBatch)}
|
|
254
270
|
or type(record["items"]) is not list
|
|
255
271
|
):
|
|
256
272
|
raise LibrusError(ErrorKind.PARSE)
|
|
273
|
+
if len(record["items"]) > 4096:
|
|
274
|
+
raise LibrusError(ErrorKind.LIMIT)
|
|
257
275
|
items = []
|
|
258
276
|
for entry in record["items"]:
|
|
259
277
|
if (
|
|
@@ -264,6 +282,8 @@ def restore_batch(payload: bytes, maximum: int) -> NotificationBatch:
|
|
|
264
282
|
):
|
|
265
283
|
raise LibrusError(ErrorKind.PARSE)
|
|
266
284
|
item = entry["item"]
|
|
285
|
+
if type(item) is dict and "provenance" not in item:
|
|
286
|
+
item["provenance"] = NotificationProvenance.OBSERVED.value
|
|
267
287
|
if type(item) is not dict or set(item) != {
|
|
268
288
|
f.name for f in fields(NotificationItem)
|
|
269
289
|
}:
|
|
@@ -282,10 +302,26 @@ def restore_batch(payload: bytes, maximum: int) -> NotificationBatch:
|
|
|
282
302
|
value,
|
|
283
303
|
parsed.identity,
|
|
284
304
|
parsed.observation,
|
|
305
|
+
parsed.provenance,
|
|
285
306
|
)
|
|
286
307
|
)
|
|
287
308
|
record["items"] = []
|
|
288
309
|
batch = typed(NotificationBatch, record, maximum)
|
|
310
|
+
_validate_batch_items(batch, items)
|
|
311
|
+
return NotificationBatch(
|
|
312
|
+
batch.receipt,
|
|
313
|
+
batch.context,
|
|
314
|
+
batch.first_run,
|
|
315
|
+
batch.categories,
|
|
316
|
+
tuple(items),
|
|
317
|
+
batch.has_more_schedule,
|
|
318
|
+
batch.messages_backend,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _validate_batch_items(
|
|
323
|
+
batch: NotificationBatch, items: list[NotificationItem]
|
|
324
|
+
) -> None:
|
|
289
325
|
if (
|
|
290
326
|
HEX.fullmatch(batch.receipt) is None
|
|
291
327
|
or HEX.fullmatch(batch.context.identifier) is None
|
|
@@ -293,18 +329,68 @@ def restore_batch(payload: bytes, maximum: int) -> NotificationBatch:
|
|
|
293
329
|
or len(set(batch.categories)) != len(batch.categories)
|
|
294
330
|
):
|
|
295
331
|
raise LibrusError(ErrorKind.PARSE)
|
|
332
|
+
for item in items:
|
|
333
|
+
if item.category not in batch.categories:
|
|
334
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
335
|
+
if item.provenance is NotificationProvenance.IMPORTED_HISTORY:
|
|
336
|
+
validate_historical_item(item)
|
|
337
|
+
if (
|
|
338
|
+
batch.categories != (NotificationCategory.AGENDA,)
|
|
339
|
+
or batch.first_run
|
|
340
|
+
or batch.has_more_schedule
|
|
341
|
+
):
|
|
342
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
343
|
+
elif (
|
|
344
|
+
item.identity is None
|
|
345
|
+
or item.observation is None
|
|
346
|
+
or item.observation.account != batch.context.alias
|
|
347
|
+
or item.identity.observation.account != batch.context.alias
|
|
348
|
+
):
|
|
349
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
350
|
+
if isinstance(item.value, (MessageSummary, ModernMessageSummary)):
|
|
351
|
+
expected = (
|
|
352
|
+
MessagingBackend.MODERN
|
|
353
|
+
if isinstance(item.value, ModernMessageSummary)
|
|
354
|
+
else MessagingBackend.LEGACY
|
|
355
|
+
)
|
|
356
|
+
if (
|
|
357
|
+
batch.messages_backend is not expected
|
|
358
|
+
or item.value.reference.account != batch.context.alias
|
|
359
|
+
):
|
|
360
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
296
361
|
if any(
|
|
297
|
-
item.
|
|
298
|
-
|
|
299
|
-
or item.identity.observation.account != batch.context.alias
|
|
300
|
-
for item in items
|
|
301
|
-
):
|
|
362
|
+
item.provenance is NotificationProvenance.IMPORTED_HISTORY for item in items
|
|
363
|
+
) and not is_historical_batch(batch, items):
|
|
302
364
|
raise LibrusError(ErrorKind.PARSE)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def is_historical_batch(
|
|
368
|
+
batch: NotificationBatch, items: list[NotificationItem] | None = None
|
|
369
|
+
) -> bool:
|
|
370
|
+
values = batch.items if items is None else items
|
|
371
|
+
return bool(values) and all(
|
|
372
|
+
item.provenance is NotificationProvenance.IMPORTED_HISTORY for item in values
|
|
310
373
|
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def validate_historical_item(item: NotificationItem) -> None:
|
|
377
|
+
if (
|
|
378
|
+
item.category is not NotificationCategory.AGENDA
|
|
379
|
+
or type(item.value) is not RecentScheduleEvent
|
|
380
|
+
or item.identity is not None
|
|
381
|
+
or item.observation is not None
|
|
382
|
+
or item.provenance is not NotificationProvenance.IMPORTED_HISTORY
|
|
383
|
+
):
|
|
384
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
385
|
+
value = item.value
|
|
386
|
+
for text, minimum, maximum in (
|
|
387
|
+
(value.date_added, 1, SCHOOL_MAX_FIELD_LENGTH),
|
|
388
|
+
(value.type, 1, SCHOOL_MAX_FIELD_LENGTH),
|
|
389
|
+
(value.data, 0, SCHOOL_MAX_CONTENT_LENGTH),
|
|
390
|
+
):
|
|
391
|
+
if type(text) is not str or not minimum <= len(text) <= maximum:
|
|
392
|
+
raise LibrusError(ErrorKind.PARSE)
|
|
393
|
+
try:
|
|
394
|
+
text.encode("utf-8")
|
|
395
|
+
except UnicodeError:
|
|
396
|
+
raise LibrusError(ErrorKind.PARSE) from None
|
librus_python_api/_storage.py
CHANGED
|
@@ -8,10 +8,11 @@ import os
|
|
|
8
8
|
import secrets
|
|
9
9
|
import sqlite3
|
|
10
10
|
import stat
|
|
11
|
+
import sys
|
|
11
12
|
from collections.abc import Callable, Coroutine, Iterator
|
|
12
13
|
from contextlib import contextmanager
|
|
13
14
|
from pathlib import Path
|
|
14
|
-
from typing import Any, Self
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
15
16
|
|
|
16
17
|
from pydantic import Field
|
|
17
18
|
|
|
@@ -19,10 +20,16 @@ from librus_python_api.config import _ValidatedConfig
|
|
|
19
20
|
from librus_python_api.exceptions import ErrorKind, LibrusError
|
|
20
21
|
from librus_python_api.lifecycle import join_owned
|
|
21
22
|
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from librus_python_api._windows_filesystem import WindowsDirectory
|
|
25
|
+
|
|
22
26
|
|
|
23
27
|
class _StorageLimits(_ValidatedConfig):
|
|
24
28
|
operations: int = Field(default=8, ge=1, le=64)
|
|
25
|
-
|
|
29
|
+
# Several local processes (one per agent host session) commonly share one
|
|
30
|
+
# store. Each transaction is short, but FULL synchronous commits can exceed
|
|
31
|
+
# 100 ms on slow disks, so ordinary operations wait up to one second.
|
|
32
|
+
busy_timeout_seconds: float = Field(default=1.0, gt=0, le=5, allow_inf_nan=False)
|
|
26
33
|
# Final saves follow upstream side effects (a dispatched send or a consumed
|
|
27
34
|
# read-once page), so they wait longer for other contexts' write locks.
|
|
28
35
|
final_busy_timeout_seconds: float = Field(
|
|
@@ -60,6 +67,7 @@ class _SQLiteStore:
|
|
|
60
67
|
self._close_task: asyncio.Task[None] | None = None
|
|
61
68
|
self._file_identity: tuple[int, int] | None = None
|
|
62
69
|
self._context_salt: bytes | None = None
|
|
70
|
+
self._windows_directory: WindowsDirectory | None = None
|
|
63
71
|
|
|
64
72
|
def context_identifier(self, context: str) -> str:
|
|
65
73
|
"""Store-local context pseudonym; requires an explicitly opened store."""
|
|
@@ -87,7 +95,7 @@ class _SQLiteStore:
|
|
|
87
95
|
await self.aclose()
|
|
88
96
|
|
|
89
97
|
async def open(self) -> None:
|
|
90
|
-
if os.name != "posix":
|
|
98
|
+
if os.name != "posix" and sys.platform != "win32":
|
|
91
99
|
raise LibrusError(ErrorKind.UNSUPPORTED_CAPABILITY)
|
|
92
100
|
if self._opened or self._opening or self._closing or self._closed:
|
|
93
101
|
raise LibrusError(ErrorKind.INVALID_INPUT)
|
|
@@ -97,6 +105,9 @@ class _SQLiteStore:
|
|
|
97
105
|
self._opened = True
|
|
98
106
|
finally:
|
|
99
107
|
self._opening = False
|
|
108
|
+
if not self._opened and self._windows_directory is not None:
|
|
109
|
+
self._windows_directory.close()
|
|
110
|
+
self._windows_directory = None
|
|
100
111
|
|
|
101
112
|
async def aclose(self) -> None:
|
|
102
113
|
if (
|
|
@@ -123,6 +134,13 @@ class _SQLiteStore:
|
|
|
123
134
|
task.cancel()
|
|
124
135
|
for task in tasks:
|
|
125
136
|
await join_owned(task)
|
|
137
|
+
if self._windows_directory is not None:
|
|
138
|
+
try:
|
|
139
|
+
self._windows_directory.close()
|
|
140
|
+
except OSError:
|
|
141
|
+
raise LibrusError(ErrorKind.STORAGE) from None
|
|
142
|
+
finally:
|
|
143
|
+
self._windows_directory = None
|
|
126
144
|
self._closed = True
|
|
127
145
|
self._opened = False
|
|
128
146
|
|
|
@@ -207,6 +225,14 @@ class _SQLiteStore:
|
|
|
207
225
|
def _check_directory(self) -> None:
|
|
208
226
|
# A private immediate parent is the trust boundary. Malicious same-user
|
|
209
227
|
# filesystem writers and unsuitable network filesystems are not isolated.
|
|
228
|
+
if sys.platform == "win32":
|
|
229
|
+
from librus_python_api._windows_filesystem import WindowsDirectory
|
|
230
|
+
|
|
231
|
+
if self._windows_directory is None:
|
|
232
|
+
self._windows_directory = WindowsDirectory(self._directory, create=True)
|
|
233
|
+
else:
|
|
234
|
+
self._windows_directory.validate()
|
|
235
|
+
return
|
|
210
236
|
if self._directory.is_symlink():
|
|
211
237
|
raise LibrusError(ErrorKind.STORAGE)
|
|
212
238
|
self._directory.mkdir(mode=0o700, exist_ok=True)
|
|
@@ -219,6 +245,14 @@ class _SQLiteStore:
|
|
|
219
245
|
raise LibrusError(ErrorKind.STORAGE)
|
|
220
246
|
|
|
221
247
|
def _check_file(self, *, create: bool = False) -> None:
|
|
248
|
+
if self._windows_directory is not None:
|
|
249
|
+
identity = self._windows_directory.check_file(
|
|
250
|
+
self._filename, self._database_bytes, create=create
|
|
251
|
+
)
|
|
252
|
+
if self._file_identity is not None and identity != self._file_identity:
|
|
253
|
+
raise LibrusError(ErrorKind.STORAGE)
|
|
254
|
+
self._file_identity = identity
|
|
255
|
+
return
|
|
222
256
|
if self._path.is_symlink():
|
|
223
257
|
raise LibrusError(ErrorKind.STORAGE)
|
|
224
258
|
flags = os.O_RDWR | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_NONBLOCK", 0)
|
|
@@ -246,6 +280,11 @@ class _SQLiteStore:
|
|
|
246
280
|
|
|
247
281
|
def _check_sidecars(self) -> None:
|
|
248
282
|
for suffix in ("-journal", "-wal", "-shm"):
|
|
283
|
+
if self._windows_directory is not None:
|
|
284
|
+
self._windows_directory.check_sidecar(
|
|
285
|
+
self._filename + suffix, self._database_bytes + 1024 * 1024
|
|
286
|
+
)
|
|
287
|
+
continue
|
|
249
288
|
path = self._path.with_name(self._path.name + suffix)
|
|
250
289
|
try:
|
|
251
290
|
status = path.lstat()
|