diff --git a/backup_box/app.py b/backup_box/app.py index 56fba0c..0928d5d 100644 --- a/backup_box/app.py +++ b/backup_box/app.py @@ -37,8 +37,13 @@ async def a_main(): print("Config:", config.get_config()) print("Hello World") from .storage.local_storage import LocalStorage + from .storage.storage import AsyncFileContextManager as FCTX from pathlib import PurePosixPath - fs = LocalStorage(r"D:\AuberyZhao\BackupBox\backup_box") + cfg = config.get_config() + stm: config.StorageItem = { "type": "LocalStorageItem", "path": "" } + for stm in cfg["storage"].values(): + break + fs = LocalStorage.from_config(stm) async for rt in fs.sync_storage(): print(rt) print(fs.dump_fs_tree()) diff --git a/backup_box/storage/local_storage.py b/backup_box/storage/local_storage.py index a94b8a3..db52ac6 100644 --- a/backup_box/storage/local_storage.py +++ b/backup_box/storage/local_storage.py @@ -1,8 +1,11 @@ from backup_box.storage.storage import AsyncFileReader, AsyncFileWriter -from pathlib import PurePath, PurePosixPath +from pathlib import PurePosixPath, PurePath from ..config import StorageItem -from .vnode import VFileSystem, VFile, VDir +from .vnode import VFileSystem, VFile, VDir, new_storage_dir, new_storage_file from typing import Literal, overload +from stat import S_ISDIR +from aiofiles import open +from aiofiles.os import remove, rmdir, stat, listdir, mkdir class LocalStorage(VFileSystem): def __init__(self, base_path: str) -> None: @@ -15,7 +18,7 @@ class LocalStorage(VFileSystem): raise ValueError("config type mismatch!") return LocalStorage(cfg["path"]) - def _vnode_path_to_syspath(self, *path: VFile | VDir): + def _vnode_path_to_syspath(self, *path: VFile | VDir) -> PurePath: filtered = [] # filter abslute path for n in path: @@ -25,33 +28,53 @@ class LocalStorage(VFileSystem): if name in [".", "..", ""]: continue filtered.append(name) - return self._base.joinpath(*filtered) + return PurePath(self._base.joinpath(*filtered)) + + async def _fill_vnode_with_real_fs(self, vparent: VDir, real_path: PurePath): + try: + for name in await listdir(real_path): + p = real_path.joinpath(name) + st = await stat(p) + if S_ISDIR(st.st_mode): + child_node = new_storage_dir(name) + await self._fill_vnode_with_real_fs(child_node, p) + else: + child_node = new_storage_file(name, int(st.st_mtime), st.st_size) + vparent["c"].append(child_node) + except FileNotFoundError: + pass async def sync_storage(self): - curr = 0 - total = 1 - parents: list[VDir] = [] self._root["c"].clear() - node = self._root - yield curr, total - while True: - if node["ty"] == "dir": - pass - if len(parents) <= 0: - break + await self._fill_vnode_with_real_fs(self._root, self._base) + yield 1, 1 async def _on_remove_node(self, node_path: list[VFile | VDir]) -> None: - raise NotImplementedError + real_path = self._vnode_path_to_syspath(*node_path) + st = await stat(real_path) + if S_ISDIR(st.st_mode): + await rmdir(real_path) + else: + await remove(real_path) async def _on_create_node(self, node_path: list[VFile | VDir]) -> None: - raise NotImplementedError + if len(node_path) > 0 and node_path[-1]["ty"] == "dir": + real_path = self._vnode_path_to_syspath(*node_path) + await mkdir(real_path) @overload - async def _on_open_node(self, node_path: list[VFile | VDir], mode: Literal['r'] = "r") -> AsyncFileReader: ... + async def _on_open_node(self, node_path: list[VFile | VDir], mode: Literal["r"] = "r") -> AsyncFileReader: ... @overload - async def _on_open_node(self, node_path: list[VFile | VDir], mode: Literal['w']) -> AsyncFileWriter: ... - async def _on_open_node(self, node_path: list[VFile | VDir], mode: Literal['r'] | Literal['w'] = "r") -> AsyncFileWriter | AsyncFileReader: - raise NotImplementedError + async def _on_open_node(self, node_path: list[VFile | VDir], mode: Literal["w"]) -> AsyncFileWriter: ... + async def _on_open_node(self, node_path: list[VFile | VDir], mode: Literal["r"] | Literal["w"] = "r") -> AsyncFileWriter | AsyncFileReader: + real_path = self._vnode_path_to_syspath(*node_path) + if mode == "r": + return await open(real_path, "rb") + elif mode == "w": + return await open(real_path, "wb") + else: + raise ValueError() + diff --git a/backup_box/storage/storage.py b/backup_box/storage/storage.py index 0ca14f4..e0f7fc0 100644 --- a/backup_box/storage/storage.py +++ b/backup_box/storage/storage.py @@ -2,24 +2,34 @@ from ..config import StorageItem from abc import ABC, abstractmethod from pathlib import PurePosixPath from collections.abc import Awaitable, AsyncIterable -from typing import overload, Literal, Protocol, SupportsBytes +from contextlib import AbstractAsyncContextManager +from typing import overload, Literal, Protocol, SupportsBytes, Self, runtime_checkable class AsyncFileReader(Protocol): - async def read(self, n: int = -1) -> bytes: + async def read(self, n: int = -1, /) -> bytes: ... async def close(self) -> None: ... class AsyncFileWriter(Protocol): - async def write(self, s: SupportsBytes) -> int: + async def write(self, s: SupportsBytes, /) -> int: ... async def close(self) -> None: ... -class AsyncFileLike(AsyncFileReader, AsyncFileWriter): - ... +class AsyncFileContextManager[T](AbstractAsyncContextManager[T]): + def __init__(self, fobj: T) -> None: + self.__fobj = fobj + + async def __aenter__(self) -> T: + return self.__fobj # type: ignore + + async def __aexit__(self, exc_type, exc, tb): + if hasattr(self.__fobj, "close"): + func = getattr(self.__fobj, "close") + await func() class Storage(ABC): @classmethod @@ -28,7 +38,7 @@ class Storage(ABC): @classmethod @abstractmethod - def from_config(cls, cfg: StorageItem) -> 'Storage': + def from_config(cls, cfg: StorageItem) -> Self: ... @abstractmethod