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 argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@ -7,13 +6,11 @@ import random
|
|||||||
import secrets
|
import secrets
|
||||||
import string
|
import string
|
||||||
import zipfile
|
import zipfile
|
||||||
from base64 import b64encode
|
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Depends
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
|
||||||
from fastapi.responses import (
|
from fastapi.responses import (
|
||||||
FileResponse,
|
FileResponse,
|
||||||
HTMLResponse,
|
HTMLResponse,
|
||||||
@ -25,28 +22,6 @@ app = FastAPI()
|
|||||||
file_mapping = {}
|
file_mapping = {}
|
||||||
indexers = []
|
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:
|
class FileIndexer:
|
||||||
def __init__(self, path: str, salt: str | None = None):
|
def __init__(self, path: str, salt: str | None = None):
|
||||||
@ -158,7 +133,7 @@ async def health_check():
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/api/{file_hash}/data")
|
@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"""
|
"""Serve a specific file by its hash"""
|
||||||
if file_hash not in file_mapping:
|
if file_hash not in file_mapping:
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
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("/")
|
@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"""
|
"""Redirect to a random file hash"""
|
||||||
random_hash = _get_random_hash()
|
random_hash = _get_random_hash()
|
||||||
return RedirectResponse(url="/{hash}".format(hash=random_hash))
|
return RedirectResponse(url=_build_url(random_hash, order, delay))
|
||||||
|
|
||||||
|
|
||||||
@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)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_navigation_data(file_hash: str, order: str | None = None):
|
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
|
# Generate navigation URLs based on current mode
|
||||||
if current_order is not None:
|
if current_order is not None:
|
||||||
# Timer mode: preserve current order and delay
|
# Timer mode: preserve current order and delay via query params
|
||||||
next_url = "/{order}/{delay}/{next_hash}".format(
|
next_url = _build_url(
|
||||||
|
navigation_data["next_hash"],
|
||||||
order=current_order,
|
order=current_order,
|
||||||
delay=current_delay,
|
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,
|
order=current_order,
|
||||||
delay=current_delay,
|
delay=current_delay,
|
||||||
prev_hash=navigation_data["prev_hash"],
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Browse mode: generate browse mode URLs
|
# Browse mode: generate browse mode URLs
|
||||||
@ -277,47 +253,36 @@ def _render_page(
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/{file_hash}")
|
@app.get("/{file_hash}")
|
||||||
async def hash_page(file_hash: str, username: str = Depends(get_current_username)):
|
async def hash_page(
|
||||||
"""Serve a page for a specific file hash with navigation"""
|
file_hash: str, order: str | None = None, delay: int | None = None
|
||||||
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),
|
|
||||||
):
|
):
|
||||||
"""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:
|
if file_hash not in file_mapping:
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
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(
|
raise HTTPException(
|
||||||
status_code=400, detail="Invalid order. Must be 'next' or 'random'"
|
status_code=400, detail="Invalid order. Must be 'next' or 'random'"
|
||||||
)
|
)
|
||||||
|
|
||||||
navigation_data = _get_navigation_data(file_hash, order=order)
|
navigation_data = _get_navigation_data(file_hash, order=order)
|
||||||
|
|
||||||
refresh_url = "/{order}/{delay}/{next_hash}".format(
|
if order is not None and delay is not None:
|
||||||
order=order, delay=delay, next_hash=navigation_data["next_hash"]
|
# 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}">'
|
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
|
# 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
|
file_hash=file_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -329,6 +294,17 @@ async def hash_page_with_refresh(
|
|||||||
current_order=order,
|
current_order=order,
|
||||||
current_delay=delay,
|
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):
|
def _find_indexer_for_hash(file_hash: str):
|
||||||
@ -359,13 +335,9 @@ if __name__ == "__main__":
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--salt", type=str, default=None, help="Salt for hashing file paths"
|
"--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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
initialize_server(args)
|
initialize_server(args)
|
||||||
set_auth_password(args.password)
|
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user