Skip to content

Static Files

Static file serving for Gemini capsules.

Configuration

StaticFilesConfig

Configuration options for static file serving.

StaticFilesConfig dataclass

StaticFilesConfig(
    index_files: list[str] = (
        lambda: ["index.gmi", "index.gemini"]
    )(),
    directory_listing: bool = False,
    max_file_size: int = 100 * 1024 * 1024,
    mime_types: dict[str, str] = dict(),
    follow_symlinks: bool = False,
)

Configuration for static file serving.

Attributes:

Name Type Description
index_files list[str]

Files to serve for directory requests.

directory_listing bool

Enable directory listing when no index found.

max_file_size int

Maximum file size to serve (bytes).

mime_types dict[str, str]

Custom MIME type mappings by extension.

follow_symlinks bool

Whether to follow symbolic links.

Handler

StaticFiles

Serve static files from a directory.

StaticFiles

StaticFiles(
    directory: Path | str,
    *,
    config: StaticFilesConfig | None = None,
    index_files: list[str] | None = None,
    directory_listing: bool | None = None,
    max_file_size: int | None = None,
    mime_types: dict[str, str] | None = None,
    follow_symlinks: bool | None = None,
)

Serve static files from a directory.

This handler serves files from a specified directory, with support for directory indexes, directory listings, custom MIME types, and security controls.

Example

from xitzin.staticfiles import StaticFiles

Basic usage

handler = StaticFiles("./public") app.mount("/files", handler)

With configuration

handler = StaticFiles( "./docs", directory_listing=True, max_file_size=50 * 1024 * 1024, )

@handler.not_found def custom_404(request, path_info): return "# File Not Found"

app.mount("/docs", handler)

Create a static file handler.

Parameters:

Name Type Description Default
directory Path | str

Directory to serve files from.

required
config StaticFilesConfig | None

Configuration object (overridden by other params).

None
index_files list[str] | None

Files to serve for directory requests.

None
directory_listing bool | None

Enable directory listing when no index found.

None
max_file_size int | None

Maximum file size to serve (bytes).

None
mime_types dict[str, str] | None

Custom MIME type mappings by extension.

None
follow_symlinks bool | None

Whether to follow symbolic links.

None

Raises:

Type Description
ValueError

If directory doesn't exist or isn't a directory.

Source code in src/xitzin/staticfiles.py
def __init__(
    self,
    directory: Path | str,
    *,
    config: StaticFilesConfig | None = None,
    index_files: list[str] | None = None,
    directory_listing: bool | None = None,
    max_file_size: int | None = None,
    mime_types: dict[str, str] | None = None,
    follow_symlinks: bool | None = None,
) -> None:
    """Create a static file handler.

    Args:
        directory: Directory to serve files from.
        config: Configuration object (overridden by other params).
        index_files: Files to serve for directory requests.
        directory_listing: Enable directory listing when no index found.
        max_file_size: Maximum file size to serve (bytes).
        mime_types: Custom MIME type mappings by extension.
        follow_symlinks: Whether to follow symbolic links.

    Raises:
        ValueError: If directory doesn't exist or isn't a directory.
    """
    self.directory = Path(directory).resolve()
    self._not_found_handler: Callable[[Any, str], Any] | None = None

    if not self.directory.exists():
        raise ValueError(f"Directory not found: {directory}")
    if not self.directory.is_dir():
        raise ValueError(f"Path is not a directory: {directory}")

    # Start with config defaults, override with explicit parameters
    base_config = config or StaticFilesConfig()

    self.index_files = (
        index_files if index_files is not None else base_config.index_files
    )
    self.directory_listing = (
        directory_listing
        if directory_listing is not None
        else base_config.directory_listing
    )
    self.max_file_size = (
        max_file_size if max_file_size is not None else base_config.max_file_size
    )
    self.follow_symlinks = (
        follow_symlinks
        if follow_symlinks is not None
        else base_config.follow_symlinks
    )

    # Build MIME type mapping: defaults + config + explicit
    self._mime_types = {**DEFAULT_MIME_TYPES}
    self._mime_types.update(base_config.mime_types)
    if mime_types:
        self._mime_types.update(mime_types)

__call__ async

__call__(
    request: Request, path_info: str
) -> GeminiResponse

Handle a request for a static file.

Parameters:

Name Type Description Default
request Request

The Gemini request.

required
path_info str

Path after the mount prefix (e.g., "/docs/page.gmi").

required

Returns:

Type Description
GeminiResponse

GeminiResponse with the file content or error.

Raises:

Type Description
NotFound

If file doesn't exist (and no custom handler).

BadRequest

If path validation fails.

Source code in src/xitzin/staticfiles.py
async def __call__(self, request: Request, path_info: str) -> GeminiResponse:
    """Handle a request for a static file.

    Args:
        request: The Gemini request.
        path_info: Path after the mount prefix (e.g., "/docs/page.gmi").

    Returns:
        GeminiResponse with the file content or error.

    Raises:
        NotFound: If file doesn't exist (and no custom handler).
        BadRequest: If path validation fails.
    """
    try:
        # Normalize path_info
        path_info = path_info.lstrip("/") if path_info else ""

        # Resolve and validate path
        file_path = self._resolve_path(path_info)

        # Handle directory
        if file_path.is_dir():
            return await self._serve_directory(request, file_path, path_info)

        # Handle file
        return await self._serve_file(file_path)

    except NotFound:
        if self._not_found_handler is not None:
            result = self._not_found_handler(request, path_info)
            if asyncio.iscoroutine(result):
                result = await result
            return self._convert_handler_result(result)
        raise

not_found

not_found(
    handler: Callable[[Any, str], Any],
) -> Callable[[Any, str], Any]

Register a custom not-found handler.

The handler receives (request, path_info) and should return a response.

Example

@handler.not_found def custom_404(request, path_info): return f"# Not Found\n\nFile {path_info} doesn't exist."

Source code in src/xitzin/staticfiles.py
def not_found(
    self, handler: Callable[[Any, str], Any]
) -> Callable[[Any, str], Any]:
    """Register a custom not-found handler.

    The handler receives (request, path_info) and should return a response.

    Example:
        @handler.not_found
        def custom_404(request, path_info):
            return f"# Not Found\\n\\nFile {path_info} doesn't exist."
    """
    self._not_found_handler = handler
    return handler