264 lines
8.5 KiB
Markdown
264 lines
8.5 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
# Install black
|
|
uv add black
|
|
|
|
# Format all code
|
|
uv run black .
|
|
```
|
|
|
|
### Type Checking
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```python
|
|
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`
|
|
|
|
```python
|
|
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
|
|
|
|
```bash
|
|
# 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
|
|
```
|