async msd image

This commit is contained in:
Maxim Devaev 2023-03-16 22:24:23 +02:00
parent 27f38ef086
commit 74a19e40da
3 changed files with 70 additions and 55 deletions

View File

@ -43,7 +43,7 @@ depends=(
"python<3.11" "python<3.11"
python-yaml python-yaml
"python-aiohttp>=3.7.4.post0-1.1" "python-aiohttp>=3.7.4.post0-1.1"
python-aiofiles "python-aiofiles>=23.1.0-1"
python-passlib python-passlib
python-pyotp python-pyotp
python-qrcode python-qrcode

View File

@ -399,7 +399,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
async def __STORAGE_create_new_image(self, name: str) -> Image: # pylint: disable=invalid-name async def __STORAGE_create_new_image(self, name: str) -> Image: # pylint: disable=invalid-name
assert self.__state.storage assert self.__state.storage
image = self.__storage.get_image_by_name(name) image = await self.__storage.get_image_by_name(name)
if image.name in self.__state.storage.images or (await image.exists()): if image.name in self.__state.storage.images or (await image.exists()):
raise MsdImageExistsError() raise MsdImageExistsError()
return image return image
@ -461,7 +461,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
logger = get_logger(0) logger = get_logger(0)
async with self.__state._lock: # pylint: disable=protected-access async with self.__state._lock: # pylint: disable=protected-access
try: try:
drive_state = self.__get_drive_state() drive_state = await self.__get_drive_state()
if self.__state.vd is None and drive_state.image is None: if self.__state.vd is None and drive_state.image is None:
# Если только что включились и образ не подключен - попробовать # Если только что включились и образ не подключен - попробовать
@ -500,7 +500,7 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
async def __setup_initial(self) -> None: async def __setup_initial(self) -> None:
if self.__initial_image: if self.__initial_image:
logger = get_logger(0) logger = get_logger(0)
image = self.__storage.get_image_by_name(self.__initial_image) image = await self.__storage.get_image_by_name(self.__initial_image)
if (await image.exists()): if (await image.exists()):
logger.info("Setting up initial image %r ...", self.__initial_image) logger.info("Setting up initial image %r ...", self.__initial_image)
try: try:
@ -524,10 +524,10 @@ class Plugin(BaseMsd): # pylint: disable=too-many-instance-attributes
images=images, images=images,
) )
def __get_drive_state(self) -> _DriveState: async def __get_drive_state(self) -> _DriveState:
path = self.__drive.get_image_path() path = self.__drive.get_image_path()
return _DriveState( return _DriveState(
image=(self.__storage.get_image_by_path(path) if path else None), image=((await self.__storage.get_image_by_path(path)) if path else None),
cdrom=self.__drive.get_cdrom_flag(), cdrom=self.__drive.get_cdrom_flag(),
rw=self.__drive.get_rw_flag(), rw=self.__drive.get_rw_flag(),
) )

View File

@ -21,6 +21,7 @@
import os import os
import asyncio
import operator import operator
import dataclasses import dataclasses
@ -43,7 +44,7 @@ from .. import MsdError
class _Image: class _Image:
name: str name: str
path: str path: str
in_storage: bool = dataclasses.field(init=False) in_storage: bool = dataclasses.field(init=False, compare=False)
complete: bool = dataclasses.field(init=False, compare=False) complete: bool = dataclasses.field(init=False, compare=False)
removable: bool = dataclasses.field(init=False, compare=False) removable: bool = dataclasses.field(init=False, compare=False)
size: int = dataclasses.field(init=False, compare=False) size: int = dataclasses.field(init=False, compare=False)
@ -56,39 +57,55 @@ class Image(_Image):
self.__storage = storage self.__storage = storage
(self.__dir_path, file_name) = os.path.split(path) (self.__dir_path, file_name) = os.path.split(path)
self.__complete_path = os.path.join(self.__dir_path, f".__{file_name}.complete") self.__complete_path = os.path.join(self.__dir_path, f".__{file_name}.complete")
self.__adopted = (storage._is_adopted(self) if storage else True) self.__adopted = False
@property async def _update(self) -> None:
def in_storage(self) -> bool: # adopted используется в последующих проверках
return bool(self.__storage) self.__adopted = await aiotools.run_async(self.__is_adopted)
(complete, removable, (size, mod_ts)) = await asyncio.gather(
self.__is_complete(),
self.__is_removable(),
self.__get_stat(),
)
object.__setattr__(self, "complete", complete)
object.__setattr__(self, "removable", removable)
object.__setattr__(self, "size", size)
object.__setattr__(self, "mod_ts", mod_ts)
@property def __is_adopted(self) -> bool:
def complete(self) -> bool: # True, если образ находится вне хранилища
# или в другой точке монтирования под ним
if self.__storage is None:
return True
path = self.path
while not os.path.ismount(path):
path = os.path.dirname(path)
return (self.__storage.get_root_path() != path)
async def __is_complete(self) -> bool:
if self.__storage: if self.__storage:
return os.path.exists(self.__complete_path) return (await aiofiles.os.path.exists(self.__complete_path))
return True return True
@property async def __is_removable(self) -> bool:
def removable(self) -> bool:
if not self.__storage: if not self.__storage:
return False return False
if not self.__adopted: if not self.__adopted:
return True return True
return os.access(self.__dir_path, os.W_OK) return (await aiofiles.os.access(self.__dir_path, os.W_OK)) # type: ignore
async def __get_stat(self) -> tuple[int, float]:
try:
st = (await aiofiles.os.stat(self.path))
return (st.st_size, st.st_mtime)
except Exception:
return (0, 0.0)
# =====
@property @property
def size(self) -> int: def in_storage(self) -> bool:
try: return bool(self.__storage)
return os.stat(self.path).st_size
except Exception:
return 0
@property
def mod_ts(self) -> float:
try:
return os.stat(self.path).st_mtime
except Exception:
return 0.0
async def exists(self) -> bool: async def exists(self) -> bool:
return (await aiofiles.os.path.exists(self.path)) return (await aiofiles.os.path.exists(self.path))
@ -119,6 +136,7 @@ class Image(_Image):
await aiofiles.os.remove(self.__complete_path) await aiofiles.os.remove(self.__complete_path)
except FileNotFoundError: except FileNotFoundError:
pass pass
await self._update()
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
@ -132,22 +150,27 @@ class Storage:
self.__path = path self.__path = path
self.__remount_cmd = remount_cmd self.__remount_cmd = remount_cmd
def get_root_path(self) -> str:
return self.__path
async def get_watchable_paths(self) -> list[str]: async def get_watchable_paths(self) -> list[str]:
return (await aiotools.run_async(self.__get_watchable_paths)) return (await aiotools.run_async(self.__inner_get_watchable_paths))
async def get_images(self) -> dict[str, Image]: async def get_images(self) -> dict[str, Image]:
return (await aiotools.run_async(self.__get_images)) return {
name: (await self.get_image_by_name(name))
for name in (await aiotools.run_async(self.__inner_get_images))
}
def __get_watchable_paths(self) -> list[str]: def __inner_get_watchable_paths(self) -> list[str]:
return list(map(operator.itemgetter(0), self.__walk(with_files=False))) return list(map(operator.itemgetter(0), self.__walk(with_files=False)))
def __get_images(self) -> dict[str, Image]: def __inner_get_images(self) -> list[str]:
images: dict[str, Image] = {} return [
for (_, files) in self.__walk(with_files=True): os.path.relpath(path, self.__path) # == name
for path in files: for (_, files) in self.__walk(with_files=True)
name = os.path.relpath(path, self.__path) for path in files
images[name] = self.get_image_by_name(name) ]
return images
def __walk(self, with_files: bool, root_path: (str | None)=None) -> Generator[tuple[str, list[str]], None, None]: def __walk(self, with_files: bool, root_path: (str | None)=None) -> Generator[tuple[str, list[str]], None, None]:
if root_path is None: if root_path is None:
@ -169,24 +192,26 @@ class Storage:
# ===== # =====
def get_image_by_name(self, name: str) -> Image: async def get_image_by_name(self, name: str) -> Image:
assert name assert name
path = os.path.join(self.__path, name) path = os.path.join(self.__path, name)
return self.__get_image(name, path, True) return (await self.__get_image(name, path, True))
def get_image_by_path(self, path: str) -> Image: async def get_image_by_path(self, path: str) -> Image:
assert path assert path
in_storage = (os.path.commonpath([self.__path, path]) == self.__path) in_storage = (os.path.commonpath([self.__path, path]) == self.__path)
if in_storage: if in_storage:
name = os.path.relpath(path, self.__path) name = os.path.relpath(path, self.__path)
else: else:
name = os.path.basename(path) name = os.path.basename(path)
return self.__get_image(name, path, in_storage) return (await self.__get_image(name, path, in_storage))
def __get_image(self, name: str, path: str, in_storage: bool) -> Image: async def __get_image(self, name: str, path: str, in_storage: bool) -> Image:
assert name assert name
assert path assert path
return Image(name, path, (self if in_storage else None)) image = Image(name, path, (self if in_storage else None))
await image._update() # pylint: disable=protected-access
return image
# ===== # =====
@ -203,16 +228,6 @@ class Storage:
free=(st.f_bavail * st.f_frsize), free=(st.f_bavail * st.f_frsize),
) )
def _is_adopted(self, image: Image) -> bool:
# True, если образ находится вне хранилища
# или в другой точке монтирования под ним
if not image.in_storage:
return True
path = image.path
while not os.path.ismount(path):
path = os.path.dirname(path)
return (self.__path != path)
async def remount_rw(self, rw: bool, fatal: bool=True) -> None: async def remount_rw(self, rw: bool, fatal: bool=True) -> None:
if not (await aiohelpers.remount("MSD", self.__remount_cmd, rw)): if not (await aiohelpers.remount("MSD", self.__remount_cmd, rw)):
if fatal: if fatal: