Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ MODE=local
MONGO_URI=mongodb://<username>:<password>@127.0.0.1:27017/?authSource=admin&retryWrites=true&w=majority
DOMAIN=https://localhost:8001
PORT=8001
API_VERSION=""
APP_NAMe="LOCAL"
API_VERSION="/api/v1"
APP_NAME="LOCAL"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ poetry.lock
*.tmp
*.temp
*.bak


assets/images/qr/*
69 changes: 60 additions & 9 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,19 @@
from contextlib import asynccontextmanager
from pathlib import Path
import logging
import traceback
import asyncio

from fastapi import FastAPI, Request

from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware

# from fastapi.exceptions import RequestValidationError
# from starlette.exceptions import HTTPException as StarletteHTTPException
from fastapi.exceptions import HTTPException as FastAPIHTTPException
from fastapi.templating import Jinja2Templates

from app.routes import ui_router
from app.utils import db
from app.utils.cache import cleanup_expired
Expand All @@ -20,6 +25,7 @@
from app.utils.config import (
CACHE_TTL,
SESSION_SECRET,
QR_DIR,
)


Expand Down Expand Up @@ -90,22 +96,67 @@ async def lifespan(app: FastAPI):

app = FastAPI(title="TinyURL", lifespan=lifespan)
app.add_middleware(SessionMiddleware, secret_key=SESSION_SECRET)
templates = Jinja2Templates(directory="app/templates")

# Mount QR static files
BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR = BASE_DIR / "static"

app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")

# Mount QR static files
app.mount(
"/static",
StaticFiles(directory=str(BASE_DIR / "static")),
name="static",
)
# Ensure QR directory exists at startup
QR_DIR.mkdir(parents=True, exist_ok=True)
app.mount(
"/qr",
StaticFiles(directory=str(QR_DIR)),
name="qr",
)

# -----------------------------
# Global error handler
# -----------------------------
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
traceback.print_exc()
# @app.exception_handler(Exception)
# async def global_exception_handler(request: Request, exc: Exception):
# traceback.print_exc()
# return JSONResponse(
# status_code=500,
# content={"success": False, "error": "INTERNAL_SERVER_ERROR"},
# )


# @app.exception_handler(404)
# async def custom_404_handler(request: Request, exc):
# return templates.TemplateResponse(
# "404.html",
# {"request": request},
# status_code=404,
# )


@app.exception_handler(FastAPIHTTPException)
async def http_exception_handler(request: Request, exc: FastAPIHTTPException):

# If it's API/UI route → return JSON
if request.url.path.startswith("/cache") or request.url.path.startswith("/api"):
return JSONResponse(
status_code=exc.status_code,
content={"error": exc.detail},
)

# If it's browser route → return HTML page
if exc.status_code == 404:
return templates.TemplateResponse(
"404.html",
{"request": request},
status_code=404,
)

return JSONResponse(
status_code=500,
content={"success": False, "error": "INTERNAL_SERVER_ERROR"},
status_code=exc.status_code,
content={"success": False, "error": exc.detail},
)


Expand Down
24 changes: 11 additions & 13 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from app.utils.cache import list_cache_clean, clear_cache
from fastapi import (
Expand All @@ -22,6 +21,7 @@
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel, Field


from app import __version__
from app.utils import db
from app.utils.cache import (
Expand All @@ -34,13 +34,12 @@
remove_cache_key,
rev_cache,
)
from app.utils.config import DOMAIN, MAX_RECENT_URLS, CACHE_PURGE_TOKEN
from app.utils.config import DOMAIN, MAX_RECENT_URLS, CACHE_PURGE_TOKEN, QR_DIR
from app.utils.helper import generate_code, is_valid_url, sanitize_url, format_date
from app.utils.qr import generate_qr_with_logo

BASE_DIR = Path(__file__).resolve().parent
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))

# templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
templates = Jinja2Templates(directory="app/templates")
# Routers
ui_router = APIRouter()
api_router = APIRouter()
Expand All @@ -67,10 +66,8 @@ async def index(request: Request):
if qr_enabled and new_short_url and short_code:
qr_data = new_short_url
qr_filename = f"{short_code}.png"
qr_dir = BASE_DIR / "static" / "qr"
qr_dir.mkdir(parents=True, exist_ok=True)
generate_qr_with_logo(qr_data, str(qr_dir / qr_filename))
qr_image = f"/static/qr/{qr_filename}"
generate_qr_with_logo(qr_data, str(QR_DIR / qr_filename))
qr_image = f"/qr/{qr_filename}"

recent_urls = db.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache(
MAX_RECENT_URLS
Expand Down Expand Up @@ -137,7 +134,7 @@ async def create_short_url(
return RedirectResponse("/", status_code=status.HTTP_303_SEE_OTHER)


@ui_router.get("/recent", response_class=HTMLResponse)
@ui_router.get("/history", response_class=HTMLResponse)
async def recent_urls(request: Request):
recent_urls_list = db.get_recent_urls(MAX_RECENT_URLS) or get_recent_from_cache(
MAX_RECENT_URLS
Expand Down Expand Up @@ -222,17 +219,19 @@ def redirect_short_ui(short_code: str, background_tasks: BackgroundTasks):
set_cache_pair(short_code, original_url)
return RedirectResponse(original_url)

return PlainTextResponse("Invalid short URL", status_code=404)
# return PlainTextResponse("Invalid short URL", status_code=404)
raise HTTPException(status_code=404, detail="Page not found")


@ui_router.delete("/recent/{short_code}")
@ui_router.delete("/history/{short_code}")
def delete_recent_api(short_code: str):
recent = get_recent_from_cache(MAX_RECENT_URLS) or []
removed_from_cache = False

for i, item in enumerate(recent):
code = item.get("short_code") or item.get("code")
if code == short_code:
recent.pop(i) # remove from cache
removed_from_cache = True
break

Expand All @@ -242,7 +241,6 @@ def delete_recent_api(short_code: str):
if db_available:
db_deleted = db.delete_by_short_code(short_code)

# ✅ If nothing was deleted anywhere → 404
if not removed_from_cache and not db_deleted:
raise HTTPException(
status_code=404, detail=f"short_code '{short_code}' not found"
Expand Down
Loading