image_server/AGENTS.md
2026-05-05 11:54:50 +00:00

8.5 KiB

AGENTS.md — Image Server Project

Project Overview

A FastAPI-based image server that serves files from directories or ZIP archives using hashed paths for secure, randomized access. Supports password-protected ZIPs via HTTP Basic Auth. Provides a full-screen image viewer with navigation controls and auto-advance.

Language & Runtime

  • Language: Python 3.13
  • Framework: FastAPI 0.128.0+
  • Server: Uvicorn 0.40.0+
  • Package manager: UV

Build, Lint, and Test Commands

Running the Server

# Run with a directory
uv run main.py /path/to/files

# Run with a ZIP archive
uv run main.py /path/to/archive.zip

# Run with a glob pattern
uv run main.py "path/to/zips/*.zip"

Testing

# Run all tests
uv run pytest

# Run a single test
uv run pytest tests/test_file_indexer.py::TestClass::test_method

# Run tests matching a pattern
uv run pytest -k "test_name_pattern"

Linting

# Install ruff (fast linter)
uv add ruff

# Run linting
ruff check .

# Auto-fix issues
ruff check --fix .

Formatting

Always format code with black after implementing every feature:

# Install black
uv add black

# Format all code
uv run black .

Type Checking

# Install mypy
uv add mypy

# Run type checking
uv run mypy main.py

Project Structure

image_server/
├── main.py              # Main application — all server logic, routing, indexing
├── frontend.html        # Single-page HTML template for the image viewer UI
├── pyproject.toml       # Project configuration (dependencies, metadata)
├── uv.lock              # UV lockfile for reproducible builds
├── README.md            # User-facing documentation
├── AGENTS.md            # This file — agent/dev documentation
└── tests/               # Test suite
    ├── conftest.py      # Pytest fixtures and configuration
    ├── test_file_indexer.py    # Tests for FileIndexer class
    ├── test_zip_indexer.py     # Tests for ZipFileIndexer class
    ├── test_encrypted_zip.py   # Tests for password-protected ZIP handling
    ├── test_endpoints.py       # Tests for API endpoints
    ├── test_navigation.py      # Tests for navigation logic
    └── testfile.zip            # Test fixture ZIP file

Code Architecture

Entry Point (main.py)

All code lives in main.py. The app is initialized via initialize_server() which is called from __main__.

Global State

file_mapping = {}   # dict[str, str] — hash -> filepath/filename (merged across all indexers)
indexers = []       # list[FileIndexer] — all active indexers

Indexer Classes

FileIndexer (base class)

Indexes files from a directory tree.

  • __init__(path: str, salt: str | None) — Initializes with a path and optional salt
  • salt (property) — Lazily generates a random salt if not provided
  • _hash_path(filepath: str) -> str — SHA-256 hash of filepath + salt
  • _index() -> dict[str, str] — Walks directory tree, builds hash -> filepath mapping
  • get_file_by_hash(file_hash: str) — Yields file content bytes (generator)
  • get_filename_by_hash(file_hash: str) -> str | None — Returns filepath by hash

ZipFileIndexer(FileIndexer)

Indexes files from a ZIP archive. Extends FileIndexer.

  • _open_zip() — Opens the ZIP file for reading
  • _index() -> dict[str, str] — Indexes files within the ZIP (hash -> internal filename)
  • _needs_password(filename: str) -> bool — Lazy detection of password protection (cached)
  • is_file_encrypted(file_hash: str) -> bool — Check if a file requires a password
  • get_file_by_hash(file_hash: str, password: bytes | None) — Yields file content, handles encryption
  • get_filename_by_hash(file_hash: str) -> str | None — Returns internal ZIP filename by hash

INDEXER_MAP

INDEXER_MAP = {".zip": ZipFileIndexer}

Maps file extensions to indexer classes. Extend this dict to support new archive formats.

Helper Functions

  • initialize_server(args: argparse.Namespace) — Bootstraps the server: creates indexers, populates file_mapping
  • _find_indexer_for_hash(file_hash: str) — Finds which indexer owns a given hash
  • _get_zip_password(file_hash: str, request: Request) -> bytes | None — Extracts password from HTTP Basic Auth header
  • _raise_unauthorized() — Raises HTTP 401 with WWW-Authenticate: Basic header
  • _get_random_hash() -> str — Returns a random hash from the indexed files
  • _build_url(file_hash, order, delay) -> str — Builds a URL with optional query params
  • _get_navigation_data(file_hash, order) — Returns dict with file_hash, next_hash, prev_hash, filename
  • _render_page(navigation_data, ...) — Renders frontend.html template with substituted values

Routes

Route Handler Description
GET / root() Redirect to random file
GET /{file_hash} hash_page() Serve image page with navigation
GET /api/{file_hash}/data get_file_data() Serve raw file data
GET /api/health health_check() Health check (status + file count)

Frontend Template (frontend.html)

Single HTML file using string.Template substitution. Placeholders:

  • $img_url — URL to fetch the image data
  • $image_click_url — URL navigated to on image click
  • $next_url / $prev_url — Navigation URLs
  • $filename — Display filename
  • $extra_meta — Extra <meta> tags (e.g., auto-refresh)
  • $play_button — Play/pause button HTML

Code Style Guidelines

Imports

  • Standard library imports first, then third-party, then local
  • Group imports by type: stdlib, third-party, local
  • Use absolute imports for local modules

Formatting Style

  • Use 4 spaces for indentation (no tabs)
  • Maximum line length: 88 characters (black default)
  • Use blank lines to separate logical sections (2 blank lines between top-level definitions)
  • One blank line between method definitions within a class
  • Always run black . after completing every feature

Types

  • Use type hints for all function arguments and return values
  • Use Python 3.13+ typing features where appropriate

Naming Conventions

  • Classes: PascalCase (e.g., FileIndexer, ZipFileIndexer)
  • Functions/variables: snake_case (e.g., get_file_by_hash, file_mapping)
  • Constants: UPPER_SNAKE_CASE (e.g., INDEXER_MAP)
  • Private methods/attributes: Prefix with underscore (e.g., _index, _salt)
  • Use descriptive, full words for names (avoid abbreviations except well-known ones)

Error Handling

  • Use exceptions for exceptional cases, not flow control
  • Raise HTTPException for HTTP-level errors in FastAPI endpoints
  • Provide meaningful error messages

Async/Await

  • Use async def for FastAPI endpoint handlers
  • Use regular functions for synchronous operations (file I/O, hashing)
  • Do not mix sync and async improperly

File Operations

  • Use pathlib.Path for path manipulations
  • Use context managers (with statements) for file operations
  • Handle both directory and archive (ZIP) file sources

Documentation

  • Use docstrings for public classes and functions
  • Follow Google-style docstring format

Best Practices

  • Keep functions small and focused (single responsibility)
  • Use properties for computed attributes with caching
  • Avoid global state where possible; use dependency injection
  • Use dataclasses or attrs for simple data containers
  • Follow PEP 8 style guidelines

Common Tasks

Adding a New Endpoint

  1. Add the route handler in main.py
  2. Use appropriate HTTP method decorator (@app.get, @app.post, etc.)
  3. Add type hints and docstrings
  4. Handle errors with HTTPException

Adding a New File Indexer

  1. Create a new class inheriting from FileIndexer
  2. Override the _index method to populate self._file_mapping
  3. Override get_file_by_hash and get_filename_by_hash as needed
  4. Register in INDEXER_MAP dictionary with the file extension as key

Modifying the Frontend

  1. Edit frontend.html for HTML/CSS/JS changes
  2. Add new string.Template placeholders as needed
  3. Update _render_page() to pass new values
  4. Ensure navigation URLs are built correctly with _build_url()

Running the Server in Development

# Direct run (rebuilds on each start)
uv run main.py /path/to/files

# With custom options
uv run main.py /path/to/files --host 127.0.0.1 --port 3000 --salt mysecret