221 lines
6 KiB
Python
221 lines
6 KiB
Python
"""
|
|
AsyncFileVault 1.1.0 - Async wrapper for FileVault
|
|
|
|
Provides async versions of FileVault I/O operations using asyncio.to_thread().
|
|
CPU-bound operations (hash generation, path creation) remain synchronous.
|
|
|
|
Usage:
|
|
vault = AsyncVault(vaultpath="vault", depth=9, use_pairs=True)
|
|
|
|
# Sync operations (fast, no I/O)
|
|
filename = vault.create_filename("seed", ".json", absolute=True)
|
|
|
|
# Async operations (file I/O)
|
|
await vault.atomic_write_json(filename, {"key": "value"})
|
|
data = await vault.safe_read_json(filename)
|
|
"""
|
|
|
|
import asyncio
|
|
from functools import partial
|
|
from typing import Any, Optional, List
|
|
|
|
from .filevault import Vault, ensure_bytes, create_vault
|
|
|
|
__version__ = "1.1.0"
|
|
__author__ = "Russell Ballestrini"
|
|
__license__ = "Public Domain"
|
|
|
|
|
|
class AsyncVault(Vault):
|
|
"""
|
|
Async wrapper for Vault with non-blocking file I/O.
|
|
|
|
Inherits all sync methods from Vault but provides async versions
|
|
of file operations that use asyncio.to_thread() to avoid blocking.
|
|
|
|
Sync methods (instant, no await needed):
|
|
- create_filename()
|
|
- create_random_filename()
|
|
- _generate_filename()
|
|
- mark_file_exists()
|
|
- clear_existence_cache()
|
|
- get_cache_stats()
|
|
- purge_cache_for_path_pattern()
|
|
|
|
Async methods (await required):
|
|
- atomic_write_json()
|
|
- safe_read_json()
|
|
- write_text_file()
|
|
- read_text_file()
|
|
- file_exists()
|
|
- remove_file()
|
|
- remove_file_if_exists()
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
vaultpath: str = "vault",
|
|
depth: int = 3,
|
|
salt: str = "changeme",
|
|
enable_memory_cache: bool = False,
|
|
use_pairs: bool = False
|
|
):
|
|
"""
|
|
Initialize AsyncVault with same parameters as Vault.
|
|
|
|
Args:
|
|
vaultpath: Base path for vault storage
|
|
depth: Directory tree depth
|
|
salt: Salt for hash generation
|
|
enable_memory_cache: Enable in-memory file existence cache
|
|
use_pairs: Use hex pairs for directories (256 dirs/level vs 16)
|
|
"""
|
|
super().__init__(
|
|
vaultpath=vaultpath,
|
|
depth=depth,
|
|
salt=salt,
|
|
enable_memory_cache=enable_memory_cache,
|
|
use_pairs=use_pairs
|
|
)
|
|
|
|
async def atomic_write_json(
|
|
self,
|
|
file_path: str,
|
|
data: Any,
|
|
indent: int = 2,
|
|
aliases: Optional[List[str]] = None
|
|
) -> None:
|
|
"""
|
|
Async version of atomic JSON write.
|
|
|
|
Args:
|
|
file_path: Target file path
|
|
data: Data to write as JSON
|
|
indent: JSON indentation level
|
|
aliases: Optional list of alias keys for symlinks
|
|
"""
|
|
await asyncio.to_thread(
|
|
super().atomic_write_json,
|
|
file_path,
|
|
data,
|
|
indent,
|
|
aliases
|
|
)
|
|
|
|
async def safe_read_json(
|
|
self,
|
|
file_path: str,
|
|
default: Any = None
|
|
) -> Any:
|
|
"""
|
|
Async version of safe JSON read.
|
|
|
|
Args:
|
|
file_path: Path to JSON file
|
|
default: Default value if file doesn't exist or is invalid
|
|
|
|
Returns:
|
|
Parsed JSON data or default value
|
|
"""
|
|
return await asyncio.to_thread(
|
|
super().safe_read_json,
|
|
file_path,
|
|
default
|
|
)
|
|
|
|
async def write_text_file(
|
|
self,
|
|
file_path: str,
|
|
content: str,
|
|
mode: str = "w"
|
|
) -> None:
|
|
"""
|
|
Async version of text file write.
|
|
|
|
Args:
|
|
file_path: Target file path
|
|
content: Text content to write
|
|
mode: File open mode ('w', 'a', etc.)
|
|
"""
|
|
await asyncio.to_thread(
|
|
super().write_text_file,
|
|
file_path,
|
|
content,
|
|
mode
|
|
)
|
|
|
|
async def read_text_file(
|
|
self,
|
|
file_path: str,
|
|
default: Optional[str] = None
|
|
) -> str:
|
|
"""
|
|
Async version of text file read.
|
|
|
|
Args:
|
|
file_path: Path to text file
|
|
default: Default value if file doesn't exist
|
|
|
|
Returns:
|
|
File content or default value
|
|
"""
|
|
return await asyncio.to_thread(
|
|
super().read_text_file,
|
|
file_path,
|
|
default
|
|
)
|
|
|
|
async def file_exists(self, file_path: str) -> bool:
|
|
"""
|
|
Async version of file existence check.
|
|
|
|
Note: If memory cache is enabled and the path is cached,
|
|
this returns immediately from cache without blocking.
|
|
|
|
Args:
|
|
file_path: Path to check
|
|
|
|
Returns:
|
|
True if file exists, False otherwise
|
|
"""
|
|
# Check cache first (no I/O needed)
|
|
if self.enable_memory_cache and self._existence_cache is not None:
|
|
if file_path in self._existence_cache:
|
|
return self._existence_cache[file_path]
|
|
|
|
# Cache miss - need to hit filesystem
|
|
return await asyncio.to_thread(super().file_exists, file_path)
|
|
|
|
async def remove_file(self, file_path: str) -> bool:
|
|
"""
|
|
Async version of file removal.
|
|
|
|
Args:
|
|
file_path: Path to file to remove
|
|
|
|
Returns:
|
|
True if file was removed, False if it didn't exist
|
|
"""
|
|
# Call parent's sync remove_file directly via Vault class
|
|
return await asyncio.to_thread(Vault.remove_file, self, file_path)
|
|
|
|
async def remove_file_if_exists(self, file_path: str) -> bool:
|
|
"""
|
|
Async version of remove_file_if_exists.
|
|
|
|
Args:
|
|
file_path: Path to file to remove
|
|
|
|
Returns:
|
|
True if file was removed, False if it didn't exist
|
|
"""
|
|
try:
|
|
return await asyncio.to_thread(Vault.remove_file, self, file_path)
|
|
except OSError:
|
|
self.mark_file_exists(file_path, False)
|
|
return False
|
|
|
|
|
|
def create_async_vault(*args, **kwargs) -> AsyncVault:
|
|
"""Factory function for creating AsyncVault instances."""
|
|
return AsyncVault(*args, **kwargs)
|