goosebit 0.1.0__py3-none-any.whl → 0.1.2__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.
Files changed (52) hide show
  1. goosebit/__init__.py +8 -5
  2. goosebit/api/__init__.py +1 -1
  3. goosebit/api/devices.py +60 -36
  4. goosebit/api/download.py +28 -14
  5. goosebit/api/firmware.py +37 -44
  6. goosebit/api/helper.py +30 -0
  7. goosebit/api/rollouts.py +87 -0
  8. goosebit/api/routes.py +15 -7
  9. goosebit/auth/__init__.py +37 -21
  10. goosebit/db.py +5 -0
  11. goosebit/models.py +125 -6
  12. goosebit/permissions.py +33 -13
  13. goosebit/realtime/__init__.py +1 -1
  14. goosebit/realtime/logs.py +4 -6
  15. goosebit/settings.py +38 -29
  16. goosebit/telemetry/__init__.py +28 -0
  17. goosebit/telemetry/prometheus.py +10 -0
  18. goosebit/ui/__init__.py +1 -1
  19. goosebit/ui/routes.py +36 -39
  20. goosebit/ui/static/js/devices.js +191 -239
  21. goosebit/ui/static/js/firmware.js +234 -88
  22. goosebit/ui/static/js/index.js +83 -84
  23. goosebit/ui/static/js/logs.js +17 -10
  24. goosebit/ui/static/js/rollouts.js +198 -0
  25. goosebit/ui/static/js/util.js +66 -0
  26. goosebit/ui/templates/devices.html +75 -42
  27. goosebit/ui/templates/firmware.html +150 -34
  28. goosebit/ui/templates/index.html +9 -23
  29. goosebit/ui/templates/login.html +58 -27
  30. goosebit/ui/templates/logs.html +18 -3
  31. goosebit/ui/templates/nav.html +78 -25
  32. goosebit/ui/templates/rollouts.html +76 -0
  33. goosebit/updater/__init__.py +1 -1
  34. goosebit/updater/controller/__init__.py +1 -1
  35. goosebit/updater/controller/v1/__init__.py +1 -1
  36. goosebit/updater/controller/v1/routes.py +112 -24
  37. goosebit/updater/manager.py +237 -94
  38. goosebit/updater/routes.py +7 -8
  39. goosebit/updates/__init__.py +70 -0
  40. goosebit/updates/swdesc.py +83 -0
  41. goosebit-0.1.2.dist-info/METADATA +123 -0
  42. goosebit-0.1.2.dist-info/RECORD +51 -0
  43. goosebit/updater/download/__init__.py +0 -1
  44. goosebit/updater/download/routes.py +0 -6
  45. goosebit/updater/download/v1/__init__.py +0 -1
  46. goosebit/updater/download/v1/routes.py +0 -26
  47. goosebit/updater/misc.py +0 -69
  48. goosebit/updater/updates.py +0 -93
  49. goosebit-0.1.0.dist-info/METADATA +0 -37
  50. goosebit-0.1.0.dist-info/RECORD +0 -48
  51. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
  52. {goosebit-0.1.0.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
@@ -1,82 +1,126 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
+ import re
4
5
  from abc import ABC, abstractmethod
5
6
  from contextlib import asynccontextmanager
6
- from typing import Callable
7
+ from datetime import datetime
8
+ from enum import StrEnum
9
+ from typing import Callable, Optional
7
10
 
8
- import aiofiles
11
+ from aiocache import Cache, cached
12
+ from aiocache.serializers import PickleSerializer
9
13
 
10
- from goosebit.models import Device
11
- from goosebit.settings import POLL_TIME, SWUPDATE_FILES_DIR, TOKEN_SWU_DIR
12
- from goosebit.updater.misc import get_newest_fw
13
- from goosebit.updater.updates import FirmwareArtifact
14
+ from goosebit.models import (
15
+ Device,
16
+ Firmware,
17
+ Hardware,
18
+ Rollout,
19
+ UpdateModeEnum,
20
+ UpdateStateEnum,
21
+ )
22
+ from goosebit.settings import POLL_TIME, POLL_TIME_UPDATING
23
+
24
+
25
+ class HandlingType(StrEnum):
26
+ SKIP = "skip"
27
+ ATTEMPT = "attempt"
28
+ FORCED = "forced"
14
29
 
15
30
 
16
31
  class UpdateManager(ABC):
32
+ device_log_subscriptions: dict[str, list[Callable]] = {}
33
+ device_poll_time: dict[str, str] = {}
34
+
17
35
  def __init__(self, dev_id: str):
18
36
  self.dev_id = dev_id
19
- self.device = None
20
- self.force_update = False
21
- self.update_complete = False
22
- self.poll_time = POLL_TIME
23
- self.log_subscribers: list[Callable] = []
24
37
 
25
38
  async def get_device(self) -> Device | None:
26
39
  return
27
40
 
28
- async def save(self) -> None:
41
+ async def update_force_update(self, force_update: bool) -> None:
29
42
  return
30
43
 
31
44
  async def update_fw_version(self, version: str) -> None:
32
45
  return
33
46
 
34
- async def update_device_state(self, state: str) -> None:
47
+ async def update_hardware(self, hardware: Hardware) -> None:
48
+ return
49
+
50
+ async def update_device_state(self, state: UpdateStateEnum) -> None:
51
+ return
52
+
53
+ async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
35
54
  return
36
55
 
37
- async def update_last_seen(self, last_seen: int) -> None:
56
+ async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
38
57
  return
39
58
 
40
- async def update_web_pwd(self, web_pwd: str) -> None:
59
+ async def update_name(self, name: str):
41
60
  return
42
61
 
43
- async def update_last_ip(self, last_ip: str) -> None:
62
+ async def update_feed(self, feed: str):
44
63
  return
45
64
 
46
- async def update_cfd_status(self, status: bool) -> None:
65
+ async def update_flavor(self, flavor: str):
47
66
  return
48
67
 
49
- async def create_cfd_token(self) -> str:
50
- return ""
68
+ async def update_config_data(self, **kwargs):
69
+ return
51
70
 
52
- async def delete_cfd_token(self):
71
+ async def update_log_complete(self, log_complete: bool):
53
72
  return
54
73
 
74
+ async def get_rollout(self) -> Optional[Rollout]:
75
+ return None
76
+
55
77
  @asynccontextmanager
56
78
  async def subscribe_log(self, callback: Callable):
57
79
  device = await self.get_device()
58
- self.log_subscribers.append(callback)
80
+ subscribers = self.log_subscribers
81
+ subscribers.append(callback)
82
+ self.log_subscribers = subscribers
59
83
  await callback(device.last_log)
60
84
  try:
61
85
  yield
62
86
  except asyncio.CancelledError:
63
87
  pass
64
88
  finally:
65
- self.log_subscribers.remove(callback)
89
+ subscribers = self.log_subscribers
90
+ subscribers.remove(callback)
91
+ self.log_subscribers = subscribers
66
92
 
67
- async def publish_log(self, log_data: str | None):
68
- for cb in self.log_subscribers:
69
- await cb(log_data)
93
+ @property
94
+ def poll_seconds(self):
95
+ time_obj = datetime.strptime(self.poll_time, "%H:%M:%S")
96
+ return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
70
97
 
71
98
  @property
72
- def cfd_provisioned(self) -> bool:
73
- return False
99
+ def log_subscribers(self):
100
+ return UpdateManager.device_log_subscriptions.get(self.dev_id, [])
74
101
 
75
- @abstractmethod
76
- async def get_update_file(self) -> FirmwareArtifact: ...
102
+ @log_subscribers.setter
103
+ def log_subscribers(self, value: list):
104
+ UpdateManager.device_log_subscriptions[self.dev_id] = value
105
+
106
+ @property
107
+ def poll_time(self):
108
+ return UpdateManager.device_poll_time.get(self.dev_id, POLL_TIME)
109
+
110
+ @poll_time.setter
111
+ def poll_time(self, value: str):
112
+ if not value == POLL_TIME:
113
+ UpdateManager.device_poll_time[self.dev_id] = value
114
+ return
115
+ if self.dev_id in UpdateManager.device_poll_time:
116
+ del UpdateManager.device_poll_time[self.dev_id]
117
+
118
+ async def publish_log(self, log_data: str | None):
119
+ for cb in self.log_subscribers:
120
+ await cb(log_data)
77
121
 
78
122
  @abstractmethod
79
- async def get_update_mode(self) -> str: ...
123
+ async def get_update(self) -> tuple[HandlingType, Firmware]: ...
80
124
 
81
125
  @abstractmethod
82
126
  async def update_log(self, log_data: str) -> None: ...
@@ -85,122 +129,221 @@ class UpdateManager(ABC):
85
129
  class UnknownUpdateManager(UpdateManager):
86
130
  def __init__(self, dev_id: str):
87
131
  super().__init__(dev_id)
88
- self.poll_time = "00:00:05"
132
+ self.poll_time = POLL_TIME_UPDATING
89
133
 
90
- async def get_update_file(self) -> FirmwareArtifact:
91
- return FirmwareArtifact(get_newest_fw())
134
+ async def _get_firmware(self) -> Firmware:
135
+ return await Firmware.latest(await self.get_device())
92
136
 
93
- async def get_update_mode(self) -> str:
94
- return "forced"
137
+ async def get_update(self) -> tuple[HandlingType, Firmware]:
138
+ firmware = await self._get_firmware()
139
+ return HandlingType.FORCED, firmware
95
140
 
96
141
  async def update_log(self, log_data: str) -> None:
97
142
  return
98
143
 
99
144
 
100
145
  class DeviceUpdateManager(UpdateManager):
146
+ hardware_default = None
147
+
148
+ @cached(
149
+ ttl=600,
150
+ key_builder=lambda fn, self: self.dev_id,
151
+ cache=Cache.MEMORY,
152
+ serializer=PickleSerializer(),
153
+ namespace="main",
154
+ )
101
155
  async def get_device(self) -> Device:
102
- if self.device:
103
- return self.device
104
- self.device = (await Device.get_or_create(uuid=self.dev_id))[0]
105
- return self.device
156
+ hardware = DeviceUpdateManager.hardware_default
157
+ if hardware is None:
158
+ hardware = (await Hardware.get_or_create(model="default", revision="default"))[0]
159
+ DeviceUpdateManager.hardware_default = hardware
106
160
 
107
- async def save(self) -> None:
108
- await self.device.save()
161
+ return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
162
+
163
+ async def save_device(self, device: Device, update_fields: list[str]):
164
+ cache = Cache(namespace="main")
165
+ await cache.set(self.dev_id, device, ttl=600)
166
+ await device.save(update_fields=update_fields)
167
+
168
+ async def update_force_update(self, force_update: bool) -> None:
169
+ device = await self.get_device()
170
+ device.force_update = force_update
171
+ await self.save_device(device, update_fields=["force_update"])
109
172
 
110
173
  async def update_fw_version(self, version: str) -> None:
111
174
  device = await self.get_device()
112
175
  device.fw_version = version
176
+ await self.save_device(device, update_fields=["fw_version"])
113
177
 
114
- async def update_device_state(self, state: str) -> None:
178
+ async def update_hardware(self, hardware: Hardware) -> None:
115
179
  device = await self.get_device()
116
- device.last_state = state
180
+ device.hardware = hardware
181
+ await self.save_device(device, update_fields=["hardware"])
117
182
 
118
- async def update_last_seen(self, last_seen: int) -> None:
183
+ async def update_device_state(self, state: UpdateStateEnum) -> None:
119
184
  device = await self.get_device()
120
- device.last_seen = last_seen
185
+ device.last_state = state
186
+ await self.save_device(device, update_fields=["last_state"])
121
187
 
122
- async def update_last_ip(self, last_ip: str) -> None:
188
+ async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
123
189
  device = await self.get_device()
190
+ device.last_seen = last_seen
124
191
  if ":" in last_ip:
125
192
  device.last_ipv6 = last_ip
193
+ await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
126
194
  else:
127
195
  device.last_ip = last_ip
196
+ await self.save_device(device, update_fields=["last_seen", "last_ip"])
128
197
 
129
- async def get_update_file(self) -> FirmwareArtifact:
198
+ async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
130
199
  device = await self.get_device()
131
- file = FirmwareArtifact(device.fw_file)
200
+ device.assigned_firmware = firmware
201
+ device.update_mode = update_mode
202
+ await self.save_device(device, update_fields=["assigned_firmware_id", "update_mode"])
132
203
 
133
- if self.force_update:
134
- return file
135
- return file
204
+ async def update_name(self, name: str):
205
+ device = await self.get_device()
206
+ device.name = name
207
+ await self.save_device(device, update_fields=["name"])
136
208
 
137
- async def get_update_mode(self) -> str:
209
+ async def update_feed(self, feed: str):
138
210
  device = await self.get_device()
211
+ device.feed = feed
212
+ await self.save_device(device, update_fields=["feed"])
139
213
 
140
- file = await self.get_update_file()
141
- if file.is_empty():
142
- mode = "skip"
214
+ async def update_flavor(self, flavor: str):
215
+ device = await self.get_device()
216
+ device.flavor = flavor
217
+ await self.save_device(device, update_fields=["flavor"])
218
+
219
+ async def update_config_data(self, **kwargs):
220
+ model = kwargs.get("hw_model") or "default"
221
+ revision = kwargs.get("hw_revision") or "default"
222
+ hardware = (await Hardware.get_or_create(model=model, revision=revision))[0]
223
+ device = await self.get_device()
224
+ modified = False
225
+
226
+ if device.hardware != hardware:
227
+ device.hardware = hardware
228
+ modified = True
229
+
230
+ if device.last_state == UpdateStateEnum.UNKNOWN:
231
+ device.last_state = UpdateStateEnum.REGISTERED
232
+ modified = True
233
+
234
+ if modified:
235
+ await self.save_device(device, update_fields=["hardware_id", "last_state"])
236
+
237
+ async def update_log_complete(self, log_complete: bool):
238
+ device = await self.get_device()
239
+ device.log_complete = log_complete
240
+ await self.save_device(device, update_fields=["log_complete"])
241
+
242
+ async def get_rollout(self) -> Optional[Rollout]:
243
+ device = await self.get_device()
244
+
245
+ if device.update_mode == UpdateModeEnum.ROLLOUT:
246
+ return (
247
+ await Rollout.filter(
248
+ feed=device.feed,
249
+ flavor=device.flavor,
250
+ paused=False,
251
+ firmware__compatibility__devices__uuid=device.uuid,
252
+ )
253
+ .order_by("-created_at")
254
+ .first()
255
+ .prefetch_related("firmware")
256
+ )
257
+
258
+ return None
259
+
260
+ async def _get_firmware(self) -> Firmware | None:
261
+ device = await self.get_device()
262
+
263
+ if device.update_mode == UpdateModeEnum.ROLLOUT:
264
+ rollout = await self.get_rollout()
265
+ if not rollout or rollout.paused:
266
+ return None
267
+ await rollout.fetch_related("firmware")
268
+ return rollout.firmware
269
+ if device.update_mode == UpdateModeEnum.ASSIGNED:
270
+ await device.fetch_related("assigned_firmware")
271
+ return device.assigned_firmware
272
+
273
+ if device.update_mode == UpdateModeEnum.LATEST:
274
+ return await Firmware.latest(device)
275
+
276
+ assert device.update_mode == UpdateModeEnum.PINNED
277
+ return None
278
+
279
+ async def get_update(self) -> tuple[HandlingType, Firmware]:
280
+ device = await self.get_device()
281
+ firmware = await self._get_firmware()
282
+
283
+ if firmware is None:
284
+ handling_type = HandlingType.SKIP
143
285
  self.poll_time = POLL_TIME
144
- elif file.name == device.fw_version:
145
- mode = "skip"
286
+
287
+ elif firmware.version == device.fw_version and not device.force_update:
288
+ handling_type = HandlingType.SKIP
146
289
  self.poll_time = POLL_TIME
147
- else:
148
- mode = "forced"
149
- self.poll_time = "00:00:05"
150
290
 
151
- if self.force_update:
152
- mode = "forced"
153
- self.poll_time = "00:00:05"
291
+ elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
292
+ handling_type = HandlingType.SKIP
293
+ self.poll_time = POLL_TIME
294
+
295
+ else:
296
+ handling_type = HandlingType.FORCED
297
+ self.poll_time = POLL_TIME_UPDATING
154
298
 
155
- if mode == "forced" and self.update_complete:
156
- self.update_complete = False
157
- await self.clear_log()
299
+ if device.log_complete:
300
+ await self.update_log_complete(False)
301
+ await self.clear_log()
158
302
 
159
- return mode
303
+ return handling_type, firmware
160
304
 
161
305
  async def update_log(self, log_data: str) -> None:
162
306
  if log_data is None:
163
307
  return
164
308
  device = await self.get_device()
309
+
165
310
  if device.last_log is None:
166
311
  device.last_log = ""
312
+
313
+ matches = re.findall(r"Downloaded (\d+)%", log_data)
314
+ if matches:
315
+ device.progress = matches[-1]
316
+
167
317
  if log_data.startswith("Installing Update Chunk Artifacts."):
168
- await self.clear_log()
169
- if log_data == "All Chunks Installed.":
170
- self.force_update = False
171
- self.update_complete = True
318
+ # clear log
319
+ device.last_log = ""
320
+ await self.publish_log(None)
321
+
172
322
  if not log_data == "Skipped Update.":
173
323
  device.last_log += f"{log_data}\n"
174
324
  await self.publish_log(f"{log_data}\n")
175
325
 
326
+ await self.save_device(
327
+ device,
328
+ update_fields=["progress", "last_log"],
329
+ )
330
+
176
331
  async def clear_log(self) -> None:
177
332
  device = await self.get_device()
178
333
  device.last_log = ""
334
+ await self.save_device(device, update_fields=["last_log"])
179
335
  await self.publish_log(None)
180
336
 
181
337
 
182
- device_managers = {"unknown": UnknownUpdateManager("unknown")}
338
+ async def get_update_manager(dev_id: str) -> UpdateManager:
339
+ if dev_id == "unknown":
340
+ return UnknownUpdateManager("unknown")
341
+ else:
342
+ return DeviceUpdateManager(dev_id)
183
343
 
184
344
 
185
- async def get_update_manager(dev_id: str) -> UpdateManager:
186
- global device_managers
187
- if device_managers.get(dev_id) is None:
188
- device_managers[dev_id] = DeviceUpdateManager(dev_id)
189
- return device_managers[dev_id]
190
-
191
-
192
- def get_update_manager_sync(dev_id: str) -> UpdateManager:
193
- global device_managers
194
- if device_managers.get(dev_id) is None:
195
- device_managers[dev_id] = DeviceUpdateManager(dev_id)
196
- return device_managers[dev_id]
197
-
198
-
199
- async def delete_device(dev_id: str) -> None:
200
- global device_managers
201
- try:
202
- updater = get_update_manager_sync(dev_id)
203
- await (await updater.get_device()).delete()
204
- del device_managers[dev_id]
205
- except KeyError:
206
- pass
345
+ async def delete_devices(ids: list[str]):
346
+ await Device.filter(id__in=ids).delete()
347
+ cache = Cache(namespace="main")
348
+ for dev_id in ids:
349
+ await cache.delete(dev_id)
@@ -3,22 +3,22 @@ import time
3
3
  from fastapi import APIRouter, Depends, HTTPException
4
4
  from fastapi.requests import Request
5
5
 
6
- from . import controller, download
7
- from .manager import get_update_manager_sync
6
+ from goosebit.settings import TENANT
7
+
8
+ from . import controller
9
+ from .manager import get_update_manager
8
10
 
9
11
 
10
12
  async def verify_tenant(tenant: str):
11
- if not tenant == "loadsync":
13
+ if not tenant == TENANT:
12
14
  raise HTTPException(404)
13
15
  return tenant
14
16
 
15
17
 
16
18
  async def log_last_connection(request: Request, dev_id: str):
17
19
  host = request.client.host
18
- updater = get_update_manager_sync(dev_id)
19
- await updater.update_last_ip(host)
20
- await updater.update_last_seen(round(time.time()))
21
- await updater.save()
20
+ updater = await get_update_manager(dev_id)
21
+ await updater.update_last_connection(round(time.time()), host)
22
22
 
23
23
 
24
24
  router = APIRouter(
@@ -27,4 +27,3 @@ router = APIRouter(
27
27
  tags=["ddi"],
28
28
  )
29
29
  router.include_router(controller.router)
30
- router.include_router(download.router)
@@ -0,0 +1,70 @@
1
+ from pathlib import Path
2
+ from urllib.parse import unquote, urlparse
3
+ from urllib.request import url2pathname
4
+
5
+ from fastapi.requests import Request
6
+
7
+ from goosebit.models import Firmware, Hardware
8
+
9
+ from . import swdesc
10
+
11
+
12
+ async def create_firmware_update(uri: str):
13
+ parsed_uri = urlparse(uri)
14
+
15
+ if parsed_uri.scheme == "file":
16
+ file_path = Path(url2pathname(unquote(parsed_uri.path)))
17
+ update_info = await swdesc.parse_file(file_path)
18
+ if update_info is None:
19
+ return
20
+ elif parsed_uri.scheme.startswith("http"):
21
+ update_info = await swdesc.parse_remote(uri)
22
+ if update_info is None:
23
+ return
24
+ else:
25
+ return
26
+
27
+ firmware = (
28
+ await Firmware.get_or_create(
29
+ uri=uri,
30
+ version=str(update_info["version"]),
31
+ size=update_info["size"],
32
+ hash=update_info["hash"],
33
+ )
34
+ )[0]
35
+
36
+ for comp in update_info["compatibility"]:
37
+ model = comp.get("hw_model", "default")
38
+ revision = comp.get("hw_revision", "default")
39
+ await firmware.compatibility.add((await Hardware.get_or_create(model=model, revision=revision))[0])
40
+ await firmware.save()
41
+ return firmware
42
+
43
+
44
+ def generate_chunk(request: Request, firmware: Firmware | None) -> list:
45
+ if firmware is None:
46
+ return []
47
+ if firmware.local:
48
+ href = str(
49
+ request.url_for(
50
+ "download_file",
51
+ file_id=firmware.id,
52
+ )
53
+ )
54
+ else:
55
+ href = firmware.uri
56
+ return [
57
+ {
58
+ "part": "os",
59
+ "version": "1",
60
+ "name": firmware.path.name,
61
+ "artifacts": [
62
+ {
63
+ "filename": firmware.path.name,
64
+ "hashes": {"sha1": firmware.hash},
65
+ "size": firmware.size,
66
+ "_links": {"download": {"href": href}},
67
+ }
68
+ ],
69
+ }
70
+ ]
@@ -0,0 +1,83 @@
1
+ import hashlib
2
+ import logging
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import aiofiles
7
+ import httpx
8
+ import libconf
9
+ import semver
10
+
11
+ from goosebit.settings import UPDATES_DIR
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def _append_compatibility(boardname, value, compatibility):
17
+ if "hardware-compatibility" in value:
18
+ for revision in value["hardware-compatibility"]:
19
+ compatibility.append({"hw_model": boardname, "hw_revision": revision})
20
+
21
+
22
+ def parse_descriptor(swdesc: libconf.AttrDict[Any, Any | None]):
23
+ swdesc_attrs = {}
24
+ try:
25
+ swdesc_attrs["version"] = semver.Version.parse(swdesc["software"]["version"])
26
+ compatibility = []
27
+ _append_compatibility("default", swdesc["software"], compatibility)
28
+
29
+ for key in swdesc["software"]:
30
+ element = swdesc["software"][key]
31
+ _append_compatibility(key, element, compatibility)
32
+
33
+ if isinstance(element, dict):
34
+ for key2 in element:
35
+ _append_compatibility(key, element[key2], compatibility)
36
+
37
+ swdesc_attrs["compatibility"] = compatibility
38
+ except KeyError as e:
39
+ logging.warning(f"Parsing swu descriptor failed, error={e}")
40
+ raise ValueError("Parsing swu descriptor failed", e)
41
+
42
+ return swdesc_attrs
43
+
44
+
45
+ async def parse_file(file: Path):
46
+ async with aiofiles.open(file, "r+b") as f:
47
+ # get file size
48
+ size = int((await f.read(110))[54:62], 16)
49
+ filename = b""
50
+ next_byte = await f.read(1)
51
+ while not next_byte == b"\x00":
52
+ filename += next_byte
53
+ next_byte = await f.read(1)
54
+ # 4 null bytes
55
+ await f.read(3)
56
+
57
+ # should always be the first file
58
+ if not filename == b"sw-description":
59
+ return None
60
+
61
+ swdesc = libconf.loads((await f.read(size)).decode("utf-8"))
62
+
63
+ swdesc_attrs = parse_descriptor(swdesc)
64
+ swdesc_attrs["size"] = file.stat().st_size
65
+ swdesc_attrs["hash"] = _sha1_hash_file(file)
66
+ return swdesc_attrs
67
+
68
+
69
+ async def parse_remote(url: str):
70
+ async with httpx.AsyncClient() as c:
71
+ file = await c.get(url)
72
+ temp_file = UPDATES_DIR.joinpath("temp.swu")
73
+ async with aiofiles.open(temp_file, "w+b") as f:
74
+ await f.write(file.content)
75
+ parsed_file = await parse_file(temp_file)
76
+ temp_file.unlink()
77
+ return parsed_file
78
+
79
+
80
+ def _sha1_hash_file(file_path: Path):
81
+ with file_path.open("rb") as f:
82
+ sha1_hash = hashlib.file_digest(f, "sha1")
83
+ return sha1_hash.hexdigest()