triflix commited on
Commit
d7e15ca
·
verified ·
1 Parent(s): 97872c7

Upload 9 files

Browse files
app.py CHANGED
@@ -8,8 +8,7 @@ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse, Redirect
8
  from fastapi.staticfiles import StaticFiles
9
  from fastapi.templating import Jinja2Templates
10
  from pathlib import Path
11
- import logging
12
- from logging.handlers import RotatingFileHandler
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
@@ -19,24 +18,19 @@ 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 = BASE_DIR / "app.log"
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 ---
30
  logger = logging.getLogger("file_app")
31
  logger.setLevel(logging.INFO)
32
- logger.propagate = False
33
 
34
- file_handler = RotatingFileHandler(LOG_FILE, maxBytes=1024*1024*5, backupCount=2, encoding='utf-8')
35
- file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s')
36
- file_handler.setFormatter(file_formatter)
37
- logger.addHandler(file_handler)
38
-
39
- logger.info("Application starting up...")
40
 
41
  app = FastAPI(title="File Uploader/Downloader")
42
 
@@ -44,19 +38,14 @@ app = FastAPI(title="File Uploader/Downloader")
44
  templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
45
 
46
  # --- Admin Panel Configuration & Logic ---
47
-
48
- # INSECURE: Hardcoded credentials. Use environment variables or a secure auth system in production!
49
  ADMIN_EMAIL = "admin@example.com"
50
- ADMIN_PASSWORD = "adminpassword" # Store hashed passwords in a real application
51
-
52
  ADMIN_SESSION_COOKIE_NAME = "admin_session_id"
53
- # This should be a very random, long, and secret string in a real app
54
  ADMIN_SESSION_SECRET_VALUE = "my_super_secret_admin_session_value_for_demo_only_change_me"
55
 
56
  admin_router = APIRouter(
57
  prefix="/admin",
58
  tags=["Admin"],
59
- # dependencies=[Depends(verify_admin_auth)] # Apply to all routes in this router, or per-route
60
  )
61
 
62
  # --- Admin Helper Functions ---
@@ -69,7 +58,7 @@ def get_file_info(file_path: Path):
69
  "size_bytes": stat_info.st_size,
70
  "size_human": format_bytes(stat_info.st_size),
71
  "modified_at": datetime.datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S UTC'),
72
- "url": f"/download/{file_path.name}" # User-facing download URL
73
  }
74
  except Exception as e:
75
  logger.error(f"Admin: Error getting file info for {file_path}: {e}")
@@ -84,20 +73,10 @@ def format_bytes(bytes_val, decimals=2):
84
  i = 0
85
  if bytes_val > 0:
86
  i = int(math.floor(math.log(bytes_val) / math.log(k)))
87
- if i >= len(sizes): i = len(sizes) -1 # handle extremely large files gracefully
88
  return f"{bytes_val / (k**i):.{dm}f} {sizes[i]}"
89
 
90
- def get_log_lines(num_lines: int = 200) -> List[str]:
91
- if not LOG_FILE.exists():
92
- logger.warning("Admin: Log file not found.")
93
- return ["Log file not found or is empty."]
94
- try:
95
- with open(LOG_FILE, "r", encoding="utf-8") as f:
96
- lines = f.readlines()
97
- return list(reversed(lines[-num_lines:]))
98
- except Exception as e:
99
- logger.error(f"Admin: Error reading log file: {e}")
100
- return [f"Error reading log file: {str(e)}"]
101
 
102
  # --- Admin Authentication Dependencies ---
103
  async def get_current_admin(request: Request):
@@ -106,7 +85,6 @@ async def get_current_admin(request: Request):
106
  return {"email": ADMIN_EMAIL}
107
 
108
  current_path = request.url.path
109
- # Allow access to login and auth paths without redirection loop
110
  if current_path not in [app.url_path_for("admin_login_page"), app.url_path_for("admin_authenticate")]:
111
  logger.info(f"Admin: Unauthorized access attempt to {current_path}, redirecting to login.")
112
  return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
@@ -114,11 +92,7 @@ async def get_current_admin(request: Request):
114
 
115
  async def verify_admin_auth(request: Request):
116
  admin_user = await get_current_admin(request)
117
- if isinstance(admin_user, RedirectResponse): # If get_current_admin decided to redirect
118
- # This will interrupt the request flow and send the redirect response.
119
- # It's a bit unusual to raise HTTPException for a redirect, but FastAPI handles it.
120
- # A cleaner way might be to just return the RedirectResponse from the endpoint if the dependency returns it.
121
- # However, for a dependency that *protects* a route, raising is common.
122
  raise HTTPException(
123
  status_code=status.HTTP_307_TEMPORARY_REDIRECT,
124
  detail="Redirecting to login.",
@@ -145,12 +119,8 @@ async def admin_authenticate(request: Request, email: str = Form(...), password:
145
  logger.info(f"Admin: Successful login for email {email}.")
146
  response = RedirectResponse(url=app.url_path_for("admin_dashboard"), status_code=status.HTTP_303_SEE_OTHER)
147
  response.set_cookie(
148
- key=ADMIN_SESSION_COOKIE_NAME,
149
- value=ADMIN_SESSION_SECRET_VALUE,
150
- httponly=True,
151
- samesite="lax",
152
- # secure=True, # Uncomment if served over HTTPS
153
- max_age=30 * 60 # 30 minutes
154
  )
155
  return response
156
  else:
@@ -166,7 +136,7 @@ async def admin_authenticate(request: Request, email: str = Form(...), password:
166
  async def admin_logout(request: Request):
167
  logger.info("Admin: Logout initiated.")
168
  response = RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_303_SEE_OTHER)
169
- response.delete_cookie(ADMIN_SESSION_COOKIE_NAME, httponly=True, samesite="lax") #, secure=True)
170
  return response
171
 
172
  @admin_router.get("/dashboard", response_class=HTMLResponse, name="admin_dashboard")
@@ -181,19 +151,10 @@ async def admin_dashboard(request: Request, admin_user: dict = Depends(get_curre
181
  except Exception as e:
182
  logger.error(f"Admin: Error counting files for dashboard: {e}")
183
 
184
- log_lines_count = 0
185
- if LOG_FILE.exists():
186
- try:
187
- with open(LOG_FILE, "r", encoding="utf-8") as f:
188
- log_lines_count = sum(1 for _ in f)
189
- except Exception as e:
190
- logger.error(f"Admin: Error counting log lines for dashboard: {e}")
191
-
192
-
193
  return templates.TemplateResponse("admin_dashboard.html", {
194
  "request": request,
195
  "num_files": num_files,
196
- "log_lines_count": log_lines_count,
197
  "admin_user": admin_user,
198
  "url_for": app.url_path_for
199
  })
@@ -213,7 +174,6 @@ async def admin_files_page(request: Request, admin_user: dict = Depends(get_curr
213
  uploaded_file_objects.append(info)
214
  except Exception as e:
215
  logger.error(f"Admin: Error listing files: {e}")
216
- # Optionally, pass an error to the template
217
 
218
  return templates.TemplateResponse("admin_files.html", {
219
  "request": request,
@@ -224,10 +184,8 @@ async def admin_files_page(request: Request, admin_user: dict = Depends(get_curr
224
 
225
  @admin_router.post("/files/delete/{filename}", name="admin_delete_file")
226
  async def admin_delete_file(request: Request, filename: str, admin_user: dict = Depends(verify_admin_auth)):
227
- # verify_admin_auth handles unauthorized access
228
-
229
  logger.info(f"Admin: Attempting to delete file '{filename}' by {admin_user.get('email')}.")
230
- safe_filename = sanitize_filename(filename) # Re-sanitize
231
  file_path = FILES_DIR / safe_filename
232
 
233
  if not file_path.resolve().is_relative_to(FILES_DIR.resolve()):
@@ -238,7 +196,6 @@ async def admin_delete_file(request: Request, filename: str, admin_user: dict =
238
  try:
239
  file_path.unlink()
240
  logger.info(f"Admin: File '{filename}' deleted successfully by {admin_user.get('email')}.")
241
- # Add a success message via query param for display (optional)
242
  return RedirectResponse(url=app.url_path_for("admin_files_page") + "?message=File+deleted+successfully", status_code=status.HTTP_303_SEE_OTHER)
243
  except Exception as e:
244
  logger.error(f"Admin: Could not delete file '{filename}': {str(e)}")
@@ -247,36 +204,20 @@ async def admin_delete_file(request: Request, filename: str, admin_user: dict =
247
  logger.warning(f"Admin: File not found for deletion '{filename}'.")
248
  raise HTTPException(status_code=404, detail="File not found for deletion.")
249
 
250
-
251
- @admin_router.get("/logs", response_class=HTMLResponse, name="admin_logs_page")
252
- async def admin_logs_page(request: Request, admin_user: dict = Depends(get_current_admin)):
253
- if isinstance(admin_user, RedirectResponse): return admin_user
254
- if not admin_user: return RedirectResponse(url=app.url_path_for("admin_login_page"), status_code=status.HTTP_307_TEMPORARY_REDIRECT)
255
-
256
- logger.info(f"Admin: Logs page accessed by {admin_user.get('email')}.")
257
- logs = get_log_lines(num_lines=500)
258
- return templates.TemplateResponse("admin_logs.html", {
259
- "request": request,
260
- "log_entries": logs,
261
- "admin_user": admin_user,
262
- "url_for": app.url_path_for
263
- })
264
 
265
  # --- Main Application Helper Functions (Original) ---
266
  def sanitize_filename(filename: str) -> str:
267
- """Basic filename sanitization."""
268
  return os.path.basename(filename).strip()
269
 
270
  # --- Main Application Routes (Original functionality with added logging) ---
271
  @app.get("/", response_class=HTMLResponse, name="read_root")
272
  async def read_root(request: Request):
273
- """Serves the main HTML page."""
274
  logger.info(f"Root page accessed by {request.client.host}")
275
  return templates.TemplateResponse("index.html", {"request": request, "url_for": app.url_path_for})
276
 
277
  @app.post("/upload/", name="upload_file")
278
  async def upload_file(request: Request, file: UploadFile = File(...)):
279
- """Handles file uploads."""
280
  original_filename = file.filename
281
  logger.info(f"Upload attempt for '{original_filename}' from {request.client.host}")
282
 
@@ -297,9 +238,9 @@ async def upload_file(request: Request, file: UploadFile = File(...)):
297
 
298
  try:
299
  async with aiofiles.open(file_path, "wb") as buffer:
300
- while content := await file.read(1024 * 1024): # Read 1MB chunks
301
  await buffer.write(content)
302
- file_size = file_path.stat().st_size # Get actual size after writing
303
  logger.info(f"File '{filename}' uploaded successfully by {request.client.host}. Size: {file_size} bytes.")
304
  except Exception as e:
305
  logger.error(f"Could not save file '{filename}' from {request.client.host}: {str(e)}")
@@ -321,7 +262,6 @@ async def upload_file(request: Request, file: UploadFile = File(...)):
321
 
322
  @app.get("/download/{filename}", name="download_file")
323
  async def download_file(request: Request, filename: str):
324
- """Handles file downloads."""
325
  clean_filename = sanitize_filename(filename)
326
  logger.info(f"Download attempt for '{clean_filename}' by {request.client.host}")
327
 
@@ -338,12 +278,10 @@ async def download_file(request: Request, filename: str):
338
  logger.info(f"File '{clean_filename}' downloaded by {request.client.host}")
339
  return FileResponse(path=file_path, filename=clean_filename, media_type='application/octet-stream')
340
 
341
- # Include the admin router
342
  app.include_router(admin_router)
343
- logger.info("Admin panel router included and configured.")
344
 
345
  if __name__ == "__main__":
346
  import uvicorn
347
  logger.info("Starting Uvicorn server for local development...")
348
- # Use "app:app" to refer to the FastAPI instance named 'app' in the file 'app.py'
349
  uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True)
 
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
 
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
 
 
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 ---
 
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}")
 
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):
 
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)
 
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.",
 
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:
 
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")
 
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
  })
 
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,
 
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()):
 
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)}")
 
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
 
 
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)}")
 
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
 
 
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)
templates/admin_dashboard.html CHANGED
@@ -16,12 +16,12 @@
16
  </div>
17
  </div>
18
  <div class="col-md-6">
19
- <div class="card text-dark bg-warning mb-3"> <!-- Consider bg-info or another color for logs -->
20
- <div class="card-header">Application Logs</div>
21
  <div class="card-body">
22
- <h5 class="card-title">{{ log_lines_count }}</h5>
23
- <p class="card-text">Total lines in the current log file (app.log).</p>
24
- <a href="{{ url_for('admin_logs_page') }}" class="btn btn-dark">View Logs <i class="bi bi-arrow-right-short"></i></a>
25
  </div>
26
  </div>
27
  </div>
@@ -30,6 +30,6 @@
30
  <div class="mt-4">
31
  <h4>Quick Info</h4>
32
  <p>Welcome, {{ admin_user.email }}!</p>
33
- <!-- Add more quick actions or system info if needed -->
34
  </div>
35
  {% endblock %}
 
16
  </div>
17
  </div>
18
  <div class="col-md-6">
19
+ <div class="card text-dark bg-secondary mb-3"> <!-- Changed color for variety -->
20
+ <div class="card-header">Application Status</div>
21
  <div class="card-body">
22
+ <h5 class="card-title text-success">Running</h5>
23
+ <p class="card-text">Application is operational. Logs are directed to the container console.</p>
24
+ <a href="{{ url_for('read_root') }}" target="_blank" class="btn btn-light">View User Site <i class="bi bi-box-arrow-up-right"></i></a>
25
  </div>
26
  </div>
27
  </div>
 
30
  <div class="mt-4">
31
  <h4>Quick Info</h4>
32
  <p>Welcome, {{ admin_user.email }}!</p>
33
+ <p>Server logs are available through the container's standard output (e.g., `docker logs`).</p>
34
  </div>
35
  {% endblock %}
templates/admin_layout.html CHANGED
@@ -50,11 +50,7 @@
50
  <i class="bi bi-file-earmark-arrow-up-fill"></i> Uploaded Files
51
  </a>
52
  </li>
53
- <li class="nav-item">
54
- <a class="nav-link {% if request.url.path == url_for('admin_logs_page') %}active{% endif %}" href="{{ url_for('admin_logs_page') }}">
55
- <i class="bi bi-card-list"></i> Application Logs
56
- </a>
57
- </li>
58
  <li class="nav-item mt-auto mb-2 border-top pt-2">
59
  <a class="nav-link" href="{{ url_for('read_root') }}" target="_blank">
60
  <i class="bi bi-box-arrow-up-right"></i> View Main Site
 
50
  <i class="bi bi-file-earmark-arrow-up-fill"></i> Uploaded Files
51
  </a>
52
  </li>
53
+ <!-- Application Logs link removed -->
 
 
 
 
54
  <li class="nav-item mt-auto mb-2 border-top pt-2">
55
  <a class="nav-link" href="{{ url_for('read_root') }}" target="_blank">
56
  <i class="bi bi-box-arrow-up-right"></i> View Main Site