|
5 | 5 |
|
6 | 6 | from fastapi import Depends, FastAPI, File, HTTPException, Query, Request, UploadFile |
7 | 7 | from fastapi.middleware.cors import CORSMiddleware |
8 | | -from fastapi.responses import FileResponse, StreamingResponse |
| 8 | +from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse |
9 | 9 | from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer |
10 | 10 | from pydantic import BaseModel, Field |
11 | 11 |
|
@@ -69,12 +69,13 @@ async def health(): |
69 | 69 |
|
70 | 70 |
|
71 | 71 |
|
72 | | -# Temporary download links: {token: (path, expiry_timestamp)} |
| 72 | +# Temporary links: {token: (path, expiry_timestamp)} |
73 | 73 | _download_links: dict[str, tuple[str, float]] = {} |
| 74 | +_upload_links: dict[str, tuple[str, float]] = {} |
74 | 75 |
|
75 | 76 |
|
76 | 77 | @app.get( |
77 | | - "/files/download", |
| 78 | + "/files/download/link", |
78 | 79 | summary="Get a file download link", |
79 | 80 | description="Returns a temporary download URL for a file. Link expires after 5 minutes and requires no authentication to use.", |
80 | 81 | dependencies=[Depends(verify_api_key)], |
@@ -102,11 +103,7 @@ async def get_file_link( |
102 | 103 |
|
103 | 104 | @app.get( |
104 | 105 | "/files/download/{token}", |
105 | | - summary="Download via link", |
106 | | - description="Download a file using a temporary token. No authentication required.", |
107 | | - responses={ |
108 | | - 404: {"description": "Invalid or expired download link."}, |
109 | | - }, |
| 106 | + include_in_schema=False, |
110 | 107 | ) |
111 | 108 | async def download_file(token: str): |
112 | 109 | import time |
@@ -157,6 +154,74 @@ async def upload_file( |
157 | 154 | return {"path": path, "size": len(content)} |
158 | 155 |
|
159 | 156 |
|
| 157 | +@app.post( |
| 158 | + "/files/upload/link", |
| 159 | + summary="Create an upload link", |
| 160 | + description="Generate a temporary, unauthenticated upload URL. Link expires after 5 minutes.", |
| 161 | + dependencies=[Depends(verify_api_key)], |
| 162 | + responses={ |
| 163 | + 401: {"description": "Invalid or missing API key."}, |
| 164 | + }, |
| 165 | +) |
| 166 | +async def create_upload_link( |
| 167 | + path: str = Query(..., description="Absolute destination path for the uploaded file."), |
| 168 | + request: Request = None, |
| 169 | +): |
| 170 | + import time |
| 171 | + import uuid |
| 172 | + |
| 173 | + token = uuid.uuid4().hex |
| 174 | + _upload_links[token] = (path, time.time() + 300) |
| 175 | + |
| 176 | + base_url = str(request.base_url).rstrip("/") |
| 177 | + return {"url": f"{base_url}/files/upload/{token}"} |
| 178 | + |
| 179 | + |
| 180 | +@app.get( |
| 181 | + "/files/upload/{token}", |
| 182 | + response_class=HTMLResponse, |
| 183 | + include_in_schema=False, |
| 184 | +) |
| 185 | +async def upload_page(token: str): |
| 186 | + import time |
| 187 | + |
| 188 | + entry = _upload_links.get(token) |
| 189 | + if not entry or time.time() > entry[1]: |
| 190 | + return HTMLResponse("Link expired.", status_code=404) |
| 191 | + |
| 192 | + return HTMLResponse( |
| 193 | + '<form method="post" enctype="multipart/form-data">' |
| 194 | + '<input type="file" name="file" required> ' |
| 195 | + '<button type="submit">Upload</button>' |
| 196 | + '</form>' |
| 197 | + ) |
| 198 | + |
| 199 | + |
| 200 | +@app.post( |
| 201 | + "/files/upload/{token}", |
| 202 | + include_in_schema=False, |
| 203 | +) |
| 204 | +async def upload_file_via_link( |
| 205 | + token: str, |
| 206 | + file: UploadFile = File(..., description="The file to upload."), |
| 207 | +): |
| 208 | + import time |
| 209 | + |
| 210 | + entry = _upload_links.pop(token, None) |
| 211 | + if not entry: |
| 212 | + raise HTTPException(status_code=404, detail="Invalid or expired upload link") |
| 213 | + |
| 214 | + path, expiry = entry |
| 215 | + if time.time() > expiry: |
| 216 | + raise HTTPException(status_code=404, detail="Upload link expired") |
| 217 | + |
| 218 | + os.makedirs(os.path.dirname(path), exist_ok=True) |
| 219 | + content = await file.read() |
| 220 | + with open(path, "wb") as f: |
| 221 | + f.write(content) |
| 222 | + return {"path": path, "size": len(content)} |
| 223 | + |
| 224 | + |
160 | 225 | @app.post( |
161 | 226 | "/execute", |
162 | 227 | summary="Execute a command", |
|
0 commit comments