diff --git a/AGENTS.md b/AGENTS.md
index 76f259a..4d6efd1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,14 +1,15 @@
-# AGENTS.md - File Server Project
+# AGENTS.md — Image Server Project
## Project Overview
-This is a FastAPI-based file server that serves files from a directory or ZIP archive using hashed paths. The server indexes files and serves them via random access or by hash lookup.
+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
@@ -20,31 +21,26 @@ 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
-No test framework is currently configured. To add tests, install pytest:
-
```bash
-uv add pytest
-```
+# Run all tests
+uv run pytest
-Run all tests:
-```bash
-pytest
-```
+# Run a single test
+uv run pytest tests/test_file_indexer.py::TestClass::test_method
-Run a single test:
-```bash
-pytest path/to/test_file.py::TestClass::test_method
-pytest -k "test_name_pattern"
+# Run tests matching a pattern
+uv run pytest -k "test_name_pattern"
```
### Linting
-No linting tool is currently configured. Recommended tools:
-
```bash
# Install ruff (fast linter)
uv add ruff
@@ -78,6 +74,101 @@ uv add mypy
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 `` tags (e.g., auto-refresh)
+- `$play_button` — Play/pause button HTML
+
## Code Style Guidelines
### Imports
@@ -85,17 +176,6 @@ uv run mypy main.py
- Standard library imports first, then third-party, then local
- Group imports by type: stdlib, third-party, local
- Use absolute imports for local modules
-- Example:
- ```python
- import os
- import sys
- from pathlib import Path
-
- from fastapi import FastAPI, HTTPException
- from fastapi.responses import FileResponse, StreamingResponse
-
- from mymodule import MyClass
- ```
### Formatting Style
@@ -109,15 +189,6 @@ uv run mypy main.py
- Use type hints for all function arguments and return values
- Use Python 3.13+ typing features where appropriate
-- Example:
- ```python
- def process_file(path: str) -> bytes:
- ...
-
- class FileIndexer:
- def __init__(self, path: str) -> None:
- ...
- ```
### Naming Conventions
@@ -132,14 +203,6 @@ uv run mypy main.py
- Use exceptions for exceptional cases, not flow control
- Raise `HTTPException` for HTTP-level errors in FastAPI endpoints
- Provide meaningful error messages
-- Example:
- ```python
- if not indexer.file_mapping:
- raise HTTPException(status_code=404, detail="No files indexed")
-
- if file_hash not in self.file_mapping:
- return None
- ```
### Async/Await
@@ -156,18 +219,7 @@ uv run mypy main.py
### Documentation
- Use docstrings for public classes and functions
-- Follow Google-style docstring format:
- ```python
- def get_file_by_hash(file_hash: str) -> str | None:
- """Get filename by hash.
-
- Args:
- file_hash: The hash identifier for the file.
-
- Returns:
- The filename if found, None otherwise.
- """
- ```
+- Follow Google-style docstring format
### Best Practices
@@ -177,16 +229,6 @@ uv run mypy main.py
- Use dataclasses or attrs for simple data containers
- Follow PEP 8 style guidelines
-## Project Structure
-
-```
-/projects/file_server/
-├── main.py # Main application entry point
-├── pyproject.toml # Project configuration
-├── .python-version # Python version specification
-└── README.md # Project documentation (currently empty)
-```
-
## Common Tasks
### Adding a New Endpoint
@@ -199,12 +241,23 @@ uv run mypy main.py
### Adding a New File Indexer
1. Create a new class inheriting from `FileIndexer`
-2. Override the `_index` method to populate `self.file_mapping`
-3. Implement `get_file_by_hash` and `get_filename_by_hash`
-4. Register in `INDEXER_MAP` dictionary
+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
-uv run main ./*.zip
+# 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
```
diff --git a/README.md b/README.md
index 99c9da5..67e1043 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,14 @@
# Image Server
-A FastAPI-based image server that serves files from directories or ZIP archives using hashed paths for secure, randomized access. The server provides a beautiful full-screen image viewing experience with navigation controls and optional auto-advance features.
+A FastAPI-based image server that serves files from directories or ZIP archives using hashed paths for secure, randomized access. The server provides a full-screen image viewing experience with navigation controls and optional auto-advance features.
## Features
- **Secure File Access**: Files are accessed via SHA-256 hashes of their paths (with salt) rather than direct file paths
- **Multiple Sources**: Supports serving from directories, individual ZIP files, or glob patterns (e.g., `*.zip`)
+- **Password-protected ZIPs**: HTTP Basic Auth for encrypted files inside ZIP archives
- **Beautiful UI**: Full-screen responsive image viewer with keyboard navigation
-- **Navigation Controls**:
+- **Navigation Controls**:
- Previous/Next buttons (clickable chevrons or arrow keys)
- Random access mode
- Ordered sequential access with configurable delay
@@ -26,20 +27,13 @@ A FastAPI-based image server that serves files from directories or ZIP archives
### Prerequisites
- Python 3.13 or higher
-- UV package manager (recommended) or pip
+- [UV](https://github.com/astral-sh/uv) package manager (recommended) or pip
### Using UV (Recommended)
```bash
-# Clone the repository
-git clone
cd image_server
-
-# Install dependencies
uv sync
-
-# Activate virtual environment
-source .venv/bin/activate
```
### Using pip
@@ -54,19 +48,19 @@ pip install fastapi uvicorn
```bash
# Serve files from a directory
-python main.py /path/to/your/images
+uv run main.py /path/to/your/images
# Serve files from a ZIP archive
-python main.py /path/to/archive.zip
+uv run main.py /path/to/archive.zip
# Serve files matching a glob pattern
-python main.py "path/to/images/*.zip"
+uv run main.py "path/to/images/*.zip"
```
### Server Options
```bash
-python main.py [SOURCE] [OPTIONS]
+uv run main.py [SOURCE] [OPTIONS]
Arguments:
SOURCE Path to directory, ZIP archive, or glob pattern (e.g., *.zip, path/to/zips/*.zip)
@@ -82,41 +76,41 @@ Options:
```bash
# Start server on default port 8000
-python main.py ./photos
+uv run main.py ./photos
# Start server on custom host and port
-python main.py ./photos --host 127.0.0.1 --port 3000
+uv run main.py ./photos --host 127.0.0.1 --port 3000
# Start server with custom salt for consistent hashing
-python main.py ./photos --salt mysecretkey123
+uv run main.py ./photos --salt mysecretkey123
# Start server serving from ZIP files
-python main.py ./archives/*.zip
+uv run main.py ./archives/*.zip
```
## API Endpoints
### File Access
-- `GET /api/{file_hash}/data` - Retrieve file data by hash
+- `GET /api/{file_hash}/data` — Retrieve file data by hash
- Returns: File content with appropriate Content-Type header
- Response: StreamingResponse of the file data
### Navigation Pages
-- `GET /{file_hash}` - View file with navigation controls
+- `GET /{file_hash}` — View file with navigation controls (browse mode)
- Displays image with previous/next navigation
-
-- `GET /{order}/{delay}/{file_hash}` - View file with auto-refresh
- - order: "next" (sequential) or "random"
- - delay: Seconds before auto-advancing
- - file_hash: Starting file hash
+
+- `GET /{file_hash}?order=next&delay=5` — View file with auto-refresh
+ - `order`: `"next"` (sequential) or `"random"`
+ - `delay`: Seconds before auto-advancing
+ - Clicking the image pauses auto-refresh
### Special Endpoints
-- `GET /` - Redirect to a random file
-- `GET /{order}/{delay}` - Redirect to random file with order/delay settings
-- `GET /api/health` - Health check endpoint
+- `GET /` — Redirect to a random file
+- `GET /?order=next&delay=5` — Redirect to random file with order/delay settings
+- `GET /api/health` — Health check endpoint
- Returns: JSON with status and file count
## How It Works
@@ -138,6 +132,7 @@ Each file is assigned a unique SHA-256 hash based on:
- Files can only be accessed via their cryptographic hashes
- Salt prevents hash prediction attacks
- No filesystem traversal vulnerabilities
+- Password-protected ZIP files require HTTP Basic Auth
### User Interface
@@ -148,45 +143,8 @@ The server serves a single-page application (`frontend.html`) that provides:
- Play/pause button for auto-advance control
- Responsive design that works on mobile and desktop
-## Development
-
-### Running in Development Mode
-
-```bash
-# With auto-reload for development
-uvicorn main:app --reload --host 0.0.0.0 --port 8000
-```
-
-### Code Structure
-
-- `main.py` - Contains all server logic, routing, and file indexing
-- `frontend.html` - HTML template for the user interface
-- `FileIndexer` - Base class for indexing files from directories
-- `ZipFileIndexer` - Extension for indexing files from ZIP archives
-- Global `file_mapping` dictionary - Maps hashes to file paths/filenames
-
-### Adding New Features
-
-1. **New Indexer Types**: Create a class inheriting from `FileIndexer` and override the `_index()` method
-2. **New Routes**: Add functions decorated with appropriate HTTP method decorators (`@app.get`, etc.)
-3. **UI Changes**: Modify `frontend.html` template and update template substitution in `_render_page()`
-
## Dependencies
-- **FastAPI** - Modern web framework for building APIs
-- **Uvicorn** - ASGI server for running the application
-- **Python Standard Library** - hashlib, mimetypes, secrets, zipfile, etc.
-
-## License
-
-This project is licensed under the MIT License - see the LICENSE file for details.
-
-## Contributing
-
-1. Fork the repository
-2. Create a feature branch
-3. Commit your changes
-4. Push to the branch
-5. Open a pull request
-
-Please ensure your code follows the existing style and includes appropriate tests.
\ No newline at end of file
+- **FastAPI** — Modern web framework for building APIs
+- **Uvicorn** — ASGI server for running the application
+- **Python Standard Library** — hashlib, mimetypes, secrets, zipfile, etc.