33import os
44from contextlib import asynccontextmanager , suppress
55from pathlib import Path
6+ from typing import Annotated
67
7- from fastapi import FastAPI , HTTPException , Request , status
8+ from fastapi import FastAPI , Header , HTTPException , Request , status
89from fastapi .concurrency import run_in_threadpool
910from fastapi .middleware .cors import CORSMiddleware
1011from fastapi .responses import FileResponse , JSONResponse
12+ from fastapi .templating import Jinja2Templates
1113
1214from backend .app import version
1315
1618from .config import settings
1719from .db import engine
1820from .migrate import run_migrations
21+ from .postprocessing import ProcessingQueue
1922
2023
2124def create_app () -> FastAPI :
@@ -24,6 +27,8 @@ def create_app() -> FastAPI:
2427 Path (settings .storage_path ).mkdir (parents = True , exist_ok = True )
2528 Path (settings .config_path ).mkdir (parents = True , exist_ok = True )
2629
30+ templates = Jinja2Templates (directory = str (Path (__file__ ).parent / "templates" ))
31+
2732 @asynccontextmanager
2833 async def lifespan (app : FastAPI ):
2934 """
@@ -36,11 +41,17 @@ async def lifespan(app: FastAPI):
3641 if not settings .skip_migrations :
3742 await run_in_threadpool (run_migrations )
3843
44+ queue = ProcessingQueue ()
45+ queue .start_worker ()
46+ app .state .processing_queue = queue
47+
3948 if not settings .skip_cleanup :
4049 app .state .cleanup_task = asyncio .create_task (start_cleanup_loop (), name = "cleanup_loop" )
4150
4251 yield
4352
53+ await queue .stop_worker ()
54+
4455 if not settings .skip_cleanup :
4556 task : asyncio .Task | None = getattr (app .state , "cleanup_task" , None )
4657 if task :
@@ -139,39 +150,143 @@ def app_version() -> dict[str, str]:
139150 app .include_router (getattr (routers , _route ).router )
140151
141152 frontend_dir : Path = Path (settings .frontend_export_path ).resolve ()
142- if frontend_dir .exists ():
143153
144- @app .get ("/{full_path:path}" , name = "static_frontend" )
145- async def frontend (full_path : str ) -> FileResponse :
146- """
147- Serve static frontend files.
154+ @app .get ("/f/{token}" , name = "share_page" )
155+ @app .get ("/f/{token}/" )
156+ async def share_page (token : str , request : Request , user_agent : Annotated [str | None , Header ()] = None ):
157+ """Handle /f/{token} with bot detection for embed preview."""
158+ from sqlalchemy import select
159+
160+ from backend .app import models , utils
161+ from backend .app .db import get_db
162+
163+ user_agent_lower : str = (user_agent or "" ).lower ()
164+ is_bot = any (bot in user_agent_lower for bot in ["discordbot" , "twitterbot" , "slackbot" , "facebookexternalhit" , "whatsapp" ])
165+
166+ if is_bot and settings .allow_public_downloads :
167+ async for db in get_db ():
168+ stmt = select (models .UploadToken ).where ((models .UploadToken .token == token ) | (models .UploadToken .download_token == token ))
169+ result = await db .execute (stmt )
170+ token_row = result .scalar_one_or_none ()
171+
172+ if token_row :
173+ uploads_stmt = (
174+ select (models .UploadRecord )
175+ .where (models .UploadRecord .token_id == token_row .id , models .UploadRecord .status == "completed" )
176+ .order_by (models .UploadRecord .created_at .desc ())
177+ )
178+ uploads_result = await db .execute (uploads_stmt )
179+ uploads = uploads_result .scalars ().all ()
180+
181+ media_files = [u for u in uploads if u .mimetype and utils .is_multimedia (u .mimetype )]
182+
183+ if media_files :
184+ first_media = media_files [0 ]
185+
186+ is_video = first_media .mimetype .startswith ("video/" )
187+ ffprobe_data = None
188+ if first_media .meta_data and isinstance (first_media .meta_data , dict ):
189+ ffprobe_data = first_media .meta_data .get ("ffprobe" )
190+
191+ video_metadata = utils .extract_video_metadata (ffprobe_data )
192+
193+ other_files = [
194+ {
195+ "name" : u .filename or "Unknown" ,
196+ "size" : utils .format_file_size (u .size_bytes ) if u .size_bytes else "Unknown" ,
197+ }
198+ for u in uploads
199+ if u .public_id != first_media .public_id
200+ ]
201+
202+ media_url = str (
203+ request .url_for ("download_file" , download_token = token_row .download_token , upload_id = first_media .public_id )
204+ )
205+ share_url = str (request .url_for ("share_page" , token = token ))
206+
207+ is_video = first_media .mimetype .startswith ("video/" )
208+ is_audio = first_media .mimetype .startswith ("audio/" )
209+
210+ context = {
211+ "request" : request ,
212+ "title" : first_media .filename or "Shared Media" ,
213+ "description" : f"{ len (uploads )} file(s) shared" if len (uploads ) > 1 else "Shared file" ,
214+ "og_type" : "video.other" if is_video else "music.song" ,
215+ "share_url" : share_url ,
216+ "media_url" : media_url ,
217+ "mime_type" : first_media .mimetype ,
218+ "is_video" : is_video ,
219+ "is_audio" : is_audio ,
220+ "width" : video_metadata .get ("width" ),
221+ "height" : video_metadata .get ("height" ),
222+ "duration" : video_metadata .get ("duration" ),
223+ "duration_formatted" : utils .format_duration (video_metadata ["duration" ])
224+ if video_metadata .get ("duration" )
225+ else None ,
226+ "file_size" : utils .format_file_size (first_media .size_bytes ) if first_media .size_bytes else None ,
227+ "other_files" : other_files ,
228+ }
229+
230+ return templates .TemplateResponse (
231+ request = request ,
232+ name = "share_preview.html" ,
233+ context = context ,
234+ status_code = status .HTTP_200_OK ,
235+ )
236+
237+ if frontend_dir .exists ():
238+ index_file = frontend_dir / "index.html"
239+ if index_file .exists ():
240+ return FileResponse (index_file , status_code = status .HTTP_200_OK )
241+ raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
148242
149- Args:
150- full_path (str): The requested file path.
243+ @app .get ("/t/{token}" , name = "upload_page" )
244+ @app .get ("/t/{token}/" )
245+ async def upload_page (token : str , request : Request , user_agent : Annotated [str | None , Header ()] = None ):
246+ """Handle /t/{token} with bot detection for embed preview."""
247+ if not frontend_dir .exists ():
248+ raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
151249
152- Returns:
153- FileResponse: The response containing the requested file.
250+ index_file = frontend_dir / "index.html"
251+ if not index_file .exists ():
252+ raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
154253
155- """
156- if full_path .startswith ("api/" ):
157- raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
254+ return FileResponse (index_file , status_code = status .HTTP_200_OK )
255+
256+ @app .get ("/{full_path:path}" , name = "static_frontend" )
257+ async def frontend (full_path : str ) -> FileResponse :
258+ """
259+ Serve static frontend files.
260+
261+ Args:
262+ full_path (str): The requested file path.
158263
159- if not full_path or "/" == full_path :
160- index_file : Path = frontend_dir / "index.html"
161- if index_file .exists ():
162- return FileResponse (index_file , status_code = status .HTTP_200_OK )
163- raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
264+ Returns:
265+ FileResponse: The response containing the requested file.
266+
267+ """
268+ if full_path .startswith ("api/" ):
269+ raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
164270
165- requested_file : Path = frontend_dir / full_path
166- if requested_file .is_file ():
167- return FileResponse (requested_file , status_code = status .HTTP_200_OK )
271+ if not frontend_dir .exists ():
272+ raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
168273
274+ if not full_path or "/" == full_path :
169275 index_file : Path = frontend_dir / "index.html"
170276 if index_file .exists ():
171277 return FileResponse (index_file , status_code = status .HTTP_200_OK )
172-
173278 raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
174279
280+ requested_file : Path = frontend_dir / full_path
281+ if requested_file .is_file ():
282+ return FileResponse (requested_file , status_code = status .HTTP_200_OK )
283+
284+ index_file : Path = frontend_dir / "index.html"
285+ if index_file .exists ():
286+ return FileResponse (index_file , status_code = status .HTTP_200_OK )
287+
288+ raise HTTPException (status_code = status .HTTP_404_NOT_FOUND )
289+
175290 return app
176291
177292
0 commit comments