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
.venv/
content/
data/
__pycache__/
*.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
from fastapi import FastAPI, Depends, Query
from fastapi import FastAPI
from fastapi.security import HTTPBasic
from passlib.context import CryptContext
from dotenv import load_dotenv
from routers import admin, user
load_dotenv()
from routers import admin, content
app = FastAPI()
security = HTTPBasic()
admin_security = HTTPBasic()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
@app.get("/health")
async def health_check():
return {"status": "healthy"}
app.include_router(admin.router)
app.include_router(user.router)
app.include_router(content.router)
if __name__ == "__main__":
uvicorn.run(

View File

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

View File

@@ -1,16 +1,6 @@
import aiosqlite
import os
from dotenv import load_dotenv
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)
from config import DATABASE_PATH
async def get_db():
async with aiosqlite.connect(DATABASE_PATH) as db: