"""Local file system storage backend.
A simple, zero-dependency implementation for local file operations.
No external dependencies like fsspec or obstore required.
"""
import shutil
from collections.abc import AsyncIterator, Iterator
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast, overload
from urllib.parse import unquote, urlparse
from mypy_extensions import mypyc_attr
from sqlspec.exceptions import FileNotFoundInStorageError
from sqlspec.storage._arrow_stream import iter_parquet_row_groups, validate_parquet_stream_options
from sqlspec.storage._paths import strip_windows_drive_prefix
from sqlspec.storage._utils import import_pyarrow_parquet
from sqlspec.storage.backends.base import AsyncArrowBatchIterator, AsyncThreadedBytesIterator
from sqlspec.storage.errors import execute_sync_storage_operation
from sqlspec.utils.sync_tools import async_
if TYPE_CHECKING:
from sqlspec.typing import ArrowRecordBatch, ArrowTable
__all__ = ("LocalStore",)
@mypyc_attr(allow_interpreted_subclasses=True)
class LocalStore:
"""Simple local file system storage backend.
Provides file system operations without requiring fsspec or obstore.
Supports file:// URIs and regular file paths.
All synchronous methods use the *_sync suffix for consistency with async methods.
"""
__slots__ = ("base_path", "protocol")
backend_type: ClassVar[str] = "local"
def __init__(self, uri: str = "", **kwargs: Any) -> None:
"""Initialize local storage backend.
Args:
uri: File URI or path
**kwargs: Additional options including:
- base_path: Subdirectory relative to URI path. If relative, it's combined
with the URI path. If absolute, it takes precedence (backward compatible).
The URI may be a file:// path (Windows style like file:///C:/path is supported).
When both URI and base_path are provided, they are combined:
- file:///home/user/storage + base_path="subdir" -> /home/user/storage/subdir
- file:///home/user/storage + base_path="/other" -> /other (absolute takes precedence)
"""
if uri.startswith("file://"):
parsed = urlparse(uri)
path = strip_windows_drive_prefix(unquote(parsed.path))
self.base_path = Path(path).resolve()
elif uri:
self.base_path = Path(uri).resolve()
else:
self.base_path = Path.cwd()
if "base_path" in kwargs:
# Combine URI path with base_path (Path division handles absolute paths correctly)
# If base_path is absolute, it takes precedence (backward compatible)
self.base_path = (self.base_path / kwargs["base_path"]).resolve()
if not self.base_path.exists():
self.base_path.mkdir(parents=True, exist_ok=True)
elif self.base_path.is_file():
self.base_path = self.base_path.parent
self.protocol = "file"