Convert order/delay from path params to query params

- Merged hash_page and hash_page_with_refresh into single /{file_hash} endpoint
- Added optional order and delay query parameters
- Updated _render_page to use _build_url for query param URLs
- Updated play/pause buttons to use query param format
- Removed old /{order}/{delay}/{file_hash} route
This commit is contained in:
Timothy Farrell 2026-04-25 05:22:21 -05:00
parent 1c6935307c
commit 8a8fff37e0
2 changed files with 108 additions and 89 deletions

47
TODO.md Normal file
View File

@ -0,0 +1,47 @@
# TODO
## Goal
Remove all authentication from the server and convert `order`/`delay` from path parameters to query parameters.
## Tasks
### 1. Remove authentication from `main.py`
- [x] Remove `HTTPBasic`, `HTTPBasicCredentials`, `Depends` imports (where used for auth)
- [x] Remove `security`, `expected_password`, `get_current_username`, and `set_auth_password` code
- [x] Remove `--password` CLI argument
- [x] Remove `set_auth_password(args.password)` call from `__main__`
- [x] Remove `username: str = Depends(get_current_username)` from all route handlers
### 2. Convert `order`/`delay` to query parameters in `main.py`
- [x] Merge `hash_page` and `hash_page_with_refresh` into a single `/{file_hash}` endpoint that accepts optional query params `order` (str, default None) and `delay` (int, default None)
- [x] Remove the `/{order}/{delay}` redirect route
- [x] Remove the `/{order}/{delay}/{file_hash}` route
- [x] Update `root` endpoint to accept optional `order`/`delay` query params and pass them through in the redirect URL
- [x] Update `_render_page` to generate URLs with query params (`/{hash}?order=next&delay=5`) instead of path segments
- [x] Update the play/pause button URLs to use query param format
### 3. Update `conftest.py`
- [ ] Remove `_dummy_auth_header` function and `Authorization` header from `client_dir`/`client_zip` fixtures
- [ ] Remove `set_auth_password(None)` calls from `initialized_dir`/`initialized_zip` fixtures
- [ ] Remove `password` field from `args_directory`/`args_zip` fixtures (or keep as None if still in argparse)
### 4. Update `test_auth.py`
- [ ] Delete the entire `test_auth.py` file (all auth tests are no longer relevant)
### 5. Update `test_endpoints.py`
- [ ] Update `TestOrderDelayRoute` tests — remove or rewrite for query param routes
- [ ] Update `TestHashPageWithRefresh` tests to use query param URLs (`/{hash}?order=next&delay=5`)
- [ ] Update `TestHashPage` tests if needed (play button URLs changed)
### 6. Update `test_navigation.py`
- [ ] Remove `password=None` from `seeded_indexers` fixture args (if `--password` arg is removed)
### 7. Format and verify
- [ ] Run `uv run black .` to format all code
- [ ] Run `uv run pytest` to verify all tests pass
## Notes
- After removing auth, the `--password` CLI arg is gone entirely
- Query param format: `/{file_hash}?order=next&delay=5`
- The `order` query param accepts `"next"` or `"random"` (validated same as before)
- When `order`/`delay` are absent, behavior is identical to current browse mode

124
main.py
View File

@ -1,4 +1,3 @@
from typing import Annotated
import argparse
import hashlib
import mimetypes
@ -7,13 +6,11 @@ import random
import secrets
import string
import zipfile
from base64 import b64encode
from glob import glob
from io import BytesIO
from pathlib import Path
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from fastapi import FastAPI, HTTPException
from fastapi.responses import (
FileResponse,
HTMLResponse,
@ -25,28 +22,6 @@ app = FastAPI()
file_mapping = {}
indexers = []
security = HTTPBasic()
expected_password: str | None = None
async def get_current_username(
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
) -> str:
"""Verify Basic Authentication credentials"""
if expected_password is not None and credentials.password != expected_password:
raise HTTPException(
status_code=401,
detail="Incorrect password",
headers={"WWW-Authenticate": "Basic"},
)
return credentials.username
def set_auth_password(password: str | None):
"""Set the expected password for authentication"""
global expected_password
expected_password = password
class FileIndexer:
def __init__(self, path: str, salt: str | None = None):
@ -158,7 +133,7 @@ async def health_check():
@app.get("/api/{file_hash}/data")
async def get_file_data(file_hash: str, username: str = Depends(get_current_username)):
async def get_file_data(file_hash: str):
"""Serve a specific file by its hash"""
if file_hash not in file_mapping:
raise HTTPException(status_code=404, detail="File not found")
@ -181,22 +156,23 @@ async def get_file_data(file_hash: str, username: str = Depends(get_current_user
)
def _build_url(
file_hash: str, order: str | None = None, delay: int | None = None
) -> str:
"""Build a URL with optional order/delay query parameters."""
base = "/{hash}".format(hash=file_hash)
if order is not None and delay is not None:
return "{base}?order={order}&delay={delay}".format(
base=base, order=order, delay=delay
)
return base
@app.get("/")
async def root(username: str = Depends(get_current_username)):
async def root(order: str | None = None, delay: int | None = None):
"""Redirect to a random file hash"""
random_hash = _get_random_hash()
return RedirectResponse(url="/{hash}".format(hash=random_hash))
@app.get("/{order}/{delay}")
async def order_delay(
order: str, delay: int, username: str = Depends(get_current_username)
):
"""Redirect to random file with order and delay"""
random_hash = _get_random_hash()
return RedirectResponse(
url="/{order}/{delay}/{hash}".format(order=order, delay=delay, hash=random_hash)
)
return RedirectResponse(url=_build_url(random_hash, order, delay))
def _get_navigation_data(file_hash: str, order: str | None = None):
@ -247,16 +223,16 @@ def _render_page(
# Generate navigation URLs based on current mode
if current_order is not None:
# Timer mode: preserve current order and delay
next_url = "/{order}/{delay}/{next_hash}".format(
# Timer mode: preserve current order and delay via query params
next_url = _build_url(
navigation_data["next_hash"],
order=current_order,
delay=current_delay,
next_hash=navigation_data["next_hash"],
)
prev_url = "/{order}/{delay}/{prev_hash}".format(
prev_url = _build_url(
navigation_data["prev_hash"],
order=current_order,
delay=current_delay,
prev_hash=navigation_data["prev_hash"],
)
else:
# Browse mode: generate browse mode URLs
@ -277,47 +253,36 @@ def _render_page(
@app.get("/{file_hash}")
async def hash_page(file_hash: str, username: str = Depends(get_current_username)):
"""Serve a page for a specific file hash with navigation"""
if file_hash not in file_mapping:
raise HTTPException(status_code=404, detail="File not found")
navigation_data = _get_navigation_data(file_hash, order=None)
play_button = '<a href="/next/5/{file_hash}" class="play-btn" title="Play next 5">⏵</a>'.format(
file_hash=file_hash
)
return _render_page(
navigation_data, play_button=play_button, current_order=None, current_delay=None
)
@app.get("/{order}/{delay}/{file_hash}")
async def hash_page_with_refresh(
order: str,
delay: int,
file_hash: str,
username: str = Depends(get_current_username),
async def hash_page(
file_hash: str, order: str | None = None, delay: int | None = None
):
"""Serve a page for a specific file hash with auto-refresh navigation"""
"""Serve a page for a specific file hash with optional auto-refresh navigation.
Args:
file_hash: The hash identifier for the file.
order: Navigation order - 'next' for sequential, 'random' for random.
delay: Delay in seconds before auto-navigating to next file.
"""
if file_hash not in file_mapping:
raise HTTPException(status_code=404, detail="File not found")
if order not in ("next", "random"):
if order is not None and order not in ("next", "random"):
raise HTTPException(
status_code=400, detail="Invalid order. Must be 'next' or 'random'"
)
navigation_data = _get_navigation_data(file_hash, order=order)
refresh_url = "/{order}/{delay}/{next_hash}".format(
order=order, delay=delay, next_hash=navigation_data["next_hash"]
if order is not None and delay is not None:
# Timer mode: auto-refresh with query params
refresh_url = _build_url(
navigation_data["next_hash"], order=order, delay=delay
)
refresh_meta = f'<meta http-equiv="refresh" content="{delay};url={refresh_url}">'
image_click_url = "/{file_hash}".format(file_hash=file_hash)
image_click_url = _build_url(file_hash)
# Create pause button to stop auto-refresh
pause_button = '<a href="/{file_hash}" class="play-btn" title="Pause">⏸</a>'.format(
pause_button = '<a href="{file_hash}" class="play-btn" title="Pause">⏸</a>'.format(
file_hash=file_hash
)
@ -329,6 +294,17 @@ async def hash_page_with_refresh(
current_order=order,
current_delay=delay,
)
else:
# Browse mode
play_button = '<a href="/{file_hash}?order=next&delay=5" class="play-btn" title="Play next 5">⏵</a>'.format(
file_hash=file_hash
)
return _render_page(
navigation_data,
play_button=play_button,
current_order=None,
current_delay=None,
)
def _find_indexer_for_hash(file_hash: str):
@ -359,13 +335,9 @@ if __name__ == "__main__":
parser.add_argument(
"--salt", type=str, default=None, help="Salt for hashing file paths"
)
parser.add_argument(
"--password", type=str, default=None, help="Password for Basic Authentication"
)
args = parser.parse_args()
initialize_server(args)
set_auth_password(args.password)
import uvicorn