mesh-sandbox 0.1.33__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.
@@ -9,12 +9,13 @@ from fastapi import BackgroundTasks, Depends, HTTPException, Request
9
9
  from fastapi import status as http_status
10
10
  from fastapi.responses import JSONResponse
11
11
 
12
- from ..common import EnvConfig, constants, index_of, strtobool
12
+ from ..common import constants, index_of, strtobool
13
13
  from ..common.exceptions import MessagingException
14
14
  from ..common.fernet import FernetHelper
15
15
  from ..common.handler_helpers import get_handler_uri
16
+ from ..common.messaging import Messaging
16
17
  from ..common.mex_headers import MexHeaders
17
- from ..dependencies import get_env_config, get_fernet, get_logger, get_store
18
+ from ..dependencies import get_fernet, get_logger, get_messaging
18
19
  from ..models.mailbox import Mailbox
19
20
  from ..models.message import (
20
21
  Message,
@@ -24,7 +25,6 @@ from ..models.message import (
24
25
  MessageStatus,
25
26
  MessageType,
26
27
  )
27
- from ..store.base import Store
28
28
  from ..views.outbox import (
29
29
  get_rich_outbox_view,
30
30
  send_message_response,
@@ -60,13 +60,11 @@ class OutboxHandler:
60
60
  # pylint: disable=too-many-arguments
61
61
  def __init__(
62
62
  self,
63
- config: EnvConfig = Depends(get_env_config),
64
- store: Store = Depends(get_store),
63
+ messaging: Messaging = Depends(get_messaging),
65
64
  fernet: FernetHelper = Depends(get_fernet),
66
65
  logger: logging.Logger = Depends(get_logger),
67
66
  ):
68
- self.config = config
69
- self.store = store
67
+ self.messaging = messaging
70
68
  self.fernet = fernet
71
69
  self.logger = logger
72
70
 
@@ -96,7 +94,7 @@ class OutboxHandler:
96
94
  status_code=http_status.HTTP_417_EXPECTATION_FAILED, detail=constants.ERROR_INVALID_HEADER_CHUNKS
97
95
  )
98
96
 
99
- recipient = await self.store.get_mailbox(mex_headers.mex_to)
97
+ recipient = await self.messaging.get_mailbox(mex_headers.mex_to)
100
98
  if not recipient:
101
99
  raise HTTPException(
102
100
  status_code=http_status.HTTP_417_EXPECTATION_FAILED, detail=constants.ERROR_UNREGISTERED_RECIPIENT
@@ -137,7 +135,7 @@ class OutboxHandler:
137
135
  partner_id=mex_headers.mex_partnerid,
138
136
  checksum=mex_headers.mex_content_checksum,
139
137
  encrypted=strtobool(mex_headers.mex_content_encrypted),
140
- is_compressed=strtobool(mex_headers.mex_content_compressed),
138
+ compressed=strtobool(mex_headers.mex_content_compressed),
141
139
  ),
142
140
  )
143
141
 
@@ -145,7 +143,7 @@ class OutboxHandler:
145
143
  if len(body) == 0:
146
144
  raise HTTPException(status_code=http_status.HTTP_417_EXPECTATION_FAILED, detail="MissingDataFile")
147
145
 
148
- await self.store.send_message(message, body, background_tasks)
146
+ await self.messaging.send_message(message=message, body=body, background_tasks=background_tasks)
149
147
 
150
148
  self.logger.info(
151
149
  (
@@ -184,7 +182,7 @@ class OutboxHandler:
184
182
  detail=constants.ERROR_INVALID_HEADER_CHUNKS,
185
183
  message_id=message_id,
186
184
  )
187
- message: Optional[Message] = await self.store.get_message(message_id)
185
+ message: Optional[Message] = await self.messaging.get_message(message_id)
188
186
 
189
187
  if not message:
190
188
  raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
@@ -205,12 +203,18 @@ class OutboxHandler:
205
203
  message_id=message_id,
206
204
  )
207
205
 
208
- await self.store.receive_chunk(message, chunk_number, await request.body(), background_tasks)
206
+ chunk = await request.body()
207
+
208
+ await self.messaging.save_chunk(
209
+ message=message, chunk_number=chunk_number, chunk=chunk, background_tasks=background_tasks
210
+ )
209
211
 
210
212
  if chunk_number < message.total_chunks:
211
213
  return upload_chunk_response(message, chunk_number, accepts_api_version)
212
214
 
213
- await self.store.accept_message(message, background_tasks)
215
+ file_size = await self.messaging.get_file_size(message)
216
+
217
+ await self.messaging.accept_message(message=message, file_size=file_size, background_tasks=background_tasks)
214
218
 
215
219
  return upload_chunk_response(message, chunk_number, accepts_api_version)
216
220
 
@@ -228,7 +232,7 @@ class OutboxHandler:
228
232
  if continue_from:
229
233
  last_key = self.fernet.decode_dict(continue_from)
230
234
 
231
- messages: list[Message] = cast(list[Message], await self.store.get_outbox(mailbox.mailbox_id))
235
+ messages: list[Message] = cast(list[Message], await self.messaging.get_outbox(mailbox.mailbox_id))
232
236
 
233
237
  def message_filter(message: Message) -> bool:
234
238
  return message.created_timestamp > from_date
@@ -4,22 +4,21 @@ from fastapi import Depends, HTTPException
4
4
  from fastapi import status as http_status
5
5
  from starlette.responses import JSONResponse
6
6
 
7
- from ..common import MESH_MEDIA_TYPES, EnvConfig, exclude_none_json_encoder
8
- from ..dependencies import get_env_config, get_store
7
+ from ..common import MESH_MEDIA_TYPES, exclude_none_json_encoder
8
+ from ..common.messaging import Messaging
9
+ from ..dependencies import get_messaging
9
10
  from ..models.mailbox import Mailbox
10
11
  from ..models.message import Message
11
- from ..store.base import Store
12
12
  from ..views.tracking import create_tracking_response
13
13
 
14
14
 
15
15
  class TrackingHandler:
16
- def __init__(self, config: EnvConfig = Depends(get_env_config), store: Store = Depends(get_store)):
17
- self.config = config
18
- self.store = store
16
+ def __init__(self, messaging: Messaging = Depends(get_messaging)):
17
+ self.messaging = messaging
19
18
 
20
19
  async def tracking_by_message_id(self, sender_mailbox: Mailbox, message_id: str, accepts_api_version: int = 1):
21
20
 
22
- message: Optional[Message] = await self.store.get_message(message_id)
21
+ message: Optional[Message] = await self.messaging.get_message(message_id)
23
22
 
24
23
  if not message:
25
24
  raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
@@ -28,7 +27,7 @@ class TrackingHandler:
28
27
  # intentionally not a 403 (matching spine)
29
28
  raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
30
29
 
31
- sender_outbox = await self.store.get_outbox(sender_mailbox.mailbox_id)
30
+ sender_outbox = await self.messaging.get_outbox(sender_mailbox.mailbox_id)
32
31
  if message.message_id not in [message.message_id for message in sender_outbox]:
33
32
  raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
34
33
 
@@ -37,7 +36,7 @@ class TrackingHandler:
37
36
 
38
37
  async def tracking_by_local_id(self, sender_mailbox: Mailbox, local_id: str):
39
38
 
40
- messages: list[Message] = await self.store.get_by_local_id(sender_mailbox.mailbox_id, local_id)
39
+ messages: list[Message] = await self.messaging.get_by_local_id(sender_mailbox.mailbox_id, local_id)
41
40
 
42
41
  if len(messages) == 0:
43
42
  raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
@@ -47,7 +46,7 @@ class TrackingHandler:
47
46
 
48
47
  message = messages[0]
49
48
 
50
- sender_outbox = await self.store.get_outbox(sender_mailbox.mailbox_id)
49
+ sender_outbox = await self.messaging.get_outbox(sender_mailbox.mailbox_id)
51
50
  if message.message_id not in [message.message_id for message in sender_outbox]:
52
51
  raise HTTPException(status_code=http_status.HTTP_404_NOT_FOUND)
53
52
 
@@ -40,7 +40,7 @@ class MessageMetadata:
40
40
  partner_id: Optional[str] = field(default=None)
41
41
  checksum: Optional[str] = field(default=None)
42
42
  encrypted: Optional[bool] = field(default=None)
43
- is_compressed: Optional[bool] = field(default=None)
43
+ compressed: Optional[bool] = field(default=None)
44
44
  etag: Optional[str] = field(default=None)
45
45
  last_modified: Optional[str] = field(default=None)
46
46
 
@@ -1,8 +1,13 @@
1
- from fastapi import APIRouter, BackgroundTasks, Depends, status
1
+ from fastapi import APIRouter, BackgroundTasks, Depends, Response, status
2
2
 
3
- from ..dependencies import EnvConfig, get_env_config, normalise_mailbox_id_path
3
+ from ..dependencies import (
4
+ EnvConfig,
5
+ get_env_config,
6
+ normalise_mailbox_id_path,
7
+ normalise_message_id_path,
8
+ )
4
9
  from ..handlers.admin import AdminHandler
5
- from ..views.admin import PutReportRequest
10
+ from ..views.admin import AddMessageEventRequest, CreateReportRequest
6
11
  from .request_logging import RequestLoggingRoute
7
12
 
8
13
  router = APIRouter(
@@ -75,10 +80,32 @@ async def reset_mailbox(
75
80
  status_code=status.HTTP_200_OK,
76
81
  response_model_exclude_none=True,
77
82
  )
78
- async def put_report(
79
- request: PutReportRequest,
83
+ async def create_report(
84
+ new_report: CreateReportRequest,
80
85
  background_tasks: BackgroundTasks,
81
86
  handler: AdminHandler = Depends(AdminHandler),
82
87
  ):
83
- message = await handler.put_report(request, background_tasks)
88
+ message = await handler.create_report(new_report, background_tasks)
84
89
  return {"message_id": message.message_id}
90
+
91
+
92
+ @router.post(
93
+ "/admin/message/{message_id}/event",
94
+ summary=f"appends a status event to a given message, if exists. {TESTING_ONLY}",
95
+ status_code=status.HTTP_200_OK,
96
+ response_model_exclude_none=True,
97
+ )
98
+ @router.post(
99
+ "/messageexchange/message/{message_id}/event",
100
+ summary=f"appends a status event to a given message, if exists. {TESTING_ONLY}",
101
+ status_code=status.HTTP_200_OK,
102
+ response_model_exclude_none=True,
103
+ )
104
+ async def add_message_event(
105
+ new_event: AddMessageEventRequest,
106
+ background_tasks: BackgroundTasks,
107
+ message_id: str = Depends(normalise_message_id_path),
108
+ handler: AdminHandler = Depends(AdminHandler),
109
+ ):
110
+ await handler.add_message_event(message_id, new_event, background_tasks)
111
+ return Response()
@@ -1,173 +1,59 @@
1
1
  import logging
2
2
  from abc import ABC, abstractmethod
3
- from typing import Callable, NamedTuple, Optional
3
+ from typing import Callable, Optional
4
4
 
5
- from fastapi import BackgroundTasks, HTTPException, status
6
-
7
- from ..common import EnvConfig, constants, generate_cipher_text
5
+ from ..common import EnvConfig
8
6
  from ..models.mailbox import Mailbox
9
- from ..models.message import Message, MessageStatus
10
-
11
-
12
- class AuthoriseHeaderParts(NamedTuple):
13
- scheme: str
14
- mailbox_id: str
15
- nonce: str
16
- nonce_count: str
17
- timestamp: str
18
- cipher_text: str
19
- parts: int
20
-
21
- def get_reasons_invalid(self) -> list[str]:
22
- reasons = []
23
- if self.parts != 5:
24
- reasons.append(f"invalid num header parts: {self.parts}")
25
-
26
- if not self.nonce_count.isdigit():
27
- reasons.append("nonce count is not digits")
28
-
29
- if self.scheme not in (MESH_AUTH_SCHEME, ""):
30
- reasons.append("invalid auth scheme or mailbox_id contains a space")
31
-
32
- if " " in self.mailbox_id:
33
- reasons.append("mailbox_id contains a space")
34
-
35
- return reasons
36
-
37
-
38
- _DEFAULT_PARTS_IF_MISSING = ["" for _ in range(5)]
39
- MESH_AUTH_SCHEME = "NHSMESH"
40
-
41
-
42
- def try_parse_authorisation_token(auth_token: str) -> Optional[AuthoriseHeaderParts]:
43
-
44
- if not auth_token:
45
- return None
46
-
47
- auth_token = auth_token.strip()
48
-
49
- scheme = MESH_AUTH_SCHEME if auth_token.upper().startswith(MESH_AUTH_SCHEME) else ""
50
-
51
- if scheme:
52
- auth_token = auth_token[len(MESH_AUTH_SCHEME) + 1 :]
53
-
54
- auth_token_parts = auth_token.split(":")
55
-
56
- num_parts = len(auth_token_parts)
57
- auth_token_parts = auth_token_parts + _DEFAULT_PARTS_IF_MISSING
58
-
59
- header_parts = AuthoriseHeaderParts(
60
- scheme=scheme,
61
- mailbox_id=auth_token_parts[0],
62
- nonce=auth_token_parts[1],
63
- nonce_count=auth_token_parts[2],
64
- timestamp=auth_token_parts[3],
65
- cipher_text=auth_token_parts[4],
66
- parts=num_parts,
67
- )
68
-
69
- return header_parts
7
+ from ..models.message import Message
70
8
 
71
9
 
72
10
  class Store(ABC):
73
11
 
74
- supports_reset = False
12
+ readonly = True
75
13
 
76
14
  def __init__(self, config: EnvConfig, logger: logging.Logger):
77
15
  self.config = config
78
16
  self.logger = logger
79
17
 
80
- def get_mailboxes_data_dir(self) -> str:
81
- raise NotImplementedError()
82
-
83
- async def reset(self):
84
- raise NotImplementedError()
85
-
86
- async def reset_mailbox(self, mailbox_id: str):
87
- raise NotImplementedError()
88
-
89
18
  @abstractmethod
90
19
  async def get_mailbox(self, mailbox_id: str, accessed: bool = False) -> Optional[Mailbox]:
91
20
  pass
92
21
 
93
- async def _validate_auth_token(self, mailbox_id: str, authorization: str) -> Optional[Mailbox]:
94
-
95
- if self.config.auth_mode == "none":
96
- return await self.get_mailbox(mailbox_id, accessed=True)
97
-
98
- authorization = (authorization or "").strip()
99
- if not authorization:
100
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_READING_AUTH_HEADER)
101
-
102
- header_parts = try_parse_authorisation_token(authorization)
103
- if not header_parts:
104
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_READING_AUTH_HEADER)
105
-
106
- if header_parts.mailbox_id != mailbox_id:
107
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_MAILBOX_TOKEN_MISMATCH)
108
-
109
- if self.config.auth_mode == "canned":
110
-
111
- if header_parts.nonce.upper() != "VALID":
112
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_INVALID_AUTH_TOKEN)
113
- return await self.get_mailbox(mailbox_id, accessed=True)
114
-
115
- if header_parts.get_reasons_invalid():
116
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_READING_AUTH_HEADER)
117
-
118
- mailbox = await self.get_mailbox(mailbox_id, accessed=True)
119
-
120
- if not mailbox:
121
- return None
122
-
123
- cypher_text = generate_cipher_text(
124
- self.config.shared_key,
125
- header_parts.mailbox_id,
126
- mailbox.password,
127
- header_parts.timestamp,
128
- header_parts.nonce,
129
- header_parts.nonce_count,
130
- )
131
-
132
- if header_parts.cipher_text != cypher_text:
133
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_INVALID_AUTH_TOKEN)
134
-
135
- return mailbox
136
-
137
- async def authorise_mailbox(self, mailbox_id: str, authorization: str) -> Optional[Mailbox]:
138
-
139
- mailbox = await self._validate_auth_token(mailbox_id, authorization)
140
-
141
- if not mailbox:
142
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=constants.ERROR_NO_MAILBOX_MATCHES)
22
+ @abstractmethod
23
+ async def get_message(self, message_id: str) -> Optional[Message]:
24
+ pass
143
25
 
144
- return mailbox
26
+ @abstractmethod
27
+ async def save_message(self, message: Message):
28
+ pass
145
29
 
146
30
  @abstractmethod
147
- async def send_message(self, message: Message, body: bytes, background_tasks: BackgroundTasks):
31
+ async def get_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
148
32
  pass
149
33
 
150
34
  @abstractmethod
151
- async def receive_chunk(self, message: Message, chunk_number: int, chunk: bytes, background_tasks: BackgroundTasks):
35
+ async def save_chunk(self, message: Message, chunk_number: int, chunk: bytes):
152
36
  pass
153
37
 
154
38
  @abstractmethod
155
- async def accept_message(self, message: Message, background_tasks: BackgroundTasks):
39
+ async def add_to_outbox(self, message: Message):
156
40
  pass
157
41
 
158
42
  @abstractmethod
159
- async def acknowledge_message(self, message: Message, background_tasks: BackgroundTasks):
43
+ async def add_to_inbox(self, message: Message):
160
44
  pass
161
45
 
162
46
  @abstractmethod
163
- async def get_message(self, message_id: str) -> Optional[Message]:
47
+ async def get_file_size(self, message: Message) -> int:
164
48
  pass
165
49
 
166
- async def get_accepted_inbox_messages(self, mailbox_id: str) -> list[Message]:
167
- def accepted_messages(msg: Message) -> bool:
168
- return msg.status == MessageStatus.ACCEPTED
50
+ @abstractmethod
51
+ async def reset(self):
52
+ pass
169
53
 
170
- return await self.get_inbox_messages(mailbox_id, accepted_messages)
54
+ @abstractmethod
55
+ async def reset_mailbox(self, mailbox_id: str):
56
+ pass
171
57
 
172
58
  @abstractmethod
173
59
  async def get_inbox_messages(
@@ -183,10 +69,6 @@ class Store(ABC):
183
69
  async def get_by_local_id(self, mailbox_id: str, local_id: str) -> list[Message]:
184
70
  pass
185
71
 
186
- @abstractmethod
187
- async def retrieve_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
188
- pass
189
-
190
72
  @abstractmethod
191
73
  async def lookup_by_ods_code_and_workflow_id(self, ods_code: str, workflow_id: str) -> list[Mailbox]:
192
74
  pass
@@ -1,8 +1,6 @@
1
- import asyncio
2
1
  import json
3
2
  import logging
4
3
  import os
5
- import threading
6
4
  from collections import defaultdict
7
5
  from datetime import datetime
8
6
  from json import JSONDecodeError
@@ -10,7 +8,6 @@ from typing import Callable, Optional, cast
10
8
  from weakref import WeakValueDictionary
11
9
 
12
10
  from dateutil.relativedelta import relativedelta
13
- from fastapi import BackgroundTasks
14
11
 
15
12
  from ..common import EnvConfig
16
13
  from ..models.mailbox import Mailbox
@@ -20,6 +17,10 @@ from .base import Store
20
17
  from .serialisation import deserialise_model
21
18
 
22
19
 
20
+ def _accepted_messages(msg: Message) -> bool:
21
+ return msg.status == MessageStatus.ACCEPTED
22
+
23
+
23
24
  class CannedStore(Store):
24
25
  """
25
26
  pre canned messages or mailboxes not editable
@@ -31,8 +32,6 @@ class CannedStore(Store):
31
32
  self._config = config
32
33
  self._canned_data_dir = os.path.join(os.path.dirname(__file__), "data")
33
34
  self._mailboxes_data_dir = self.get_mailboxes_data_dir()
34
- self._sync_lock = threading.Lock()
35
- self._lock: Optional[asyncio.Lock] = None
36
35
  self._filter_expired = filter_expired
37
36
  super().__init__(self._config, logger)
38
37
 
@@ -54,18 +53,6 @@ class CannedStore(Store):
54
53
  self._fill_boxes()
55
54
  self.messages = cast(dict[str, Message], WeakValueDictionary(self.messages))
56
55
 
57
- @property
58
- def lock(self):
59
-
60
- if self._lock is not None:
61
- return self._lock
62
-
63
- with self._sync_lock:
64
- if self._lock is not None:
65
- return self._lock
66
- self._lock = asyncio.Lock()
67
- return self._lock
68
-
69
56
  def _fill_boxes(self):
70
57
  for message in self.messages.values():
71
58
  if message.sender.mailbox_id and message.sender.mailbox_id in self.mailboxes:
@@ -205,25 +192,40 @@ class CannedStore(Store):
205
192
  if not mailbox:
206
193
  return None
207
194
 
208
- mailbox.inbox_count = len(await self.get_accepted_inbox_messages(mailbox_id))
195
+ mailbox.inbox_count = len(await self.get_inbox_messages(mailbox_id, _accepted_messages))
209
196
  if accessed:
210
197
  mailbox.last_accessed = datetime.utcnow()
211
198
  return mailbox
212
199
 
213
- async def send_message(self, message: Message, body: bytes, background_tasks: BackgroundTasks):
214
- pass
200
+ async def get_message(self, message_id: str) -> Optional[Message]:
201
+ return self.messages.get(message_id)
215
202
 
216
- async def accept_message(self, message: Message, background_tasks: BackgroundTasks):
217
- pass
203
+ async def get_file_size(self, message: Message) -> int:
204
+ return sum(len(chunk or b"") for chunk in self.chunks.get(message.message_id, []))
218
205
 
219
- async def acknowledge_message(self, message: Message, background_tasks: BackgroundTasks):
220
- pass
206
+ async def add_to_outbox(self, message: Message):
207
+ """does nothing on this readonly store..."""
221
208
 
222
- async def receive_chunk(self, message: Message, chunk_number: int, chunk: bytes, background_tasks: BackgroundTasks):
223
- pass
209
+ async def add_to_inbox(self, message: Message):
210
+ """does nothing on this readonly store..."""
224
211
 
225
- async def get_message(self, message_id: str) -> Optional[Message]:
226
- return self.messages.get(message_id)
212
+ async def save_message(self, message: Message):
213
+ raise NotImplementedError()
214
+
215
+ async def get_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
216
+ parts = self.chunks.get(message.message_id, [])
217
+ if not parts or len(parts) < chunk_number:
218
+ return None
219
+ return parts[chunk_number - 1]
220
+
221
+ async def save_chunk(self, message: Message, chunk_number: int, chunk: bytes):
222
+ raise NotImplementedError()
223
+
224
+ async def reset(self):
225
+ raise NotImplementedError()
226
+
227
+ async def reset_mailbox(self, mailbox_id: str):
228
+ raise NotImplementedError()
227
229
 
228
230
  async def get_inbox_messages(
229
231
  self, mailbox_id: str, predicate: Optional[Callable[[Message], bool]] = None
@@ -240,12 +242,6 @@ class CannedStore(Store):
240
242
  async def get_by_local_id(self, mailbox_id: str, local_id: str) -> list[Message]:
241
243
  return self.local_ids.get(mailbox_id, {}).get(local_id, [])
242
244
 
243
- async def retrieve_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
244
- parts = self.chunks.get(message.message_id, [])
245
- if not parts or len(parts) < chunk_number:
246
- return None
247
- return parts[chunk_number - 1]
248
-
249
245
  async def lookup_by_ods_code_and_workflow_id(self, ods_code: str, workflow_id: str) -> list[Mailbox]:
250
246
  return self.endpoints.get(f"{ods_code}/{workflow_id}", [])
251
247
 
@@ -17,16 +17,17 @@ class FileStore(MemoryStore):
17
17
  def get_mailboxes_data_dir(self) -> str:
18
18
  return self._config.mailboxes_dir
19
19
 
20
- async def _get_file_size(self, message: Message) -> int:
21
- size = 0
22
- if message.total_chunks < 1:
23
- return 0
20
+ def message_path(self, message: Message) -> str:
21
+ return os.path.join(self._mailboxes_data_dir, message.recipient.mailbox_id, "in", message.message_id)
24
22
 
25
- message_dir = self.message_path(message)
26
- for chunk_no in range(message.total_chunks):
27
- stat = os.stat(f"{message_dir}/{chunk_no+1}")
28
- size += stat.st_size
29
- return size
23
+ def chunk_path(self, message: Message, chunk_number: int) -> str:
24
+ return os.path.join(
25
+ self._mailboxes_data_dir, message.recipient.mailbox_id, "in", message.message_id, str(chunk_number)
26
+ )
27
+
28
+ def _load_chunks(self) -> dict[str, list[Optional[bytes]]]:
29
+ """overrides canned store default data load"""
30
+ return defaultdict(list)
30
31
 
31
32
  async def save_message(self, message: Message):
32
33
  await super().save_message(message)
@@ -35,18 +36,6 @@ class FileStore(MemoryStore):
35
36
  with open(message_json_path, "w+", encoding="utf-8") as f:
36
37
  json.dump(serialise_model(message), f)
37
38
 
38
- def inbox_path(self, mailbox_id: str) -> str:
39
- return os.path.join(self._mailboxes_data_dir, mailbox_id, "in")
40
-
41
- def message_path(self, message: Message) -> str:
42
- return os.path.join(self.inbox_path(message.recipient.mailbox_id), message.message_id)
43
-
44
- def chunk_path(self, message: Message, chunk_number: int) -> str:
45
- return os.path.join(self.message_path(message), str(chunk_number))
46
-
47
- def _load_chunks(self) -> dict[str, list[Optional[bytes]]]:
48
- return defaultdict(list)
49
-
50
39
  async def save_chunk(self, message: Message, chunk_number: int, chunk: Optional[bytes]):
51
40
  chunk_path = self.chunk_path(message, chunk_number)
52
41
  if chunk is None:
@@ -59,7 +48,7 @@ class FileStore(MemoryStore):
59
48
  with open(chunk_path, "wb+") as f:
60
49
  f.write(chunk)
61
50
 
62
- async def retrieve_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
51
+ async def get_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]:
63
52
 
64
53
  chunk_path = self.chunk_path(message, chunk_number)
65
54
  if not os.path.exists(chunk_path):
@@ -67,3 +56,14 @@ class FileStore(MemoryStore):
67
56
 
68
57
  with open(chunk_path, "rb") as f:
69
58
  return f.read()
59
+
60
+ async def get_file_size(self, message: Message) -> int:
61
+ size = 0
62
+ if message.total_chunks < 1:
63
+ return 0
64
+
65
+ message_dir = self.message_path(message)
66
+ for chunk_no in range(message.total_chunks):
67
+ stat = os.stat(f"{message_dir}/{chunk_no+1}")
68
+ size += stat.st_size
69
+ return size