8.5 KiB
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 saltsalt(property) — Lazily generates a random salt if not provided_hash_path(filepath: str) -> str— SHA-256 hash offilepath + salt_index() -> dict[str, str]— Walks directory tree, builds hash -> filepath mappingget_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 passwordget_file_by_hash(file_hash: str, password: bytes | None)— Yields file content, handles encryptionget_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, populatesfile_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 withWWW-Authenticate: Basicheader_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 withfile_hash,next_hash,prev_hash,filename_render_page(navigation_data, ...)— Rendersfrontend.htmltemplate 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
HTTPExceptionfor HTTP-level errors in FastAPI endpoints - Provide meaningful error messages
Async/Await
- Use
async deffor FastAPI endpoint handlers - Use regular functions for synchronous operations (file I/O, hashing)
- Do not mix sync and async improperly
File Operations
- Use
pathlib.Pathfor path manipulations - Use context managers (
withstatements) 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
- Add the route handler in
main.py - Use appropriate HTTP method decorator (
@app.get,@app.post, etc.) - Add type hints and docstrings
- Handle errors with
HTTPException
Adding a New File Indexer
- Create a new class inheriting from
FileIndexer - Override the
_indexmethod to populateself._file_mapping - Override
get_file_by_hashandget_filename_by_hashas needed - Register in
INDEXER_MAPdictionary with the file extension as key
Modifying the Frontend
- Edit
frontend.htmlfor HTML/CSS/JS changes - Add new
string.Templateplaceholders as needed - Update
_render_page()to pass new values - 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