triflix commited on
Commit
0053eb2
·
verified ·
1 Parent(s): d65b5f7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +295 -286
app.py CHANGED
@@ -1,287 +1,296 @@
1
- import os
2
- import shutil
3
- import aiofiles
4
- from fastapi import (
5
- FastAPI, UploadFile, File, HTTPException, Request, Depends, Form, APIRouter, status
6
- )
7
- from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse, Response
8
- from fastapi.staticfiles import StaticFiles
9
- from fastapi.templating import Jinja2Templates
10
- from pathlib import Path
11
- import logging # Standard logging
12
- import datetime # For admin logic timestamps
13
- import math # For admin logic format_bytes
14
- from typing import List, Optional # For admin logic type hinting
15
-
16
- # --- Configuration ---
17
- BASE_DIR = Path(__file__).resolve().parent
18
- FILES_DIR = BASE_DIR / "uploaded_files"
19
- STATIC_DIR = BASE_DIR / "static"
20
- TEMPLATES_DIR = BASE_DIR / "templates"
21
- # LOG_FILE constant removed
22
-
23
- # Create directories if they don't exist
24
- FILES_DIR.mkdir(parents=True, exist_ok=True)
25
- STATIC_DIR.mkdir(parents=True, exist_ok=True)
26
- TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
27
-
28
- # --- Logging Configuration (Console Only) ---
29
- logger = logging.getLogger("file_app")
30
- logger.setLevel(logging.INFO)
31
- # No file handler, no propagate=False. Logs will go to console via root logger.
32
-
33
- logger.info("Application starting up... Logging to console.")
34
-
35
- app = FastAPI(title="File Uploader/Downloader")
36
-
37
- # Setup Jinja2 templates
38
- templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
39
-
40
- # --- Admin Panel Configuration & Logic ---
41
- ADMIN_EMAIL = "admin@example.com"
42
- ADMIN_PASSWORD = "adminpassword"
43
- ADMIN_SESSION_COOKIE_NAME = "admin_session_id"
44
- ADMIN_SESSION_SECRET_VALUE = "my_super_secret_admin_session_value_for_demo_only_change_me"
45
-
46
- admin_router = APIRouter(
47
- prefix="/admin",
48
- tags=["Admin"],
49
- )
50
-
51
- # --- Admin Helper Functions ---
52
- def get_file_info(file_path: Path):
53
- try:
54
- if file_path.is_file():
55
- stat_info = file_path.stat()
56
- return {
57
- "name": file_path.name,
58
- "size_bytes": stat_info.st_size,
59
- "size_human": format_bytes(stat_info.st_size),
60
- "modified_at": datetime.datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S UTC'),
61
- "url": f"/download/{file_path.name}"
62
- }
63
- except Exception as e:
64
- logger.error(f"Admin: Error getting file info for {file_path}: {e}")
65
- return None
66
- return None
67
-
68
- def format_bytes(bytes_val, decimals=2):
69
- if bytes_val == 0: return '0 Bytes'
70
- k = 1024
71
- dm = decimals if decimals >= 0 else 0
72
- sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
73
- i = 0
74
- if bytes_val > 0:
75
- i = int(math.floor(math.log(bytes_val) / math.log(k)))
76
- if i >= len(sizes): i = len(sizes) -1
77
- return f"{bytes_val / (k**i):.{dm}f} {sizes[i]}"
78
-
79
- # get_log_lines function removed as file logging is removed.
80
-
81
- # --- Admin Authentication Dependencies ---
82
- async def get_current_admin(request: Request):
83
- session_cookie = request.cookies.get(ADMIN_SESSION_COOKIE_NAME)
84
- if session_cookie == ADMIN_SESSION_SECRET_VALUE:
85
- return {"email": ADMIN_EMAIL}
86
-
87
- current_path = request.url.path
88
- if current_path not in [app.url_path_for("admin_login_page"), app.url_path_for("admin_authenticate")]:
89
- logger.info(f"Admin: Unauthorized access attempt to {current_path}, redirecting to login.")
90
- return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
91
- return None
92
-
93
- async def verify_admin_auth(request: Request):
94
- admin_user = await get_current_admin(request)
95
- if isinstance(admin_user, RedirectResponse):
96
- raise HTTPException(
97
- status_code=status.HTTP_307_TEMPORARY_REDIRECT,
98
- detail="Redirecting to login.",
99
- headers={"Location": app.url_path_for("admin_login_page")},
100
- )
101
- if not admin_user:
102
- logger.warning(f"Admin: Strict auth verification failed for {request.url.path}.")
103
- raise HTTPException(
104
- status_code=status.HTTP_401_UNAUTHORIZED,
105
- detail="Not authenticated",
106
- headers={"WWW-Authenticate": "Bearer"},
107
- )
108
- return admin_user
109
-
110
- # --- Admin Routes ---
111
- @admin_router.get("/login", response_class=HTMLResponse, name="admin_login_page")
112
- async def admin_login_page(request: Request):
113
- logger.info("Admin: Login page accessed.")
114
- return templates.TemplateResponse("admin_login.html", {"request": request, "url_for": app.url_path_for})
115
-
116
- @admin_router.post("/auth", name="admin_authenticate")
117
- async def admin_authenticate(request: Request, email: str = Form(...), password: str = Form(...)):
118
- if email == ADMIN_EMAIL and password == ADMIN_PASSWORD:
119
- logger.info(f"Admin: Successful login for email {email}.")
120
- response = RedirectResponse(url=app.url_path_for("admin_dashboard"), status_code=status.HTTP_303_SEE_OTHER)
121
- response.set_cookie(
122
- key=ADMIN_SESSION_COOKIE_NAME, value=ADMIN_SESSION_SECRET_VALUE,
123
- httponly=True, samesite="lax", max_age=30 * 60
124
- )
125
- return response
126
- else:
127
- logger.warning(f"Admin: Failed login attempt for email {email}.")
128
- error_msg = "Invalid email or password."
129
- return templates.TemplateResponse(
130
- "admin_login.html",
131
- {"request": request, "error": error_msg, "url_for": app.url_path_for},
132
- status_code=status.HTTP_401_UNAUTHORIZED
133
- )
134
-
135
- @admin_router.get("/logout", name="admin_logout")
136
- async def admin_logout(request: Request):
137
- logger.info("Admin: Logout initiated.")
138
- response = RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_303_SEE_OTHER)
139
- response.delete_cookie(ADMIN_SESSION_COOKIE_NAME, httponly=True, samesite="lax")
140
- return response
141
-
142
- @admin_router.get("/dashboard", response_class=HTMLResponse, name="admin_dashboard")
143
- async def admin_dashboard(request: Request, admin_user: dict = Depends(get_current_admin)):
144
- if isinstance(admin_user, RedirectResponse): return admin_user
145
- if not admin_user: return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
146
-
147
- logger.info(f"Admin: Dashboard accessed by {admin_user.get('email')}.")
148
- num_files = 0
149
- try:
150
- num_files = len([name for name in os.listdir(FILES_DIR) if (FILES_DIR / name).is_file()])
151
- except Exception as e:
152
- logger.error(f"Admin: Error counting files for dashboard: {e}")
153
-
154
- # log_lines_count removed
155
- return templates.TemplateResponse("admin_dashboard.html", {
156
- "request": request,
157
- "num_files": num_files,
158
- "admin_user": admin_user,
159
- "url_for": app.url_path_for
160
- })
161
-
162
- @admin_router.get("/files", response_class=HTMLResponse, name="admin_files_page")
163
- async def admin_files_page(request: Request, admin_user: dict = Depends(get_current_admin)):
164
- if isinstance(admin_user, RedirectResponse): return admin_user
165
- if not admin_user: return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
166
-
167
- logger.info(f"Admin: Files page accessed by {admin_user.get('email')}.")
168
- uploaded_file_objects = []
169
- try:
170
- for item_name in sorted(os.listdir(FILES_DIR)):
171
- item_path = FILES_DIR / item_name
172
- info = get_file_info(item_path)
173
- if info:
174
- uploaded_file_objects.append(info)
175
- except Exception as e:
176
- logger.error(f"Admin: Error listing files: {e}")
177
-
178
- return templates.TemplateResponse("admin_files.html", {
179
- "request": request,
180
- "files": uploaded_file_objects,
181
- "admin_user": admin_user,
182
- "url_for": app.url_path_for
183
- })
184
-
185
- @admin_router.post("/files/delete/{filename}", name="admin_delete_file")
186
- async def admin_delete_file(request: Request, filename: str, admin_user: dict = Depends(verify_admin_auth)):
187
- logger.info(f"Admin: Attempting to delete file '{filename}' by {admin_user.get('email')}.")
188
- safe_filename = sanitize_filename(filename)
189
- file_path = FILES_DIR / safe_filename
190
-
191
- if not file_path.resolve().is_relative_to(FILES_DIR.resolve()):
192
- logger.error(f"Admin: Invalid file path for deletion (traversal attempt) '{filename}'.")
193
- raise HTTPException(status_code=400, detail="Invalid file path for deletion.")
194
-
195
- if file_path.is_file():
196
- try:
197
- file_path.unlink()
198
- logger.info(f"Admin: File '{filename}' deleted successfully by {admin_user.get('email')}.")
199
- return RedirectResponse(url=app.url_path_for("admin_files_page") + "?message=File+deleted+successfully", status_code=status.HTTP_303_SEE_OTHER)
200
- except Exception as e:
201
- logger.error(f"Admin: Could not delete file '{filename}': {str(e)}")
202
- raise HTTPException(status_code=500, detail=f"Could not delete file: {str(e)}")
203
- else:
204
- logger.warning(f"Admin: File not found for deletion '{filename}'.")
205
- raise HTTPException(status_code=404, detail="File not found for deletion.")
206
-
207
- # /admin/logs route and admin_logs_page function removed
208
-
209
- # --- Main Application Helper Functions (Original) ---
210
- def sanitize_filename(filename: str) -> str:
211
- return os.path.basename(filename).strip()
212
-
213
- # --- Main Application Routes (Original functionality with added logging) ---
214
- @app.get("/", response_class=HTMLResponse, name="read_root")
215
- async def read_root(request: Request):
216
- logger.info(f"Root page accessed by {request.client.host}")
217
- return templates.TemplateResponse("index.html", {"request": request, "url_for": app.url_path_for})
218
-
219
- @app.post("/upload/", name="upload_file")
220
- async def upload_file(request: Request, file: UploadFile = File(...)):
221
- original_filename = file.filename
222
- logger.info(f"Upload attempt for '{original_filename}' from {request.client.host}")
223
-
224
- if not original_filename:
225
- logger.warning(f"Upload failed: No filename provided by {request.client.host}")
226
- raise HTTPException(status_code=400, detail="No filename provided.")
227
-
228
- filename = sanitize_filename(original_filename)
229
- if not filename:
230
- logger.warning(f"Upload failed: Invalid filename '{original_filename}' after sanitization by {request.client.host}")
231
- raise HTTPException(status_code=400, detail="Invalid filename after sanitization.")
232
-
233
- file_path = FILES_DIR / filename
234
-
235
- if not file_path.resolve().is_relative_to(FILES_DIR.resolve()):
236
- logger.error(f"Upload security violation: Attempted directory traversal for '{filename}' by {request.client.host}")
237
- raise HTTPException(status_code=400, detail="Invalid file path (attempted directory traversal).")
238
-
239
- try:
240
- async with aiofiles.open(file_path, "wb") as buffer:
241
- while content := await file.read(1024 * 1024):
242
- await buffer.write(content)
243
- file_size = file_path.stat().st_size
244
- logger.info(f"File '{filename}' uploaded successfully by {request.client.host}. Size: {file_size} bytes.")
245
- except Exception as e:
246
- logger.error(f"Could not save file '{filename}' from {request.client.host}: {str(e)}")
247
- if file_path.exists():
248
- try:
249
- file_path.unlink()
250
- logger.info(f"Cleaned up partially uploaded file '{filename}'.")
251
- except Exception as unlink_e:
252
- logger.error(f"Error cleaning up partially uploaded file '{filename}': {unlink_e}")
253
- raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}")
254
-
255
- return JSONResponse(
256
- content={
257
- "message": "File uploaded successfully",
258
- "filename": filename,
259
- "download_url": app.url_path_for("download_file", filename=filename)
260
- }
261
- )
262
-
263
- @app.get("/download/{filename}", name="download_file")
264
- async def download_file(request: Request, filename: str):
265
- clean_filename = sanitize_filename(filename)
266
- logger.info(f"Download attempt for '{clean_filename}' by {request.client.host}")
267
-
268
- if not clean_filename:
269
- logger.warning(f"Download failed: Invalid filename '{filename}' by {request.client.host}")
270
- raise HTTPException(status_code=400, detail="Invalid filename.")
271
-
272
- file_path = FILES_DIR / clean_filename
273
-
274
- if not file_path.resolve().is_relative_to(FILES_DIR.resolve()) or not file_path.is_file():
275
- logger.warning(f"Download failed: File not found or access denied for '{clean_filename}' by {request.client.host}")
276
- raise HTTPException(status_code=404, detail="File not found or access denied.")
277
-
278
- logger.info(f"File '{clean_filename}' downloaded by {request.client.host}")
279
- return FileResponse(path=file_path, filename=clean_filename, media_type='application/octet-stream')
280
-
281
- app.include_router(admin_router)
282
- logger.info("Admin panel router included. File-based logging is disabled.")
283
-
284
- if __name__ == "__main__":
285
- import uvicorn
286
- logger.info("Starting Uvicorn server for local development...")
 
 
 
 
 
 
 
 
 
287
  uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)
 
1
+ import os
2
+ import shutil
3
+ import aiofiles
4
+ from fastapi import (
5
+ FastAPI, UploadFile, File, HTTPException, Request, Depends, Form, APIRouter, status
6
+ )
7
+ import datetime
8
+ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, RedirectResponse, Response
9
+ from fastapi.staticfiles import StaticFiles
10
+ from fastapi.templating import Jinja2Templates
11
+ from pathlib import Path
12
+ import logging # Standard logging
13
+ import datetime # For admin logic timestamps
14
+ import math # For admin logic format_bytes
15
+ from typing import List, Optional # For admin logic type hinting
16
+
17
+ # --- Configuration ---
18
+ BASE_DIR = Path(__file__).resolve().parent
19
+ FILES_DIR = BASE_DIR / "uploaded_files"
20
+ STATIC_DIR = BASE_DIR / "static"
21
+ TEMPLATES_DIR = BASE_DIR / "templates"
22
+ # LOG_FILE constant removed
23
+
24
+ # Create directories if they don't exist
25
+ FILES_DIR.mkdir(parents=True, exist_ok=True)
26
+ STATIC_DIR.mkdir(parents=True, exist_ok=True)
27
+ TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
28
+
29
+ # --- Logging Configuration (Console Only) ---
30
+ logger = logging.getLogger("file_app")
31
+ logger.setLevel(logging.INFO)
32
+ # No file handler, no propagate=False. Logs will go to console via root logger.
33
+
34
+ logger.info("Application starting up... Logging to console.")
35
+
36
+ app = FastAPI(title="File Uploader/Downloader")
37
+
38
+ # Setup Jinja2 templates
39
+ templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
40
+
41
+ # --- Admin Panel Configuration & Logic ---
42
+ ADMIN_EMAIL = "admin@example.com"
43
+ ADMIN_PASSWORD = "adminpassword"
44
+ ADMIN_SESSION_COOKIE_NAME = "admin_session_id"
45
+ ADMIN_SESSION_SECRET_VALUE = "my_super_secret_admin_session_value_for_demo_only_change_me"
46
+
47
+ admin_router = APIRouter(
48
+ prefix="/admin",
49
+ tags=["Admin"],
50
+ )
51
+
52
+ # --- Admin Helper Functions ---
53
+ def get_file_info(file_path: Path):
54
+ try:
55
+ if file_path.is_file():
56
+ stat_info = file_path.stat()
57
+ return {
58
+ "name": file_path.name,
59
+ "size_bytes": stat_info.st_size,
60
+ "size_human": format_bytes(stat_info.st_size),
61
+ "modified_at": datetime.datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S UTC'),
62
+ "url": f"/download/{file_path.name}"
63
+ }
64
+ except Exception as e:
65
+ logger.error(f"Admin: Error getting file info for {file_path}: {e}")
66
+ return None
67
+ return None
68
+
69
+ def format_bytes(bytes_val, decimals=2):
70
+ if bytes_val == 0: return '0 Bytes'
71
+ k = 1024
72
+ dm = decimals if decimals >= 0 else 0
73
+ sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']
74
+ i = 0
75
+ if bytes_val > 0:
76
+ i = int(math.floor(math.log(bytes_val) / math.log(k)))
77
+ if i >= len(sizes): i = len(sizes) -1
78
+ return f"{bytes_val / (k**i):.{dm}f} {sizes[i]}"
79
+
80
+ # get_log_lines function removed as file logging is removed.
81
+
82
+ # --- Admin Authentication Dependencies ---
83
+ async def get_current_admin(request: Request):
84
+ session_cookie = request.cookies.get(ADMIN_SESSION_COOKIE_NAME)
85
+ if session_cookie == ADMIN_SESSION_SECRET_VALUE:
86
+ return {"email": ADMIN_EMAIL}
87
+
88
+ current_path = request.url.path
89
+ if current_path not in [app.url_path_for("admin_login_page"), app.url_path_for("admin_authenticate")]:
90
+ logger.info(f"Admin: Unauthorized access attempt to {current_path}, redirecting to login.")
91
+ return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
92
+ return None
93
+
94
+ async def verify_admin_auth(request: Request):
95
+ admin_user = await get_current_admin(request)
96
+ if isinstance(admin_user, RedirectResponse):
97
+ raise HTTPException(
98
+ status_code=status.HTTP_307_TEMPORARY_REDIRECT,
99
+ detail="Redirecting to login.",
100
+ headers={"Location": app.url_path_for("admin_login_page")},
101
+ )
102
+ if not admin_user:
103
+ logger.warning(f"Admin: Strict auth verification failed for {request.url.path}.")
104
+ raise HTTPException(
105
+ status_code=status.HTTP_401_UNAUTHORIZED,
106
+ detail="Not authenticated",
107
+ headers={"WWW-Authenticate": "Bearer"},
108
+ )
109
+ return admin_user
110
+
111
+ # --- Admin Routes ---
112
+ @admin_router.get("/login", response_class=HTMLResponse, name="admin_login_page")
113
+ async def admin_login_page(request: Request):
114
+ logger.info("Admin: Login page accessed.")
115
+ current_year = datetime.datetime.now(datetime.timezone.utc).year # Get current UTC year
116
+ return templates.TemplateResponse(
117
+ "admin_login.html",
118
+ {
119
+ "request": request,
120
+ "url_for": app.url_path_for,
121
+ "current_year": current_year # Add current year to context
122
+ }
123
+ )
124
+
125
+ @admin_router.post("/auth", name="admin_authenticate")
126
+ async def admin_authenticate(request: Request, email: str = Form(...), password: str = Form(...)):
127
+ if email == ADMIN_EMAIL and password == ADMIN_PASSWORD:
128
+ logger.info(f"Admin: Successful login for email {email}.")
129
+ response = RedirectResponse(url=app.url_path_for("admin_dashboard"), status_code=status.HTTP_303_SEE_OTHER)
130
+ response.set_cookie(
131
+ key=ADMIN_SESSION_COOKIE_NAME, value=ADMIN_SESSION_SECRET_VALUE,
132
+ httponly=True, samesite="lax", max_age=30 * 60
133
+ )
134
+ return response
135
+ else:
136
+ logger.warning(f"Admin: Failed login attempt for email {email}.")
137
+ error_msg = "Invalid email or password."
138
+ return templates.TemplateResponse(
139
+ "admin_login.html",
140
+ {"request": request, "error": error_msg, "url_for": app.url_path_for},
141
+ status_code=status.HTTP_401_UNAUTHORIZED
142
+ )
143
+
144
+ @admin_router.get("/logout", name="admin_logout")
145
+ async def admin_logout(request: Request):
146
+ logger.info("Admin: Logout initiated.")
147
+ response = RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_303_SEE_OTHER)
148
+ response.delete_cookie(ADMIN_SESSION_COOKIE_NAME, httponly=True, samesite="lax")
149
+ return response
150
+
151
+ @admin_router.get("/dashboard", response_class=HTMLResponse, name="admin_dashboard")
152
+ async def admin_dashboard(request: Request, admin_user: dict = Depends(get_current_admin)):
153
+ if isinstance(admin_user, RedirectResponse): return admin_user
154
+ if not admin_user: return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
155
+
156
+ logger.info(f"Admin: Dashboard accessed by {admin_user.get('email')}.")
157
+ num_files = 0
158
+ try:
159
+ num_files = len([name for name in os.listdir(FILES_DIR) if (FILES_DIR / name).is_file()])
160
+ except Exception as e:
161
+ logger.error(f"Admin: Error counting files for dashboard: {e}")
162
+
163
+ # log_lines_count removed
164
+ return templates.TemplateResponse("admin_dashboard.html", {
165
+ "request": request,
166
+ "num_files": num_files,
167
+ "admin_user": admin_user,
168
+ "url_for": app.url_path_for
169
+ })
170
+
171
+ @admin_router.get("/files", response_class=HTMLResponse, name="admin_files_page")
172
+ async def admin_files_page(request: Request, admin_user: dict = Depends(get_current_admin)):
173
+ if isinstance(admin_user, RedirectResponse): return admin_user
174
+ if not admin_user: return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
175
+
176
+ logger.info(f"Admin: Files page accessed by {admin_user.get('email')}.")
177
+ uploaded_file_objects = []
178
+ try:
179
+ for item_name in sorted(os.listdir(FILES_DIR)):
180
+ item_path = FILES_DIR / item_name
181
+ info = get_file_info(item_path)
182
+ if info:
183
+ uploaded_file_objects.append(info)
184
+ except Exception as e:
185
+ logger.error(f"Admin: Error listing files: {e}")
186
+
187
+ return templates.TemplateResponse("admin_files.html", {
188
+ "request": request,
189
+ "files": uploaded_file_objects,
190
+ "admin_user": admin_user,
191
+ "url_for": app.url_path_for
192
+ })
193
+
194
+ @admin_router.post("/files/delete/{filename}", name="admin_delete_file")
195
+ async def admin_delete_file(request: Request, filename: str, admin_user: dict = Depends(verify_admin_auth)):
196
+ logger.info(f"Admin: Attempting to delete file '{filename}' by {admin_user.get('email')}.")
197
+ safe_filename = sanitize_filename(filename)
198
+ file_path = FILES_DIR / safe_filename
199
+
200
+ if not file_path.resolve().is_relative_to(FILES_DIR.resolve()):
201
+ logger.error(f"Admin: Invalid file path for deletion (traversal attempt) '{filename}'.")
202
+ raise HTTPException(status_code=400, detail="Invalid file path for deletion.")
203
+
204
+ if file_path.is_file():
205
+ try:
206
+ file_path.unlink()
207
+ logger.info(f"Admin: File '{filename}' deleted successfully by {admin_user.get('email')}.")
208
+ return RedirectResponse(url=app.url_path_for("admin_files_page") + "?message=File+deleted+successfully", status_code=status.HTTP_303_SEE_OTHER)
209
+ except Exception as e:
210
+ logger.error(f"Admin: Could not delete file '{filename}': {str(e)}")
211
+ raise HTTPException(status_code=500, detail=f"Could not delete file: {str(e)}")
212
+ else:
213
+ logger.warning(f"Admin: File not found for deletion '{filename}'.")
214
+ raise HTTPException(status_code=404, detail="File not found for deletion.")
215
+
216
+ # /admin/logs route and admin_logs_page function removed
217
+
218
+ # --- Main Application Helper Functions (Original) ---
219
+ def sanitize_filename(filename: str) -> str:
220
+ return os.path.basename(filename).strip()
221
+
222
+ # --- Main Application Routes (Original functionality with added logging) ---
223
+ @app.get("/", response_class=HTMLResponse, name="read_root")
224
+ async def read_root(request: Request):
225
+ logger.info(f"Root page accessed by {request.client.host}")
226
+ return templates.TemplateResponse("index.html", {"request": request, "url_for": app.url_path_for})
227
+
228
+ @app.post("/upload/", name="upload_file")
229
+ async def upload_file(request: Request, file: UploadFile = File(...)):
230
+ original_filename = file.filename
231
+ logger.info(f"Upload attempt for '{original_filename}' from {request.client.host}")
232
+
233
+ if not original_filename:
234
+ logger.warning(f"Upload failed: No filename provided by {request.client.host}")
235
+ raise HTTPException(status_code=400, detail="No filename provided.")
236
+
237
+ filename = sanitize_filename(original_filename)
238
+ if not filename:
239
+ logger.warning(f"Upload failed: Invalid filename '{original_filename}' after sanitization by {request.client.host}")
240
+ raise HTTPException(status_code=400, detail="Invalid filename after sanitization.")
241
+
242
+ file_path = FILES_DIR / filename
243
+
244
+ if not file_path.resolve().is_relative_to(FILES_DIR.resolve()):
245
+ logger.error(f"Upload security violation: Attempted directory traversal for '{filename}' by {request.client.host}")
246
+ raise HTTPException(status_code=400, detail="Invalid file path (attempted directory traversal).")
247
+
248
+ try:
249
+ async with aiofiles.open(file_path, "wb") as buffer:
250
+ while content := await file.read(1024 * 1024):
251
+ await buffer.write(content)
252
+ file_size = file_path.stat().st_size
253
+ logger.info(f"File '{filename}' uploaded successfully by {request.client.host}. Size: {file_size} bytes.")
254
+ except Exception as e:
255
+ logger.error(f"Could not save file '{filename}' from {request.client.host}: {str(e)}")
256
+ if file_path.exists():
257
+ try:
258
+ file_path.unlink()
259
+ logger.info(f"Cleaned up partially uploaded file '{filename}'.")
260
+ except Exception as unlink_e:
261
+ logger.error(f"Error cleaning up partially uploaded file '{filename}': {unlink_e}")
262
+ raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}")
263
+
264
+ return JSONResponse(
265
+ content={
266
+ "message": "File uploaded successfully",
267
+ "filename": filename,
268
+ "download_url": app.url_path_for("download_file", filename=filename)
269
+ }
270
+ )
271
+
272
+ @app.get("/download/{filename}", name="download_file")
273
+ async def download_file(request: Request, filename: str):
274
+ clean_filename = sanitize_filename(filename)
275
+ logger.info(f"Download attempt for '{clean_filename}' by {request.client.host}")
276
+
277
+ if not clean_filename:
278
+ logger.warning(f"Download failed: Invalid filename '{filename}' by {request.client.host}")
279
+ raise HTTPException(status_code=400, detail="Invalid filename.")
280
+
281
+ file_path = FILES_DIR / clean_filename
282
+
283
+ if not file_path.resolve().is_relative_to(FILES_DIR.resolve()) or not file_path.is_file():
284
+ logger.warning(f"Download failed: File not found or access denied for '{clean_filename}' by {request.client.host}")
285
+ raise HTTPException(status_code=404, detail="File not found or access denied.")
286
+
287
+ logger.info(f"File '{clean_filename}' downloaded by {request.client.host}")
288
+ return FileResponse(path=file_path, filename=clean_filename, media_type='application/octet-stream')
289
+
290
+ app.include_router(admin_router)
291
+ logger.info("Admin panel router included. File-based logging is disabled.")
292
+
293
+ if __name__ == "__main__":
294
+ import uvicorn
295
+ logger.info("Starting Uvicorn server for local development...")
296
  uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)