From fbaf035253351caac2e7760f4bf551db3918e40f Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Tue, 5 May 2026 11:54:50 +0000 Subject: [PATCH] Update the docs --- AGENTS.md | 193 ++++++++++++++++++++++++++++++++++-------------------- README.md | 94 ++++++++------------------ 2 files changed, 149 insertions(+), 138 deletions(-) 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.