6b7e6b44f1
Python Build and Type Check / python-ci (ubuntu-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.11) (push) Has been cancelled
Python Build and Type Check / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Integration Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Notebook Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Smoke Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (ubuntu-latest, 3.13) (push) Has been cancelled
Python Unit Tests / python-ci (windows-latest, 3.13) (push) Has been cancelled
gh-pages / build (push) Has been cancelled
Python Publish (pypi) / Upload release to PyPI (push) Has been cancelled
Spellcheck / spellcheck (push) Has been cancelled
155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
# Copyright (c) 2024 Microsoft Corporation.
|
|
# Licensed under the MIT License
|
|
|
|
"""File-based Storage implementation of Storage."""
|
|
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
from collections.abc import Iterator
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, cast
|
|
|
|
import aiofiles
|
|
from aiofiles.os import remove
|
|
from aiofiles.ospath import exists
|
|
|
|
from graphrag_storage.storage import (
|
|
Storage,
|
|
get_timestamp_formatted_with_local_tz,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FileStorage(Storage):
|
|
"""File storage class definition."""
|
|
|
|
_base_dir: Path
|
|
_encoding: str
|
|
|
|
def __init__(self, base_dir: str, encoding: str = "utf-8", **kwargs: Any) -> None:
|
|
"""Create a file based storage."""
|
|
self._base_dir = Path(base_dir).resolve()
|
|
self._encoding = encoding
|
|
logger.info("Creating file storage at [%s]", self._base_dir)
|
|
self._base_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def find(
|
|
self,
|
|
file_pattern: re.Pattern[str],
|
|
) -> Iterator[str]:
|
|
"""Find files in the storage using a file pattern."""
|
|
logger.info(
|
|
"Search [%s] for files matching [%s]", self._base_dir, file_pattern.pattern
|
|
)
|
|
all_files = list(self._base_dir.rglob("**/*"))
|
|
logger.debug("All files and folders: %s", [file.name for file in all_files])
|
|
num_loaded = 0
|
|
num_total = len(all_files)
|
|
num_filtered = 0
|
|
for file in all_files:
|
|
match = file_pattern.search(f"{file}")
|
|
if match:
|
|
filename = f"{file}".replace(str(self._base_dir), "", 1)
|
|
if filename.startswith(os.sep):
|
|
filename = filename[1:]
|
|
yield filename
|
|
num_loaded += 1
|
|
else:
|
|
num_filtered += 1
|
|
logger.debug(
|
|
"Files loaded: %d, filtered: %d, total: %d",
|
|
num_loaded,
|
|
num_filtered,
|
|
num_total,
|
|
)
|
|
|
|
async def get(
|
|
self, key: str, as_bytes: bool | None = False, encoding: str | None = None
|
|
) -> Any:
|
|
"""Get method definition."""
|
|
file_path = _join_path(self._base_dir, key)
|
|
|
|
if await self.has(key):
|
|
return await self._read_file(file_path, as_bytes, encoding)
|
|
|
|
return None
|
|
|
|
async def _read_file(
|
|
self,
|
|
path: str | Path,
|
|
as_bytes: bool | None = False,
|
|
encoding: str | None = None,
|
|
) -> Any:
|
|
"""Read the contents of a file."""
|
|
read_type = "rb" if as_bytes else "r"
|
|
encoding = None if as_bytes else (encoding or self._encoding)
|
|
|
|
async with aiofiles.open(
|
|
path,
|
|
cast("Any", read_type),
|
|
encoding=encoding,
|
|
) as f:
|
|
return await f.read()
|
|
|
|
async def set(self, key: str, value: Any, encoding: str | None = None) -> None:
|
|
"""Set method definition."""
|
|
is_bytes = isinstance(value, bytes)
|
|
write_type = "wb" if is_bytes else "w"
|
|
encoding = None if is_bytes else encoding or self._encoding
|
|
async with aiofiles.open(
|
|
_join_path(self._base_dir, key),
|
|
cast("Any", write_type),
|
|
encoding=encoding,
|
|
) as f:
|
|
await f.write(value)
|
|
|
|
async def has(self, key: str) -> bool:
|
|
"""Has method definition."""
|
|
return await exists(_join_path(self._base_dir, key))
|
|
|
|
async def delete(self, key: str) -> None:
|
|
"""Delete method definition."""
|
|
if await self.has(key):
|
|
await remove(_join_path(self._base_dir, key))
|
|
|
|
async def clear(self) -> None:
|
|
"""Clear method definition."""
|
|
for file in self._base_dir.glob("*"):
|
|
if file.is_dir():
|
|
shutil.rmtree(file)
|
|
else:
|
|
file.unlink()
|
|
|
|
def child(self, name: str | None) -> "Storage":
|
|
"""Create a child storage instance."""
|
|
if name is None:
|
|
return self
|
|
child_path = str(self._base_dir / name)
|
|
return FileStorage(base_dir=child_path, encoding=self._encoding)
|
|
|
|
def keys(self) -> list[str]:
|
|
"""Return the keys in the storage."""
|
|
return [item.name for item in self._base_dir.iterdir() if item.is_file()]
|
|
|
|
async def get_creation_date(self, key: str) -> str:
|
|
"""Get the creation date of a file."""
|
|
file_path = _join_path(self._base_dir, key)
|
|
|
|
creation_timestamp = file_path.stat().st_ctime
|
|
creation_time_utc = datetime.fromtimestamp(creation_timestamp, tz=timezone.utc)
|
|
|
|
return get_timestamp_formatted_with_local_tz(creation_time_utc)
|
|
|
|
def get_path(self, key: str) -> Path:
|
|
"""Get the full file path for a key (for streaming access)."""
|
|
return _join_path(self._base_dir, key)
|
|
|
|
|
|
def _join_path(file_path: Path, file_name: str) -> Path:
|
|
"""Join a path and a file. Independent of the OS."""
|
|
return (file_path / Path(file_name).parent / Path(file_name).name).resolve()
|