feat: add source adapter base class and local filesystem adapter
- Abstract SourceAdapter with FileInfo dataclass - LocalAdapter for filesystem backups - Adapter factory for extensibility
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from typing import Dict, Any
|
||||
from .base import SourceAdapter
|
||||
from .local import LocalAdapter
|
||||
|
||||
ADAPTER_MAP = {
|
||||
"local": LocalAdapter,
|
||||
}
|
||||
|
||||
def get_adapter(source_type: str, config: Dict[str, Any]) -> SourceAdapter:
|
||||
adapter_class = ADAPTER_MAP.get(source_type)
|
||||
if not adapter_class:
|
||||
raise ValueError(f"Unknown source type: {source_type}")
|
||||
return adapter_class(config)
|
||||
@@ -0,0 +1,39 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import List, Dict, Any, AsyncIterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class FileInfo:
|
||||
path: str
|
||||
size: int
|
||||
modified_time: float
|
||||
is_directory: bool
|
||||
|
||||
class SourceAdapter(ABC):
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.config = config
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self) -> None:
|
||||
"""Establish connection to source."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def disconnect(self) -> None:
|
||||
"""Close connection to source."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def list_files(self, path: str = "") -> List[FileInfo]:
|
||||
"""List files at given path."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def read_file(self, path: str) -> AsyncIterator[bytes]:
|
||||
"""Read file in chunks."""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def get_database_dump(self, config: Dict[str, Any]) -> AsyncIterator[bytes]:
|
||||
"""Get database dump. Only implemented for database adapters."""
|
||||
pass
|
||||
@@ -0,0 +1,47 @@
|
||||
import os
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
from typing import List, AsyncIterator, Dict, Any
|
||||
from .base import SourceAdapter, FileInfo
|
||||
|
||||
class LocalAdapter(SourceAdapter):
|
||||
async def connect(self) -> None:
|
||||
base_path = self.config.get("path", ".")
|
||||
if not os.path.exists(base_path):
|
||||
raise FileNotFoundError(f"Path not found: {base_path}")
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
pass
|
||||
|
||||
async def list_files(self, path: str = "") -> List[FileInfo]:
|
||||
base_path = Path(self.config.get("path", "."))
|
||||
target_path = base_path / path if path else base_path
|
||||
|
||||
files = []
|
||||
exclude_patterns = self.config.get("exclude", [])
|
||||
|
||||
for item in target_path.iterdir():
|
||||
# Check exclude patterns
|
||||
if any(item.match(pattern) for pattern in exclude_patterns):
|
||||
continue
|
||||
|
||||
stat = item.stat()
|
||||
files.append(FileInfo(
|
||||
path=str(item.relative_to(base_path)),
|
||||
size=stat.st_size,
|
||||
modified_time=stat.st_mtime,
|
||||
is_directory=item.is_dir()
|
||||
))
|
||||
|
||||
return files
|
||||
|
||||
async def read_file(self, path: str) -> AsyncIterator[bytes]:
|
||||
base_path = Path(self.config.get("path", "."))
|
||||
file_path = base_path / path
|
||||
|
||||
async with aiofiles.open(file_path, "rb") as f:
|
||||
while chunk := await f.read(8192):
|
||||
yield chunk
|
||||
|
||||
async def get_database_dump(self, config: Dict[str, Any]) -> AsyncIterator[bytes]:
|
||||
raise NotImplementedError("Local adapter does not support database dumps")
|
||||
Reference in New Issue
Block a user