audex 1.0.7a3__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.
- audex/__init__.py +9 -0
- audex/__main__.py +7 -0
- audex/cli/__init__.py +189 -0
- audex/cli/apis/__init__.py +12 -0
- audex/cli/apis/init/__init__.py +34 -0
- audex/cli/apis/init/gencfg.py +130 -0
- audex/cli/apis/init/setup.py +330 -0
- audex/cli/apis/init/vprgroup.py +125 -0
- audex/cli/apis/serve.py +141 -0
- audex/cli/args.py +356 -0
- audex/cli/exceptions.py +44 -0
- audex/cli/helper/__init__.py +0 -0
- audex/cli/helper/ansi.py +193 -0
- audex/cli/helper/display.py +288 -0
- audex/config/__init__.py +64 -0
- audex/config/core/__init__.py +30 -0
- audex/config/core/app.py +29 -0
- audex/config/core/audio.py +45 -0
- audex/config/core/logging.py +163 -0
- audex/config/core/session.py +11 -0
- audex/config/helper/__init__.py +1 -0
- audex/config/helper/client/__init__.py +1 -0
- audex/config/helper/client/http.py +28 -0
- audex/config/helper/client/websocket.py +21 -0
- audex/config/helper/provider/__init__.py +1 -0
- audex/config/helper/provider/dashscope.py +13 -0
- audex/config/helper/provider/unisound.py +18 -0
- audex/config/helper/provider/xfyun.py +23 -0
- audex/config/infrastructure/__init__.py +31 -0
- audex/config/infrastructure/cache.py +51 -0
- audex/config/infrastructure/database.py +48 -0
- audex/config/infrastructure/recorder.py +32 -0
- audex/config/infrastructure/store.py +19 -0
- audex/config/provider/__init__.py +18 -0
- audex/config/provider/transcription.py +109 -0
- audex/config/provider/vpr.py +99 -0
- audex/container.py +40 -0
- audex/entity/__init__.py +468 -0
- audex/entity/doctor.py +109 -0
- audex/entity/doctor.pyi +51 -0
- audex/entity/fields.py +401 -0
- audex/entity/segment.py +115 -0
- audex/entity/segment.pyi +38 -0
- audex/entity/session.py +133 -0
- audex/entity/session.pyi +47 -0
- audex/entity/utterance.py +142 -0
- audex/entity/utterance.pyi +48 -0
- audex/entity/vp.py +68 -0
- audex/entity/vp.pyi +35 -0
- audex/exceptions.py +157 -0
- audex/filters/__init__.py +692 -0
- audex/filters/generated/__init__.py +21 -0
- audex/filters/generated/doctor.py +987 -0
- audex/filters/generated/segment.py +723 -0
- audex/filters/generated/session.py +978 -0
- audex/filters/generated/utterance.py +939 -0
- audex/filters/generated/vp.py +815 -0
- audex/helper/__init__.py +1 -0
- audex/helper/hash.py +33 -0
- audex/helper/mixin.py +65 -0
- audex/helper/net.py +19 -0
- audex/helper/settings/__init__.py +830 -0
- audex/helper/settings/fields.py +317 -0
- audex/helper/stream.py +153 -0
- audex/injectors/__init__.py +1 -0
- audex/injectors/config.py +12 -0
- audex/injectors/lifespan.py +7 -0
- audex/lib/__init__.py +1 -0
- audex/lib/cache/__init__.py +383 -0
- audex/lib/cache/inmemory.py +513 -0
- audex/lib/database/__init__.py +83 -0
- audex/lib/database/sqlite.py +406 -0
- audex/lib/exporter.py +189 -0
- audex/lib/injectors/__init__.py +1 -0
- audex/lib/injectors/cache.py +25 -0
- audex/lib/injectors/container.py +47 -0
- audex/lib/injectors/exporter.py +26 -0
- audex/lib/injectors/recorder.py +33 -0
- audex/lib/injectors/server.py +17 -0
- audex/lib/injectors/session.py +18 -0
- audex/lib/injectors/sqlite.py +24 -0
- audex/lib/injectors/store.py +13 -0
- audex/lib/injectors/transcription.py +42 -0
- audex/lib/injectors/usb.py +12 -0
- audex/lib/injectors/vpr.py +65 -0
- audex/lib/injectors/wifi.py +7 -0
- audex/lib/recorder.py +844 -0
- audex/lib/repos/__init__.py +149 -0
- audex/lib/repos/container.py +23 -0
- audex/lib/repos/database/__init__.py +1 -0
- audex/lib/repos/database/sqlite.py +672 -0
- audex/lib/repos/decorators.py +74 -0
- audex/lib/repos/doctor.py +286 -0
- audex/lib/repos/segment.py +302 -0
- audex/lib/repos/session.py +285 -0
- audex/lib/repos/tables/__init__.py +70 -0
- audex/lib/repos/tables/doctor.py +137 -0
- audex/lib/repos/tables/segment.py +113 -0
- audex/lib/repos/tables/session.py +140 -0
- audex/lib/repos/tables/utterance.py +131 -0
- audex/lib/repos/tables/vp.py +102 -0
- audex/lib/repos/utterance.py +288 -0
- audex/lib/repos/vp.py +286 -0
- audex/lib/restful.py +251 -0
- audex/lib/server/__init__.py +97 -0
- audex/lib/server/auth.py +98 -0
- audex/lib/server/handlers.py +248 -0
- audex/lib/server/templates/index.html.j2 +226 -0
- audex/lib/server/templates/login.html.j2 +111 -0
- audex/lib/server/templates/static/script.js +68 -0
- audex/lib/server/templates/static/style.css +579 -0
- audex/lib/server/types.py +123 -0
- audex/lib/session.py +503 -0
- audex/lib/store/__init__.py +238 -0
- audex/lib/store/localfile.py +411 -0
- audex/lib/transcription/__init__.py +33 -0
- audex/lib/transcription/dashscope.py +525 -0
- audex/lib/transcription/events.py +62 -0
- audex/lib/usb.py +554 -0
- audex/lib/vpr/__init__.py +38 -0
- audex/lib/vpr/unisound/__init__.py +185 -0
- audex/lib/vpr/unisound/types.py +469 -0
- audex/lib/vpr/xfyun/__init__.py +483 -0
- audex/lib/vpr/xfyun/types.py +679 -0
- audex/lib/websocket/__init__.py +8 -0
- audex/lib/websocket/connection.py +485 -0
- audex/lib/websocket/pool.py +991 -0
- audex/lib/wifi.py +1146 -0
- audex/lifespan.py +75 -0
- audex/service/__init__.py +27 -0
- audex/service/decorators.py +73 -0
- audex/service/doctor/__init__.py +652 -0
- audex/service/doctor/const.py +36 -0
- audex/service/doctor/exceptions.py +96 -0
- audex/service/doctor/types.py +54 -0
- audex/service/export/__init__.py +236 -0
- audex/service/export/const.py +17 -0
- audex/service/export/exceptions.py +34 -0
- audex/service/export/types.py +21 -0
- audex/service/injectors/__init__.py +1 -0
- audex/service/injectors/container.py +53 -0
- audex/service/injectors/doctor.py +34 -0
- audex/service/injectors/export.py +27 -0
- audex/service/injectors/session.py +49 -0
- audex/service/session/__init__.py +754 -0
- audex/service/session/const.py +34 -0
- audex/service/session/exceptions.py +67 -0
- audex/service/session/types.py +91 -0
- audex/types.py +39 -0
- audex/utils.py +287 -0
- audex/valueobj/__init__.py +81 -0
- audex/valueobj/common/__init__.py +1 -0
- audex/valueobj/common/auth.py +84 -0
- audex/valueobj/common/email.py +16 -0
- audex/valueobj/common/ops.py +22 -0
- audex/valueobj/common/phone.py +84 -0
- audex/valueobj/common/version.py +72 -0
- audex/valueobj/session.py +19 -0
- audex/valueobj/utterance.py +15 -0
- audex/view/__init__.py +51 -0
- audex/view/container.py +17 -0
- audex/view/decorators.py +303 -0
- audex/view/pages/__init__.py +1 -0
- audex/view/pages/dashboard/__init__.py +286 -0
- audex/view/pages/dashboard/wifi.py +407 -0
- audex/view/pages/login.py +110 -0
- audex/view/pages/recording.py +348 -0
- audex/view/pages/register.py +202 -0
- audex/view/pages/sessions/__init__.py +196 -0
- audex/view/pages/sessions/details.py +224 -0
- audex/view/pages/sessions/export.py +443 -0
- audex/view/pages/settings.py +374 -0
- audex/view/pages/voiceprint/__init__.py +1 -0
- audex/view/pages/voiceprint/enroll.py +195 -0
- audex/view/pages/voiceprint/update.py +195 -0
- audex/view/static/css/dashboard.css +452 -0
- audex/view/static/css/glass.css +22 -0
- audex/view/static/css/global.css +541 -0
- audex/view/static/css/login.css +386 -0
- audex/view/static/css/recording.css +439 -0
- audex/view/static/css/register.css +293 -0
- audex/view/static/css/sessions/styles.css +501 -0
- audex/view/static/css/settings.css +186 -0
- audex/view/static/css/voiceprint/enroll.css +43 -0
- audex/view/static/css/voiceprint/styles.css +209 -0
- audex/view/static/css/voiceprint/update.css +44 -0
- audex/view/static/images/logo.svg +95 -0
- audex/view/static/js/recording.js +42 -0
- audex-1.0.7a3.dist-info/METADATA +361 -0
- audex-1.0.7a3.dist-info/RECORD +192 -0
- audex-1.0.7a3.dist-info/WHEEL +4 -0
- audex-1.0.7a3.dist-info/entry_points.txt +3 -0
audex/lib/usb.py
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
import asyncio
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import pathlib
|
|
8
|
+
import platform
|
|
9
|
+
import shutil
|
|
10
|
+
import typing as t
|
|
11
|
+
|
|
12
|
+
from audex.helper.mixin import AsyncLifecycleMixin
|
|
13
|
+
from audex.helper.mixin import LoggingMixin
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class USBDevice(t.NamedTuple):
|
|
17
|
+
"""Represents a USB storage device.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
device_node: Device node path (e.g., /dev/sdb1 on Linux, E: on Windows).
|
|
21
|
+
mount_point: Where the device is mounted (e.g., /media/usb or E:\\).
|
|
22
|
+
label: Volume label of the device.
|
|
23
|
+
fs_type: Filesystem type (e.g., vfat, exfat, ntfs).
|
|
24
|
+
size_bytes: Total size in bytes.
|
|
25
|
+
vendor: Device vendor name.
|
|
26
|
+
model: Device model name.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
device_node: str
|
|
30
|
+
mount_point: str
|
|
31
|
+
label: str | None
|
|
32
|
+
fs_type: str | None
|
|
33
|
+
size_bytes: int | None
|
|
34
|
+
vendor: str | None
|
|
35
|
+
model: str | None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class USBExportTask(t.NamedTuple):
|
|
39
|
+
"""Represents a file/directory export task.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
source: Source file or directory path.
|
|
43
|
+
dest_name: Destination name (relative to USB root).
|
|
44
|
+
is_directory: Whether the source is a directory.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
source: pathlib.Path
|
|
48
|
+
dest_name: str
|
|
49
|
+
is_directory: bool
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class USBBackend(abc.ABC):
|
|
53
|
+
"""Abstract base class for USB device backends."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, logger: t.Any) -> None:
|
|
56
|
+
self.logger = logger
|
|
57
|
+
|
|
58
|
+
@abc.abstractmethod
|
|
59
|
+
def list_devices(self) -> list[USBDevice]:
|
|
60
|
+
"""List all currently connected USB storage devices."""
|
|
61
|
+
|
|
62
|
+
@abc.abstractmethod
|
|
63
|
+
async def start_monitoring(self, callback: t.Callable[[str, t.Any], None]) -> None:
|
|
64
|
+
"""Start monitoring for USB device events."""
|
|
65
|
+
|
|
66
|
+
@abc.abstractmethod
|
|
67
|
+
async def stop_monitoring(self) -> None:
|
|
68
|
+
"""Stop monitoring for USB device events."""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class LinuxUSBBackend(USBBackend):
|
|
72
|
+
"""Linux USB backend using pyudev."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, logger: t.Any) -> None:
|
|
75
|
+
super().__init__(logger)
|
|
76
|
+
try:
|
|
77
|
+
import pyudev
|
|
78
|
+
|
|
79
|
+
self._pyudev = pyudev
|
|
80
|
+
self._context = pyudev.Context()
|
|
81
|
+
self._monitor: pyudev.Monitor | None = None
|
|
82
|
+
self._observer: pyudev.MonitorObserver | None = None
|
|
83
|
+
except ImportError as e:
|
|
84
|
+
raise RuntimeError(
|
|
85
|
+
"pyudev is required for Linux USB support. Install it with: pip install pyudev"
|
|
86
|
+
) from e
|
|
87
|
+
|
|
88
|
+
def list_devices(self) -> list[USBDevice]:
|
|
89
|
+
"""List all currently connected USB storage devices."""
|
|
90
|
+
devices: list[USBDevice] = []
|
|
91
|
+
|
|
92
|
+
for device in self._context.list_devices(subsystem="block", DEVTYPE="partition"):
|
|
93
|
+
if device.find_parent("usb") is None:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
mount_point = self._get_mount_point(device.device_node)
|
|
97
|
+
if not mount_point:
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
id_vendor = device.get("ID_VENDOR", "Unknown")
|
|
101
|
+
id_model = device.get("ID_MODEL", "Unknown")
|
|
102
|
+
id_fs_type = device.get("ID_FS_TYPE")
|
|
103
|
+
id_fs_label = device.get("ID_FS_LABEL")
|
|
104
|
+
|
|
105
|
+
size_bytes = None
|
|
106
|
+
size_file = pathlib.Path(f"/sys/class/block/{device.sys_name}/size")
|
|
107
|
+
if size_file.exists():
|
|
108
|
+
try:
|
|
109
|
+
sectors = int(size_file.read_text().strip())
|
|
110
|
+
size_bytes = sectors * 512
|
|
111
|
+
except (ValueError, OSError):
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
usb_device = USBDevice(
|
|
115
|
+
device_node=device.device_node,
|
|
116
|
+
mount_point=mount_point,
|
|
117
|
+
label=id_fs_label,
|
|
118
|
+
fs_type=id_fs_type,
|
|
119
|
+
size_bytes=size_bytes,
|
|
120
|
+
vendor=id_vendor,
|
|
121
|
+
model=id_model,
|
|
122
|
+
)
|
|
123
|
+
devices.append(usb_device)
|
|
124
|
+
self.logger.debug(f"Found USB device: {usb_device}")
|
|
125
|
+
|
|
126
|
+
return devices
|
|
127
|
+
|
|
128
|
+
def _get_mount_point(self, device_node: str) -> str | None:
|
|
129
|
+
"""Get the mount point for a device node."""
|
|
130
|
+
try:
|
|
131
|
+
with pathlib.Path("/proc/mounts").open("r") as f:
|
|
132
|
+
for line in f:
|
|
133
|
+
parts = line.split()
|
|
134
|
+
if len(parts) >= 2 and parts[0] == device_node:
|
|
135
|
+
return parts[1]
|
|
136
|
+
except OSError as e:
|
|
137
|
+
self.logger.warning(f"Failed to read /proc/mounts: {e}")
|
|
138
|
+
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
async def start_monitoring(self, callback: t.Callable[[str, t.Any], None]) -> None:
|
|
142
|
+
"""Start monitoring for USB device events."""
|
|
143
|
+
self._monitor = self._pyudev.Monitor.from_netlink(self._context)
|
|
144
|
+
self._monitor.filter_by(subsystem="block")
|
|
145
|
+
|
|
146
|
+
self._observer = self._pyudev.MonitorObserver(self._monitor, callback)
|
|
147
|
+
self._observer.start()
|
|
148
|
+
self.logger.debug("Started Linux USB monitoring")
|
|
149
|
+
|
|
150
|
+
async def stop_monitoring(self) -> None:
|
|
151
|
+
"""Stop monitoring for USB device events."""
|
|
152
|
+
if self._observer:
|
|
153
|
+
self._observer.stop()
|
|
154
|
+
self._observer = None
|
|
155
|
+
|
|
156
|
+
self._monitor = None
|
|
157
|
+
self.logger.debug("Stopped Linux USB monitoring")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class WindowsUSBBackend(USBBackend):
|
|
161
|
+
"""Windows USB backend using win32api and WMI."""
|
|
162
|
+
|
|
163
|
+
def __init__(self, logger: t.Any) -> None:
|
|
164
|
+
super().__init__(logger)
|
|
165
|
+
try:
|
|
166
|
+
import win32api
|
|
167
|
+
import win32file
|
|
168
|
+
import wmi
|
|
169
|
+
|
|
170
|
+
self._win32api = win32api
|
|
171
|
+
self._win32file = win32file
|
|
172
|
+
self._wmi = wmi
|
|
173
|
+
self._wmi_connection = wmi.WMI()
|
|
174
|
+
self._monitoring = False
|
|
175
|
+
self._monitor_task: asyncio.Task[None] | None = None
|
|
176
|
+
except ImportError as e:
|
|
177
|
+
raise RuntimeError(
|
|
178
|
+
"pywin32 and WMI are required for Windows USB support. "
|
|
179
|
+
"Install them with: pip install pywin32 wmi"
|
|
180
|
+
) from e
|
|
181
|
+
|
|
182
|
+
def list_devices(self) -> list[USBDevice]:
|
|
183
|
+
"""List all currently connected USB storage devices."""
|
|
184
|
+
devices: list[USBDevice] = []
|
|
185
|
+
|
|
186
|
+
# Get all removable drives
|
|
187
|
+
drive_types = self._win32api.GetLogicalDriveStrings()
|
|
188
|
+
drives = [d for d in drive_types.split("\x00") if d]
|
|
189
|
+
|
|
190
|
+
for drive in drives:
|
|
191
|
+
try:
|
|
192
|
+
drive_type = self._win32file.GetDriveType(drive)
|
|
193
|
+
# DRIVE_REMOVABLE = 2
|
|
194
|
+
if drive_type != 2:
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Get drive information
|
|
198
|
+
try:
|
|
199
|
+
volume_info = self._win32api.GetVolumeInformation(drive)
|
|
200
|
+
label = volume_info[0]
|
|
201
|
+
fs_type = volume_info[4]
|
|
202
|
+
except Exception:
|
|
203
|
+
label = None
|
|
204
|
+
fs_type = None
|
|
205
|
+
|
|
206
|
+
# Get drive size
|
|
207
|
+
size_bytes = None
|
|
208
|
+
try:
|
|
209
|
+
_, total_bytes, _ = self._win32api.GetDiskFreeSpaceEx(drive)
|
|
210
|
+
size_bytes = total_bytes
|
|
211
|
+
except Exception:
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
# Try to get vendor and model from WMI
|
|
215
|
+
vendor = None
|
|
216
|
+
model = None
|
|
217
|
+
try:
|
|
218
|
+
for disk in self._wmi_connection.Win32_DiskDrive():
|
|
219
|
+
if disk.MediaType == "Removable Media":
|
|
220
|
+
for partition in disk.associators("Win32_DiskDriveToDiskPartition"):
|
|
221
|
+
for logical_disk in partition.associators(
|
|
222
|
+
"Win32_LogicalDiskToPartition"
|
|
223
|
+
):
|
|
224
|
+
if logical_disk.DeviceID == drive.rstrip("\\"):
|
|
225
|
+
vendor = disk.Manufacturer
|
|
226
|
+
model = disk.Model
|
|
227
|
+
break
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
|
|
231
|
+
device_node = drive.rstrip("\\")
|
|
232
|
+
mount_point = drive
|
|
233
|
+
|
|
234
|
+
usb_device = USBDevice(
|
|
235
|
+
device_node=device_node,
|
|
236
|
+
mount_point=mount_point,
|
|
237
|
+
label=label,
|
|
238
|
+
fs_type=fs_type,
|
|
239
|
+
size_bytes=size_bytes,
|
|
240
|
+
vendor=vendor,
|
|
241
|
+
model=model,
|
|
242
|
+
)
|
|
243
|
+
devices.append(usb_device)
|
|
244
|
+
self.logger.debug(f"Found USB device: {usb_device}")
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
self.logger.warning(f"Error checking drive {drive}: {e}")
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
return devices
|
|
251
|
+
|
|
252
|
+
async def start_monitoring(self, callback: t.Callable[[str, t.Any], None]) -> None:
|
|
253
|
+
"""Start monitoring for USB device events using WMI."""
|
|
254
|
+
self._monitoring = True
|
|
255
|
+
self._monitor_task = asyncio.create_task(self._monitor_loop(callback))
|
|
256
|
+
self.logger.debug("Started Windows USB monitoring")
|
|
257
|
+
|
|
258
|
+
async def stop_monitoring(self) -> None:
|
|
259
|
+
"""Stop monitoring for USB device events."""
|
|
260
|
+
self._monitoring = False
|
|
261
|
+
if self._monitor_task:
|
|
262
|
+
self._monitor_task.cancel()
|
|
263
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
264
|
+
await self._monitor_task
|
|
265
|
+
self._monitor_task = None
|
|
266
|
+
self.logger.debug("Stopped Windows USB monitoring")
|
|
267
|
+
|
|
268
|
+
async def _monitor_loop(self, callback: t.Callable[[str, t.Any], None]) -> None:
|
|
269
|
+
"""Monitor loop for Windows USB events."""
|
|
270
|
+
# Watch for Win32_VolumeChangeEvent
|
|
271
|
+
watcher = self._wmi_connection.Win32_VolumeChangeEvent.watch_for(
|
|
272
|
+
notification_type="Creation",
|
|
273
|
+
delay_secs=1,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
while self._monitoring:
|
|
277
|
+
try:
|
|
278
|
+
# Check for events (non-blocking with timeout)
|
|
279
|
+
await asyncio.sleep(1)
|
|
280
|
+
|
|
281
|
+
# Poll for new devices
|
|
282
|
+
try:
|
|
283
|
+
event = watcher(timeout_ms=100)
|
|
284
|
+
if event and event.EventType == 2: # Event types: 2=inserted, 3=removed
|
|
285
|
+
self.logger.info("USB device connected (Windows)")
|
|
286
|
+
# Create a simple device object for callback
|
|
287
|
+
callback("add", {"device_type": "volume"})
|
|
288
|
+
except Exception:
|
|
289
|
+
# Timeout or no event
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
except Exception as e:
|
|
293
|
+
self.logger.error(f"Error in Windows USB monitor loop: {e}")
|
|
294
|
+
await asyncio.sleep(5)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class USBManager(LoggingMixin, AsyncLifecycleMixin):
|
|
298
|
+
"""Cross-platform USB storage device manager for file export.
|
|
299
|
+
|
|
300
|
+
This manager monitors USB device connections and provides functionality
|
|
301
|
+
to export files and directories to connected USB drives. It automatically
|
|
302
|
+
selects the appropriate backend (Linux/Windows) based on the platform.
|
|
303
|
+
|
|
304
|
+
Example:
|
|
305
|
+
```python
|
|
306
|
+
# Create manager
|
|
307
|
+
manager = USBManager()
|
|
308
|
+
|
|
309
|
+
# Add export tasks
|
|
310
|
+
manager.add_export_task(
|
|
311
|
+
source="/var/log/app.log",
|
|
312
|
+
dest_name="logs/app.log",
|
|
313
|
+
)
|
|
314
|
+
manager.add_export_task(
|
|
315
|
+
source="/data/recordings",
|
|
316
|
+
dest_name="recordings",
|
|
317
|
+
is_directory=True,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Start monitoring
|
|
321
|
+
await manager.start()
|
|
322
|
+
|
|
323
|
+
# Export to specific device manually
|
|
324
|
+
devices = manager.list_devices()
|
|
325
|
+
if devices:
|
|
326
|
+
await manager.export(devices[0])
|
|
327
|
+
|
|
328
|
+
# Stop monitoring
|
|
329
|
+
await manager.stop()
|
|
330
|
+
```
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
__logtag__ = "audex.lib.usb.manager"
|
|
334
|
+
|
|
335
|
+
def __init__(self) -> None:
|
|
336
|
+
super().__init__()
|
|
337
|
+
self.export_tasks: list[USBExportTask] = []
|
|
338
|
+
|
|
339
|
+
# Initialize platform-specific backend
|
|
340
|
+
system = platform.system()
|
|
341
|
+
if system == "Linux":
|
|
342
|
+
self._backend: USBBackend = LinuxUSBBackend(self.logger)
|
|
343
|
+
self.logger.info("Initialized Linux USB backend")
|
|
344
|
+
elif system == "Windows":
|
|
345
|
+
self._backend = WindowsUSBBackend(self.logger)
|
|
346
|
+
self.logger.info("Initialized Windows USB backend")
|
|
347
|
+
else:
|
|
348
|
+
raise RuntimeError(f"Unsupported platform: {system}")
|
|
349
|
+
|
|
350
|
+
self._running = False
|
|
351
|
+
|
|
352
|
+
def add_export_task(
|
|
353
|
+
self,
|
|
354
|
+
source: str | pathlib.Path | os.PathLike[str],
|
|
355
|
+
dest_name: str,
|
|
356
|
+
*,
|
|
357
|
+
is_directory: bool = False,
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Add a file or directory to export when USB is connected.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
source: Source file or directory path.
|
|
363
|
+
dest_name: Destination name on USB (e.g., "logs/app.log" or "data").
|
|
364
|
+
is_directory: Whether the source is a directory.
|
|
365
|
+
|
|
366
|
+
Example:
|
|
367
|
+
```python
|
|
368
|
+
# Export single file
|
|
369
|
+
manager.add_export_task(
|
|
370
|
+
source="/var/log/app.log",
|
|
371
|
+
dest_name="logs/app.log",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Export directory
|
|
375
|
+
manager.add_export_task(
|
|
376
|
+
source="/data/sessions",
|
|
377
|
+
dest_name="sessions",
|
|
378
|
+
is_directory=True,
|
|
379
|
+
)
|
|
380
|
+
```
|
|
381
|
+
"""
|
|
382
|
+
source_path = pathlib.Path(source)
|
|
383
|
+
if not source_path.exists():
|
|
384
|
+
self.logger.warning(f"Source path does not exist: {source_path}")
|
|
385
|
+
|
|
386
|
+
task = USBExportTask(
|
|
387
|
+
source=source_path,
|
|
388
|
+
dest_name=dest_name,
|
|
389
|
+
is_directory=is_directory,
|
|
390
|
+
)
|
|
391
|
+
self.export_tasks.append(task)
|
|
392
|
+
self.logger.debug(f"Added export task: {source_path} -> {dest_name}")
|
|
393
|
+
|
|
394
|
+
def remove_export_task(self, dest_name: str) -> bool:
|
|
395
|
+
"""Remove an export task by destination name.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
dest_name: The destination name of the task to remove.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
True if task was removed, False if not found.
|
|
402
|
+
"""
|
|
403
|
+
initial_count = len(self.export_tasks)
|
|
404
|
+
self.export_tasks = [t for t in self.export_tasks if t.dest_name != dest_name]
|
|
405
|
+
removed = len(self.export_tasks) < initial_count
|
|
406
|
+
|
|
407
|
+
if removed:
|
|
408
|
+
self.logger.debug(f"Removed export task: {dest_name}")
|
|
409
|
+
|
|
410
|
+
return removed
|
|
411
|
+
|
|
412
|
+
def clear_export_tasks(self) -> None:
|
|
413
|
+
"""Clear all export tasks."""
|
|
414
|
+
self.export_tasks.clear()
|
|
415
|
+
self.logger.debug("Cleared all export tasks")
|
|
416
|
+
|
|
417
|
+
def list_devices(self) -> list[USBDevice]:
|
|
418
|
+
"""List all currently connected USB storage devices.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
List of connected USB storage devices.
|
|
422
|
+
|
|
423
|
+
Example:
|
|
424
|
+
```python
|
|
425
|
+
devices = manager.list_devices()
|
|
426
|
+
for device in devices:
|
|
427
|
+
print(
|
|
428
|
+
f"Found: {device.vendor} {device.model} at {device.mount_point}"
|
|
429
|
+
)
|
|
430
|
+
```
|
|
431
|
+
"""
|
|
432
|
+
return self._backend.list_devices()
|
|
433
|
+
|
|
434
|
+
async def export(
|
|
435
|
+
self,
|
|
436
|
+
device: USBDevice,
|
|
437
|
+
*,
|
|
438
|
+
tasks: list[USBExportTask] | None = None,
|
|
439
|
+
) -> dict[str, bool]:
|
|
440
|
+
"""Export files/directories to a USB device.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
device: Target USB device.
|
|
444
|
+
tasks: List of export tasks. If None, uses self.export_tasks.
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Dictionary mapping dest_name to success status.
|
|
448
|
+
|
|
449
|
+
Example:
|
|
450
|
+
```python
|
|
451
|
+
devices = manager.list_devices()
|
|
452
|
+
if devices:
|
|
453
|
+
results = await manager.export(devices[0])
|
|
454
|
+
for dest, success in results.items():
|
|
455
|
+
if success:
|
|
456
|
+
print(f"Exported {dest}")
|
|
457
|
+
else:
|
|
458
|
+
print(f"Failed to export {dest}")
|
|
459
|
+
```
|
|
460
|
+
"""
|
|
461
|
+
tasks_to_export = tasks or self.export_tasks
|
|
462
|
+
if not tasks_to_export:
|
|
463
|
+
self.logger.warning("No export tasks defined")
|
|
464
|
+
return {}
|
|
465
|
+
|
|
466
|
+
results: dict[str, bool] = {}
|
|
467
|
+
usb_root = pathlib.Path(device.mount_point)
|
|
468
|
+
|
|
469
|
+
self.logger.info(f"Starting export of {len(tasks_to_export)} tasks to {device.mount_point}")
|
|
470
|
+
|
|
471
|
+
for task in tasks_to_export:
|
|
472
|
+
try:
|
|
473
|
+
dest_path = usb_root / task.dest_name
|
|
474
|
+
|
|
475
|
+
# Check if source exists
|
|
476
|
+
if not task.source.exists():
|
|
477
|
+
self.logger.warning(f"Source does not exist: {task.source}")
|
|
478
|
+
results[task.dest_name] = False
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
# Create parent directories
|
|
482
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
483
|
+
|
|
484
|
+
# Copy file or directory
|
|
485
|
+
if task.is_directory:
|
|
486
|
+
if dest_path.exists():
|
|
487
|
+
shutil.rmtree(dest_path)
|
|
488
|
+
shutil.copytree(task.source, dest_path)
|
|
489
|
+
self.logger.info(f"Exported directory: {task.source} -> {dest_path}")
|
|
490
|
+
else:
|
|
491
|
+
shutil.copy2(task.source, dest_path)
|
|
492
|
+
self.logger.info(f"Exported file: {task.source} -> {dest_path}")
|
|
493
|
+
|
|
494
|
+
results[task.dest_name] = True
|
|
495
|
+
|
|
496
|
+
except Exception as e:
|
|
497
|
+
self.logger.error(f"Failed to export {task.dest_name}: {e}", exc_info=True)
|
|
498
|
+
results[task.dest_name] = False
|
|
499
|
+
|
|
500
|
+
success_count = sum(1 for success in results.values() if success)
|
|
501
|
+
self.logger.info(f"Export completed: {success_count}/{len(tasks_to_export)} successful")
|
|
502
|
+
|
|
503
|
+
return results
|
|
504
|
+
|
|
505
|
+
async def start(self) -> None:
|
|
506
|
+
"""Start monitoring for USB device connections.
|
|
507
|
+
|
|
508
|
+
Will automatically detect when USB devices are connected.
|
|
509
|
+
|
|
510
|
+
Example:
|
|
511
|
+
```python
|
|
512
|
+
manager = USBManager()
|
|
513
|
+
manager.add_export_task(
|
|
514
|
+
"/data/logs", "logs", is_directory=True
|
|
515
|
+
)
|
|
516
|
+
await manager.start()
|
|
517
|
+
# Will monitor for USB connections
|
|
518
|
+
```
|
|
519
|
+
"""
|
|
520
|
+
if self._running:
|
|
521
|
+
self.logger.warning("USB monitor already running")
|
|
522
|
+
return
|
|
523
|
+
|
|
524
|
+
self._running = True
|
|
525
|
+
await self._backend.start_monitoring(self._handle_device_event)
|
|
526
|
+
self.logger.info("Started USB device monitoring")
|
|
527
|
+
|
|
528
|
+
async def stop(self) -> None:
|
|
529
|
+
"""Stop monitoring for USB device connections."""
|
|
530
|
+
if not self._running:
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
self._running = False
|
|
534
|
+
await self._backend.stop_monitoring()
|
|
535
|
+
self.logger.info("Stopped USB device monitoring")
|
|
536
|
+
|
|
537
|
+
def _handle_device_event(self, action: str, device: t.Any) -> None:
|
|
538
|
+
"""Handle USB device events.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
action: Event action (add, remove, change).
|
|
542
|
+
device: The device that triggered the event.
|
|
543
|
+
"""
|
|
544
|
+
if action != "add":
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
# For Linux, check device type
|
|
548
|
+
if hasattr(device, "device_type"):
|
|
549
|
+
if device.device_type != "partition":
|
|
550
|
+
return
|
|
551
|
+
if "usb" not in device.device_path.lower():
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
self.logger.info("USB device connected")
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
|
|
5
|
+
from audex.exceptions import AudexError
|
|
6
|
+
from audex.helper.mixin import LoggingMixin
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class VPR(LoggingMixin, abc.ABC):
|
|
10
|
+
group_id: str | None
|
|
11
|
+
|
|
12
|
+
def __init__(self, group_id: str | None = None) -> None:
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.group_id = group_id
|
|
15
|
+
|
|
16
|
+
@abc.abstractmethod
|
|
17
|
+
async def create_group(self, name: str, gid: str | None = None) -> str: ...
|
|
18
|
+
|
|
19
|
+
@abc.abstractmethod
|
|
20
|
+
async def enroll(self, data: bytes, sr: int, uid: str | None = None) -> str: ...
|
|
21
|
+
|
|
22
|
+
@abc.abstractmethod
|
|
23
|
+
async def update(self, uid: str, data: bytes, sr: int) -> None: ...
|
|
24
|
+
|
|
25
|
+
@abc.abstractmethod
|
|
26
|
+
async def verify(self, uid: str, data: bytes, sr: int) -> float: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VPRError(AudexError):
|
|
30
|
+
default_message = "VPR error occurred"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GroupAlreadyExistsError(VPRError):
|
|
34
|
+
default_message = "VPR group already exists"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GroupNotFoundError(VPRError):
|
|
38
|
+
default_message = "VPR group not found"
|