goosebit 0.1.1__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 +5 -2
  2. goosebit/api/__init__.py +1 -1
  3. goosebit/api/devices.py +59 -39
  4. goosebit/api/download.py +28 -14
  5. goosebit/api/firmware.py +40 -34
  6. goosebit/api/helper.py +30 -0
  7. goosebit/api/rollouts.py +64 -13
  8. goosebit/api/routes.py +14 -7
  9. goosebit/auth/__init__.py +14 -6
  10. goosebit/db.py +5 -0
  11. goosebit/models.py +110 -10
  12. goosebit/permissions.py +26 -20
  13. goosebit/realtime/__init__.py +1 -1
  14. goosebit/realtime/logs.py +3 -6
  15. goosebit/settings.py +4 -6
  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 +33 -40
  20. goosebit/ui/static/js/devices.js +187 -250
  21. goosebit/ui/static/js/firmware.js +229 -92
  22. goosebit/ui/static/js/index.js +79 -90
  23. goosebit/ui/static/js/logs.js +14 -11
  24. goosebit/ui/static/js/rollouts.js +169 -27
  25. goosebit/ui/static/js/util.js +66 -0
  26. goosebit/ui/templates/devices.html +75 -51
  27. goosebit/ui/templates/firmware.html +149 -35
  28. goosebit/ui/templates/index.html +9 -26
  29. goosebit/ui/templates/login.html +58 -27
  30. goosebit/ui/templates/logs.html +15 -5
  31. goosebit/ui/templates/nav.html +77 -26
  32. goosebit/ui/templates/rollouts.html +62 -39
  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 +53 -35
  37. goosebit/updater/manager.py +205 -103
  38. goosebit/updater/routes.py +4 -7
  39. goosebit/updates/__init__.py +70 -0
  40. goosebit/updates/swdesc.py +83 -0
  41. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/METADATA +53 -3
  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 -13
  47. goosebit/updater/misc.py +0 -57
  48. goosebit/updates/artifacts.py +0 -89
  49. goosebit/updates/version.py +0 -38
  50. goosebit-0.1.1.dist-info/RECORD +0 -53
  51. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/LICENSE +0 -0
  52. {goosebit-0.1.1.dist-info → goosebit-0.1.2.dist-info}/WHEEL +0 -0
@@ -5,87 +5,122 @@ import re
5
5
  from abc import ABC, abstractmethod
6
6
  from contextlib import asynccontextmanager
7
7
  from datetime import datetime
8
+ from enum import StrEnum
8
9
  from typing import Callable, Optional
9
10
 
10
- from goosebit.models import Device, Rollout
11
+ from aiocache import Cache, cached
12
+ from aiocache.serializers import PickleSerializer
13
+
14
+ from goosebit.models import (
15
+ Device,
16
+ Firmware,
17
+ Hardware,
18
+ Rollout,
19
+ UpdateModeEnum,
20
+ UpdateStateEnum,
21
+ )
11
22
  from goosebit.settings import POLL_TIME, POLL_TIME_UPDATING
12
- from goosebit.updates.artifacts import FirmwareArtifact
23
+
24
+
25
+ class HandlingType(StrEnum):
26
+ SKIP = "skip"
27
+ ATTEMPT = "attempt"
28
+ FORCED = "forced"
13
29
 
14
30
 
15
31
  class UpdateManager(ABC):
32
+ device_log_subscriptions: dict[str, list[Callable]] = {}
33
+ device_poll_time: dict[str, str] = {}
34
+
16
35
  def __init__(self, dev_id: str):
17
36
  self.dev_id = dev_id
18
- self.config_data = {}
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_hw_model(self, hw_model: str) -> None:
47
+ async def update_hardware(self, hardware: Hardware) -> None:
35
48
  return
36
49
 
37
- async def update_hw_revision(self, hw_revision: str) -> None:
50
+ async def update_device_state(self, state: UpdateStateEnum) -> None:
38
51
  return
39
52
 
40
- async def update_device_state(self, state: str) -> None:
53
+ async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
41
54
  return
42
55
 
43
- async def update_last_seen(self, last_seen: int) -> None:
56
+ async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
44
57
  return
45
58
 
46
- async def update_last_ip(self, last_ip: str) -> None:
59
+ async def update_name(self, name: str):
47
60
  return
48
61
 
49
- async def get_rollout(self) -> Optional[Rollout]:
50
- return None
62
+ async def update_feed(self, feed: str):
63
+ return
64
+
65
+ async def update_flavor(self, flavor: str):
66
+ return
51
67
 
52
68
  async def update_config_data(self, **kwargs):
53
- await self.update_hw_model(kwargs.get("hw_model") or "default")
54
- await self.update_hw_revision(kwargs.get("hw_revision") or "default")
69
+ return
55
70
 
56
- device = await self.get_device()
57
- if device.last_state == "unknown":
58
- await self.update_device_state("registered")
59
- await self.save()
71
+ async def update_log_complete(self, log_complete: bool):
72
+ return
60
73
 
61
- self.config_data.update(kwargs)
74
+ async def get_rollout(self) -> Optional[Rollout]:
75
+ return None
62
76
 
63
77
  @asynccontextmanager
64
78
  async def subscribe_log(self, callback: Callable):
65
79
  device = await self.get_device()
66
- self.log_subscribers.append(callback)
80
+ subscribers = self.log_subscribers
81
+ subscribers.append(callback)
82
+ self.log_subscribers = subscribers
67
83
  await callback(device.last_log)
68
84
  try:
69
85
  yield
70
86
  except asyncio.CancelledError:
71
87
  pass
72
88
  finally:
73
- self.log_subscribers.remove(callback)
89
+ subscribers = self.log_subscribers
90
+ subscribers.remove(callback)
91
+ self.log_subscribers = subscribers
74
92
 
75
93
  @property
76
94
  def poll_seconds(self):
77
95
  time_obj = datetime.strptime(self.poll_time, "%H:%M:%S")
78
96
  return time_obj.hour * 3600 + time_obj.minute * 60 + time_obj.second
79
97
 
98
+ @property
99
+ def log_subscribers(self):
100
+ return UpdateManager.device_log_subscriptions.get(self.dev_id, [])
101
+
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
+
80
118
  async def publish_log(self, log_data: str | None):
81
119
  for cb in self.log_subscribers:
82
120
  await cb(log_data)
83
121
 
84
122
  @abstractmethod
85
- async def get_update_file(self) -> FirmwareArtifact: ...
86
-
87
- @abstractmethod
88
- async def get_update_mode(self) -> str: ...
123
+ async def get_update(self) -> tuple[HandlingType, Firmware]: ...
89
124
 
90
125
  @abstractmethod
91
126
  async def update_log(self, log_data: str) -> None: ...
@@ -96,152 +131,219 @@ class UnknownUpdateManager(UpdateManager):
96
131
  super().__init__(dev_id)
97
132
  self.poll_time = POLL_TIME_UPDATING
98
133
 
99
- async def get_update_file(self) -> FirmwareArtifact:
100
- return FirmwareArtifact("latest")
134
+ async def _get_firmware(self) -> Firmware:
135
+ return await Firmware.latest(await self.get_device())
101
136
 
102
- async def get_update_mode(self) -> str:
103
- return "forced"
137
+ async def get_update(self) -> tuple[HandlingType, Firmware]:
138
+ firmware = await self._get_firmware()
139
+ return HandlingType.FORCED, firmware
104
140
 
105
141
  async def update_log(self, log_data: str) -> None:
106
142
  return
107
143
 
108
144
 
109
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
+ )
110
155
  async def get_device(self) -> Device:
111
- if self.device:
112
- return self.device
113
- self.device = (await Device.get_or_create(uuid=self.dev_id))[0]
114
- 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
115
160
 
116
- async def save(self) -> None:
117
- await self.device.save()
161
+ return (await Device.get_or_create(uuid=self.dev_id, defaults={"hardware": hardware}))[0]
118
162
 
119
- async def update_fw_version(self, version: str) -> None:
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:
120
169
  device = await self.get_device()
121
- device.fw_version = version
170
+ device.force_update = force_update
171
+ await self.save_device(device, update_fields=["force_update"])
122
172
 
123
- async def update_hw_model(self, hw_model: str) -> None:
173
+ async def update_fw_version(self, version: str) -> None:
124
174
  device = await self.get_device()
125
- device.hw_model = hw_model
175
+ device.fw_version = version
176
+ await self.save_device(device, update_fields=["fw_version"])
126
177
 
127
- async def update_hw_revision(self, hw_revision: str) -> None:
178
+ async def update_hardware(self, hardware: Hardware) -> None:
128
179
  device = await self.get_device()
129
- device.hw_revision = hw_revision
180
+ device.hardware = hardware
181
+ await self.save_device(device, update_fields=["hardware"])
130
182
 
131
- async def update_device_state(self, state: str) -> None:
183
+ async def update_device_state(self, state: UpdateStateEnum) -> None:
132
184
  device = await self.get_device()
133
185
  device.last_state = state
186
+ await self.save_device(device, update_fields=["last_state"])
134
187
 
135
- async def update_last_seen(self, last_seen: int) -> None:
188
+ async def update_last_connection(self, last_seen: int, last_ip: str) -> None:
136
189
  device = await self.get_device()
137
190
  device.last_seen = last_seen
138
-
139
- async def update_last_ip(self, last_ip: str) -> None:
140
- device = await self.get_device()
141
191
  if ":" in last_ip:
142
192
  device.last_ipv6 = last_ip
193
+ await self.save_device(device, update_fields=["last_seen", "last_ipv6"])
143
194
  else:
144
195
  device.last_ip = last_ip
196
+ await self.save_device(device, update_fields=["last_seen", "last_ip"])
197
+
198
+ async def update_update(self, update_mode: UpdateModeEnum, firmware: Firmware | None):
199
+ device = await self.get_device()
200
+ device.assigned_firmware = firmware
201
+ device.update_mode = update_mode
202
+ await self.save_device(device, update_fields=["assigned_firmware_id", "update_mode"])
203
+
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"])
208
+
209
+ async def update_feed(self, feed: str):
210
+ device = await self.get_device()
211
+ device.feed = feed
212
+ await self.save_device(device, update_fields=["feed"])
213
+
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"])
145
241
 
146
242
  async def get_rollout(self) -> Optional[Rollout]:
147
243
  device = await self.get_device()
148
244
 
149
- if device.fw_file == "none":
245
+ if device.update_mode == UpdateModeEnum.ROLLOUT:
150
246
  return (
151
247
  await Rollout.filter(
152
- hw_model=device.hw_model,
153
- hw_revision=device.hw_revision,
154
248
  feed=device.feed,
155
249
  flavor=device.flavor,
250
+ paused=False,
251
+ firmware__compatibility__devices__uuid=device.uuid,
156
252
  )
157
253
  .order_by("-created_at")
158
254
  .first()
255
+ .prefetch_related("firmware")
159
256
  )
160
257
 
161
258
  return None
162
259
 
163
- async def get_update_file(self) -> FirmwareArtifact:
260
+ async def _get_firmware(self) -> Firmware | None:
164
261
  device = await self.get_device()
165
- file = device.fw_file
166
262
 
167
- if file == "none":
263
+ if device.update_mode == UpdateModeEnum.ROLLOUT:
168
264
  rollout = await self.get_rollout()
169
- if rollout and not rollout.paused:
170
- file = rollout.fw_file
171
-
172
- return FirmwareArtifact(file, device.hw_model, device.hw_revision)
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
173
278
 
174
- async def get_update_mode(self) -> str:
279
+ async def get_update(self) -> tuple[HandlingType, Firmware]:
175
280
  device = await self.get_device()
281
+ firmware = await self._get_firmware()
176
282
 
177
- file = await self.get_update_file()
178
- if file.is_empty():
179
- mode = "skip"
283
+ if firmware is None:
284
+ handling_type = HandlingType.SKIP
180
285
  self.poll_time = POLL_TIME
181
- elif file.name == device.fw_version and not self.force_update:
182
- mode = "skip"
286
+
287
+ elif firmware.version == device.fw_version and not device.force_update:
288
+ handling_type = HandlingType.SKIP
183
289
  self.poll_time = POLL_TIME
184
- elif device.last_state == "error" and not self.force_update:
185
- mode = "skip"
290
+
291
+ elif device.last_state == UpdateStateEnum.ERROR and not device.force_update:
292
+ handling_type = HandlingType.SKIP
186
293
  self.poll_time = POLL_TIME
294
+
187
295
  else:
188
- mode = "forced"
296
+ handling_type = HandlingType.FORCED
189
297
  self.poll_time = POLL_TIME_UPDATING
190
298
 
191
- if self.update_complete:
192
- self.update_complete = False
299
+ if device.log_complete:
300
+ await self.update_log_complete(False)
193
301
  await self.clear_log()
194
302
 
195
- return mode
303
+ return handling_type, firmware
196
304
 
197
305
  async def update_log(self, log_data: str) -> None:
198
306
  if log_data is None:
199
307
  return
200
308
  device = await self.get_device()
309
+
310
+ if device.last_log is None:
311
+ device.last_log = ""
312
+
201
313
  matches = re.findall(r"Downloaded (\d+)%", log_data)
202
314
  if matches:
203
315
  device.progress = matches[-1]
204
- if device.last_log is None:
205
- device.last_log = ""
316
+
206
317
  if log_data.startswith("Installing Update Chunk Artifacts."):
207
- await self.clear_log()
208
- if log_data == "All Chunks Installed.":
209
- self.force_update = False
210
- self.update_complete = True
318
+ # clear log
319
+ device.last_log = ""
320
+ await self.publish_log(None)
321
+
211
322
  if not log_data == "Skipped Update.":
212
323
  device.last_log += f"{log_data}\n"
213
324
  await self.publish_log(f"{log_data}\n")
214
- await device.save()
325
+
326
+ await self.save_device(
327
+ device,
328
+ update_fields=["progress", "last_log"],
329
+ )
215
330
 
216
331
  async def clear_log(self) -> None:
217
332
  device = await self.get_device()
218
333
  device.last_log = ""
219
- await device.save()
334
+ await self.save_device(device, update_fields=["last_log"])
220
335
  await self.publish_log(None)
221
336
 
222
337
 
223
- 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)
224
343
 
225
344
 
226
- async def get_update_manager(dev_id: str) -> UpdateManager:
227
- global device_managers
228
- if device_managers.get(dev_id) is None:
229
- device_managers[dev_id] = DeviceUpdateManager(dev_id)
230
- return device_managers[dev_id]
231
-
232
-
233
- def get_update_manager_sync(dev_id: str) -> UpdateManager:
234
- global device_managers
235
- if device_managers.get(dev_id) is None:
236
- device_managers[dev_id] = DeviceUpdateManager(dev_id)
237
- return device_managers[dev_id]
238
-
239
-
240
- async def delete_device(dev_id: str) -> None:
241
- global device_managers
242
- try:
243
- updater = get_update_manager_sync(dev_id)
244
- await (await updater.get_device()).delete()
245
- del device_managers[dev_id]
246
- except KeyError:
247
- 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)
@@ -5,8 +5,8 @@ from fastapi.requests import Request
5
5
 
6
6
  from goosebit.settings import TENANT
7
7
 
8
- from . import controller, download
9
- from .manager import get_update_manager_sync
8
+ from . import controller
9
+ from .manager import get_update_manager
10
10
 
11
11
 
12
12
  async def verify_tenant(tenant: str):
@@ -17,10 +17,8 @@ async def verify_tenant(tenant: str):
17
17
 
18
18
  async def log_last_connection(request: Request, dev_id: str):
19
19
  host = request.client.host
20
- updater = get_update_manager_sync(dev_id)
21
- await updater.update_last_ip(host)
22
- await updater.update_last_seen(round(time.time()))
23
- await updater.save()
20
+ updater = await get_update_manager(dev_id)
21
+ await updater.update_last_connection(round(time.time()), host)
24
22
 
25
23
 
26
24
  router = APIRouter(
@@ -29,4 +27,3 @@ router = APIRouter(
29
27
  tags=["ddi"],
30
28
  )
31
29
  router.include_router(controller.router)
32
- 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()