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:
parent
1c6935307c
commit
8a8fff37e0
47
TODO.md
Normal file
47
TODO.md
Normal 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
124
main.py
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user