Created content endpoint to serve files from content directory

This commit is contained in:
2025-05-05 16:14:13 -05:00
parent 2ad1d713cb
commit cc95e35c7f
8 changed files with 78 additions and 52 deletions

1
.gitignore vendored
View File

@@ -1,5 +1,6 @@
.env .env
.venv/ .venv/
content/
data/ data/
__pycache__/ __pycache__/
*.egg-info/ *.egg-info/

30
src/config.py Normal file
View File

@@ -0,0 +1,30 @@
import os
from dotenv import load_dotenv
# Load environment variables from a .env file if it exists
load_dotenv()
# Super admin credentials for basic auth
# Reads from environment variables SUPER_ADMIN_USER and SUPER_ADMIN_PASSWORD
SUPER_ADMIN_USER = os.getenv("SUPER_ADMIN_USER", "admin")
SUPER_ADMIN_PASSWORD = os.getenv("SUPER_ADMIN_PASSWORD", "adminpassword")
# Define the directory where content files are stored
# Reads from environment variable CONTENT_PATH or defaults to '/content'
CONTENT_PATH = os.getenv("CONTENT_PATH", "/content")
# Ensure the directory for the content exists
content_path = os.path.dirname(CONTENT_PATH)
if content_path and not os.path.exists(content_path):
print(f"Creating directory for serving content: {CONTENT_PATH}")
os.makedirs(content_path, exist_ok=True)
# Database file path
# Reads from environment variable DATABASE_PATH or defaults to '/data/users.db'
DATABASE_PATH = os.getenv("DATABASE_PATH", "/data/users.db")
# Ensure the directory for the database exists
db_dir = os.path.dirname(DATABASE_PATH)
if db_dir and not os.path.exists(db_dir):
print(f"Creating directory for database: {DATABASE_PATH}")
os.makedirs(db_dir, exist_ok=True)

View File

@@ -1,23 +1,18 @@
import uvicorn import uvicorn
from fastapi import FastAPI, Depends, Query from fastapi import FastAPI
from fastapi.security import HTTPBasic from fastapi.security import HTTPBasic
from passlib.context import CryptContext from routers import admin, content
from dotenv import load_dotenv
from routers import admin, user
load_dotenv()
app = FastAPI() app = FastAPI()
security = HTTPBasic() security = HTTPBasic()
admin_security = HTTPBasic() admin_security = HTTPBasic()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
return {"status": "healthy"} return {"status": "healthy"}
app.include_router(admin.router) app.include_router(admin.router)
app.include_router(user.router) app.include_router(content.router)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run( uvicorn.run(

View File

@@ -1,8 +1,8 @@
from fastapi import APIRouter, Depends, HTTPException
import aiosqlite import aiosqlite
from fastapi import APIRouter, Depends, HTTPException
from passlib.context import CryptContext from passlib.context import CryptContext
from utils.auth import verify_superadmin, get_user from utils.auth import verify_superadmin, get_user
from utils.database import DATABASE_PATH, get_db from utils.database import get_db
from models.models import User from models.models import User
# Assuming pwd_context is initialized in auth.py and used there # Assuming pwd_context is initialized in auth.py and used there

33
src/routers/content.py Normal file
View File

@@ -0,0 +1,33 @@
from fastapi import APIRouter, Depends, Path, HTTPException
from fastapi.responses import FileResponse
from utils.auth import authenticate_user_query
from pathlib import Path as FilePath
from config import CONTENT_PATH
router = APIRouter(prefix="/content", tags=["content"])
@router.get("/{filename}")
async def serve_content(
username: str = Depends(authenticate_user_query),
filename: str = Path(..., min_length=1), # Ensure filename is not empty
):
"""
Serves a specific file from the configured content directory by filename only.
Prevents directory traversal and ensures files are served from the base content directory.
"""
base_dir = FilePath(CONTENT_PATH).resolve()
requested_file = base_dir / filename
print(f"Requested file: {requested_file}")
# Security checks
try:
# Resolve the full path and check it's within base directory
requested_file = requested_file.resolve()
if not requested_file.is_relative_to(base_dir):
raise ValueError("Invalid path")
except (ValueError, FileNotFoundError):
raise HTTPException(status_code=404, detail="File not found")
if not requested_file.is_file():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(requested_file)

View File

@@ -1,16 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query
# Import the specific query authentication dependency
from utils.auth import authenticate_user_query
router = APIRouter(prefix="/user", tags=["user"])
@router.get("/stream")
async def stream_content(
# Use the dependency that gets username/password from query params
# The dependency handles the authentication check and raises HTTPException on failure
username: str = Depends(authenticate_user_query)
):
# If the dependency returns, authentication succeeded.
# The `username` variable holds the authenticated user's username.
# You can now use `username` for any user-specific logic.
return {"content": f"protected stream data for user {username}"}

View File

@@ -1,16 +1,12 @@
import os
from fastapi import Depends, HTTPException, status, Query
from fastapi.security import HTTPBasic, HTTPBasicCredentials
from passlib.context import CryptContext
import aiosqlite import aiosqlite
from utils.database import get_db, DATABASE_PATH from fastapi import Depends, HTTPException, status, Query
from dotenv import load_dotenv from fastapi.security import HTTPBasicCredentials, HTTPBasic
from passlib.context import CryptContext
load_dotenv() from config import SUPER_ADMIN_USER, SUPER_ADMIN_PASSWORD, DATABASE_PATH
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBasic() # For general user authentication security = HTTPBasic()
admin_security = HTTPBasic() # Could potentially use a separate one if needed, but HTTPBasic is fine admin_security = HTTPBasic()
async def get_user(username: str): async def get_user(username: str):
# Use the dependency pattern even for internal calls for consistency # Use the dependency pattern even for internal calls for consistency
@@ -53,11 +49,8 @@ async def authenticate_user_query(
return user[0] return user[0]
def verify_superadmin(credentials: HTTPBasicCredentials = Depends(admin_security)): def verify_superadmin(credentials: HTTPBasicCredentials = Depends(admin_security)):
correct_username = os.getenv("SUPER_ADMIN_USER") if not (credentials.username == SUPER_ADMIN_USER and
correct_password = os.getenv("SUPER_ADMIN_PASSWORD") credentials.password == SUPER_ADMIN_PASSWORD):
if not (credentials.username == correct_username and
credentials.password == correct_password):
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect superadmin credentials", detail="Incorrect superadmin credentials",

View File

@@ -1,16 +1,6 @@
import aiosqlite import aiosqlite
import os
from dotenv import load_dotenv from dotenv import load_dotenv
from config import DATABASE_PATH
load_dotenv()
DATABASE_PATH = os.getenv("DATABASE_PATH", "users.db")
# Ensure the directory for the database exists
db_dir = os.path.dirname(DATABASE_PATH)
if db_dir and not os.path.exists(db_dir):
print(f"Creating directory for database: {DATABASE_PATH}")
os.makedirs(db_dir, exist_ok=True)
async def get_db(): async def get_db():
async with aiosqlite.connect(DATABASE_PATH) as db: async with aiosqlite.connect(DATABASE_PATH) as db: