triflix commited on
Commit
ad0e3b3
·
verified ·
1 Parent(s): 8396b83

Upload 10 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Create directories that the application might need to write to or expect to exist.
6
+ # These are created by root during the build.
7
+ RUN mkdir -p /app/templates \
8
+ /app/static \
9
+ /app/uploaded_files
10
+
11
+ # Copy application code
12
+ COPY ./app.py /app/
13
+ COPY ./admin_logic.py /app/ # Added: Copy the new admin logic file
14
+ COPY ./templates/ /app/templates/
15
+ # If you had local static files, you'd copy them here:
16
+ # COPY ./static/ /app/static/
17
+
18
+ # Grant write permissions to the 'uploaded_files' directory for the user running the app.
19
+ # Hugging Face Spaces (and many other container platforms) often run containers
20
+ # as a non-root user (e.g., UID 1000).
21
+ # We give ownership to user 1000 and group 1000.
22
+ # You could also use `chmod -R 777 /app/uploaded_files` but chown is more specific.
23
+ RUN chown -R 1000:1000 /app/uploaded_files \
24
+ && chmod -R u+w /app/uploaded_files # Ensure the owner (user 1000) has write permissions \
25
+ && chown 1000:1000 /app # Also ensure the app directory and log file can be written by user 1000
26
+
27
+ # Install dependencies
28
+ RUN pip install --no-cache-dir fastapi "uvicorn[standard]" aiofiles Jinja2 python-multipart
29
+
30
+ EXPOSE 7860
31
+
32
+ # The log file app.log will be created in /app/ which should be writable by user 1000
33
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
admin_logic.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import datetime as dt
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from fastapi import APIRouter, Request, Depends, Form, HTTPException, status
8
+ from fastapi.responses import HTMLResponse, RedirectResponse, Response
9
+ from fastapi.templating import Jinja2Templates
10
+ import aiofiles # For async file operations if needed in admin, though mostly sync for listing/deleting
11
+
12
+ # --- Admin Configuration ---
13
+ BASE_DIR = Path(__file__).resolve().parent
14
+ TEMPLATES_DIR = BASE_DIR / "templates"
15
+ FILES_DIR = BASE_DIR / "uploaded_files"
16
+ LOG_FILE = BASE_DIR / "app.log"
17
+
18
+ # Ensure directories exist (though app.py also does this)
19
+ FILES_DIR.mkdir(parents=True, exist_ok=True)
20
+
21
+ templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
22
+
23
+ # INSECURE: Hardcoded credentials. Use environment variables in production!
24
+ ADMIN_EMAIL = "admin@example.com"
25
+ ADMIN_PASSWORD = "adminpassword" # Store hashed passwords in a real application
26
+
27
+ ADMIN_SESSION_COOKIE_NAME = "admin_session_id"
28
+ # This should be a very random, long, and secret string in a real app
29
+ ADMIN_SESSION_SECRET_VALUE = "my_super_secret_admin_session_value_for_demo"
30
+
31
+ admin_router = APIRouter(
32
+ prefix="/admin",
33
+ tags=["Admin"]
34
+ )
35
+
36
+ # --- Admin Authentication Dependencies ---
37
+ async def get_current_admin(request: Request):
38
+ """
39
+ Dependency to check if the admin is logged in.
40
+ Redirects to login page if not authenticated.
41
+ """
42
+ session_cookie = request.cookies.get(ADMIN_SESSION_COOKIE_NAME)
43
+ if session_cookie == ADMIN_SESSION_SECRET_VALUE:
44
+ return {"email": ADMIN_EMAIL} # Return a dummy admin object
45
+
46
+ # If trying to access a protected page without being logged in, redirect to login
47
+ # Allow access to login page itself without this check.
48
+ if request.url.path != "/admin/login" and request.url.path != "/admin/auth":
49
+ return RedirectResponse(url="/admin/login", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
50
+ # If on login/auth page, don't raise/redirect, let the route handler proceed
51
+ return None
52
+
53
+
54
+ async def verify_admin_auth(request: Request):
55
+ """
56
+ Stricter dependency for protected routes. Raises HTTPException if not authenticated.
57
+ Used for API-like endpoints or where a redirect isn't the primary action.
58
+ """
59
+ admin = await get_current_admin(request)
60
+ if not admin and request.url.path not in ["/admin/login", "/admin/auth"]: # Check if it's already a redirect response
61
+ raise HTTPException(
62
+ status_code=status.HTTP_401_UNAUTHORIZED,
63
+ detail="Not authenticated",
64
+ headers={"WWW-Authenticate": "Bearer"}, # Though we are using cookies
65
+ )
66
+ if isinstance(admin, RedirectResponse): # if get_current_admin decided to redirect
67
+ raise HTTPException(
68
+ status_code=status.HTTP_307_TEMPORARY_REDIRECT,
69
+ detail="Not authenticated, redirecting.",
70
+ headers={"Location": "/admin/login"},
71
+ )
72
+ return admin
73
+
74
+
75
+ # --- Helper Functions for Admin Panel ---
76
+ def get_file_info(file_path: Path):
77
+ try:
78
+ if file_path.is_file():
79
+ stat_info = file_path.stat()
80
+ return {
81
+ "name": file_path.name,
82
+ "size_bytes": stat_info.st_size,
83
+ "size_human": format_bytes(stat_info.st_size),
84
+ "modified_at": dt.datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
85
+ "url": f"/download/{file_path.name}" # User-facing download URL
86
+ }
87
+ except Exception:
88
+ return None
89
+ return None
90
+
91
+ def format_bytes(bytes_val, decimals=2):
92
+ if bytes_val == 0: return '0 Bytes'
93
+ k = 1024
94
+ dm = decimals if decimals >= 0 else 0
95
+ sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']
96
+ i = 0
97
+ if bytes_val > 0:
98
+ i = int(dt.math.floor(dt.math.log(bytes_val) / dt.math.log(k)))
99
+ return f"{bytes_val / (k**i):.{dm}f} {sizes[i]}"
100
+
101
+ def get_log_lines(num_lines: int = 200) -> List[str]:
102
+ if not LOG_FILE.exists():
103
+ return ["Log file not found or is empty."]
104
+ try:
105
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
106
+ lines = f.readlines()
107
+ # Display latest lines at the top for easier reading
108
+ return list(reversed(lines[-num_lines:]))
109
+ except Exception as e:
110
+ return [f"Error reading log file: {str(e)}"]
111
+
112
+ # --- Admin Routes ---
113
+ @admin_router.get("/login", response_class=HTMLResponse)
114
+ async def admin_login_page(request: Request):
115
+ return templates.TemplateResponse("admin_login.html", {"request": request})
116
+
117
+ @admin_router.post("/auth")
118
+ async def admin_authenticate(request: Request, email: str = Form(...), password: str = Form(...)):
119
+ error_msg: Optional[str] = None
120
+ if email == ADMIN_EMAIL and password == ADMIN_PASSWORD:
121
+ response = RedirectResponse(url="/admin/dashboard", status_code=status.HTTP_303_SEE_OTHER)
122
+ response.set_cookie(
123
+ key=ADMIN_SESSION_COOKIE_NAME,
124
+ value=ADMIN_SESSION_SECRET_VALUE,
125
+ httponly=True, # Makes it inaccessible to JavaScript
126
+ samesite="lax", # Protects against CSRF to some extent
127
+ # secure=True, # Uncomment if served over HTTPS
128
+ max_age=1800 # 30 minutes
129
+ )
130
+ return response
131
+ else:
132
+ error_msg = "Invalid email or password."
133
+ return templates.TemplateResponse("admin_login.html", {"request": request, "error": error_msg}, status_code=status.HTTP_401_UNAUTHORIZED)
134
+
135
+ @admin_router.get("/logout")
136
+ async def admin_logout(request: Request):
137
+ response = RedirectResponse(url="/admin/login", status_code=status.HTTP_303_SEE_OTHER)
138
+ response.delete_cookie(ADMIN_SESSION_COOKIE_NAME, httponly=True, samesite="lax") #, secure=True)
139
+ return response
140
+
141
+ @admin_router.get("/dashboard", response_class=HTMLResponse)
142
+ async def admin_dashboard(request: Request, admin_user: dict = Depends(get_current_admin)):
143
+ if isinstance(admin_user, RedirectResponse): return admin_user # Handle redirect from dependency
144
+ if not admin_user: # Should be caught by dependency, but as a fallback
145
+ return RedirectResponse(url="/admin/login", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
146
+
147
+ num_files = len([name for name in os.listdir(FILES_DIR) if (FILES_DIR / name).is_file()])
148
+ log_lines_count = 0
149
+ if LOG_FILE.exists():
150
+ with open(LOG_FILE, "r", encoding="utf-8") as f:
151
+ log_lines_count = sum(1 for _ in f)
152
+
153
+ return templates.TemplateResponse("admin_dashboard.html", {
154
+ "request": request,
155
+ "num_files": num_files,
156
+ "log_lines_count": log_lines_count
157
+ })
158
+
159
+ @admin_router.get("/files", response_class=HTMLResponse)
160
+ async def admin_files_page(request: Request, admin_user: dict = Depends(get_current_admin)):
161
+ if isinstance(admin_user, RedirectResponse): return admin_user
162
+ if not admin_user: return RedirectResponse(url="/admin/login", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
163
+
164
+ uploaded_file_objects = []
165
+ try:
166
+ for item_name in sorted(os.listdir(FILES_DIR)):
167
+ item_path = FILES_DIR / item_name
168
+ info = get_file_info(item_path)
169
+ if info:
170
+ uploaded_file_objects.append(info)
171
+ except Exception as e:
172
+ # Log this error, maybe show it on the page
173
+ print(f"Error listing files: {e}") # Replace with proper logging
174
+ pass # Or raise HTTPException
175
+
176
+ return templates.TemplateResponse("admin_files.html", {
177
+ "request": request,
178
+ "files": uploaded_file_objects
179
+ })
180
+
181
+ @admin_router.post("/files/delete/{filename}")
182
+ async def admin_delete_file(request: Request, filename: str, admin_user: dict = Depends(verify_admin_auth)):
183
+ if isinstance(admin_user, RedirectResponse): return admin_user # Should not happen with verify_admin_auth
184
+ if not admin_user: return RedirectResponse(url="/admin/login", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
185
+
186
+ # Sanitize filename again, though it comes from our listed files
187
+ safe_filename = os.path.basename(filename)
188
+ file_path = FILES_DIR / safe_filename
189
+
190
+ # Extra check to ensure we are deleting within FILES_DIR
191
+ if not file_path.resolve().is_relative_to(FILES_DIR.resolve()):
192
+ raise HTTPException(status_code=400, detail="Invalid file path for deletion.")
193
+
194
+ if file_path.is_file():
195
+ try:
196
+ file_path.unlink()
197
+ # Optionally, add a success message to be displayed (e.g., via query params or session flash if implemented)
198
+ # For now, just redirect.
199
+ return RedirectResponse(url="/admin/files", status_code=status.HTTP_303_SEE_OTHER)
200
+ except Exception as e:
201
+ # Log this error
202
+ raise HTTPException(status_code=500, detail=f"Could not delete file: {str(e)}")
203
+ else:
204
+ raise HTTPException(status_code=404, detail="File not found for deletion.")
205
+
206
+
207
+ @admin_router.get("/logs", response_class=HTMLResponse)
208
+ async def admin_logs_page(request: Request, admin_user: dict = Depends(get_current_admin)):
209
+ if isinstance(admin_user, RedirectResponse): return admin_user
210
+ if not admin_user: return RedirectResponse(url="/admin/login", status_code=status.HTTP_307_TEMPORARY_REDIRECT)
211
+
212
+ logs = get_log_lines(num_lines=500) # Show last 500 lines
213
+ return templates.TemplateResponse("admin_logs.html", {
214
+ "request": request,
215
+ "log_entries": logs
216
+ })
app.py ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import aiofiles # For async file operations, good practice with FastAPI
4
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Request
5
+ from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
6
+ from fastapi.staticfiles import StaticFiles
7
+ from fastapi.templating import Jinja2Templates
8
+ from pathlib import Path
9
+ import logging # Added for logging
10
+ from logging.handlers import RotatingFileHandler # Added for logging
11
+
12
+ # --- Configuration ---
13
+ BASE_DIR = Path(__file__).resolve().parent
14
+ FILES_DIR = BASE_DIR / "uploaded_files"
15
+ STATIC_DIR = BASE_DIR / "static"
16
+ TEMPLATES_DIR = BASE_DIR / "templates"
17
+ LOG_FILE = BASE_DIR / "app.log" # Added for logging
18
+
19
+ # Create directories if they don't exist
20
+ FILES_DIR.mkdir(parents=True, exist_ok=True)
21
+ STATIC_DIR.mkdir(parents=True, exist_ok=True)
22
+ TEMPLATES_DIR.mkdir(parents=True, exist_ok=True)
23
+
24
+ # --- Logging Configuration (New) ---
25
+ logger = logging.getLogger("file_app")
26
+ logger.setLevel(logging.INFO)
27
+ # Prevent duplicate logs if uvicorn also logs
28
+ logger.propagate = False
29
+
30
+ # File handler
31
+ # Rotate log file if it exceeds 5MB, keep 2 backup files
32
+ file_handler = RotatingFileHandler(LOG_FILE, maxBytes=1024*1024*5, backupCount=2, encoding='utf-8')
33
+ file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(module)s:%(lineno)d - %(message)s')
34
+ file_handler.setFormatter(file_formatter)
35
+ logger.addHandler(file_handler)
36
+
37
+ # Console handler (optional, as uvicorn also logs to console)
38
+ # console_handler = logging.StreamHandler()
39
+ # console_handler.setFormatter(file_formatter)
40
+ # logger.addHandler(console_handler)
41
+
42
+ logger.info("Application starting up...")
43
+
44
+
45
+ app = FastAPI(title="File Uploader/Downloader")
46
+
47
+ # Mount static files
48
+ # app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
49
+
50
+ # Setup Jinja2 templates
51
+ templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
52
+
53
+ # --- Import and Mount Admin Router (New) ---
54
+ from admin_logic import admin_router # Make sure admin_logic.py is in the same directory
55
+ app.include_router(admin_router)
56
+ logger.info("Admin panel router included.")
57
+
58
+ # --- Helper Functions ---
59
+ def sanitize_filename(filename: str) -> str:
60
+ """Basic filename sanitization."""
61
+ return os.path.basename(filename).strip()
62
+
63
+ # --- Routes (Original functionality with added logging) ---
64
+ @app.get("/", response_class=HTMLResponse)
65
+ async def read_root(request: Request):
66
+ """Serves the main HTML page."""
67
+ logger.info(f"Root page accessed by {request.client.host}")
68
+ return templates.TemplateResponse("index.html", {"request": request})
69
+
70
+ @app.post("/upload/")
71
+ async def upload_file(request: Request, file: UploadFile = File(...)):
72
+ """Handles file uploads."""
73
+ original_filename = file.filename
74
+ logger.info(f"Upload attempt for '{original_filename}' from {request.client.host}")
75
+
76
+ if not original_filename:
77
+ logger.warning(f"Upload failed: No filename provided by {request.client.host}")
78
+ raise HTTPException(status_code=400, detail="No filename provided.")
79
+
80
+ filename = sanitize_filename(original_filename)
81
+ if not filename: # After sanitization, if it's empty
82
+ logger.warning(f"Upload failed: Invalid filename '{original_filename}' after sanitization by {request.client.host}")
83
+ raise HTTPException(status_code=400, detail="Invalid filename after sanitization.")
84
+
85
+ file_path = FILES_DIR / filename
86
+
87
+ if not file_path.resolve().is_relative_to(FILES_DIR.resolve()):
88
+ logger.error(f"Upload security violation: Attempted directory traversal for '{filename}' by {request.client.host}")
89
+ raise HTTPException(status_code=400, detail="Invalid file path (attempted directory traversal).")
90
+
91
+ try:
92
+ async with aiofiles.open(file_path, "wb") as buffer:
93
+ while content := await file.read(1024 * 1024): # Read 1MB chunks
94
+ await buffer.write(content)
95
+ logger.info(f"File '{filename}' uploaded successfully by {request.client.host}. Size: {file.size} bytes.")
96
+ except Exception as e:
97
+ logger.error(f"Could not save file '{filename}' from {request.client.host}: {str(e)}")
98
+ if file_path.exists():
99
+ try:
100
+ file_path.unlink()
101
+ logger.info(f"Cleaned up partially uploaded file '{filename}'.")
102
+ except Exception as unlink_e:
103
+ logger.error(f"Error cleaning up partially uploaded file '{filename}': {unlink_e}")
104
+ raise HTTPException(status_code=500, detail=f"Could not save file: {str(e)}")
105
+
106
+ return JSONResponse(
107
+ content={
108
+ "message": "File uploaded successfully",
109
+ "filename": filename,
110
+ "download_url": f"/download/{filename}"
111
+ }
112
+ )
113
+
114
+ @app.get("/download/{filename}")
115
+ async def download_file(request: Request, filename: str):
116
+ """Handles file downloads."""
117
+ clean_filename = sanitize_filename(filename)
118
+ logger.info(f"Download attempt for '{clean_filename}' by {request.client.host}")
119
+
120
+ if not clean_filename:
121
+ logger.warning(f"Download failed: Invalid filename '{filename}' by {request.client.host}")
122
+ raise HTTPException(status_code=400, detail="Invalid filename.")
123
+
124
+ file_path = FILES_DIR / clean_filename
125
+
126
+ if not file_path.resolve().is_relative_to(FILES_DIR.resolve()) or not file_path.is_file():
127
+ logger.warning(f"Download failed: File not found or access denied for '{clean_filename}' by {request.client.host}")
128
+ raise HTTPException(status_code=404, detail="File not found or access denied.")
129
+
130
+ logger.info(f"File '{clean_filename}' downloaded by {request.client.host}")
131
+ return FileResponse(path=file_path, filename=clean_filename, media_type='application/octet-stream')
132
+
133
+ if __name__ == "__main__":
134
+ import uvicorn
135
+ logger.info("Starting Uvicorn server for local development...")
136
+ uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=True) # Added reload for easier dev
static/dummy ADDED
File without changes
templates/admin_dashboard.html ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin_layout.html" %}
2
+
3
+ {% block title %}Dashboard{% endblock %}
4
+ {% block page_title %}Dashboard{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="row">
8
+ <div class="col-md-6">
9
+ <div class="card text-white bg-primary mb-3">
10
+ <div class="card-header">Uploaded Files</div>
11
+ <div class="card-body">
12
+ <h5 class="card-title">{{ num_files }}</h5>
13
+ <p class="card-text">Total files currently stored.</p>
14
+ <a href="{{ url_for('admin_files_page') }}" class="btn btn-light">Manage Files <i class="bi bi-arrow-right-short"></i></a>
15
+ </div>
16
+ </div>
17
+ </div>
18
+ <div class="col-md-6">
19
+ <div class="card text-dark bg-warning mb-3">
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.</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>
28
+ </div>
29
+
30
+ <div class="mt-4">
31
+ <h4>Quick Actions</h4>
32
+ <ul>
33
+ <li><a href="{{ url_for('read_root') }}" target="_blank">View User Upload Page</a></li>
34
+ <!-- Add more quick actions if needed -->
35
+ </ul>
36
+ </div>
37
+
38
+ <div class="mt-4">
39
+ <h4>System Information (Example)</h4>
40
+ <p>This is a basic admin dashboard. You can expand it with more system information, statistics, or quick management tools.</p>
41
+ <p><strong>Admin Email:</strong> {{ request.user.email if request.user else "N/A" }} (Note: This is illustrative from dummy user)</p>
42
+ </div>
43
+ {% endblock %}
templates/admin_files.html ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin_layout.html" %}
2
+
3
+ {% block title %}Uploaded Files{% endblock %}
4
+ {% block page_title %}Uploaded Files{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="table-responsive">
8
+ <table class="table table-striped table-sm">
9
+ <thead>
10
+ <tr>
11
+ <th scope="col">#</th>
12
+ <th scope="col">Filename</th>
13
+ <th scope="col">Size</th>
14
+ <th scope="col">Last Modified (UTC)</th>
15
+ <th scope="col">Download URL</th>
16
+ <th scope="col">Actions</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ {% if files %}
21
+ {% for file in files %}
22
+ <tr>
23
+ <td>{{ loop.index }}</td>
24
+ <td>{{ file.name }}</td>
25
+ <td>{{ file.size_human }}</td>
26
+ <td>{{ file.modified_at }}</td>
27
+ <td><a href="{{ file.url }}" target="_blank" title="User download link">Link</a></td>
28
+ <td>
29
+ <form method="post" action="{{ url_for('admin_delete_file', filename=file.name) }}" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete {{ file.name }}?');">
30
+ <button type="submit" class="btn btn-danger btn-sm">
31
+ <i class="bi bi-trash-fill"></i> Delete
32
+ </button>
33
+ </form>
34
+ </td>
35
+ </tr>
36
+ {% endfor %}
37
+ {% else %}
38
+ <tr>
39
+ <td colspan="6" class="text-center">No files uploaded yet.</td>
40
+ </tr>
41
+ {% endif %}
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+ {% endblock %}
templates/admin_layout.html ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin Panel - {% block title %}Dashboard{% endblock %}</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
9
+ <style>
10
+ body {
11
+ font-size: .875rem;
12
+ }
13
+ .sidebar {
14
+ position: fixed;
15
+ top: 0;
16
+ bottom: 0;
17
+ left: 0;
18
+ z-index: 100;
19
+ padding: 48px 0 0;
20
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
21
+ }
22
+ .sidebar-sticky {
23
+ position: relative;
24
+ top: 0;
25
+ height: calc(100vh - 48px);
26
+ padding-top: .5rem;
27
+ overflow-x: hidden;
28
+ overflow-y: auto;
29
+ }
30
+ .nav-link {
31
+ font-weight: 500;
32
+ color: #333;
33
+ }
34
+ .nav-link .bi {
35
+ margin-right: 4px;
36
+ color: #727272;
37
+ }
38
+ .nav-link.active {
39
+ color: #007bff;
40
+ }
41
+ .nav-link:hover .bi,
42
+ .nav-link.active .bi {
43
+ color: inherit;
44
+ }
45
+ .navbar-brand {
46
+ padding-top: .75rem;
47
+ padding-bottom: .75rem;
48
+ font-size: 1rem;
49
+ background-color: rgba(0, 0, 0, .25);
50
+ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
51
+ }
52
+ .navbar .navbar-toggler {
53
+ top: .25rem;
54
+ right: 1rem;
55
+ }
56
+ </style>
57
+ </head>
58
+ <body>
59
+ <header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
60
+ <a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="{{ url_for('admin_dashboard') }}">Admin Panel</a>
61
+ <button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
62
+ <span class="navbar-toggler-icon"></span>
63
+ </button>
64
+ <!-- Optional: Search bar or other elements -->
65
+ <div class="navbar-nav">
66
+ <div class="nav-item text-nowrap">
67
+ <a class="nav-link px-3" href="{{ url_for('admin_logout') }}">Sign out</a>
68
+ </div>
69
+ </div>
70
+ </header>
71
+
72
+ <div class="container-fluid">
73
+ <div class="row">
74
+ <nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
75
+ <div class="position-sticky pt-3">
76
+ <ul class="nav flex-column">
77
+ <li class="nav-item">
78
+ <a class="nav-link {% if request.url.path == url_for('admin_dashboard') %}active{% endif %}" aria-current="page" href="{{ url_for('admin_dashboard') }}">
79
+ <i class="bi bi-house-door-fill"></i> Dashboard
80
+ </a>
81
+ </li>
82
+ <li class="nav-item">
83
+ <a class="nav-link {% if request.url.path == url_for('admin_files_page') %}active{% endif %}" href="{{ url_for('admin_files_page') }}">
84
+ <i class="bi bi-file-earmark-arrow-up-fill"></i> Uploaded Files
85
+ </a>
86
+ </li>
87
+ <li class="nav-item">
88
+ <a class="nav-link {% if request.url.path == url_for('admin_logs_page') %}active{% endif %}" href="{{ url_for('admin_logs_page') }}">
89
+ <i class="bi bi-card-list"></i> Application Logs
90
+ </a>
91
+ </li>
92
+ <li class="nav-item mt-auto mb-2">
93
+ <a class="nav-link" href="{{ url_for('read_root') }}" target="_blank">
94
+ <i class="bi bi-box-arrow-up-right"></i> View Main Site
95
+ </a>
96
+ </li>
97
+ </ul>
98
+ </div>
99
+ </nav>
100
+
101
+ <main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
102
+ <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
103
+ <h1 class="h2">{% block page_title %}{% endblock %}</h1>
104
+ </div>
105
+
106
+ {% block content %}{% endblock %}
107
+ </main>
108
+ </div>
109
+ </div>
110
+
111
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
112
+ </body>
113
+ </html>
templates/admin_login.html ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Admin Login</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ display: flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ min-height: 100vh;
14
+ background-color: #f5f5f5;
15
+ }
16
+ .form-signin {
17
+ width: 100%;
18
+ max-width: 330px;
19
+ padding: 15px;
20
+ margin: auto;
21
+ }
22
+ </style>
23
+ </head>
24
+ <body>
25
+ <main class="form-signin text-center">
26
+ <form method="post" action="{{ url_for('admin_authenticate') }}">
27
+ <!-- <img class="mb-4" src="/docs/5.3/assets/brand/bootstrap-logo.svg" alt="" width="72" height="57"> -->
28
+ <h1 class="h3 mb-3 fw-normal">Admin Panel Login</h1>
29
+
30
+ {% if error %}
31
+ <div class="alert alert-danger" role="alert">
32
+ {{ error }}
33
+ </div>
34
+ {% endif %}
35
+
36
+ <div class="form-floating mb-2">
37
+ <input type="email" class="form-control" id="email" name="email" placeholder="name@example.com" required value="admin@example.com">
38
+ <label for="email">Email address</label>
39
+ </div>
40
+ <div class="form-floating">
41
+ <input type="password" class="form-control" id="password" name="password" placeholder="Password" required value="adminpassword">
42
+ <label for="password">Password</label>
43
+ </div>
44
+
45
+ <!-- <div class="checkbox mb-3">
46
+ <label>
47
+ <input type="checkbox" value="remember-me"> Remember me
48
+ </label>
49
+ </div> -->
50
+ <button class="w-100 btn btn-lg btn-primary mt-3" type="submit">Sign in</button>
51
+ <p class="mt-5 mb-3 text-muted">© Your App Name {% now 'local', '%Y' %}</p>
52
+ </form>
53
+ </main>
54
+ </body>
55
+ </html>
templates/admin_logs.html ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin_layout.html" %}
2
+
3
+ {% block title %}Application Logs{% endblock %}
4
+ {% block page_title %}Application Logs{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="mb-3">
8
+ <p>Showing the last {{ log_entries|length }} log entries (newest first). The full log is in <code>app.log</code>.</p>
9
+ <a href="{{ url_for('admin_logs_page') }}" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-clockwise"></i> Refresh Logs</a>
10
+ </div>
11
+
12
+ <div class="log-container bg-light p-3 rounded border" style="max-height: 600px; overflow-y: auto; font-family: monospace; font-size: 0.85em; white-space: pre-wrap; word-break: break-all;">
13
+ {% if log_entries %}
14
+ {% for entry in log_entries %}
15
+ <div>{{ entry.strip() }}</div>
16
+ {% endfor %}
17
+ {% else %}
18
+ <div>No log entries found or log file is empty.</div>
19
+ {% endif %}
20
+ </div>
21
+ {% endblock %}
templates/index.html ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>File Uploader</title>
7
+ <!-- Bootstrap CSS -->
8
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <style>
10
+ body {
11
+ padding-top: 40px;
12
+ padding-bottom: 40px;
13
+ background-color: #f5f5f5;
14
+ }
15
+ .container {
16
+ max-width: 600px;
17
+ }
18
+ .upload-form {
19
+ background-color: #fff;
20
+ padding: 30px;
21
+ border-radius: 8px;
22
+ box-shadow: 0 0 10px rgba(0,0,0,0.1);
23
+ }
24
+ .progress-container {
25
+ margin-top: 20px;
26
+ display: none; /* Hidden by default */
27
+ }
28
+ #uploadInfo {
29
+ margin-top: 10px;
30
+ font-size: 0.9em;
31
+ }
32
+ #downloadLinkContainer {
33
+ margin-top: 20px;
34
+ display: none; /* Hidden by default */
35
+ }
36
+ .loader {
37
+ border: 5px solid #f3f3f3; /* Light grey */
38
+ border-top: 5px solid #3498db; /* Blue */
39
+ border-radius: 50%;
40
+ width: 30px;
41
+ height: 30px;
42
+ animation: spin 1s linear infinite;
43
+ display: none; /* Hidden by default */
44
+ margin: 10px auto;
45
+ }
46
+ @keyframes spin {
47
+ 0% { transform: rotate(0deg); }
48
+ 100% { transform: rotate(360deg); }
49
+ }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="container">
54
+ <div class="upload-form">
55
+ <h2 class="text-center mb-4">Upload a File</h2>
56
+ <form id="uploadForm" enctype="multipart/form-data">
57
+ <div class="mb-3">
58
+ <label for="fileInput" class="form-label">Choose file</label>
59
+ <input class="form-control" type="file" id="fileInput" name="file" required>
60
+ </div>
61
+ <button type="submit" class="btn btn-primary w-100">Upload</button>
62
+ </form>
63
+
64
+ <div class="loader" id="loader"></div>
65
+
66
+ <div class="progress-container" id="progressContainer">
67
+ <div class="progress">
68
+ <div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
69
+ </div>
70
+ <div id="uploadInfo" class="text-muted"></div>
71
+ </div>
72
+
73
+ <div id="downloadLinkContainer" class="alert alert-success" role="alert">
74
+ <strong>Success!</strong> File uploaded. <br>
75
+ Download link: <a href="#" id="downloadLink" target="_blank"></a>
76
+ </div>
77
+
78
+ <div id="errorContainer" class="alert alert-danger mt-3" role="alert" style="display: none;">
79
+ <strong>Error:</strong> <span id="errorMessage"></span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Bootstrap JS Bundle (Popper.js included) -->
85
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
86
+ <script>
87
+ const uploadForm = document.getElementById('uploadForm');
88
+ const fileInput = document.getElementById('fileInput');
89
+ const loader = document.getElementById('loader');
90
+ const progressContainer = document.getElementById('progressContainer');
91
+ const progressBar = document.getElementById('progressBar');
92
+ const uploadInfo = document.getElementById('uploadInfo');
93
+ const downloadLinkContainer = document.getElementById('downloadLinkContainer');
94
+ const downloadLink = document.getElementById('downloadLink');
95
+ const errorContainer = document.getElementById('errorContainer');
96
+ const errorMessage = document.getElementById('errorMessage');
97
+ let startTime;
98
+ uploadForm.addEventListener('submit', async function(event) {
99
+ event.preventDefault();
100
+ const file = fileInput.files[0];
101
+ if (!file) {
102
+ alert('Please select a file to upload.');
103
+ return;
104
+ }
105
+ const formData = new FormData();
106
+ formData.append('file', file);
107
+ // Reset UI
108
+ loader.style.display = 'block';
109
+ progressContainer.style.display = 'block';
110
+ progressBar.style.width = '0%';
111
+ progressBar.textContent = '0%';
112
+ uploadInfo.textContent = 'Starting upload...';
113
+ downloadLinkContainer.style.display = 'none';
114
+ errorContainer.style.display = 'none';
115
+ startTime = Date.now();
116
+ const xhr = new XMLHttpRequest();
117
+ xhr.open('POST', '/upload/', true);
118
+ xhr.upload.onprogress = function(event) {
119
+ if (event.lengthComputable) {
120
+ const percentComplete = Math.round((event.loaded / event.total) * 100);
121
+ progressBar.style.width = percentComplete + '%';
122
+ progressBar.textContent = percentComplete + '%';
123
+ const elapsedTime = (Date.now() - startTime) / 1000; // seconds
124
+ const speed = event.loaded / elapsedTime; // bytes per second
125
+ const speedMbps = (speed * 8 / 1000000).toFixed(2); // Mbps
126
+ uploadInfo.textContent = `Uploaded ${formatBytes(event.loaded)} of ${formatBytes(event.total)} (${percentComplete}%) at ${speedMbps} Mbps`;
127
+ }
128
+ };
129
+ xhr.onload = function() {
130
+ loader.style.display = 'none';
131
+ if (xhr.status === 200) {
132
+ const response = JSON.parse(xhr.responseText);
133
+ uploadInfo.textContent = 'Upload complete!';
134
+ progressBar.classList.remove('progress-bar-animated');
135
+ progressBar.classList.add('bg-success');
136
+ downloadLink.href = response.download_url;
137
+ // The filename in the link text should be the one returned by the server (sanitized)
138
+ downloadLink.textContent = response.filename;
139
+ downloadLinkContainer.style.display = 'block';
140
+ } else {
141
+ progressBar.classList.remove('progress-bar-animated');
142
+ progressBar.classList.add('bg-danger');
143
+ try {
144
+ const errorResponse = JSON.parse(xhr.responseText);
145
+ errorMessage.textContent = errorResponse.detail || `Server error: ${xhr.status}`;
146
+ } catch (e) {
147
+ errorMessage.textContent = `Server error: ${xhr.status} - ${xhr.statusText}`;
148
+ }
149
+ errorContainer.style.display = 'block';
150
+ uploadInfo.textContent = 'Upload failed.';
151
+ }
152
+ };
153
+ xhr.onerror = function() {
154
+ loader.style.display = 'none';
155
+ progressBar.classList.remove('progress-bar-animated');
156
+ progressBar.classList.add('bg-danger');
157
+ errorMessage.textContent = 'Network error or server unavailable.';
158
+ errorContainer.style.display = 'block';
159
+ uploadInfo.textContent = 'Upload failed.';
160
+ };
161
+ xhr.send(formData);
162
+ });
163
+ function formatBytes(bytes, decimals = 2) {
164
+ if (bytes === 0) return '0 Bytes';
165
+ const k = 1024;
166
+ const dm = decimals < 0 ? 0 : decimals;
167
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
168
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
169
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
170
+ }
171
+ </script>
172
+ </body>
173
+ </html>