Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .gitignore +4 -1
- app/main.py +200 -3
- app/routers/drive/storage.py +363 -52
- app/routers/image/bgremover.py +403 -18
- app/routers/image/jpgcompressor.py +379 -67
- app/routers/pdf/pdf_tools.py +406 -13
- app/routers/security/password_generator.py +410 -22
- app/routers/testers/help.py +233 -10
- app/routers/testers/server_status.py +313 -13
.gitignore
CHANGED
|
@@ -15,4 +15,7 @@ uploaded_files
|
|
| 15 |
|
| 16 |
# vs code config
|
| 17 |
.vscode
|
| 18 |
-
backend.code-workspace
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# vs code config
|
| 17 |
.vscode
|
| 18 |
+
backend.code-workspace
|
| 19 |
+
|
| 20 |
+
llm_backend_structure_documentation.md
|
| 21 |
+
raw_api_docs.md
|
app/main.py
CHANGED
|
@@ -63,7 +63,204 @@ app.include_router(bgremover.router) # Background Remover
|
|
| 63 |
@app.get("/", response_class=HTMLResponse)
|
| 64 |
def greet_json():
|
| 65 |
return """
|
| 66 |
-
|
| 67 |
-
<
|
| 68 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
"""
|
|
|
|
| 63 |
@app.get("/", response_class=HTMLResponse)
|
| 64 |
def greet_json():
|
| 65 |
return """
|
| 66 |
+
<!DOCTYPE html>
|
| 67 |
+
<html lang="en" data-theme="light">
|
| 68 |
+
<head>
|
| 69 |
+
<meta charset="UTF-8">
|
| 70 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 71 |
+
<title>Easy Tools Hub | by Sam</title>
|
| 72 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 73 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 74 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 75 |
+
|
| 76 |
+
<style>
|
| 77 |
+
:root {
|
| 78 |
+
--primary: #6366f1;
|
| 79 |
+
--primary-hover: #4f46e5;
|
| 80 |
+
--bg-body: #f8fafc;
|
| 81 |
+
--bg-card: #ffffff;
|
| 82 |
+
--text-main: #0f172a;
|
| 83 |
+
--text-sub: #64748b;
|
| 84 |
+
--border: #e2e8f0;
|
| 85 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 86 |
+
--gradient-1: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
[data-theme="dark"] {
|
| 90 |
+
--primary: #818cf8;
|
| 91 |
+
--primary-hover: #6366f1;
|
| 92 |
+
--bg-body: #0f172a;
|
| 93 |
+
--bg-card: #1e293b;
|
| 94 |
+
--text-main: #f8fafc;
|
| 95 |
+
--text-sub: #94a3b8;
|
| 96 |
+
--border: #334155;
|
| 97 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 98 |
+
--gradient-1: linear-gradient(135deg, #4f46e5 0%, #7e22ce 100%);
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: all 0.3s ease; }
|
| 102 |
+
|
| 103 |
+
body {
|
| 104 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 105 |
+
background-color: var(--bg-body);
|
| 106 |
+
color: var(--text-main);
|
| 107 |
+
min-height: 100vh;
|
| 108 |
+
display: flex;
|
| 109 |
+
flex-direction: column;
|
| 110 |
+
align-items: center;
|
| 111 |
+
padding: 2rem 1rem;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
/* Header */
|
| 115 |
+
.header-container {
|
| 116 |
+
text-align: center; margin-bottom: 3rem; animation: fadeIn 0.8s ease-out;
|
| 117 |
+
}
|
| 118 |
+
.title {
|
| 119 |
+
font-size: 2.5rem; font-weight: 800; margin-bottom: 0.5rem;
|
| 120 |
+
background: var(--gradient-1); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 121 |
+
}
|
| 122 |
+
.subtitle { color: var(--text-sub); font-size: 1.1rem; }
|
| 123 |
+
|
| 124 |
+
.theme-toggle {
|
| 125 |
+
position: absolute; top: 1rem; right: 1rem;
|
| 126 |
+
background: var(--bg-card); border: 1px solid var(--border);
|
| 127 |
+
color: var(--text-main); padding: 10px; border-radius: 50%; cursor: pointer;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* Grid */
|
| 131 |
+
.tools-grid {
|
| 132 |
+
display: grid;
|
| 133 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 134 |
+
gap: 1.5rem;
|
| 135 |
+
width: 100%; max-width: 1000px;
|
| 136 |
+
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
/* Cards */
|
| 140 |
+
.tool-card {
|
| 141 |
+
background: var(--bg-card);
|
| 142 |
+
border-radius: 20px;
|
| 143 |
+
padding: 2rem;
|
| 144 |
+
border: 1px solid var(--border);
|
| 145 |
+
box-shadow: var(--shadow);
|
| 146 |
+
text-decoration: none;
|
| 147 |
+
color: var(--text-main);
|
| 148 |
+
display: flex; flex-direction: column; align-items: center; text-align: center;
|
| 149 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 150 |
+
position: relative; overflow: hidden;
|
| 151 |
+
}
|
| 152 |
+
.tool-card:hover {
|
| 153 |
+
transform: translateY(-5px);
|
| 154 |
+
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 10px 10px -5px rgb(0 0 0 / 0.04);
|
| 155 |
+
border-color: var(--primary);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.icon { font-size: 3rem; margin-bottom: 1rem; }
|
| 159 |
+
.card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 0.5rem; }
|
| 160 |
+
.card-desc { font-size: 0.9rem; color: var(--text-sub); line-height: 1.5; }
|
| 161 |
+
|
| 162 |
+
/* Footer */
|
| 163 |
+
.dev-footer {
|
| 164 |
+
margin-top: 4rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
|
| 165 |
+
padding: 2rem; width: 100%; max-width: 600px; border-top: 1px solid var(--border);
|
| 166 |
+
animation: fadeIn 1.2s ease-out;
|
| 167 |
+
}
|
| 168 |
+
.dev-badge {
|
| 169 |
+
display: inline-block; background: var(--bg-card); padding: 8px 20px;
|
| 170 |
+
border-radius: 30px; border: 1px solid var(--border); font-weight: 600; margin-top: 10px;
|
| 171 |
+
}
|
| 172 |
+
.dev-name { color: var(--primary); font-weight: 800; }
|
| 173 |
+
.github-link {
|
| 174 |
+
display: inline-flex; align-items: center; gap: 8px; margin-top: 15px;
|
| 175 |
+
color: var(--text-main); text-decoration: none; font-weight: 600;
|
| 176 |
+
padding: 8px 16px; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border);
|
| 177 |
+
}
|
| 178 |
+
.github-link:hover { background: var(--bg-body); }
|
| 179 |
+
|
| 180 |
+
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
| 181 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 182 |
+
</style>
|
| 183 |
+
</head>
|
| 184 |
+
<body>
|
| 185 |
+
|
| 186 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">π</button>
|
| 187 |
+
|
| 188 |
+
<div class="header-container">
|
| 189 |
+
<div class="title">Easy Tools Suite</div>
|
| 190 |
+
<div class="subtitle">Sameer Banchhor</div>
|
| 191 |
+
</div>
|
| 192 |
+
|
| 193 |
+
<div class="tools-grid">
|
| 194 |
+
|
| 195 |
+
<a href="/image/ui" class="tool-card">
|
| 196 |
+
<div class="icon">πΌοΈ</div>
|
| 197 |
+
<div class="card-title">Image Tools</div>
|
| 198 |
+
<div class="card-desc">Compress JPEGs to specific sizes & remove backgrounds instantly using AI.</div>
|
| 199 |
+
</a>
|
| 200 |
+
|
| 201 |
+
<a href="/pdf/ui" class="tool-card">
|
| 202 |
+
<div class="icon">π</div>
|
| 203 |
+
<div class="card-title">PDF Master</div>
|
| 204 |
+
<div class="card-desc">Merge multiple documents or rotate pages with a simple drag & drop interface.</div>
|
| 205 |
+
</a>
|
| 206 |
+
|
| 207 |
+
<a href="/drive/storage/ui" class="tool-card">
|
| 208 |
+
<div class="icon">βοΈ</div>
|
| 209 |
+
<div class="card-title">Cloud Drive</div>
|
| 210 |
+
<div class="card-desc">Upload files locally or use Aria2 for high-speed remote server downloads.</div>
|
| 211 |
+
</a>
|
| 212 |
+
|
| 213 |
+
<a href="/security/password-generator/ui" class="tool-card">
|
| 214 |
+
<div class="icon">π</div>
|
| 215 |
+
<div class="card-title">Security</div>
|
| 216 |
+
<div class="card-desc">Generate cryptographically strong passwords with advanced custom rules.</div>
|
| 217 |
+
</a>
|
| 218 |
+
|
| 219 |
+
<a href="/server-status/ui" class="tool-card">
|
| 220 |
+
<div class="icon">π</div>
|
| 221 |
+
<div class="card-title">Server Monitor</div>
|
| 222 |
+
<div class="card-desc">Real-time dashboard for CPU, RAM, Disk usage, and Network traffic.</div>
|
| 223 |
+
</a>
|
| 224 |
+
|
| 225 |
+
<a href="/docs" class="tool-card" style="border-style: dashed;">
|
| 226 |
+
<div class="icon">β‘</div>
|
| 227 |
+
<div class="card-title">API Docs</div>
|
| 228 |
+
<div class="card-desc">Explore the raw Swagger UI for testing API endpoints directly.</div>
|
| 229 |
+
</a>
|
| 230 |
+
|
| 231 |
+
</div>
|
| 232 |
+
|
| 233 |
+
<footer class="dev-footer">
|
| 234 |
+
<div>Designed & Developed by</div>
|
| 235 |
+
<div class="dev-badge">
|
| 236 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 237 |
+
</div>
|
| 238 |
+
<br>
|
| 239 |
+
<div style="font-size: 0.85rem; margin-top: 5px; opacity: 0.8;">MSc in System Design, Kalyan PG College</div>
|
| 240 |
+
|
| 241 |
+
<a href="https://github.com/sameerbanchhor-git" target="_blank" class="github-link">
|
| 242 |
+
<svg height="20" width="20" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
| 243 |
+
GitHub Profile
|
| 244 |
+
</a>
|
| 245 |
+
</footer>
|
| 246 |
+
|
| 247 |
+
<script>
|
| 248 |
+
// Theme Logic
|
| 249 |
+
const html = document.documentElement;
|
| 250 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 251 |
+
|
| 252 |
+
function toggleTheme() {
|
| 253 |
+
const current = html.getAttribute('data-theme');
|
| 254 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 255 |
+
html.setAttribute('data-theme', next);
|
| 256 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 257 |
+
localStorage.setItem('theme', next);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 261 |
+
html.setAttribute('data-theme', savedTheme);
|
| 262 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 263 |
+
</script>
|
| 264 |
+
</body>
|
| 265 |
+
</html>
|
| 266 |
"""
|
app/routers/drive/storage.py
CHANGED
|
@@ -12,9 +12,16 @@ from fastapi import (
|
|
| 12 |
WebSocket,
|
| 13 |
WebSocketDisconnect
|
| 14 |
)
|
|
|
|
| 15 |
|
| 16 |
-
# Import Auth System
|
| 17 |
-
from app.routers.auth.system import get_current_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
# Router Configuration
|
| 20 |
router = APIRouter(
|
|
@@ -27,9 +34,8 @@ UPLOAD_DIR = "uploaded_files"
|
|
| 27 |
MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024 # 1GB Limit
|
| 28 |
|
| 29 |
# π Auth Toggle
|
| 30 |
-
AUTH_ENABLED = False
|
| 31 |
|
| 32 |
-
# Auth Wrapper (returns dependency or None)
|
| 33 |
def auth_dependency():
|
| 34 |
return Depends(get_current_user) if AUTH_ENABLED else None
|
| 35 |
|
|
@@ -40,12 +46,10 @@ os.makedirs(UPLOAD_DIR, exist_ok=True)
|
|
| 40 |
# π οΈ Helper: Safe Username Extraction
|
| 41 |
# ==========================================
|
| 42 |
def get_safe_username(user_obj):
|
| 43 |
-
if not user_obj:
|
| 44 |
-
|
| 45 |
-
if
|
| 46 |
-
|
| 47 |
-
return user_obj.get("username", "anonymous")
|
| 48 |
-
|
| 49 |
|
| 50 |
# ==========================================
|
| 51 |
# π€ Standard File Upload
|
|
@@ -80,46 +84,8 @@ async def upload_file(
|
|
| 80 |
"message": "File uploaded successfully"
|
| 81 |
}
|
| 82 |
|
| 83 |
-
|
| 84 |
-
# ==========================================
|
| 85 |
-
# π Remote Upload (Aria2)
|
| 86 |
-
# ==========================================
|
| 87 |
-
@router.post("/remote-upload/")
|
| 88 |
-
async def remote_url_upload(
|
| 89 |
-
url: str,
|
| 90 |
-
custom_filename: str = None,
|
| 91 |
-
current_user: object = auth_dependency()
|
| 92 |
-
):
|
| 93 |
-
username = get_safe_username(current_user)
|
| 94 |
-
|
| 95 |
-
command = ["aria2c", "-x", "6", "-s", "6", "-d", UPLOAD_DIR, url]
|
| 96 |
-
if custom_filename:
|
| 97 |
-
command.extend(["-o", custom_filename])
|
| 98 |
-
|
| 99 |
-
try:
|
| 100 |
-
process = await asyncio.create_subprocess_exec(
|
| 101 |
-
*command,
|
| 102 |
-
stdout=asyncio.subprocess.PIPE,
|
| 103 |
-
stderr=asyncio.subprocess.PIPE
|
| 104 |
-
)
|
| 105 |
-
stdout, stderr = await process.communicate()
|
| 106 |
-
|
| 107 |
-
if process.returncode != 0:
|
| 108 |
-
raise HTTPException(status_code=500, detail=f"Aria2 Error: {stderr.decode()}")
|
| 109 |
-
|
| 110 |
-
except Exception as e:
|
| 111 |
-
raise HTTPException(status_code=500, detail=f"System Error: {str(e)}")
|
| 112 |
-
|
| 113 |
-
return {
|
| 114 |
-
"user": username,
|
| 115 |
-
"source": url,
|
| 116 |
-
"filename": custom_filename or "Auto-detected",
|
| 117 |
-
"message": "Download completed via Aria2"
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
|
| 121 |
# ==========================================
|
| 122 |
-
# π‘ WebSocket Remote Upload
|
| 123 |
# ==========================================
|
| 124 |
@router.websocket("/ws/remote-upload/")
|
| 125 |
async def websocket_remote_upload(websocket: WebSocket):
|
|
@@ -135,11 +101,17 @@ async def websocket_remote_upload(websocket: WebSocket):
|
|
| 135 |
await websocket.close()
|
| 136 |
return
|
| 137 |
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
if custom_filename:
|
| 140 |
command.extend(["-o", custom_filename])
|
| 141 |
|
| 142 |
-
await websocket.send_text(f"π Starting download: {target_url}")
|
| 143 |
|
| 144 |
process = await asyncio.create_subprocess_exec(
|
| 145 |
*command,
|
|
@@ -147,6 +119,7 @@ async def websocket_remote_upload(websocket: WebSocket):
|
|
| 147 |
stderr=asyncio.subprocess.PIPE
|
| 148 |
)
|
| 149 |
|
|
|
|
| 150 |
while True:
|
| 151 |
line = await process.stdout.readline()
|
| 152 |
if not line:
|
|
@@ -160,10 +133,12 @@ async def websocket_remote_upload(websocket: WebSocket):
|
|
| 160 |
await websocket.send_text(decoded_line)
|
| 161 |
|
| 162 |
await process.wait()
|
|
|
|
| 163 |
if process.returncode == 0:
|
| 164 |
await websocket.send_text("β
Download Complete.")
|
| 165 |
else:
|
| 166 |
-
await
|
|
|
|
| 167 |
|
| 168 |
await websocket.close()
|
| 169 |
|
|
@@ -175,3 +150,339 @@ async def websocket_remote_upload(websocket: WebSocket):
|
|
| 175 |
await websocket.close()
|
| 176 |
except:
|
| 177 |
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
WebSocket,
|
| 13 |
WebSocketDisconnect
|
| 14 |
)
|
| 15 |
+
from fastapi.responses import HTMLResponse
|
| 16 |
|
| 17 |
+
# Import Auth System (Assuming this exists in your project structure)
|
| 18 |
+
# from app.routers.auth.system import get_current_user
|
| 19 |
+
|
| 20 |
+
# Mocking auth for standalone functionality if import fails
|
| 21 |
+
try:
|
| 22 |
+
from app.routers.auth.system import get_current_user
|
| 23 |
+
except ImportError:
|
| 24 |
+
def get_current_user(): return "anonymous"
|
| 25 |
|
| 26 |
# Router Configuration
|
| 27 |
router = APIRouter(
|
|
|
|
| 34 |
MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024 # 1GB Limit
|
| 35 |
|
| 36 |
# π Auth Toggle
|
| 37 |
+
AUTH_ENABLED = False
|
| 38 |
|
|
|
|
| 39 |
def auth_dependency():
|
| 40 |
return Depends(get_current_user) if AUTH_ENABLED else None
|
| 41 |
|
|
|
|
| 46 |
# π οΈ Helper: Safe Username Extraction
|
| 47 |
# ==========================================
|
| 48 |
def get_safe_username(user_obj):
|
| 49 |
+
if not user_obj: return "anonymous"
|
| 50 |
+
if hasattr(user_obj, "username"): return user_obj.username
|
| 51 |
+
if isinstance(user_obj, dict): return user_obj.get("username", "anonymous")
|
| 52 |
+
return str(user_obj)
|
|
|
|
|
|
|
| 53 |
|
| 54 |
# ==========================================
|
| 55 |
# π€ Standard File Upload
|
|
|
|
| 84 |
"message": "File uploaded successfully"
|
| 85 |
}
|
| 86 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
# ==========================================
|
| 88 |
+
# π‘ WebSocket Remote Upload
|
| 89 |
# ==========================================
|
| 90 |
@router.websocket("/ws/remote-upload/")
|
| 91 |
async def websocket_remote_upload(websocket: WebSocket):
|
|
|
|
| 101 |
await websocket.close()
|
| 102 |
return
|
| 103 |
|
| 104 |
+
# Check if aria2c is installed
|
| 105 |
+
if shutil.which("aria2c") is None:
|
| 106 |
+
await websocket.send_text("β Error: 'aria2c' is not installed on the server.")
|
| 107 |
+
await websocket.close()
|
| 108 |
+
return
|
| 109 |
+
|
| 110 |
+
command = ["aria2c", "-x", "8", "-s", "8", "-d", UPLOAD_DIR, target_url]
|
| 111 |
if custom_filename:
|
| 112 |
command.extend(["-o", custom_filename])
|
| 113 |
|
| 114 |
+
await websocket.send_text(f"π Starting download: {target_url}...")
|
| 115 |
|
| 116 |
process = await asyncio.create_subprocess_exec(
|
| 117 |
*command,
|
|
|
|
| 119 |
stderr=asyncio.subprocess.PIPE
|
| 120 |
)
|
| 121 |
|
| 122 |
+
# Stream stdout line by line
|
| 123 |
while True:
|
| 124 |
line = await process.stdout.readline()
|
| 125 |
if not line:
|
|
|
|
| 133 |
await websocket.send_text(decoded_line)
|
| 134 |
|
| 135 |
await process.wait()
|
| 136 |
+
|
| 137 |
if process.returncode == 0:
|
| 138 |
await websocket.send_text("β
Download Complete.")
|
| 139 |
else:
|
| 140 |
+
stderr_data = await process.stderr.read()
|
| 141 |
+
await websocket.send_text(f"β Download Failed: {stderr_data.decode()}")
|
| 142 |
|
| 143 |
await websocket.close()
|
| 144 |
|
|
|
|
| 150 |
await websocket.close()
|
| 151 |
except:
|
| 152 |
pass
|
| 153 |
+
|
| 154 |
+
# ==========================================
|
| 155 |
+
# π¨ Modern UI Endpoint
|
| 156 |
+
# ==========================================
|
| 157 |
+
# Path becomes: /drive/storage/ui
|
| 158 |
+
@router.get("/storage/ui", response_class=HTMLResponse)
|
| 159 |
+
async def drive_ui():
|
| 160 |
+
html_content = """
|
| 161 |
+
<!DOCTYPE html>
|
| 162 |
+
<html lang="en" data-theme="light">
|
| 163 |
+
<head>
|
| 164 |
+
<meta charset="UTF-8">
|
| 165 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 166 |
+
<title>Cloud Drive | by Sam</title>
|
| 167 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 168 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 169 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 170 |
+
|
| 171 |
+
<style>
|
| 172 |
+
/* --- Shared Theme Config --- */
|
| 173 |
+
:root {
|
| 174 |
+
--primary: #3b82f6;
|
| 175 |
+
--primary-hover: #2563eb;
|
| 176 |
+
--bg-body: #f8fafc;
|
| 177 |
+
--bg-card: #ffffff;
|
| 178 |
+
--text-main: #0f172a;
|
| 179 |
+
--text-sub: #64748b;
|
| 180 |
+
--border: #e2e8f0;
|
| 181 |
+
--drop-bg: #f1f5f9;
|
| 182 |
+
--drop-hover: #dbeafe;
|
| 183 |
+
--term-bg: #1e293b;
|
| 184 |
+
--term-text: #34d399;
|
| 185 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
[data-theme="dark"] {
|
| 189 |
+
--primary: #60a5fa;
|
| 190 |
+
--primary-hover: #3b82f6;
|
| 191 |
+
--bg-body: #0f172a;
|
| 192 |
+
--bg-card: #1e293b;
|
| 193 |
+
--text-main: #f8fafc;
|
| 194 |
+
--text-sub: #94a3b8;
|
| 195 |
+
--border: #334155;
|
| 196 |
+
--drop-bg: #1e293b;
|
| 197 |
+
--drop-hover: #1e3a8a;
|
| 198 |
+
--term-bg: #0f172a;
|
| 199 |
+
--term-text: #34d399;
|
| 200 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
|
| 204 |
+
|
| 205 |
+
body {
|
| 206 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 207 |
+
background-color: var(--bg-body);
|
| 208 |
+
color: var(--text-main);
|
| 209 |
+
min-height: 100vh;
|
| 210 |
+
display: flex;
|
| 211 |
+
flex-direction: column;
|
| 212 |
+
align-items: center;
|
| 213 |
+
justify-content: center;
|
| 214 |
+
padding: 1rem;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
/* --- Navbar --- */
|
| 218 |
+
.navbar {
|
| 219 |
+
width: 100%; max-width: 1000px; display: flex;
|
| 220 |
+
justify-content: space-between; align-items: center; margin-bottom: 2rem;
|
| 221 |
+
animation: fadeIn 0.8s ease-out;
|
| 222 |
+
}
|
| 223 |
+
.brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
|
| 224 |
+
.brand span { color: var(--primary); }
|
| 225 |
+
|
| 226 |
+
.theme-toggle {
|
| 227 |
+
background: none; border: none; cursor: pointer; color: var(--text-main);
|
| 228 |
+
font-size: 1.2rem; padding: 8px; border-radius: 50%;
|
| 229 |
+
background-color: var(--bg-card); border: 1px solid var(--border);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
/* --- Container & Tabs --- */
|
| 233 |
+
.container {
|
| 234 |
+
background: var(--bg-card); width: 100%; max-width: 800px;
|
| 235 |
+
border-radius: 24px; box-shadow: var(--shadow);
|
| 236 |
+
border: 1px solid var(--border); overflow: hidden;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.tabs { display: flex; border-bottom: 1px solid var(--border); }
|
| 240 |
+
.tab-btn {
|
| 241 |
+
flex: 1; padding: 1rem; border: none; background: transparent;
|
| 242 |
+
font-family: inherit; font-weight: 600; color: var(--text-sub);
|
| 243 |
+
cursor: pointer; transition: all 0.2s; border-bottom: 3px solid transparent;
|
| 244 |
+
}
|
| 245 |
+
.tab-btn:hover { background-color: var(--drop-bg); }
|
| 246 |
+
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
|
| 247 |
+
|
| 248 |
+
.tab-content { padding: 2.5rem; display: none; animation: fadeIn 0.4s ease; }
|
| 249 |
+
.tab-content.active { display: block; }
|
| 250 |
+
|
| 251 |
+
/* --- Forms --- */
|
| 252 |
+
.input-group { margin-bottom: 1.5rem; }
|
| 253 |
+
.input-group label { display: block; font-size: 0.9rem; font-weight: 600; color: var(--text-sub); margin-bottom: 0.5rem; }
|
| 254 |
+
.input-field {
|
| 255 |
+
width: 100%; padding: 0.8rem; border-radius: 12px; border: 1px solid var(--border);
|
| 256 |
+
background-color: var(--bg-body); color: var(--text-main); font-family: inherit;
|
| 257 |
+
}
|
| 258 |
+
.input-field:focus { outline: 2px solid var(--primary); border-color: transparent; }
|
| 259 |
+
|
| 260 |
+
/* --- Upload Zone --- */
|
| 261 |
+
.drop-area {
|
| 262 |
+
border: 2px dashed var(--border); border-radius: 16px; padding: 3rem 1rem;
|
| 263 |
+
text-align: center; cursor: pointer; background-color: var(--drop-bg);
|
| 264 |
+
transition: all 0.3s; margin-bottom: 1rem;
|
| 265 |
+
}
|
| 266 |
+
.drop-area:hover, .drop-area.dragover {
|
| 267 |
+
border-color: var(--primary); background-color: var(--drop-hover); transform: scale(1.01);
|
| 268 |
+
}
|
| 269 |
+
.drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; }
|
| 270 |
+
|
| 271 |
+
/* --- Terminal --- */
|
| 272 |
+
.terminal {
|
| 273 |
+
background-color: var(--term-bg); color: var(--term-text);
|
| 274 |
+
font-family: 'JetBrains Mono', monospace; font-size: 0.85rem;
|
| 275 |
+
padding: 1rem; border-radius: 12px; height: 250px;
|
| 276 |
+
overflow-y: auto; margin-top: 1.5rem; display: none;
|
| 277 |
+
border: 1px solid var(--border); box-shadow: inset 0 2px 4px rgba(0,0,0,0.3);
|
| 278 |
+
}
|
| 279 |
+
.terminal-line { margin-bottom: 4px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 2px; }
|
| 280 |
+
|
| 281 |
+
/* --- Buttons --- */
|
| 282 |
+
.btn {
|
| 283 |
+
padding: 0.8rem 2rem; border-radius: 12px; font-weight: 600; width: 100%;
|
| 284 |
+
cursor: pointer; border: none; font-size: 1rem; background-color: var(--primary); color: white;
|
| 285 |
+
transition: transform 0.2s, opacity 0.2s;
|
| 286 |
+
}
|
| 287 |
+
.btn:hover { opacity: 0.9; transform: translateY(-1px); }
|
| 288 |
+
.btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
| 289 |
+
|
| 290 |
+
/* --- Footer --- */
|
| 291 |
+
.dev-footer {
|
| 292 |
+
margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
|
| 293 |
+
padding: 1rem; border-top: 1px solid var(--border); width: 100%; max-width: 600px;
|
| 294 |
+
}
|
| 295 |
+
.dev-badge {
|
| 296 |
+
display: inline-block; background: var(--bg-card); padding: 5px 15px;
|
| 297 |
+
border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
|
| 298 |
+
}
|
| 299 |
+
.dev-name { color: var(--primary); font-weight: 700; }
|
| 300 |
+
|
| 301 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 302 |
+
</style>
|
| 303 |
+
</head>
|
| 304 |
+
<body>
|
| 305 |
+
|
| 306 |
+
<nav class="navbar">
|
| 307 |
+
<div class="brand">βοΈ Cloud Storage <span>Manager</span></div>
|
| 308 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">π</button>
|
| 309 |
+
</nav>
|
| 310 |
+
|
| 311 |
+
<div class="container">
|
| 312 |
+
<div class="tabs">
|
| 313 |
+
<button class="tab-btn active" onclick="switchTab('upload')">File Upload</button>
|
| 314 |
+
<button class="tab-btn" onclick="switchTab('remote')">Remote URL (Aria2)</button>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
<div id="tab-upload" class="tab-content active">
|
| 318 |
+
<div class="drop-area" id="dropArea">
|
| 319 |
+
<input type="file" id="fileInput" hidden>
|
| 320 |
+
<span class="drop-icon">π</span>
|
| 321 |
+
<h3 id="dropText" style="font-weight: 600; margin-bottom: 0.5rem;">Click to upload or drag & drop</h3>
|
| 322 |
+
<p style="color: var(--text-sub); font-size: 0.9rem;">Max file size: 1GB</p>
|
| 323 |
+
</div>
|
| 324 |
+
<button class="btn" id="uploadBtn" onclick="uploadFile()" disabled>Upload File</button>
|
| 325 |
+
<p id="uploadStatus" style="text-align: center; margin-top: 1rem; font-weight: 500;"></p>
|
| 326 |
+
</div>
|
| 327 |
+
|
| 328 |
+
<div id="tab-remote" class="tab-content">
|
| 329 |
+
<div class="input-group">
|
| 330 |
+
<label>Target URL</label>
|
| 331 |
+
<input type="text" id="remoteUrl" class="input-field" placeholder="https://example.com/file.zip">
|
| 332 |
+
</div>
|
| 333 |
+
<div class="input-group">
|
| 334 |
+
<label>Custom Filename (Optional)</label>
|
| 335 |
+
<input type="text" id="remoteName" class="input-field" placeholder="my_downloaded_file.zip">
|
| 336 |
+
</div>
|
| 337 |
+
<button class="btn" onclick="startRemoteUpload()">Start Remote Download</button>
|
| 338 |
+
|
| 339 |
+
<div class="terminal" id="terminal"></div>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<footer class="dev-footer">
|
| 344 |
+
<div>Designed & Developed by</div>
|
| 345 |
+
<div class="dev-badge">
|
| 346 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 347 |
+
</div>
|
| 348 |
+
</footer>
|
| 349 |
+
|
| 350 |
+
<script>
|
| 351 |
+
// --- Theme Logic ---
|
| 352 |
+
const html = document.documentElement;
|
| 353 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 354 |
+
function toggleTheme() {
|
| 355 |
+
const current = html.getAttribute('data-theme');
|
| 356 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 357 |
+
html.setAttribute('data-theme', next);
|
| 358 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 359 |
+
localStorage.setItem('theme', next);
|
| 360 |
+
}
|
| 361 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 362 |
+
html.setAttribute('data-theme', savedTheme);
|
| 363 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 364 |
+
|
| 365 |
+
// --- Tab Logic ---
|
| 366 |
+
function switchTab(tab) {
|
| 367 |
+
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
| 368 |
+
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
| 369 |
+
|
| 370 |
+
document.getElementById('tab-' + tab).classList.add('active');
|
| 371 |
+
// Find button by text roughly or index - simple hack for 2 tabs
|
| 372 |
+
const btns = document.querySelectorAll('.tab-btn');
|
| 373 |
+
if(tab === 'upload') btns[0].classList.add('active');
|
| 374 |
+
else btns[1].classList.add('active');
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// --- Local Upload Logic ---
|
| 378 |
+
const dropArea = document.getElementById('dropArea');
|
| 379 |
+
const fileInput = document.getElementById('fileInput');
|
| 380 |
+
const uploadBtn = document.getElementById('uploadBtn');
|
| 381 |
+
const dropText = document.getElementById('dropText');
|
| 382 |
+
const uploadStatus = document.getElementById('uploadStatus');
|
| 383 |
+
|
| 384 |
+
dropArea.addEventListener('click', () => fileInput.click());
|
| 385 |
+
|
| 386 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
| 387 |
+
dropArea.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); }, false);
|
| 388 |
+
});
|
| 389 |
+
dropArea.addEventListener('dragover', () => dropArea.classList.add('dragover'));
|
| 390 |
+
dropArea.addEventListener('dragleave', () => dropArea.classList.remove('dragover'));
|
| 391 |
+
dropArea.addEventListener('drop', (e) => {
|
| 392 |
+
if(e.dataTransfer.files.length) {
|
| 393 |
+
fileInput.files = e.dataTransfer.files;
|
| 394 |
+
handleFileSelect();
|
| 395 |
+
}
|
| 396 |
+
});
|
| 397 |
+
|
| 398 |
+
fileInput.addEventListener('change', handleFileSelect);
|
| 399 |
+
|
| 400 |
+
function handleFileSelect() {
|
| 401 |
+
const file = fileInput.files[0];
|
| 402 |
+
if(file) {
|
| 403 |
+
dropText.textContent = "Selected: " + file.name;
|
| 404 |
+
uploadBtn.disabled = false;
|
| 405 |
+
uploadStatus.textContent = "";
|
| 406 |
+
}
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
async function uploadFile() {
|
| 410 |
+
const file = fileInput.files[0];
|
| 411 |
+
if(!file) return;
|
| 412 |
+
|
| 413 |
+
uploadBtn.disabled = true;
|
| 414 |
+
uploadBtn.textContent = "Uploading...";
|
| 415 |
+
|
| 416 |
+
const formData = new FormData();
|
| 417 |
+
formData.append('file', file);
|
| 418 |
+
|
| 419 |
+
try {
|
| 420 |
+
const res = await fetch('/drive/upload/', { method: 'POST', body: formData });
|
| 421 |
+
const data = await res.json();
|
| 422 |
+
|
| 423 |
+
if(res.ok) {
|
| 424 |
+
uploadStatus.style.color = "var(--primary)";
|
| 425 |
+
uploadStatus.textContent = "β
" + data.message;
|
| 426 |
+
} else {
|
| 427 |
+
throw new Error(data.detail || "Upload failed");
|
| 428 |
+
}
|
| 429 |
+
} catch(err) {
|
| 430 |
+
uploadStatus.style.color = "#ef4444";
|
| 431 |
+
uploadStatus.textContent = "β Error: " + err.message;
|
| 432 |
+
} finally {
|
| 433 |
+
uploadBtn.disabled = false;
|
| 434 |
+
uploadBtn.textContent = "Upload File";
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
// --- Remote Upload Logic (WebSocket) ---
|
| 439 |
+
const terminal = document.getElementById('terminal');
|
| 440 |
+
|
| 441 |
+
function logToTerminal(msg) {
|
| 442 |
+
terminal.style.display = 'block';
|
| 443 |
+
const div = document.createElement('div');
|
| 444 |
+
div.className = 'terminal-line';
|
| 445 |
+
div.textContent = "> " + msg;
|
| 446 |
+
terminal.appendChild(div);
|
| 447 |
+
terminal.scrollTop = terminal.scrollHeight;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
function startRemoteUpload() {
|
| 451 |
+
const url = document.getElementById('remoteUrl').value;
|
| 452 |
+
const filename = document.getElementById('remoteName').value;
|
| 453 |
+
|
| 454 |
+
if(!url) { alert("Please enter a URL"); return; }
|
| 455 |
+
|
| 456 |
+
terminal.innerHTML = ""; // Clear logs
|
| 457 |
+
logToTerminal("Initializing WebSocket connection...");
|
| 458 |
+
|
| 459 |
+
// Determine protocol (ws or wss)
|
| 460 |
+
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
| 461 |
+
// Note: The websocket endpoint remains the same, logic is handled by router prefix
|
| 462 |
+
const wsUrl = `${protocol}://${window.location.host}/drive/ws/remote-upload/`;
|
| 463 |
+
|
| 464 |
+
const ws = new WebSocket(wsUrl);
|
| 465 |
+
|
| 466 |
+
ws.onopen = () => {
|
| 467 |
+
logToTerminal("Connected. Sending download command...");
|
| 468 |
+
ws.send(JSON.stringify({ url: url, filename: filename }));
|
| 469 |
+
};
|
| 470 |
+
|
| 471 |
+
ws.onmessage = (event) => {
|
| 472 |
+
logToTerminal(event.data);
|
| 473 |
+
};
|
| 474 |
+
|
| 475 |
+
ws.onerror = (error) => {
|
| 476 |
+
logToTerminal("β WebSocket Error");
|
| 477 |
+
console.error(error);
|
| 478 |
+
};
|
| 479 |
+
|
| 480 |
+
ws.onclose = () => {
|
| 481 |
+
logToTerminal("Connection closed.");
|
| 482 |
+
};
|
| 483 |
+
}
|
| 484 |
+
</script>
|
| 485 |
+
</body>
|
| 486 |
+
</html>
|
| 487 |
+
"""
|
| 488 |
+
return html_content
|
app/routers/image/bgremover.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
|
|
| 1 |
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
|
| 2 |
-
from fastapi.responses import Response
|
| 3 |
from rembg import remove
|
| 4 |
from PIL import Image
|
| 5 |
import io
|
|
@@ -15,32 +16,416 @@ router = APIRouter(
|
|
| 15 |
# ==========================================
|
| 16 |
# π Auth Toggle
|
| 17 |
# ==========================================
|
| 18 |
-
AUTH_ENABLED = False
|
| 19 |
|
| 20 |
-
# Logic to conditionally apply authentication
|
| 21 |
-
# If True, the endpoint will require a valid token.
|
| 22 |
-
# If False, the dependency list is empty.
|
| 23 |
security_dependencies = [Depends(get_current_user)] if AUTH_ENABLED else []
|
| 24 |
|
| 25 |
@router.post("/remove-bg", dependencies=security_dependencies)
|
| 26 |
async def remove_background(file: UploadFile = File(...)):
|
| 27 |
-
"""
|
| 28 |
-
Upload an image and receive it back with the background removed (PNG).
|
| 29 |
-
"""
|
| 30 |
-
# 1. Validate file type
|
| 31 |
if not file.content_type.startswith("image/"):
|
| 32 |
raise HTTPException(status_code=400, detail="Invalid file type. Please upload an image.")
|
| 33 |
-
|
| 34 |
try:
|
| 35 |
-
# 2. Read image data
|
| 36 |
image_data = await file.read()
|
| 37 |
-
|
| 38 |
-
# 3. Process image using rembg
|
| 39 |
-
# We pass the raw bytes to remove() and it returns bytes (PNG)
|
| 40 |
output_data = remove(image_data)
|
| 41 |
-
|
| 42 |
-
# 4. Return the result as a raw image response
|
| 43 |
return Response(content=output_data, media_type="image/png")
|
| 44 |
-
|
| 45 |
except Exception as e:
|
| 46 |
-
raise HTTPException(status_code=500, detail=f"Image processing failed: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from plistlib import UID
|
| 2 |
from fastapi import APIRouter, UploadFile, File, Depends, HTTPException
|
| 3 |
+
from fastapi.responses import Response, HTMLResponse
|
| 4 |
from rembg import remove
|
| 5 |
from PIL import Image
|
| 6 |
import io
|
|
|
|
| 16 |
# ==========================================
|
| 17 |
# π Auth Toggle
|
| 18 |
# ==========================================
|
| 19 |
+
AUTH_ENABLED = False
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
security_dependencies = [Depends(get_current_user)] if AUTH_ENABLED else []
|
| 22 |
|
| 23 |
@router.post("/remove-bg", dependencies=security_dependencies)
|
| 24 |
async def remove_background(file: UploadFile = File(...)):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
if not file.content_type.startswith("image/"):
|
| 26 |
raise HTTPException(status_code=400, detail="Invalid file type. Please upload an image.")
|
|
|
|
| 27 |
try:
|
|
|
|
| 28 |
image_data = await file.read()
|
|
|
|
|
|
|
|
|
|
| 29 |
output_data = remove(image_data)
|
|
|
|
|
|
|
| 30 |
return Response(content=output_data, media_type="image/png")
|
|
|
|
| 31 |
except Exception as e:
|
| 32 |
+
raise HTTPException(status_code=500, detail=f"Image processing failed: {str(e)}")
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
# =================================
|
| 36 |
+
# MODERN UI ENDPOINT
|
| 37 |
+
# =================================
|
| 38 |
+
|
| 39 |
+
@router.get("/ui", response_class=HTMLResponse)
|
| 40 |
+
async def image_ui():
|
| 41 |
+
html_content = """
|
| 42 |
+
<!DOCTYPE html>
|
| 43 |
+
<html lang="en" data-theme="light">
|
| 44 |
+
<head>
|
| 45 |
+
<meta charset="UTF-8">
|
| 46 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 47 |
+
<title>RemBG Pro | AI Background Remover</title>
|
| 48 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 49 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 50 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 51 |
+
|
| 52 |
+
<style>
|
| 53 |
+
/* --- CSS Variables & Theme Config --- */
|
| 54 |
+
:root {
|
| 55 |
+
--primary: #6366f1;
|
| 56 |
+
--primary-hover: #4f46e5;
|
| 57 |
+
--bg-body: #f8fafc;
|
| 58 |
+
--bg-card: #ffffff;
|
| 59 |
+
--text-main: #0f172a;
|
| 60 |
+
--text-sub: #64748b;
|
| 61 |
+
--border: #e2e8f0;
|
| 62 |
+
--drop-bg: #f1f5f9;
|
| 63 |
+
--drop-hover: #e0e7ff;
|
| 64 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 65 |
+
--checker-1: #e2e8f0;
|
| 66 |
+
--checker-2: transparent;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
[data-theme="dark"] {
|
| 70 |
+
--primary: #818cf8;
|
| 71 |
+
--primary-hover: #6366f1;
|
| 72 |
+
--bg-body: #0f172a;
|
| 73 |
+
--bg-card: #1e293b;
|
| 74 |
+
--text-main: #f8fafc;
|
| 75 |
+
--text-sub: #94a3b8;
|
| 76 |
+
--border: #334155;
|
| 77 |
+
--drop-bg: #1e293b;
|
| 78 |
+
--drop-hover: #312e81;
|
| 79 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 80 |
+
--checker-1: #334155;
|
| 81 |
+
--checker-2: #1e293b;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
|
| 85 |
+
|
| 86 |
+
body {
|
| 87 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 88 |
+
background-color: var(--bg-body);
|
| 89 |
+
color: var(--text-main);
|
| 90 |
+
min-height: 100vh;
|
| 91 |
+
display: flex;
|
| 92 |
+
flex-direction: column;
|
| 93 |
+
align-items: center;
|
| 94 |
+
justify-content: center;
|
| 95 |
+
padding: 1rem;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* --- Animations --- */
|
| 99 |
+
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
| 100 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 101 |
+
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4); } 70% { box-shadow: 0 0 0 10px rgba(99, 102, 241, 0); } 100% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0); } }
|
| 102 |
+
|
| 103 |
+
/* --- Layout Components --- */
|
| 104 |
+
.navbar {
|
| 105 |
+
width: 100%;
|
| 106 |
+
max-width: 1000px;
|
| 107 |
+
display: flex;
|
| 108 |
+
justify-content: space-between;
|
| 109 |
+
align-items: center;
|
| 110 |
+
margin-bottom: 2rem;
|
| 111 |
+
animation: fadeIn 0.8s ease-out;
|
| 112 |
+
}
|
| 113 |
+
.brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
|
| 114 |
+
.brand span { color: var(--primary); }
|
| 115 |
+
|
| 116 |
+
.theme-toggle {
|
| 117 |
+
background: none; border: none; cursor: pointer; color: var(--text-main);
|
| 118 |
+
font-size: 1.2rem; padding: 8px; border-radius: 50%;
|
| 119 |
+
background-color: var(--bg-card); border: 1px solid var(--border);
|
| 120 |
+
}
|
| 121 |
+
.theme-toggle:hover { background-color: var(--drop-bg); }
|
| 122 |
+
|
| 123 |
+
.container {
|
| 124 |
+
background: var(--bg-card);
|
| 125 |
+
width: 100%;
|
| 126 |
+
max-width: 900px;
|
| 127 |
+
border-radius: 24px;
|
| 128 |
+
box-shadow: var(--shadow);
|
| 129 |
+
padding: 2.5rem;
|
| 130 |
+
border: 1px solid var(--border);
|
| 131 |
+
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
h1 { font-size: 2rem; font-weight: 700; text-align: center; margin-bottom: 0.5rem; }
|
| 135 |
+
p.subtitle { text-align: center; color: var(--text-sub); margin-bottom: 2rem; }
|
| 136 |
+
|
| 137 |
+
/* --- Drop Zone --- */
|
| 138 |
+
.drop-area {
|
| 139 |
+
border: 2px dashed var(--border);
|
| 140 |
+
border-radius: 16px;
|
| 141 |
+
padding: 3rem 1rem;
|
| 142 |
+
text-align: center;
|
| 143 |
+
cursor: pointer;
|
| 144 |
+
background-color: var(--drop-bg);
|
| 145 |
+
position: relative;
|
| 146 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 147 |
+
}
|
| 148 |
+
.drop-area:hover, .drop-area.dragover {
|
| 149 |
+
border-color: var(--primary);
|
| 150 |
+
background-color: var(--drop-hover);
|
| 151 |
+
transform: scale(1.01);
|
| 152 |
+
}
|
| 153 |
+
.drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; animation: bounce 2s infinite; }
|
| 154 |
+
|
| 155 |
+
/* --- Buttons --- */
|
| 156 |
+
.btn-action {
|
| 157 |
+
margin-top: 1.5rem;
|
| 158 |
+
display: flex; justify-content: center; gap: 1rem;
|
| 159 |
+
}
|
| 160 |
+
.btn {
|
| 161 |
+
padding: 0.8rem 2rem;
|
| 162 |
+
border-radius: 12px;
|
| 163 |
+
font-weight: 600;
|
| 164 |
+
cursor: pointer;
|
| 165 |
+
border: none;
|
| 166 |
+
font-size: 1rem;
|
| 167 |
+
transition: transform 0.2s;
|
| 168 |
+
display: inline-flex; align-items: center; gap: 8px;
|
| 169 |
+
}
|
| 170 |
+
.btn-primary {
|
| 171 |
+
background-color: var(--primary);
|
| 172 |
+
color: white;
|
| 173 |
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
| 174 |
+
}
|
| 175 |
+
.btn-primary:hover { background-color: var(--primary-hover); transform: translateY(-2px); }
|
| 176 |
+
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
| 177 |
+
|
| 178 |
+
.btn-download {
|
| 179 |
+
background-color: #10b981; color: white; text-decoration: none;
|
| 180 |
+
}
|
| 181 |
+
.btn-download:hover { background-color: #059669; }
|
| 182 |
+
|
| 183 |
+
/* --- Results --- */
|
| 184 |
+
.results-container {
|
| 185 |
+
display: none;
|
| 186 |
+
grid-template-columns: 1fr 1fr;
|
| 187 |
+
gap: 1.5rem;
|
| 188 |
+
margin-top: 2rem;
|
| 189 |
+
animation: fadeIn 0.5s ease;
|
| 190 |
+
}
|
| 191 |
+
.img-card {
|
| 192 |
+
background: var(--bg-body);
|
| 193 |
+
padding: 1rem;
|
| 194 |
+
border-radius: 16px;
|
| 195 |
+
border: 1px solid var(--border);
|
| 196 |
+
}
|
| 197 |
+
.card-header { font-size: 0.9rem; font-weight: 600; color: var(--text-sub); margin-bottom: 0.8rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
| 198 |
+
|
| 199 |
+
.img-wrapper {
|
| 200 |
+
width: 100%; height: 250px;
|
| 201 |
+
border-radius: 12px;
|
| 202 |
+
overflow: hidden;
|
| 203 |
+
display: flex; align-items: center; justify-content: center;
|
| 204 |
+
/* Checkered pattern */
|
| 205 |
+
background-image:
|
| 206 |
+
linear-gradient(45deg, var(--checker-1) 25%, var(--checker-2) 25%),
|
| 207 |
+
linear-gradient(-45deg, var(--checker-1) 25%, var(--checker-2) 25%),
|
| 208 |
+
linear-gradient(45deg, var(--checker-2) 75%, var(--checker-1) 75%),
|
| 209 |
+
linear-gradient(-45deg, var(--checker-2) 75%, var(--checker-1) 75%);
|
| 210 |
+
background-size: 20px 20px;
|
| 211 |
+
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
|
| 212 |
+
}
|
| 213 |
+
.img-wrapper img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
| 214 |
+
|
| 215 |
+
/* --- Footer --- */
|
| 216 |
+
.dev-footer {
|
| 217 |
+
margin-top: 3rem;
|
| 218 |
+
text-align: center;
|
| 219 |
+
font-size: 0.9rem;
|
| 220 |
+
color: var(--text-sub);
|
| 221 |
+
padding: 1rem;
|
| 222 |
+
border-top: 1px solid var(--border);
|
| 223 |
+
width: 100%;
|
| 224 |
+
max-width: 600px;
|
| 225 |
+
animation: fadeIn 1.2s ease-out;
|
| 226 |
+
}
|
| 227 |
+
.dev-badge {
|
| 228 |
+
display: inline-block;
|
| 229 |
+
background: var(--bg-card);
|
| 230 |
+
padding: 5px 15px;
|
| 231 |
+
border-radius: 20px;
|
| 232 |
+
border: 1px solid var(--border);
|
| 233 |
+
font-weight: 500;
|
| 234 |
+
margin-top: 5px;
|
| 235 |
+
}
|
| 236 |
+
.dev-name { color: var(--primary); font-weight: 700; }
|
| 237 |
+
|
| 238 |
+
/* --- Loaders --- */
|
| 239 |
+
.loading-spinner {
|
| 240 |
+
display: none; width: 24px; height: 24px;
|
| 241 |
+
border: 3px solid rgba(255,255,255,0.3);
|
| 242 |
+
border-radius: 50%; border-top-color: white;
|
| 243 |
+
animation: spin 1s ease-in-out infinite;
|
| 244 |
+
}
|
| 245 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 246 |
+
|
| 247 |
+
/* --- Responsive Design --- */
|
| 248 |
+
@media (max-width: 768px) {
|
| 249 |
+
.results-container { grid-template-columns: 1fr; }
|
| 250 |
+
h1 { font-size: 1.5rem; }
|
| 251 |
+
.container { padding: 1.5rem; }
|
| 252 |
+
}
|
| 253 |
+
</style>
|
| 254 |
+
</head>
|
| 255 |
+
<body>
|
| 256 |
+
|
| 257 |
+
<nav class="navbar">
|
| 258 |
+
<div class="brand">β¨ Background removal tool <span>by sam</span></div>
|
| 259 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn" title="Toggle Dark Mode">
|
| 260 |
+
π
|
| 261 |
+
</button>
|
| 262 |
+
</nav>
|
| 263 |
+
|
| 264 |
+
<div class="container">
|
| 265 |
+
<h1>Remove Backgrounds Instantly</h1>
|
| 266 |
+
<p class="subtitle">Upload your image and let our AI handle the magic.</p>
|
| 267 |
+
|
| 268 |
+
<div class="drop-area" id="dropArea">
|
| 269 |
+
<input type="file" id="fileInput" accept="image/*" hidden>
|
| 270 |
+
<span class="drop-icon">π€</span>
|
| 271 |
+
<h3 style="font-weight: 600; margin-bottom: 0.5rem;">Click to upload or drag & drop</h3>
|
| 272 |
+
<p style="color: var(--text-sub); font-size: 0.9rem;">SVG, PNG, JPG or GIF</p>
|
| 273 |
+
</div>
|
| 274 |
+
|
| 275 |
+
<div class="btn-action">
|
| 276 |
+
<button class="btn btn-primary" id="processBtn" onclick="processImage()" disabled>
|
| 277 |
+
<div class="loading-spinner" id="spinner"></div>
|
| 278 |
+
<span id="btnText">Remove Background</span>
|
| 279 |
+
</button>
|
| 280 |
+
</div>
|
| 281 |
+
|
| 282 |
+
<p id="errorMsg" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
|
| 283 |
+
|
| 284 |
+
<div class="results-container" id="resultsGrid">
|
| 285 |
+
<div class="img-card">
|
| 286 |
+
<div class="card-header">Original</div>
|
| 287 |
+
<div class="img-wrapper">
|
| 288 |
+
<img id="originalImg" src="" alt="Original">
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
<div class="img-card">
|
| 292 |
+
<div class="card-header" style="display:flex; justify-content:space-between; align-items:center;">
|
| 293 |
+
<span>Result</span>
|
| 294 |
+
<a id="downloadLink" href="#" download="rembg_result.png" class="btn btn-download" style="padding: 0.3rem 0.8rem; font-size: 0.8rem; border-radius: 6px;">
|
| 295 |
+
β¬ Save
|
| 296 |
+
</a>
|
| 297 |
+
</div>
|
| 298 |
+
<div class="img-wrapper">
|
| 299 |
+
<img id="processedImg" src="" alt="Processed">
|
| 300 |
+
</div>
|
| 301 |
+
</div>
|
| 302 |
+
</div>
|
| 303 |
+
</div>
|
| 304 |
+
|
| 305 |
+
<footer class="dev-footer">
|
| 306 |
+
<div>Designed & Developed by</div>
|
| 307 |
+
<div class="dev-badge">
|
| 308 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 309 |
+
</div>
|
| 310 |
+
</footer>
|
| 311 |
+
|
| 312 |
+
<script>
|
| 313 |
+
// --- DOM Elements ---
|
| 314 |
+
const dropArea = document.getElementById('dropArea');
|
| 315 |
+
const fileInput = document.getElementById('fileInput');
|
| 316 |
+
const processBtn = document.getElementById('processBtn');
|
| 317 |
+
const spinner = document.getElementById('spinner');
|
| 318 |
+
const btnText = document.getElementById('btnText');
|
| 319 |
+
const resultsGrid = document.getElementById('resultsGrid');
|
| 320 |
+
const originalImg = document.getElementById('originalImg');
|
| 321 |
+
const processedImg = document.getElementById('processedImg');
|
| 322 |
+
const downloadLink = document.getElementById('downloadLink');
|
| 323 |
+
const errorMsg = document.getElementById('errorMsg');
|
| 324 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 325 |
+
const html = document.documentElement;
|
| 326 |
+
|
| 327 |
+
// --- Theme Logic ---
|
| 328 |
+
function toggleTheme() {
|
| 329 |
+
const current = html.getAttribute('data-theme');
|
| 330 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 331 |
+
html.setAttribute('data-theme', next);
|
| 332 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 333 |
+
localStorage.setItem('theme', next);
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
// Init Theme
|
| 337 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 338 |
+
html.setAttribute('data-theme', savedTheme);
|
| 339 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 340 |
+
|
| 341 |
+
// --- Upload Logic ---
|
| 342 |
+
dropArea.addEventListener('click', () => fileInput.click());
|
| 343 |
+
|
| 344 |
+
fileInput.addEventListener('change', handleFileSelect);
|
| 345 |
+
|
| 346 |
+
// Drag & Drop events
|
| 347 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
| 348 |
+
dropArea.addEventListener(eventName, preventDefaults, false);
|
| 349 |
+
});
|
| 350 |
+
|
| 351 |
+
function preventDefaults(e) { e.preventDefault(); e.stopPropagation(); }
|
| 352 |
+
|
| 353 |
+
['dragenter', 'dragover'].forEach(eventName => {
|
| 354 |
+
dropArea.addEventListener(eventName, () => dropArea.classList.add('dragover'), false);
|
| 355 |
+
});
|
| 356 |
+
|
| 357 |
+
['dragleave', 'drop'].forEach(eventName => {
|
| 358 |
+
dropArea.addEventListener(eventName, () => dropArea.classList.remove('dragover'), false);
|
| 359 |
+
});
|
| 360 |
+
|
| 361 |
+
dropArea.addEventListener('drop', (e) => {
|
| 362 |
+
const dt = e.dataTransfer;
|
| 363 |
+
const files = dt.files;
|
| 364 |
+
if(files.length) {
|
| 365 |
+
fileInput.files = files;
|
| 366 |
+
handleFileSelect();
|
| 367 |
+
}
|
| 368 |
+
});
|
| 369 |
+
|
| 370 |
+
function handleFileSelect() {
|
| 371 |
+
const file = fileInput.files[0];
|
| 372 |
+
if (file) {
|
| 373 |
+
originalImg.src = URL.createObjectURL(file);
|
| 374 |
+
processBtn.disabled = false;
|
| 375 |
+
processBtn.style.animation = "pulse 1.5s infinite";
|
| 376 |
+
resultsGrid.style.display = 'none'; // Hide old results
|
| 377 |
+
errorMsg.style.display = 'none';
|
| 378 |
+
|
| 379 |
+
// Update Drop Area Text to show selection
|
| 380 |
+
dropArea.querySelector('h3').textContent = "Selected: " + file.name;
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
// --- API Logic ---
|
| 385 |
+
async function processImage() {
|
| 386 |
+
const file = fileInput.files[0];
|
| 387 |
+
if (!file) return;
|
| 388 |
+
|
| 389 |
+
// Loading State
|
| 390 |
+
processBtn.disabled = true;
|
| 391 |
+
processBtn.style.animation = "none";
|
| 392 |
+
spinner.style.display = 'block';
|
| 393 |
+
btnText.textContent = 'Processing...';
|
| 394 |
+
errorMsg.style.display = 'none';
|
| 395 |
+
|
| 396 |
+
const formData = new FormData();
|
| 397 |
+
formData.append('file', file);
|
| 398 |
+
|
| 399 |
+
try {
|
| 400 |
+
const response = await fetch('/image/remove-bg', {
|
| 401 |
+
method: 'POST',
|
| 402 |
+
body: formData
|
| 403 |
+
});
|
| 404 |
+
|
| 405 |
+
if (!response.ok) throw new Error('Failed to process image');
|
| 406 |
+
|
| 407 |
+
const blob = await response.blob();
|
| 408 |
+
const objectURL = URL.createObjectURL(blob);
|
| 409 |
+
|
| 410 |
+
processedImg.src = objectURL;
|
| 411 |
+
downloadLink.href = objectURL;
|
| 412 |
+
|
| 413 |
+
// Show result with animation
|
| 414 |
+
resultsGrid.style.display = 'grid';
|
| 415 |
+
resultsGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 416 |
+
|
| 417 |
+
} catch (err) {
|
| 418 |
+
console.error(err);
|
| 419 |
+
errorMsg.textContent = "Error: " + err.message;
|
| 420 |
+
errorMsg.style.display = 'block';
|
| 421 |
+
} finally {
|
| 422 |
+
processBtn.disabled = false;
|
| 423 |
+
spinner.style.display = 'none';
|
| 424 |
+
btnText.textContent = 'Remove Background';
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
</script>
|
| 428 |
+
</body>
|
| 429 |
+
</html>
|
| 430 |
+
"""
|
| 431 |
+
return html_content
|
app/routers/image/jpgcompressor.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
|
| 2 |
-
from fastapi.responses import StreamingResponse
|
| 3 |
from PIL import Image
|
| 4 |
from io import BytesIO
|
| 5 |
|
|
@@ -8,46 +8,29 @@ from io import BytesIO
|
|
| 8 |
# ==========================
|
| 9 |
router = APIRouter(
|
| 10 |
prefix="/image",
|
| 11 |
-
tags=["Image
|
| 12 |
)
|
| 13 |
|
| 14 |
# ==========================
|
| 15 |
-
# π¦
|
| 16 |
# ==========================
|
| 17 |
-
@router.post("/
|
| 18 |
async def compress_jpg_to_size(
|
| 19 |
file: UploadFile = File(..., description="The JPG image file to compress."),
|
| 20 |
target_size_kb: int = Form(..., description="Target file size in KB (e.g., 240).")
|
| 21 |
):
|
| 22 |
"""
|
| 23 |
π§ Compresses a JPEG image to fit within a specific file size (in KB).
|
| 24 |
-
Uses binary search to find the best quality and, if necessary, resizes dimensions.
|
| 25 |
"""
|
| 26 |
-
|
| 27 |
-
# --------------------------
|
| 28 |
-
# π Validation Checks
|
| 29 |
-
# --------------------------
|
| 30 |
if file.content_type not in ["image/jpeg", "image/jpg"]:
|
| 31 |
-
raise HTTPException(
|
| 32 |
-
|
| 33 |
-
detail="Invalid file type. Only JPEG files are supported."
|
| 34 |
-
)
|
| 35 |
-
|
| 36 |
if target_size_kb <= 0:
|
| 37 |
-
raise HTTPException(
|
| 38 |
-
status_code=400,
|
| 39 |
-
detail="Target size must be greater than 0 KB."
|
| 40 |
-
)
|
| 41 |
|
| 42 |
-
# Calculate target bytes
|
| 43 |
target_bytes = target_size_kb * 1024
|
| 44 |
-
|
| 45 |
-
# --------------------------
|
| 46 |
-
# π₯ Read Image Bytes
|
| 47 |
-
# --------------------------
|
| 48 |
content = await file.read()
|
| 49 |
-
|
| 50 |
-
# β‘ Optimization: If original is already smaller than target, return original
|
| 51 |
if len(content) <= target_bytes:
|
| 52 |
return StreamingResponse(
|
| 53 |
BytesIO(content),
|
|
@@ -59,86 +42,415 @@ async def compress_jpg_to_size(
|
|
| 59 |
|
| 60 |
try:
|
| 61 |
img = Image.open(input_buffer)
|
| 62 |
-
|
| 63 |
-
# Convert to RGB if necessary (e.g., if input was RGBA)
|
| 64 |
if img.mode != 'RGB':
|
| 65 |
img = img.convert('RGB')
|
| 66 |
|
| 67 |
output_buffer = BytesIO()
|
| 68 |
|
| 69 |
-
#
|
| 70 |
-
|
| 71 |
-
# --------------------------
|
| 72 |
-
# We search for the best quality between 1 and 95
|
| 73 |
-
min_quality = 1
|
| 74 |
-
max_quality = 95
|
| 75 |
best_buffer = None
|
| 76 |
|
| 77 |
while min_quality <= max_quality:
|
| 78 |
quality = (min_quality + max_quality) // 2
|
| 79 |
-
|
| 80 |
-
# Clear buffer and save with current quality
|
| 81 |
output_buffer.seek(0)
|
| 82 |
output_buffer.truncate()
|
| 83 |
img.save(output_buffer, format="JPEG", quality=quality)
|
| 84 |
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# This fits! But can we get better quality?
|
| 89 |
-
best_buffer = BytesIO(output_buffer.getvalue()) # Store success
|
| 90 |
-
min_quality = quality + 1 # Try higher quality
|
| 91 |
else:
|
| 92 |
-
# Too big, reduce quality
|
| 93 |
max_quality = quality - 1
|
| 94 |
|
| 95 |
-
#
|
| 96 |
-
# β οΈ Fallback: Resize
|
| 97 |
-
# --------------------------
|
| 98 |
-
# If even quality=1 is too big, we must reduce image dimensions
|
| 99 |
if best_buffer is None:
|
| 100 |
resize_factor = 0.9
|
| 101 |
while True:
|
| 102 |
width, height = img.size
|
| 103 |
-
new_width = int(width * resize_factor)
|
| 104 |
-
new_height = int(height * resize_factor)
|
| 105 |
|
| 106 |
-
|
| 107 |
-
if new_width < 10 or new_height < 10:
|
| 108 |
-
break
|
| 109 |
|
| 110 |
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
| 111 |
-
|
| 112 |
output_buffer.seek(0)
|
| 113 |
output_buffer.truncate()
|
| 114 |
-
img.save(output_buffer, format="JPEG", quality=5)
|
| 115 |
|
| 116 |
if output_buffer.tell() <= target_bytes:
|
| 117 |
best_buffer = output_buffer
|
| 118 |
break
|
| 119 |
-
|
| 120 |
-
resize_factor *= 0.9 # Reduce size by another 10%
|
| 121 |
|
| 122 |
-
# If we still failed (extremely rare), just return the last attempt
|
| 123 |
if best_buffer is None:
|
| 124 |
output_buffer.seek(0)
|
| 125 |
best_buffer = output_buffer
|
| 126 |
|
| 127 |
best_buffer.seek(0)
|
| 128 |
-
|
| 129 |
-
# --------------------------
|
| 130 |
-
# π€ Return as Stream
|
| 131 |
-
# --------------------------
|
| 132 |
return StreamingResponse(
|
| 133 |
best_buffer,
|
| 134 |
media_type="image/jpeg",
|
| 135 |
-
headers={
|
| 136 |
-
"Content-Disposition": f"attachment; filename=compressed_{file.filename}"
|
| 137 |
-
}
|
| 138 |
)
|
| 139 |
|
| 140 |
except Exception as e:
|
| 141 |
-
raise HTTPException(
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
|
| 2 |
+
from fastapi.responses import StreamingResponse, HTMLResponse
|
| 3 |
from PIL import Image
|
| 4 |
from io import BytesIO
|
| 5 |
|
|
|
|
| 8 |
# ==========================
|
| 9 |
router = APIRouter(
|
| 10 |
prefix="/image",
|
| 11 |
+
tags=["Image Compression"]
|
| 12 |
)
|
| 13 |
|
| 14 |
# ==========================
|
| 15 |
+
# π¦ API Endpoint (Backend)
|
| 16 |
# ==========================
|
| 17 |
+
@router.post("/compress-jpg")
|
| 18 |
async def compress_jpg_to_size(
|
| 19 |
file: UploadFile = File(..., description="The JPG image file to compress."),
|
| 20 |
target_size_kb: int = Form(..., description="Target file size in KB (e.g., 240).")
|
| 21 |
):
|
| 22 |
"""
|
| 23 |
π§ Compresses a JPEG image to fit within a specific file size (in KB).
|
|
|
|
| 24 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
if file.content_type not in ["image/jpeg", "image/jpg"]:
|
| 26 |
+
raise HTTPException(status_code=400, detail="Invalid file type. Only JPEG files are supported.")
|
| 27 |
+
|
|
|
|
|
|
|
|
|
|
| 28 |
if target_size_kb <= 0:
|
| 29 |
+
raise HTTPException(status_code=400, detail="Target size must be greater than 0 KB.")
|
|
|
|
|
|
|
|
|
|
| 30 |
|
|
|
|
| 31 |
target_bytes = target_size_kb * 1024
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
content = await file.read()
|
| 33 |
+
|
|
|
|
| 34 |
if len(content) <= target_bytes:
|
| 35 |
return StreamingResponse(
|
| 36 |
BytesIO(content),
|
|
|
|
| 42 |
|
| 43 |
try:
|
| 44 |
img = Image.open(input_buffer)
|
|
|
|
|
|
|
| 45 |
if img.mode != 'RGB':
|
| 46 |
img = img.convert('RGB')
|
| 47 |
|
| 48 |
output_buffer = BytesIO()
|
| 49 |
|
| 50 |
+
# Binary Search
|
| 51 |
+
min_quality, max_quality = 1, 95
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
best_buffer = None
|
| 53 |
|
| 54 |
while min_quality <= max_quality:
|
| 55 |
quality = (min_quality + max_quality) // 2
|
|
|
|
|
|
|
| 56 |
output_buffer.seek(0)
|
| 57 |
output_buffer.truncate()
|
| 58 |
img.save(output_buffer, format="JPEG", quality=quality)
|
| 59 |
|
| 60 |
+
if output_buffer.tell() <= target_bytes:
|
| 61 |
+
best_buffer = BytesIO(output_buffer.getvalue())
|
| 62 |
+
min_quality = quality + 1
|
|
|
|
|
|
|
|
|
|
| 63 |
else:
|
|
|
|
| 64 |
max_quality = quality - 1
|
| 65 |
|
| 66 |
+
# Resize Fallback
|
|
|
|
|
|
|
|
|
|
| 67 |
if best_buffer is None:
|
| 68 |
resize_factor = 0.9
|
| 69 |
while True:
|
| 70 |
width, height = img.size
|
| 71 |
+
new_width, new_height = int(width * resize_factor), int(height * resize_factor)
|
|
|
|
| 72 |
|
| 73 |
+
if new_width < 10 or new_height < 10: break
|
|
|
|
|
|
|
| 74 |
|
| 75 |
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
|
|
| 76 |
output_buffer.seek(0)
|
| 77 |
output_buffer.truncate()
|
| 78 |
+
img.save(output_buffer, format="JPEG", quality=5)
|
| 79 |
|
| 80 |
if output_buffer.tell() <= target_bytes:
|
| 81 |
best_buffer = output_buffer
|
| 82 |
break
|
| 83 |
+
resize_factor *= 0.9
|
|
|
|
| 84 |
|
|
|
|
| 85 |
if best_buffer is None:
|
| 86 |
output_buffer.seek(0)
|
| 87 |
best_buffer = output_buffer
|
| 88 |
|
| 89 |
best_buffer.seek(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
return StreamingResponse(
|
| 91 |
best_buffer,
|
| 92 |
media_type="image/jpeg",
|
| 93 |
+
headers={"Content-Disposition": f"attachment; filename=compressed_{file.filename}"}
|
|
|
|
|
|
|
| 94 |
)
|
| 95 |
|
| 96 |
except Exception as e:
|
| 97 |
+
raise HTTPException(status_code=500, detail=f"Image processing error: {str(e)}")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
# ==========================
|
| 101 |
+
# π¨ Modern UI Endpoint
|
| 102 |
+
# ==========================
|
| 103 |
+
@router.get("/compress-jpg/ui", response_class=HTMLResponse)
|
| 104 |
+
async def compressor_ui():
|
| 105 |
+
html_content = """
|
| 106 |
+
<!DOCTYPE html>
|
| 107 |
+
<html lang="en" data-theme="light">
|
| 108 |
+
<head>
|
| 109 |
+
<meta charset="UTF-8">
|
| 110 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 111 |
+
<title>JPEG Compressor | by Sam</title>
|
| 112 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 113 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 114 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 115 |
+
|
| 116 |
+
<style>
|
| 117 |
+
/* --- Shared Theme Config --- */
|
| 118 |
+
:root {
|
| 119 |
+
--primary: #10b981;
|
| 120 |
+
--primary-hover: #059669;
|
| 121 |
+
--bg-body: #f8fafc;
|
| 122 |
+
--bg-card: #ffffff;
|
| 123 |
+
--text-main: #0f172a;
|
| 124 |
+
--text-sub: #64748b;
|
| 125 |
+
--border: #e2e8f0;
|
| 126 |
+
--drop-bg: #f1f5f9;
|
| 127 |
+
--drop-hover: #d1fae5;
|
| 128 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
[data-theme="dark"] {
|
| 132 |
+
--primary: #34d399;
|
| 133 |
+
--primary-hover: #10b981;
|
| 134 |
+
--bg-body: #0f172a;
|
| 135 |
+
--bg-card: #1e293b;
|
| 136 |
+
--text-main: #f8fafc;
|
| 137 |
+
--text-sub: #94a3b8;
|
| 138 |
+
--border: #334155;
|
| 139 |
+
--drop-bg: #1e293b;
|
| 140 |
+
--drop-hover: #064e3b;
|
| 141 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
|
| 145 |
+
|
| 146 |
+
body {
|
| 147 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 148 |
+
background-color: var(--bg-body);
|
| 149 |
+
color: var(--text-main);
|
| 150 |
+
min-height: 100vh;
|
| 151 |
+
display: flex;
|
| 152 |
+
flex-direction: column;
|
| 153 |
+
align-items: center;
|
| 154 |
+
justify-content: center;
|
| 155 |
+
padding: 1rem;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
/* --- Animations --- */
|
| 159 |
+
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
| 160 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 161 |
+
|
| 162 |
+
/* --- Navbar --- */
|
| 163 |
+
.navbar {
|
| 164 |
+
width: 100%; max-width: 1000px; display: flex;
|
| 165 |
+
justify-content: space-between; align-items: center; margin-bottom: 2rem;
|
| 166 |
+
animation: fadeIn 0.8s ease-out;
|
| 167 |
+
}
|
| 168 |
+
.brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
|
| 169 |
+
.brand span { color: var(--primary); }
|
| 170 |
+
|
| 171 |
+
.theme-toggle {
|
| 172 |
+
background: none; border: none; cursor: pointer; color: var(--text-main);
|
| 173 |
+
font-size: 1.2rem; padding: 8px; border-radius: 50%;
|
| 174 |
+
background-color: var(--bg-card); border: 1px solid var(--border);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* --- Main Container --- */
|
| 178 |
+
.container {
|
| 179 |
+
background: var(--bg-card); width: 100%; max-width: 900px;
|
| 180 |
+
border-radius: 24px; box-shadow: var(--shadow); padding: 2.5rem;
|
| 181 |
+
border: 1px solid var(--border);
|
| 182 |
+
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
h1 { font-size: 2rem; font-weight: 700; text-align: center; margin-bottom: 0.5rem; }
|
| 186 |
+
p.subtitle { text-align: center; color: var(--text-sub); margin-bottom: 2rem; }
|
| 187 |
+
|
| 188 |
+
/* --- Controls --- */
|
| 189 |
+
.controls {
|
| 190 |
+
display: flex; justify-content: center; gap: 1rem; margin-bottom: 1.5rem;
|
| 191 |
+
align-items: center; flex-wrap: wrap;
|
| 192 |
+
}
|
| 193 |
+
.input-group {
|
| 194 |
+
display: flex; flex-direction: column; gap: 0.5rem;
|
| 195 |
+
}
|
| 196 |
+
.input-group label { font-size: 0.9rem; font-weight: 600; color: var(--text-sub); }
|
| 197 |
+
.input-field {
|
| 198 |
+
padding: 0.8rem; border-radius: 12px; border: 1px solid var(--border);
|
| 199 |
+
background-color: var(--bg-body); color: var(--text-main);
|
| 200 |
+
font-family: inherit; font-size: 1rem; width: 150px;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
/* --- Drop Zone --- */
|
| 204 |
+
.drop-area {
|
| 205 |
+
border: 2px dashed var(--border); border-radius: 16px; padding: 2rem 1rem;
|
| 206 |
+
text-align: center; cursor: pointer; background-color: var(--drop-bg);
|
| 207 |
+
transition: all 0.3s;
|
| 208 |
+
}
|
| 209 |
+
.drop-area:hover, .drop-area.dragover {
|
| 210 |
+
border-color: var(--primary); background-color: var(--drop-hover); transform: scale(1.01);
|
| 211 |
+
}
|
| 212 |
+
.drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; }
|
| 213 |
+
|
| 214 |
+
/* --- Buttons --- */
|
| 215 |
+
.btn-action { margin-top: 1.5rem; display: flex; justify-content: center; }
|
| 216 |
+
.btn {
|
| 217 |
+
padding: 0.8rem 2rem; border-radius: 12px; font-weight: 600;
|
| 218 |
+
cursor: pointer; border: none; font-size: 1rem; display: inline-flex; align-items: center; gap: 8px;
|
| 219 |
+
transition: transform 0.2s;
|
| 220 |
+
}
|
| 221 |
+
.btn-primary {
|
| 222 |
+
background-color: var(--primary); color: white;
|
| 223 |
+
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
| 224 |
+
}
|
| 225 |
+
.btn-primary:hover { background-color: var(--primary-hover); transform: translateY(-2px); }
|
| 226 |
+
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
|
| 227 |
+
|
| 228 |
+
.btn-download {
|
| 229 |
+
background-color: var(--primary); color: white; text-decoration: none;
|
| 230 |
+
padding: 0.4rem 1rem; border-radius: 8px; font-size: 0.9rem;
|
| 231 |
+
}
|
| 232 |
+
.btn-download:hover { opacity: 0.9; }
|
| 233 |
+
|
| 234 |
+
/* --- Results --- */
|
| 235 |
+
.results-container {
|
| 236 |
+
display: none; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-top: 2rem;
|
| 237 |
+
animation: fadeIn 0.5s ease;
|
| 238 |
+
}
|
| 239 |
+
.img-card {
|
| 240 |
+
background: var(--bg-body); padding: 1rem; border-radius: 16px; border: 1px solid var(--border);
|
| 241 |
+
}
|
| 242 |
+
.card-header {
|
| 243 |
+
font-size: 0.9rem; font-weight: 600; color: var(--text-sub);
|
| 244 |
+
margin-bottom: 0.8rem; text-transform: uppercase;
|
| 245 |
+
display: flex; justify-content: space-between; align-items: center;
|
| 246 |
+
}
|
| 247 |
+
.img-wrapper {
|
| 248 |
+
width: 100%; height: 250px; border-radius: 12px; overflow: hidden;
|
| 249 |
+
display: flex; align-items: center; justify-content: center;
|
| 250 |
+
background-color: var(--drop-bg);
|
| 251 |
+
}
|
| 252 |
+
.img-wrapper img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
| 253 |
+
|
| 254 |
+
.loading-spinner {
|
| 255 |
+
display: none; width: 24px; height: 24px;
|
| 256 |
+
border: 3px solid rgba(255,255,255,0.3); border-radius: 50%; border-top-color: white;
|
| 257 |
+
animation: spin 1s infinite linear;
|
| 258 |
+
}
|
| 259 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 260 |
+
|
| 261 |
+
/* --- Developer Footer --- */
|
| 262 |
+
.dev-footer {
|
| 263 |
+
margin-top: 3rem;
|
| 264 |
+
text-align: center;
|
| 265 |
+
font-size: 0.9rem;
|
| 266 |
+
color: var(--text-sub);
|
| 267 |
+
padding: 1rem;
|
| 268 |
+
border-top: 1px solid var(--border);
|
| 269 |
+
width: 100%;
|
| 270 |
+
max-width: 600px;
|
| 271 |
+
animation: fadeIn 1.2s ease-out;
|
| 272 |
+
}
|
| 273 |
+
.dev-badge {
|
| 274 |
+
display: inline-block;
|
| 275 |
+
background: var(--bg-card);
|
| 276 |
+
padding: 5px 15px;
|
| 277 |
+
border-radius: 20px;
|
| 278 |
+
border: 1px solid var(--border);
|
| 279 |
+
font-weight: 500;
|
| 280 |
+
margin-top: 5px;
|
| 281 |
+
}
|
| 282 |
+
.dev-name { color: var(--primary); font-weight: 700; }
|
| 283 |
+
|
| 284 |
+
@media (max-width: 768px) {
|
| 285 |
+
.results-container { grid-template-columns: 1fr; }
|
| 286 |
+
}
|
| 287 |
+
</style>
|
| 288 |
+
</head>
|
| 289 |
+
<body>
|
| 290 |
+
<nav class="navbar">
|
| 291 |
+
<div class="brand">β‘ JPEG Compressor <span>by sam</span></div>
|
| 292 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">π</button>
|
| 293 |
+
</nav>
|
| 294 |
+
|
| 295 |
+
<div class="container">
|
| 296 |
+
<h1>Smart Image Compression</h1>
|
| 297 |
+
<p class="subtitle">Reduce file size without sacrificing quality.</p>
|
| 298 |
+
|
| 299 |
+
<div class="controls">
|
| 300 |
+
<div class="input-group">
|
| 301 |
+
<label for="sizeInput">Target Size (KB)</label>
|
| 302 |
+
<input type="number" id="sizeInput" class="input-field" value="200" min="10" placeholder="e.g. 200">
|
| 303 |
+
</div>
|
| 304 |
+
</div>
|
| 305 |
+
|
| 306 |
+
<div class="drop-area" id="dropArea">
|
| 307 |
+
<input type="file" id="fileInput" accept="image/jpeg, image/jpg" hidden>
|
| 308 |
+
<span class="drop-icon">π¦</span>
|
| 309 |
+
<h3 id="dropText" style="font-weight: 600; margin-bottom: 0.5rem;">Click to upload or drag & drop</h3>
|
| 310 |
+
<p style="color: var(--text-sub); font-size: 0.9rem;">JPG or JPEG only</p>
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<div class="btn-action">
|
| 314 |
+
<button class="btn btn-primary" id="processBtn" onclick="processImage()" disabled>
|
| 315 |
+
<div class="loading-spinner" id="spinner"></div>
|
| 316 |
+
<span id="btnText">Compress Image</span>
|
| 317 |
+
</button>
|
| 318 |
+
</div>
|
| 319 |
+
|
| 320 |
+
<p id="errorMsg" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
|
| 321 |
+
|
| 322 |
+
<div class="results-container" id="resultsGrid">
|
| 323 |
+
<div class="img-card">
|
| 324 |
+
<div class="card-header">Original</div>
|
| 325 |
+
<div class="img-wrapper">
|
| 326 |
+
<img id="originalImg" src="" alt="Original">
|
| 327 |
+
</div>
|
| 328 |
+
</div>
|
| 329 |
+
<div class="img-card">
|
| 330 |
+
<div class="card-header">
|
| 331 |
+
<span>Compressed</span>
|
| 332 |
+
<a id="downloadLink" href="#" class="btn-download">β¬ Save</a>
|
| 333 |
+
</div>
|
| 334 |
+
<div class="img-wrapper">
|
| 335 |
+
<img id="processedImg" src="" alt="Compressed">
|
| 336 |
+
</div>
|
| 337 |
+
</div>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
<footer class="dev-footer">
|
| 342 |
+
<div>Designed & Developed by</div>
|
| 343 |
+
<div class="dev-badge">
|
| 344 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 345 |
+
</div>
|
| 346 |
+
</footer>
|
| 347 |
+
|
| 348 |
+
<script>
|
| 349 |
+
// Elements
|
| 350 |
+
const dropArea = document.getElementById('dropArea');
|
| 351 |
+
const fileInput = document.getElementById('fileInput');
|
| 352 |
+
const processBtn = document.getElementById('processBtn');
|
| 353 |
+
const sizeInput = document.getElementById('sizeInput');
|
| 354 |
+
const spinner = document.getElementById('spinner');
|
| 355 |
+
const btnText = document.getElementById('btnText');
|
| 356 |
+
const resultsGrid = document.getElementById('resultsGrid');
|
| 357 |
+
const originalImg = document.getElementById('originalImg');
|
| 358 |
+
const processedImg = document.getElementById('processedImg');
|
| 359 |
+
const downloadLink = document.getElementById('downloadLink');
|
| 360 |
+
const errorMsg = document.getElementById('errorMsg');
|
| 361 |
+
const dropText = document.getElementById('dropText');
|
| 362 |
+
|
| 363 |
+
// Theme Logic
|
| 364 |
+
const html = document.documentElement;
|
| 365 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 366 |
+
|
| 367 |
+
function toggleTheme() {
|
| 368 |
+
const current = html.getAttribute('data-theme');
|
| 369 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 370 |
+
html.setAttribute('data-theme', next);
|
| 371 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 372 |
+
localStorage.setItem('theme', next);
|
| 373 |
+
}
|
| 374 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 375 |
+
html.setAttribute('data-theme', savedTheme);
|
| 376 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 377 |
+
|
| 378 |
+
// Drag & Drop
|
| 379 |
+
dropArea.addEventListener('click', () => fileInput.click());
|
| 380 |
+
|
| 381 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
| 382 |
+
dropArea.addEventListener(eventName, (e) => {
|
| 383 |
+
e.preventDefault(); e.stopPropagation();
|
| 384 |
+
}, false);
|
| 385 |
+
});
|
| 386 |
+
|
| 387 |
+
['dragenter', 'dragover'].forEach(name => dropArea.classList.add('dragover'));
|
| 388 |
+
['dragleave', 'drop'].forEach(name => dropArea.classList.remove('dragover'));
|
| 389 |
+
|
| 390 |
+
dropArea.addEventListener('drop', (e) => {
|
| 391 |
+
if(e.dataTransfer.files.length) {
|
| 392 |
+
fileInput.files = e.dataTransfer.files;
|
| 393 |
+
handleFileSelect();
|
| 394 |
+
}
|
| 395 |
+
});
|
| 396 |
+
|
| 397 |
+
fileInput.addEventListener('change', handleFileSelect);
|
| 398 |
+
|
| 399 |
+
function handleFileSelect() {
|
| 400 |
+
const file = fileInput.files[0];
|
| 401 |
+
if (file) {
|
| 402 |
+
originalImg.src = URL.createObjectURL(file);
|
| 403 |
+
processBtn.disabled = false;
|
| 404 |
+
dropText.textContent = "Selected: " + file.name;
|
| 405 |
+
resultsGrid.style.display = 'none';
|
| 406 |
+
errorMsg.style.display = 'none';
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Process Logic
|
| 411 |
+
async function processImage() {
|
| 412 |
+
const file = fileInput.files[0];
|
| 413 |
+
const kbSize = sizeInput.value;
|
| 414 |
+
if (!file || !kbSize) return;
|
| 415 |
+
|
| 416 |
+
processBtn.disabled = true;
|
| 417 |
+
spinner.style.display = 'block';
|
| 418 |
+
btnText.textContent = 'Compressing...';
|
| 419 |
+
|
| 420 |
+
const formData = new FormData();
|
| 421 |
+
formData.append('file', file);
|
| 422 |
+
formData.append('target_size_kb', kbSize);
|
| 423 |
+
|
| 424 |
+
try {
|
| 425 |
+
const response = await fetch('/image/compress-jpg', {
|
| 426 |
+
method: 'POST',
|
| 427 |
+
body: formData
|
| 428 |
+
});
|
| 429 |
+
|
| 430 |
+
if (!response.ok) throw new Error(await response.text());
|
| 431 |
+
|
| 432 |
+
const blob = await response.blob();
|
| 433 |
+
const objectURL = URL.createObjectURL(blob);
|
| 434 |
+
|
| 435 |
+
processedImg.src = objectURL;
|
| 436 |
+
downloadLink.href = objectURL;
|
| 437 |
+
downloadLink.download = "compressed_" + file.name;
|
| 438 |
+
|
| 439 |
+
resultsGrid.style.display = 'grid';
|
| 440 |
+
resultsGrid.scrollIntoView({ behavior: 'smooth' });
|
| 441 |
+
|
| 442 |
+
} catch (err) {
|
| 443 |
+
console.error(err);
|
| 444 |
+
errorMsg.textContent = "Error: " + (err.message || "Compression failed");
|
| 445 |
+
errorMsg.style.display = 'block';
|
| 446 |
+
} finally {
|
| 447 |
+
processBtn.disabled = false;
|
| 448 |
+
spinner.style.display = 'none';
|
| 449 |
+
btnText.textContent = 'Compress Image';
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
</script>
|
| 453 |
+
</body>
|
| 454 |
+
</html>
|
| 455 |
+
"""
|
| 456 |
+
return html_content
|
app/routers/pdf/pdf_tools.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
-
from fastapi import APIRouter, UploadFile, File, HTTPException,
|
| 2 |
-
from fastapi.responses import FileResponse
|
| 3 |
from pypdf import PdfReader, PdfWriter
|
| 4 |
import os
|
| 5 |
import shutil
|
| 6 |
import tempfile
|
|
|
|
| 7 |
|
| 8 |
# Define the APIRouter object
|
| 9 |
router = APIRouter(
|
|
@@ -11,8 +12,11 @@ router = APIRouter(
|
|
| 11 |
tags=["PDF Manipulation"],
|
| 12 |
)
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
@router.post("/merge")
|
| 15 |
-
async def merge_pdfs(files:
|
| 16 |
"""
|
| 17 |
Merges multiple uploaded PDF files into a single PDF.
|
| 18 |
"""
|
|
@@ -22,9 +26,13 @@ async def merge_pdfs(files: list[UploadFile] = File(description="List of PDFs to
|
|
| 22 |
pdf_writer = PdfWriter()
|
| 23 |
|
| 24 |
# Create a temporary directory to store files
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
for file in files:
|
| 29 |
# Save the uploaded file temporarily
|
| 30 |
temp_file_path = os.path.join(temp_dir, file.filename)
|
|
@@ -43,13 +51,19 @@ async def merge_pdfs(files: list[UploadFile] = File(description="List of PDFs to
|
|
| 43 |
with open(output_path, "wb") as output_file:
|
| 44 |
pdf_writer.write(output_file)
|
| 45 |
|
| 46 |
-
# Return the merged file as a response
|
| 47 |
return FileResponse(output_path, media_type="application/pdf", filename="merged_document.pdf")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
|
|
|
|
|
|
|
|
|
| 49 |
@router.post("/rotate")
|
| 50 |
async def rotate_pdf(
|
| 51 |
-
file: UploadFile = File(description="PDF file to rotate"),
|
| 52 |
-
degrees: int = 90
|
| 53 |
):
|
| 54 |
"""
|
| 55 |
Rotates all pages of a single PDF by a specified degree (90, 180, or 270).
|
|
@@ -59,10 +73,11 @@ async def rotate_pdf(
|
|
| 59 |
|
| 60 |
pdf_writer = PdfWriter()
|
| 61 |
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
|
|
|
|
| 66 |
# Save the uploaded file temporarily
|
| 67 |
with open(input_path, "wb") as buffer:
|
| 68 |
shutil.copyfileobj(file.file, buffer)
|
|
@@ -80,4 +95,382 @@ async def rotate_pdf(
|
|
| 80 |
with open(output_path, "wb") as output_file:
|
| 81 |
pdf_writer.write(output_file)
|
| 82 |
|
| 83 |
-
return FileResponse(output_path, media_type="application/pdf", filename=f"rotated_{degrees}.pdf")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, UploadFile, File, HTTPException, Form
|
| 2 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
| 3 |
from pypdf import PdfReader, PdfWriter
|
| 4 |
import os
|
| 5 |
import shutil
|
| 6 |
import tempfile
|
| 7 |
+
from typing import List
|
| 8 |
|
| 9 |
# Define the APIRouter object
|
| 10 |
router = APIRouter(
|
|
|
|
| 12 |
tags=["PDF Manipulation"],
|
| 13 |
)
|
| 14 |
|
| 15 |
+
# ==========================
|
| 16 |
+
# π§© PDF Merge Endpoint
|
| 17 |
+
# ==========================
|
| 18 |
@router.post("/merge")
|
| 19 |
+
async def merge_pdfs(files: List[UploadFile] = File(..., description="List of PDFs to merge")):
|
| 20 |
"""
|
| 21 |
Merges multiple uploaded PDF files into a single PDF.
|
| 22 |
"""
|
|
|
|
| 26 |
pdf_writer = PdfWriter()
|
| 27 |
|
| 28 |
# Create a temporary directory to store files
|
| 29 |
+
# Note: We rely on the OS to clean up the temp dir eventually,
|
| 30 |
+
# or we return a FileResponse which keeps the file handle open briefly.
|
| 31 |
+
# For a robust production app, consider BackgroundTasks for cleanup.
|
| 32 |
+
temp_dir = tempfile.mkdtemp()
|
| 33 |
+
output_path = os.path.join(temp_dir, "merged_output.pdf")
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
for file in files:
|
| 37 |
# Save the uploaded file temporarily
|
| 38 |
temp_file_path = os.path.join(temp_dir, file.filename)
|
|
|
|
| 51 |
with open(output_path, "wb") as output_file:
|
| 52 |
pdf_writer.write(output_file)
|
| 53 |
|
|
|
|
| 54 |
return FileResponse(output_path, media_type="application/pdf", filename="merged_document.pdf")
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
shutil.rmtree(temp_dir) # Cleanup on error
|
| 58 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 59 |
|
| 60 |
+
# ==========================
|
| 61 |
+
# π PDF Rotate Endpoint
|
| 62 |
+
# ==========================
|
| 63 |
@router.post("/rotate")
|
| 64 |
async def rotate_pdf(
|
| 65 |
+
file: UploadFile = File(..., description="PDF file to rotate"),
|
| 66 |
+
degrees: int = 90
|
| 67 |
):
|
| 68 |
"""
|
| 69 |
Rotates all pages of a single PDF by a specified degree (90, 180, or 270).
|
|
|
|
| 73 |
|
| 74 |
pdf_writer = PdfWriter()
|
| 75 |
|
| 76 |
+
temp_dir = tempfile.mkdtemp()
|
| 77 |
+
input_path = os.path.join(temp_dir, file.filename)
|
| 78 |
+
output_path = os.path.join(temp_dir, f"rotated_{degrees}.pdf")
|
| 79 |
|
| 80 |
+
try:
|
| 81 |
# Save the uploaded file temporarily
|
| 82 |
with open(input_path, "wb") as buffer:
|
| 83 |
shutil.copyfileobj(file.file, buffer)
|
|
|
|
| 95 |
with open(output_path, "wb") as output_file:
|
| 96 |
pdf_writer.write(output_file)
|
| 97 |
|
| 98 |
+
return FileResponse(output_path, media_type="application/pdf", filename=f"rotated_{degrees}.pdf")
|
| 99 |
+
|
| 100 |
+
except Exception as e:
|
| 101 |
+
shutil.rmtree(temp_dir)
|
| 102 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
# ==========================
|
| 106 |
+
# π¨ Modern UI Endpoint
|
| 107 |
+
# ==========================
|
| 108 |
+
@router.get("/ui", response_class=HTMLResponse)
|
| 109 |
+
async def pdf_tools_ui():
|
| 110 |
+
html_content = """
|
| 111 |
+
<!DOCTYPE html>
|
| 112 |
+
<html lang="en" data-theme="light">
|
| 113 |
+
<head>
|
| 114 |
+
<meta charset="UTF-8">
|
| 115 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 116 |
+
<title>PDF Tools | by Sam</title>
|
| 117 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 118 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 119 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 120 |
+
|
| 121 |
+
<style>
|
| 122 |
+
/* --- Shared Theme Config --- */
|
| 123 |
+
:root {
|
| 124 |
+
--primary: #ef4444; /* PDF Red */
|
| 125 |
+
--primary-hover: #dc2626;
|
| 126 |
+
--bg-body: #f8fafc;
|
| 127 |
+
--bg-card: #ffffff;
|
| 128 |
+
--text-main: #0f172a;
|
| 129 |
+
--text-sub: #64748b;
|
| 130 |
+
--border: #e2e8f0;
|
| 131 |
+
--drop-bg: #fef2f2;
|
| 132 |
+
--drop-hover: #fee2e2;
|
| 133 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
[data-theme="dark"] {
|
| 137 |
+
--primary: #f87171;
|
| 138 |
+
--primary-hover: #ef4444;
|
| 139 |
+
--bg-body: #0f172a;
|
| 140 |
+
--bg-card: #1e293b;
|
| 141 |
+
--text-main: #f8fafc;
|
| 142 |
+
--text-sub: #94a3b8;
|
| 143 |
+
--border: #334155;
|
| 144 |
+
--drop-bg: #1e293b;
|
| 145 |
+
--drop-hover: #450a0a;
|
| 146 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s ease, color 0.3s ease; }
|
| 150 |
+
|
| 151 |
+
body {
|
| 152 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 153 |
+
background-color: var(--bg-body);
|
| 154 |
+
color: var(--text-main);
|
| 155 |
+
min-height: 100vh;
|
| 156 |
+
display: flex;
|
| 157 |
+
flex-direction: column;
|
| 158 |
+
align-items: center;
|
| 159 |
+
justify-content: center;
|
| 160 |
+
padding: 1rem;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
/* --- Navbar --- */
|
| 164 |
+
.navbar {
|
| 165 |
+
width: 100%; max-width: 1000px; display: flex;
|
| 166 |
+
justify-content: space-between; align-items: center; margin-bottom: 2rem;
|
| 167 |
+
animation: fadeIn 0.8s ease-out;
|
| 168 |
+
}
|
| 169 |
+
.brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
|
| 170 |
+
.brand span { color: var(--primary); }
|
| 171 |
+
.theme-toggle { background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 8px; border-radius: 50%; background-color: var(--bg-card); border: 1px solid var(--border); }
|
| 172 |
+
|
| 173 |
+
/* --- Container & Tabs --- */
|
| 174 |
+
.container {
|
| 175 |
+
background: var(--bg-card); width: 100%; max-width: 800px;
|
| 176 |
+
border-radius: 24px; box-shadow: var(--shadow);
|
| 177 |
+
border: 1px solid var(--border); overflow: hidden;
|
| 178 |
+
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
| 179 |
+
}
|
| 180 |
+
.tabs { display: flex; border-bottom: 1px solid var(--border); }
|
| 181 |
+
.tab-btn {
|
| 182 |
+
flex: 1; padding: 1rem; border: none; background: transparent;
|
| 183 |
+
font-family: inherit; font-weight: 600; color: var(--text-sub);
|
| 184 |
+
cursor: pointer; transition: all 0.2s; border-bottom: 3px solid transparent;
|
| 185 |
+
}
|
| 186 |
+
.tab-btn:hover { background-color: var(--drop-bg); }
|
| 187 |
+
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
|
| 188 |
+
|
| 189 |
+
.tab-content { padding: 2.5rem; display: none; animation: fadeIn 0.4s ease; }
|
| 190 |
+
.tab-content.active { display: block; }
|
| 191 |
+
|
| 192 |
+
/* --- Controls --- */
|
| 193 |
+
.drop-area {
|
| 194 |
+
border: 2px dashed var(--border); border-radius: 16px; padding: 3rem 1rem;
|
| 195 |
+
text-align: center; cursor: pointer; background-color: var(--drop-bg);
|
| 196 |
+
transition: all 0.3s; margin-bottom: 1.5rem;
|
| 197 |
+
}
|
| 198 |
+
.drop-area:hover, .drop-area.dragover { border-color: var(--primary); background-color: var(--drop-hover); transform: scale(1.01); }
|
| 199 |
+
.drop-icon { font-size: 3rem; margin-bottom: 1rem; display: block; }
|
| 200 |
+
|
| 201 |
+
.file-list { margin-bottom: 1rem; text-align: left; background: var(--bg-body); padding: 1rem; border-radius: 8px; display: none; }
|
| 202 |
+
.file-item { font-size: 0.9rem; color: var(--text-sub); padding: 4px 0; border-bottom: 1px solid var(--border); }
|
| 203 |
+
|
| 204 |
+
.rotate-options { display: flex; gap: 1rem; justify-content: center; margin-bottom: 1.5rem; }
|
| 205 |
+
.rotate-btn {
|
| 206 |
+
padding: 0.5rem 1rem; border: 1px solid var(--border); border-radius: 8px;
|
| 207 |
+
background: var(--bg-body); color: var(--text-sub); cursor: pointer;
|
| 208 |
+
}
|
| 209 |
+
.rotate-btn.selected { background: var(--primary); color: white; border-color: var(--primary); }
|
| 210 |
+
|
| 211 |
+
.btn {
|
| 212 |
+
padding: 0.8rem 2rem; border-radius: 12px; font-weight: 600; width: 100%;
|
| 213 |
+
cursor: pointer; border: none; font-size: 1rem; background-color: var(--primary); color: white;
|
| 214 |
+
display: flex; justify-content: center; align-items: center; gap: 10px;
|
| 215 |
+
}
|
| 216 |
+
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
|
| 217 |
+
|
| 218 |
+
/* --- Footer --- */
|
| 219 |
+
.dev-footer {
|
| 220 |
+
margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
|
| 221 |
+
padding: 1rem; border-top: 1px solid var(--border); width: 100%; max-width: 600px;
|
| 222 |
+
}
|
| 223 |
+
.dev-badge {
|
| 224 |
+
display: inline-block; background: var(--bg-card); padding: 5px 15px;
|
| 225 |
+
border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
|
| 226 |
+
}
|
| 227 |
+
.dev-name { color: var(--primary); font-weight: 700; }
|
| 228 |
+
|
| 229 |
+
.spinner { width: 20px; height: 20px; border: 2px solid #ffffff; border-top-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite; display: none; }
|
| 230 |
+
@keyframes spin { to { transform: rotate(360deg); } }
|
| 231 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 232 |
+
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
| 233 |
+
|
| 234 |
+
</style>
|
| 235 |
+
</head>
|
| 236 |
+
<body>
|
| 237 |
+
|
| 238 |
+
<nav class="navbar">
|
| 239 |
+
<div class="brand">π PDF Tools <span>Suite</span></div>
|
| 240 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">π</button>
|
| 241 |
+
</nav>
|
| 242 |
+
|
| 243 |
+
<div class="container">
|
| 244 |
+
<div class="tabs">
|
| 245 |
+
<button class="tab-btn active" onclick="switchTab('merge')">Merge PDFs</button>
|
| 246 |
+
<button class="tab-btn" onclick="switchTab('rotate')">Rotate PDF</button>
|
| 247 |
+
</div>
|
| 248 |
+
|
| 249 |
+
<div id="tab-merge" class="tab-content active">
|
| 250 |
+
<p style="text-align: center; color: var(--text-sub); margin-bottom: 1rem;">Combine multiple PDF files into one.</p>
|
| 251 |
+
|
| 252 |
+
<div class="drop-area" id="mergeDrop">
|
| 253 |
+
<input type="file" id="mergeInput" accept="application/pdf" multiple hidden>
|
| 254 |
+
<span class="drop-icon">π</span>
|
| 255 |
+
<h3 style="font-weight: 600;">Drag & Drop PDFs here</h3>
|
| 256 |
+
<p style="color: var(--text-sub); font-size: 0.9rem;">or click to browse</p>
|
| 257 |
+
</div>
|
| 258 |
+
|
| 259 |
+
<div class="file-list" id="mergeFileList"></div>
|
| 260 |
+
|
| 261 |
+
<button class="btn" id="mergeBtn" onclick="processMerge()" disabled>
|
| 262 |
+
<div class="spinner" id="mergeSpinner"></div>
|
| 263 |
+
<span id="mergeBtnText">Merge Files</span>
|
| 264 |
+
</button>
|
| 265 |
+
<p id="mergeError" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
|
| 266 |
+
</div>
|
| 267 |
+
|
| 268 |
+
<div id="tab-rotate" class="tab-content">
|
| 269 |
+
<p style="text-align: center; color: var(--text-sub); margin-bottom: 1rem;">Rotate all pages in a PDF document.</p>
|
| 270 |
+
|
| 271 |
+
<div class="drop-area" id="rotateDrop">
|
| 272 |
+
<input type="file" id="rotateInput" accept="application/pdf" hidden>
|
| 273 |
+
<span class="drop-icon">π</span>
|
| 274 |
+
<h3 id="rotateText" style="font-weight: 600;">Upload PDF to Rotate</h3>
|
| 275 |
+
</div>
|
| 276 |
+
|
| 277 |
+
<div class="rotate-options">
|
| 278 |
+
<button class="rotate-btn selected" onclick="setRotation(90)">90Β° CW</button>
|
| 279 |
+
<button class="rotate-btn" onclick="setRotation(180)">180Β°</button>
|
| 280 |
+
<button class="rotate-btn" onclick="setRotation(270)">270Β° CW</button>
|
| 281 |
+
</div>
|
| 282 |
+
|
| 283 |
+
<button class="btn" id="rotateBtn" onclick="processRotate()" disabled>
|
| 284 |
+
<div class="spinner" id="rotateSpinner"></div>
|
| 285 |
+
<span id="rotateBtnText">Rotate PDF</span>
|
| 286 |
+
</button>
|
| 287 |
+
<p id="rotateError" style="color: #ef4444; text-align: center; margin-top: 10px; display: none;"></p>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
|
| 291 |
+
<footer class="dev-footer">
|
| 292 |
+
<div>Designed & Developed by</div>
|
| 293 |
+
<div class="dev-badge">
|
| 294 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 295 |
+
</div>
|
| 296 |
+
</footer>
|
| 297 |
+
|
| 298 |
+
<script>
|
| 299 |
+
// --- Theme & Tab Logic ---
|
| 300 |
+
const html = document.documentElement;
|
| 301 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 302 |
+
function toggleTheme() {
|
| 303 |
+
const current = html.getAttribute('data-theme');
|
| 304 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 305 |
+
html.setAttribute('data-theme', next);
|
| 306 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 307 |
+
localStorage.setItem('theme', next);
|
| 308 |
+
}
|
| 309 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 310 |
+
html.setAttribute('data-theme', savedTheme);
|
| 311 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 312 |
+
|
| 313 |
+
function switchTab(tab) {
|
| 314 |
+
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
| 315 |
+
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
|
| 316 |
+
document.getElementById('tab-' + tab).classList.add('active');
|
| 317 |
+
// Simple active class toggle for buttons
|
| 318 |
+
const btns = document.querySelectorAll('.tab-btn');
|
| 319 |
+
if(tab === 'merge') btns[0].classList.add('active');
|
| 320 |
+
else btns[1].classList.add('active');
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
// --- Merge Logic ---
|
| 324 |
+
const mergeDrop = document.getElementById('mergeDrop');
|
| 325 |
+
const mergeInput = document.getElementById('mergeInput');
|
| 326 |
+
const mergeFileList = document.getElementById('mergeFileList');
|
| 327 |
+
const mergeBtn = document.getElementById('mergeBtn');
|
| 328 |
+
let mergeFiles = [];
|
| 329 |
+
|
| 330 |
+
mergeDrop.addEventListener('click', () => mergeInput.click());
|
| 331 |
+
|
| 332 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
|
| 333 |
+
mergeDrop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); });
|
| 334 |
+
});
|
| 335 |
+
mergeDrop.addEventListener('drop', (e) => {
|
| 336 |
+
if(e.dataTransfer.files.length) handleMergeFiles(e.dataTransfer.files);
|
| 337 |
+
});
|
| 338 |
+
mergeInput.addEventListener('change', () => handleMergeFiles(mergeInput.files));
|
| 339 |
+
|
| 340 |
+
function handleMergeFiles(files) {
|
| 341 |
+
mergeFiles = Array.from(files); // Convert FileList to Array
|
| 342 |
+
if(mergeFiles.length < 2) {
|
| 343 |
+
alert("Please select at least 2 PDF files.");
|
| 344 |
+
return;
|
| 345 |
+
}
|
| 346 |
+
updateFileList();
|
| 347 |
+
mergeBtn.disabled = false;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function updateFileList() {
|
| 351 |
+
mergeFileList.style.display = 'block';
|
| 352 |
+
mergeFileList.innerHTML = mergeFiles.map((f, i) =>
|
| 353 |
+
`<div class="file-item">${i+1}. ${f.name} (${(f.size/1024/1024).toFixed(2)} MB)</div>`
|
| 354 |
+
).join('');
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
async function processMerge() {
|
| 358 |
+
if(mergeFiles.length < 2) return;
|
| 359 |
+
setLoading('merge', true);
|
| 360 |
+
|
| 361 |
+
const formData = new FormData();
|
| 362 |
+
mergeFiles.forEach(file => formData.append('files', file));
|
| 363 |
+
|
| 364 |
+
try {
|
| 365 |
+
const res = await fetch('/pdf/merge', { method: 'POST', body: formData });
|
| 366 |
+
if(!res.ok) throw new Error("Merge failed");
|
| 367 |
+
|
| 368 |
+
const blob = await res.blob();
|
| 369 |
+
downloadBlob(blob, 'merged_document.pdf');
|
| 370 |
+
} catch(e) {
|
| 371 |
+
showError('merge', e.message);
|
| 372 |
+
} finally {
|
| 373 |
+
setLoading('merge', false);
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
// --- Rotate Logic ---
|
| 378 |
+
const rotateDrop = document.getElementById('rotateDrop');
|
| 379 |
+
const rotateInput = document.getElementById('rotateInput');
|
| 380 |
+
const rotateText = document.getElementById('rotateText');
|
| 381 |
+
const rotateBtn = document.getElementById('rotateBtn');
|
| 382 |
+
let rotateFile = null;
|
| 383 |
+
let currentRotation = 90;
|
| 384 |
+
|
| 385 |
+
rotateDrop.addEventListener('click', () => rotateInput.click());
|
| 386 |
+
|
| 387 |
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(evt => {
|
| 388 |
+
rotateDrop.addEventListener(evt, (e) => { e.preventDefault(); e.stopPropagation(); });
|
| 389 |
+
});
|
| 390 |
+
rotateDrop.addEventListener('drop', (e) => {
|
| 391 |
+
if(e.dataTransfer.files.length) handleRotateFile(e.dataTransfer.files[0]);
|
| 392 |
+
});
|
| 393 |
+
rotateInput.addEventListener('change', () => handleRotateFile(rotateInput.files[0]));
|
| 394 |
+
|
| 395 |
+
function handleRotateFile(file) {
|
| 396 |
+
if(!file || file.type !== 'application/pdf') {
|
| 397 |
+
alert("Please select a valid PDF."); return;
|
| 398 |
+
}
|
| 399 |
+
rotateFile = file;
|
| 400 |
+
rotateText.textContent = "Selected: " + file.name;
|
| 401 |
+
rotateBtn.disabled = false;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
function setRotation(deg) {
|
| 405 |
+
currentRotation = deg;
|
| 406 |
+
document.querySelectorAll('.rotate-btn').forEach(b => b.classList.remove('selected'));
|
| 407 |
+
// Identify button by onclick text content or order.
|
| 408 |
+
// Hardcoding logic for simplicity:
|
| 409 |
+
const btns = document.querySelectorAll('.rotate-btn');
|
| 410 |
+
if(deg === 90) btns[0].classList.add('selected');
|
| 411 |
+
if(deg === 180) btns[1].classList.add('selected');
|
| 412 |
+
if(deg === 270) btns[2].classList.add('selected');
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
async function processRotate() {
|
| 416 |
+
if(!rotateFile) return;
|
| 417 |
+
setLoading('rotate', true);
|
| 418 |
+
|
| 419 |
+
const formData = new FormData();
|
| 420 |
+
formData.append('file', rotateFile);
|
| 421 |
+
|
| 422 |
+
try {
|
| 423 |
+
const res = await fetch(`/pdf/rotate?degrees=${currentRotation}`, {
|
| 424 |
+
method: 'POST',
|
| 425 |
+
body: formData
|
| 426 |
+
});
|
| 427 |
+
if(!res.ok) throw new Error("Rotation failed");
|
| 428 |
+
|
| 429 |
+
const blob = await res.blob();
|
| 430 |
+
downloadBlob(blob, `rotated_${currentRotation}_${rotateFile.name}`);
|
| 431 |
+
} catch(e) {
|
| 432 |
+
showError('rotate', e.message);
|
| 433 |
+
} finally {
|
| 434 |
+
setLoading('rotate', false);
|
| 435 |
+
}
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
// --- Helpers ---
|
| 439 |
+
function downloadBlob(blob, filename) {
|
| 440 |
+
const url = window.URL.createObjectURL(blob);
|
| 441 |
+
const a = document.createElement('a');
|
| 442 |
+
a.href = url;
|
| 443 |
+
a.download = filename;
|
| 444 |
+
document.body.appendChild(a);
|
| 445 |
+
a.click();
|
| 446 |
+
a.remove();
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
function setLoading(type, isLoading) {
|
| 450 |
+
const btn = document.getElementById(`${type}Btn`);
|
| 451 |
+
const spinner = document.getElementById(`${type}Spinner`);
|
| 452 |
+
const text = document.getElementById(`${type}BtnText`);
|
| 453 |
+
const error = document.getElementById(`${type}Error`);
|
| 454 |
+
|
| 455 |
+
error.style.display = 'none';
|
| 456 |
+
if(isLoading) {
|
| 457 |
+
btn.disabled = true;
|
| 458 |
+
spinner.style.display = 'block';
|
| 459 |
+
text.textContent = 'Processing...';
|
| 460 |
+
} else {
|
| 461 |
+
btn.disabled = false;
|
| 462 |
+
spinner.style.display = 'none';
|
| 463 |
+
text.textContent = type === 'merge' ? 'Merge Files' : 'Rotate PDF';
|
| 464 |
+
}
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
function showError(type, msg) {
|
| 468 |
+
const el = document.getElementById(`${type}Error`);
|
| 469 |
+
el.textContent = "Error: " + msg;
|
| 470 |
+
el.style.display = 'block';
|
| 471 |
+
}
|
| 472 |
+
</script>
|
| 473 |
+
</body>
|
| 474 |
+
</html>
|
| 475 |
+
"""
|
| 476 |
+
return html_content
|
app/routers/security/password_generator.py
CHANGED
|
@@ -1,44 +1,432 @@
|
|
| 1 |
import random
|
| 2 |
import string
|
| 3 |
-
from fastapi import APIRouter, Query, Depends
|
| 4 |
-
from
|
|
|
|
| 5 |
|
| 6 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
router = APIRouter(
|
| 8 |
-
prefix="/security",
|
| 9 |
tags=["Password Tools"],
|
| 10 |
)
|
| 11 |
|
|
|
|
|
|
|
|
|
|
| 12 |
@router.get("/generate-password")
|
| 13 |
def generate_password(
|
| 14 |
-
length: int = Query(
|
| 15 |
include_uppercase: bool = True,
|
|
|
|
| 16 |
include_numbers: bool = True,
|
| 17 |
include_symbols: bool = True,
|
| 18 |
-
|
| 19 |
-
current_user: dict =
|
| 20 |
):
|
| 21 |
"""
|
| 22 |
-
Generates a
|
| 23 |
-
**Requires Authentication.**
|
| 24 |
"""
|
| 25 |
-
#
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
if include_uppercase:
|
| 30 |
-
|
|
|
|
| 31 |
if include_numbers:
|
| 32 |
-
|
|
|
|
| 33 |
if include_symbols:
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
-
|
| 37 |
-
generated_password = "".join(random.choice(chars) for _ in range(length))
|
| 38 |
|
| 39 |
return {
|
| 40 |
-
"requested_by":
|
| 41 |
-
"password":
|
| 42 |
"length": length,
|
| 43 |
-
"
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import random
|
| 2 |
import string
|
| 3 |
+
from fastapi import APIRouter, Query, Depends, HTTPException
|
| 4 |
+
from fastapi.responses import HTMLResponse
|
| 5 |
+
from typing import Optional
|
| 6 |
|
| 7 |
+
# ==========================
|
| 8 |
+
# π Auth & Configuration
|
| 9 |
+
# ==========================
|
| 10 |
+
# Try to import the auth system, otherwise mock it for standalone use
|
| 11 |
+
try:
|
| 12 |
+
from app.routers.auth.system import get_current_user
|
| 13 |
+
except ImportError:
|
| 14 |
+
def get_current_user(): return {"username": "Anonymous"}
|
| 15 |
+
|
| 16 |
+
AUTH_ENABLED = False # Set to True to enforce login
|
| 17 |
+
|
| 18 |
+
def auth_dependency():
|
| 19 |
+
return Depends(get_current_user) if AUTH_ENABLED else None
|
| 20 |
+
|
| 21 |
+
# ==========================
|
| 22 |
+
# π οΈ Router Setup
|
| 23 |
+
# ==========================
|
| 24 |
router = APIRouter(
|
| 25 |
+
prefix="/security",
|
| 26 |
tags=["Password Tools"],
|
| 27 |
)
|
| 28 |
|
| 29 |
+
# ==========================
|
| 30 |
+
# π² Password Logic (API)
|
| 31 |
+
# ==========================
|
| 32 |
@router.get("/generate-password")
|
| 33 |
def generate_password(
|
| 34 |
+
length: int = Query(16, ge=8, le=128, description="Length of the password"),
|
| 35 |
include_uppercase: bool = True,
|
| 36 |
+
include_lowercase: bool = True,
|
| 37 |
include_numbers: bool = True,
|
| 38 |
include_symbols: bool = True,
|
| 39 |
+
exclude_ambiguous: bool = False,
|
| 40 |
+
current_user: dict = auth_dependency()
|
| 41 |
):
|
| 42 |
"""
|
| 43 |
+
Generates a cryptographically strong random password with advanced options.
|
|
|
|
| 44 |
"""
|
| 45 |
+
# Character Sets
|
| 46 |
+
lower = string.ascii_lowercase
|
| 47 |
+
upper = string.ascii_uppercase
|
| 48 |
+
nums = string.digits
|
| 49 |
+
syms = "!@#$%^&*()_+-=[]{}|;:,.<>?"
|
| 50 |
+
ambiguous = "il1Lo0O"
|
| 51 |
+
|
| 52 |
+
# Filter ambiguous characters if requested
|
| 53 |
+
if exclude_ambiguous:
|
| 54 |
+
lower = "".join([c for c in lower if c not in ambiguous])
|
| 55 |
+
upper = "".join([c for c in upper if c not in ambiguous])
|
| 56 |
+
nums = "".join([c for c in nums if c not in ambiguous])
|
| 57 |
+
syms = "".join([c for c in syms if c not in ambiguous])
|
| 58 |
+
|
| 59 |
+
# Build Pool
|
| 60 |
+
pool = ""
|
| 61 |
+
guaranteed_chars = []
|
| 62 |
+
|
| 63 |
+
if include_lowercase:
|
| 64 |
+
pool += lower
|
| 65 |
+
guaranteed_chars.append(random.choice(lower))
|
| 66 |
if include_uppercase:
|
| 67 |
+
pool += upper
|
| 68 |
+
guaranteed_chars.append(random.choice(upper))
|
| 69 |
if include_numbers:
|
| 70 |
+
pool += nums
|
| 71 |
+
guaranteed_chars.append(random.choice(nums))
|
| 72 |
if include_symbols:
|
| 73 |
+
pool += syms
|
| 74 |
+
guaranteed_chars.append(random.choice(syms))
|
| 75 |
+
|
| 76 |
+
if not pool:
|
| 77 |
+
raise HTTPException(status_code=400, detail="At least one character type must be selected.")
|
| 78 |
+
|
| 79 |
+
# Generate remaining characters
|
| 80 |
+
remaining_length = length - len(guaranteed_chars)
|
| 81 |
+
if remaining_length < 0:
|
| 82 |
+
password_chars = guaranteed_chars[:length]
|
| 83 |
+
else:
|
| 84 |
+
password_chars = guaranteed_chars + [random.choice(pool) for _ in range(remaining_length)]
|
| 85 |
+
|
| 86 |
+
# Shuffle to remove predictable patterns
|
| 87 |
+
random.shuffle(password_chars)
|
| 88 |
+
final_password = "".join(password_chars)
|
| 89 |
+
|
| 90 |
+
# Calculate basic entropy/strength
|
| 91 |
+
pool_size = len(pool)
|
| 92 |
+
entropy = length * (len(bin(pool_size)) - 2)
|
| 93 |
+
|
| 94 |
+
strength = "Weak"
|
| 95 |
+
if entropy > 50: strength = "Medium"
|
| 96 |
+
if entropy > 80: strength = "Strong"
|
| 97 |
+
if entropy > 120: strength = "Very Strong"
|
| 98 |
|
| 99 |
+
user_info = current_user.get("username", "Anonymous") if current_user else "Anonymous"
|
|
|
|
| 100 |
|
| 101 |
return {
|
| 102 |
+
"requested_by": user_info,
|
| 103 |
+
"password": final_password,
|
| 104 |
"length": length,
|
| 105 |
+
"strength": strength,
|
| 106 |
+
"entropy_bits": int(entropy)
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
# ==========================
|
| 111 |
+
# π¨ Modern UI Endpoint
|
| 112 |
+
# ==========================
|
| 113 |
+
# Path becomes: /security/password-generator/ui
|
| 114 |
+
@router.get("/password-generator/ui", response_class=HTMLResponse)
|
| 115 |
+
async def password_ui():
|
| 116 |
+
html_content = """
|
| 117 |
+
<!DOCTYPE html>
|
| 118 |
+
<html lang="en" data-theme="light">
|
| 119 |
+
<head>
|
| 120 |
+
<meta charset="UTF-8">
|
| 121 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 122 |
+
<title>Password Tool</title>
|
| 123 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 124 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 125 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
|
| 126 |
+
|
| 127 |
+
<style>
|
| 128 |
+
:root {
|
| 129 |
+
--primary: #6366f1;
|
| 130 |
+
--primary-hover: #4f46e5;
|
| 131 |
+
--bg-body: #f8fafc;
|
| 132 |
+
--bg-card: #ffffff;
|
| 133 |
+
--text-main: #0f172a;
|
| 134 |
+
--text-sub: #64748b;
|
| 135 |
+
--border: #e2e8f0;
|
| 136 |
+
--input-bg: #f1f5f9;
|
| 137 |
+
--success: #10b981;
|
| 138 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
[data-theme="dark"] {
|
| 142 |
+
--primary: #818cf8;
|
| 143 |
+
--primary-hover: #6366f1;
|
| 144 |
+
--bg-body: #0f172a;
|
| 145 |
+
--bg-card: #1e293b;
|
| 146 |
+
--text-main: #f8fafc;
|
| 147 |
+
--text-sub: #94a3b8;
|
| 148 |
+
--border: #334155;
|
| 149 |
+
--input-bg: #0f172a;
|
| 150 |
+
--success: #34d399;
|
| 151 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: background-color 0.3s, color 0.3s; }
|
| 155 |
+
|
| 156 |
+
body {
|
| 157 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 158 |
+
background-color: var(--bg-body);
|
| 159 |
+
color: var(--text-main);
|
| 160 |
+
min-height: 100vh;
|
| 161 |
+
display: flex;
|
| 162 |
+
flex-direction: column;
|
| 163 |
+
align-items: center;
|
| 164 |
+
justify-content: center;
|
| 165 |
+
padding: 1rem;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* Navbar */
|
| 169 |
+
.navbar {
|
| 170 |
+
width: 100%; max-width: 600px; display: flex;
|
| 171 |
+
justify-content: space-between; align-items: center; margin-bottom: 2rem;
|
| 172 |
+
}
|
| 173 |
+
.brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
|
| 174 |
+
.brand span { color: var(--primary); }
|
| 175 |
+
.theme-toggle { background: none; border: none; cursor: pointer; font-size: 1.2rem; padding: 8px; border-radius: 50%; background-color: var(--bg-card); border: 1px solid var(--border); }
|
| 176 |
+
|
| 177 |
+
/* Card */
|
| 178 |
+
.container {
|
| 179 |
+
background: var(--bg-card); width: 100%; max-width: 500px;
|
| 180 |
+
border-radius: 24px; box-shadow: var(--shadow);
|
| 181 |
+
border: 1px solid var(--border); padding: 2rem;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
/* Password Display */
|
| 185 |
+
.password-box {
|
| 186 |
+
background: var(--input-bg);
|
| 187 |
+
border-radius: 12px;
|
| 188 |
+
padding: 1.5rem;
|
| 189 |
+
margin-bottom: 2rem;
|
| 190 |
+
position: relative;
|
| 191 |
+
border: 1px solid var(--border);
|
| 192 |
+
display: flex; align-items: center; justify-content: space-between;
|
| 193 |
+
}
|
| 194 |
+
.password-text {
|
| 195 |
+
font-family: 'JetBrains Mono', monospace;
|
| 196 |
+
font-size: 1.5rem;
|
| 197 |
+
word-break: break-all;
|
| 198 |
+
color: var(--text-main);
|
| 199 |
+
font-weight: 500;
|
| 200 |
+
}
|
| 201 |
+
.copy-btn {
|
| 202 |
+
background: var(--bg-card);
|
| 203 |
+
border: 1px solid var(--border);
|
| 204 |
+
border-radius: 8px;
|
| 205 |
+
padding: 0.5rem;
|
| 206 |
+
cursor: pointer;
|
| 207 |
+
color: var(--text-sub);
|
| 208 |
+
transition: all 0.2s;
|
| 209 |
+
margin-left: 1rem; flex-shrink: 0;
|
| 210 |
+
}
|
| 211 |
+
.copy-btn:hover { border-color: var(--primary); color: var(--primary); }
|
| 212 |
+
.copy-btn.copied { background-color: var(--success); color: white; border-color: var(--success); }
|
| 213 |
+
|
| 214 |
+
/* Controls */
|
| 215 |
+
.controls { display: grid; gap: 1rem; }
|
| 216 |
+
|
| 217 |
+
.range-group { margin-bottom: 1rem; }
|
| 218 |
+
.range-header { display: flex; justify-content: space-between; margin-bottom: 0.5rem; font-weight: 600; font-size: 0.9rem; }
|
| 219 |
+
input[type="range"] { width: 100%; cursor: pointer; accent-color: var(--primary); }
|
| 220 |
+
|
| 221 |
+
.options-grid {
|
| 222 |
+
display: grid; grid-template-columns: 1fr 1fr; gap: 0.8rem;
|
| 223 |
+
}
|
| 224 |
+
.checkbox-wrapper {
|
| 225 |
+
display: flex; align-items: center; gap: 0.5rem;
|
| 226 |
+
font-size: 0.9rem; color: var(--text-sub);
|
| 227 |
+
cursor: pointer;
|
| 228 |
+
}
|
| 229 |
+
input[type="checkbox"] { width: 1.2rem; height: 1.2rem; accent-color: var(--primary); cursor: pointer; }
|
| 230 |
+
|
| 231 |
+
/* Action Button */
|
| 232 |
+
.btn-generate {
|
| 233 |
+
width: 100%;
|
| 234 |
+
margin-top: 1.5rem;
|
| 235 |
+
padding: 1rem;
|
| 236 |
+
border-radius: 12px;
|
| 237 |
+
background-color: var(--primary);
|
| 238 |
+
color: white;
|
| 239 |
+
font-weight: 700;
|
| 240 |
+
font-size: 1rem;
|
| 241 |
+
border: none;
|
| 242 |
+
cursor: pointer;
|
| 243 |
+
transition: transform 0.1s, background-color 0.2s;
|
| 244 |
+
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
| 245 |
+
}
|
| 246 |
+
.btn-generate:hover { background-color: var(--primary-hover); transform: translateY(-2px); }
|
| 247 |
+
.btn-generate:active { transform: scale(0.98); }
|
| 248 |
+
|
| 249 |
+
/* Footer */
|
| 250 |
+
.dev-footer {
|
| 251 |
+
margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
|
| 252 |
+
padding: 1rem; width: 100%;
|
| 253 |
+
}
|
| 254 |
+
.dev-badge {
|
| 255 |
+
display: inline-block; background: var(--bg-card); padding: 5px 15px;
|
| 256 |
+
border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
|
| 257 |
+
}
|
| 258 |
+
.dev-name { color: var(--primary); font-weight: 700; }
|
| 259 |
+
|
| 260 |
+
.strength-tag {
|
| 261 |
+
font-size: 0.75rem; padding: 2px 8px; border-radius: 4px;
|
| 262 |
+
background: #e2e8f0; color: #64748b; font-weight: 600; margin-top: 5px; display: inline-block;
|
| 263 |
+
}
|
| 264 |
+
</style>
|
| 265 |
+
</head>
|
| 266 |
+
<body>
|
| 267 |
+
|
| 268 |
+
<nav class="navbar">
|
| 269 |
+
<div class="brand">π password generator</div>
|
| 270 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">π</button>
|
| 271 |
+
</nav>
|
| 272 |
+
|
| 273 |
+
<div class="container">
|
| 274 |
+
<div class="password-box">
|
| 275 |
+
<div style="width: 100%;">
|
| 276 |
+
<div id="passwordResult" class="password-text">...</div>
|
| 277 |
+
<div id="strengthLabel" class="strength-tag">Strength: -</div>
|
| 278 |
+
</div>
|
| 279 |
+
<button class="copy-btn" id="copyBtn" onclick="copyPassword()" title="Copy to Clipboard">
|
| 280 |
+
π
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
|
| 284 |
+
<div class="controls">
|
| 285 |
+
|
| 286 |
+
<div class="range-group">
|
| 287 |
+
<div class="range-header">
|
| 288 |
+
<label>Password Length</label>
|
| 289 |
+
<span id="lengthVal" style="color: var(--primary);">16</span>
|
| 290 |
+
</div>
|
| 291 |
+
<input type="range" id="lengthInput" min="8" max="64" value="16" oninput="updateLength(this.value)">
|
| 292 |
+
</div>
|
| 293 |
+
|
| 294 |
+
<div class="options-grid">
|
| 295 |
+
<label class="checkbox-wrapper">
|
| 296 |
+
<input type="checkbox" id="chkUpper" checked> Uppercase (A-Z)
|
| 297 |
+
</label>
|
| 298 |
+
<label class="checkbox-wrapper">
|
| 299 |
+
<input type="checkbox" id="chkLower" checked> Lowercase (a-z)
|
| 300 |
+
</label>
|
| 301 |
+
<label class="checkbox-wrapper">
|
| 302 |
+
<input type="checkbox" id="chkNumbers" checked> Numbers (0-9)
|
| 303 |
+
</label>
|
| 304 |
+
<label class="checkbox-wrapper">
|
| 305 |
+
<input type="checkbox" id="chkSymbols" checked> Symbols (!@#)
|
| 306 |
+
</label>
|
| 307 |
+
<label class="checkbox-wrapper">
|
| 308 |
+
<input type="checkbox" id="chkAmbiguous"> Exclude Ambiguous
|
| 309 |
+
</label>
|
| 310 |
+
</div>
|
| 311 |
+
|
| 312 |
+
<button class="btn-generate" onclick="generate()">β‘ Generate Password</button>
|
| 313 |
+
</div>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<footer class="dev-footer">
|
| 317 |
+
<div>Designed & Developed by</div>
|
| 318 |
+
<div class="dev-badge">
|
| 319 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 320 |
+
</div>
|
| 321 |
+
</footer>
|
| 322 |
+
|
| 323 |
+
<script>
|
| 324 |
+
// --- Theme Logic ---
|
| 325 |
+
const html = document.documentElement;
|
| 326 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 327 |
+
function toggleTheme() {
|
| 328 |
+
const current = html.getAttribute('data-theme');
|
| 329 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 330 |
+
html.setAttribute('data-theme', next);
|
| 331 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 332 |
+
localStorage.setItem('theme', next);
|
| 333 |
+
}
|
| 334 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 335 |
+
html.setAttribute('data-theme', savedTheme);
|
| 336 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 337 |
+
|
| 338 |
+
// --- UI Logic ---
|
| 339 |
+
function updateLength(val) {
|
| 340 |
+
document.getElementById('lengthVal').textContent = val;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
async function generate() {
|
| 344 |
+
const length = document.getElementById('lengthInput').value;
|
| 345 |
+
const upper = document.getElementById('chkUpper').checked;
|
| 346 |
+
const lower = document.getElementById('chkLower').checked;
|
| 347 |
+
const nums = document.getElementById('chkNumbers').checked;
|
| 348 |
+
const syms = document.getElementById('chkSymbols').checked;
|
| 349 |
+
const ambig = document.getElementById('chkAmbiguous').checked;
|
| 350 |
+
|
| 351 |
+
// Validation
|
| 352 |
+
if(!upper && !lower && !nums && !syms) {
|
| 353 |
+
alert("Please select at least one character type.");
|
| 354 |
+
return;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
const params = new URLSearchParams({
|
| 358 |
+
length: length,
|
| 359 |
+
include_uppercase: upper,
|
| 360 |
+
include_lowercase: lower,
|
| 361 |
+
include_numbers: nums,
|
| 362 |
+
include_symbols: syms,
|
| 363 |
+
exclude_ambiguous: ambig
|
| 364 |
+
});
|
| 365 |
+
|
| 366 |
+
try {
|
| 367 |
+
const res = await fetch(`/security/generate-password?${params}`);
|
| 368 |
+
const data = await res.json();
|
| 369 |
+
|
| 370 |
+
if(res.ok) {
|
| 371 |
+
const pwd = data.password;
|
| 372 |
+
const strength = data.strength;
|
| 373 |
+
|
| 374 |
+
// Animate text change
|
| 375 |
+
const resultEl = document.getElementById('passwordResult');
|
| 376 |
+
resultEl.style.opacity = 0;
|
| 377 |
+
setTimeout(() => {
|
| 378 |
+
resultEl.textContent = pwd;
|
| 379 |
+
document.getElementById('strengthLabel').textContent = `Strength: ${strength}`;
|
| 380 |
+
updateStrengthColor(strength);
|
| 381 |
+
resultEl.style.opacity = 1;
|
| 382 |
+
}, 150);
|
| 383 |
+
|
| 384 |
+
// Reset copy button
|
| 385 |
+
const copyBtn = document.getElementById('copyBtn');
|
| 386 |
+
copyBtn.classList.remove('copied');
|
| 387 |
+
copyBtn.textContent = "π";
|
| 388 |
+
} else {
|
| 389 |
+
alert(data.detail || "Error generating password");
|
| 390 |
+
}
|
| 391 |
+
} catch(e) {
|
| 392 |
+
console.error(e);
|
| 393 |
+
alert("Network error");
|
| 394 |
+
}
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
function updateStrengthColor(strength) {
|
| 398 |
+
const badge = document.getElementById('strengthLabel');
|
| 399 |
+
let color = "#64748b";
|
| 400 |
+
let bg = "#e2e8f0";
|
| 401 |
+
|
| 402 |
+
if(strength === "Strong") { color = "#10b981"; bg = "#d1fae5"; }
|
| 403 |
+
if(strength === "Very Strong") { color = "#6366f1"; bg = "#e0e7ff"; }
|
| 404 |
+
if(strength === "Weak") { color = "#ef4444"; bg = "#fee2e2"; }
|
| 405 |
+
|
| 406 |
+
badge.style.color = color;
|
| 407 |
+
badge.style.backgroundColor = bg;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
function copyPassword() {
|
| 411 |
+
const text = document.getElementById('passwordResult').textContent;
|
| 412 |
+
if(text === "...") return;
|
| 413 |
+
|
| 414 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 415 |
+
const btn = document.getElementById('copyBtn');
|
| 416 |
+
btn.classList.add('copied');
|
| 417 |
+
btn.textContent = "β
";
|
| 418 |
+
|
| 419 |
+
setTimeout(() => {
|
| 420 |
+
btn.classList.remove('copied');
|
| 421 |
+
btn.textContent = "π";
|
| 422 |
+
}, 2000);
|
| 423 |
+
});
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
// Generate one on load
|
| 427 |
+
window.onload = generate;
|
| 428 |
+
</script>
|
| 429 |
+
</body>
|
| 430 |
+
</html>
|
| 431 |
+
"""
|
| 432 |
+
return html_content
|
app/routers/testers/help.py
CHANGED
|
@@ -1,22 +1,245 @@
|
|
| 1 |
-
from fastapi import APIRouter
|
| 2 |
-
from
|
| 3 |
|
| 4 |
-
#
|
|
|
|
|
|
|
| 5 |
router = APIRouter(
|
| 6 |
prefix="/help",
|
| 7 |
tags=["Information"]
|
| 8 |
)
|
| 9 |
|
|
|
|
|
|
|
|
|
|
| 10 |
@router.get("/")
|
| 11 |
-
def get_api_info(
|
| 12 |
"""
|
| 13 |
Returns general information about the API and available endpoints.
|
|
|
|
| 14 |
"""
|
| 15 |
return {
|
| 16 |
-
"requested_by":
|
| 17 |
-
"api_name": "
|
| 18 |
-
"
|
| 19 |
-
"
|
| 20 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from fastapi.responses import HTMLResponse
|
| 3 |
|
| 4 |
+
# ==========================
|
| 5 |
+
# π οΈ Router Setup
|
| 6 |
+
# ==========================
|
| 7 |
router = APIRouter(
|
| 8 |
prefix="/help",
|
| 9 |
tags=["Information"]
|
| 10 |
)
|
| 11 |
|
| 12 |
+
# ==========================
|
| 13 |
+
# βΉοΈ API Info Endpoint
|
| 14 |
+
# ==========================
|
| 15 |
@router.get("/")
|
| 16 |
+
def get_api_info():
|
| 17 |
"""
|
| 18 |
Returns general information about the API and available endpoints.
|
| 19 |
+
No authentication required.
|
| 20 |
"""
|
| 21 |
return {
|
| 22 |
+
"requested_by": "Guest",
|
| 23 |
+
"api_name": "Easy Tools API",
|
| 24 |
+
"developer": "Sameer Banchhor",
|
| 25 |
+
"version": "1.0.0",
|
| 26 |
+
"available_tools": [
|
| 27 |
+
"/image (Compressor & BG Remover)",
|
| 28 |
+
"/pdf (Merge & Rotate)",
|
| 29 |
+
"/drive (File Storage & Aria2)",
|
| 30 |
+
"/security (Password Generator)",
|
| 31 |
+
"/server-status (System Monitor)"
|
| 32 |
+
],
|
| 33 |
+
"message": "Use the /help/ui endpoint to access the visual dashboard.",
|
| 34 |
+
"developer_info": "Created by Sameer Banchhor, Data Scientist (MSc in System Design, Kalyan PG College).",
|
| 35 |
+
"git_link": "https://github.com/sameerbanchhor-git"
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
# ==========================
|
| 39 |
+
# π§ Central Hub UI
|
| 40 |
+
# ==========================
|
| 41 |
+
@router.get("/ui", response_class=HTMLResponse)
|
| 42 |
+
async def help_ui():
|
| 43 |
+
html_content = """
|
| 44 |
+
<!DOCTYPE html>
|
| 45 |
+
<html lang="en" data-theme="light">
|
| 46 |
+
<head>
|
| 47 |
+
<meta charset="UTF-8">
|
| 48 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 49 |
+
<title>Easy Tools Hub | by Sam</title>
|
| 50 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 51 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 52 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 53 |
|
| 54 |
+
<style>
|
| 55 |
+
:root {
|
| 56 |
+
--primary: #6366f1;
|
| 57 |
+
--primary-hover: #4f46e5;
|
| 58 |
+
--bg-body: #f8fafc;
|
| 59 |
+
--bg-card: #ffffff;
|
| 60 |
+
--text-main: #0f172a;
|
| 61 |
+
--text-sub: #64748b;
|
| 62 |
+
--border: #e2e8f0;
|
| 63 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
| 64 |
+
--gradient-1: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
[data-theme="dark"] {
|
| 68 |
+
--primary: #818cf8;
|
| 69 |
+
--primary-hover: #6366f1;
|
| 70 |
+
--bg-body: #0f172a;
|
| 71 |
+
--bg-card: #1e293b;
|
| 72 |
+
--text-main: #f8fafc;
|
| 73 |
+
--text-sub: #94a3b8;
|
| 74 |
+
--border: #334155;
|
| 75 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 76 |
+
--gradient-1: linear-gradient(135deg, #4f46e5 0%, #7e22ce 100%);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: all 0.3s ease; }
|
| 80 |
+
|
| 81 |
+
body {
|
| 82 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 83 |
+
background-color: var(--bg-body);
|
| 84 |
+
color: var(--text-main);
|
| 85 |
+
min-height: 100vh;
|
| 86 |
+
display: flex;
|
| 87 |
+
flex-direction: column;
|
| 88 |
+
align-items: center;
|
| 89 |
+
padding: 2rem 1rem;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
/* Header */
|
| 93 |
+
.header-container {
|
| 94 |
+
text-align: center; margin-bottom: 3rem; animation: fadeIn 0.8s ease-out;
|
| 95 |
+
}
|
| 96 |
+
.title {
|
| 97 |
+
font-size: 2.5rem; font-weight: 800; margin-bottom: 0.5rem;
|
| 98 |
+
background: var(--gradient-1); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
| 99 |
+
}
|
| 100 |
+
.subtitle { color: var(--text-sub); font-size: 1.1rem; }
|
| 101 |
+
|
| 102 |
+
.theme-toggle {
|
| 103 |
+
position: absolute; top: 1rem; right: 1rem;
|
| 104 |
+
background: var(--bg-card); border: 1px solid var(--border);
|
| 105 |
+
color: var(--text-main); padding: 10px; border-radius: 50%; cursor: pointer;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
/* Grid */
|
| 109 |
+
.tools-grid {
|
| 110 |
+
display: grid;
|
| 111 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 112 |
+
gap: 1.5rem;
|
| 113 |
+
width: 100%; max-width: 1000px;
|
| 114 |
+
animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* Cards */
|
| 118 |
+
.tool-card {
|
| 119 |
+
background: var(--bg-card);
|
| 120 |
+
border-radius: 20px;
|
| 121 |
+
padding: 2rem;
|
| 122 |
+
border: 1px solid var(--border);
|
| 123 |
+
box-shadow: var(--shadow);
|
| 124 |
+
text-decoration: none;
|
| 125 |
+
color: var(--text-main);
|
| 126 |
+
display: flex; flex-direction: column; align-items: center; text-align: center;
|
| 127 |
+
transition: transform 0.2s, box-shadow 0.2s;
|
| 128 |
+
position: relative; overflow: hidden;
|
| 129 |
+
}
|
| 130 |
+
.tool-card:hover {
|
| 131 |
+
transform: translateY(-5px);
|
| 132 |
+
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 10px 10px -5px rgb(0 0 0 / 0.04);
|
| 133 |
+
border-color: var(--primary);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.icon { font-size: 3rem; margin-bottom: 1rem; }
|
| 137 |
+
.card-title { font-size: 1.25rem; font-weight: 700; margin-bottom: 0.5rem; }
|
| 138 |
+
.card-desc { font-size: 0.9rem; color: var(--text-sub); line-height: 1.5; }
|
| 139 |
+
|
| 140 |
+
/* Footer */
|
| 141 |
+
.dev-footer {
|
| 142 |
+
margin-top: 4rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
|
| 143 |
+
padding: 2rem; width: 100%; max-width: 600px; border-top: 1px solid var(--border);
|
| 144 |
+
animation: fadeIn 1.2s ease-out;
|
| 145 |
+
}
|
| 146 |
+
.dev-badge {
|
| 147 |
+
display: inline-block; background: var(--bg-card); padding: 8px 20px;
|
| 148 |
+
border-radius: 30px; border: 1px solid var(--border); font-weight: 600; margin-top: 10px;
|
| 149 |
+
}
|
| 150 |
+
.dev-name { color: var(--primary); font-weight: 800; }
|
| 151 |
+
.github-link {
|
| 152 |
+
display: inline-flex; align-items: center; gap: 8px; margin-top: 15px;
|
| 153 |
+
color: var(--text-main); text-decoration: none; font-weight: 600;
|
| 154 |
+
padding: 8px 16px; border-radius: 8px; background: var(--bg-card); border: 1px solid var(--border);
|
| 155 |
+
}
|
| 156 |
+
.github-link:hover { background: var(--bg-body); }
|
| 157 |
+
|
| 158 |
+
@keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
|
| 159 |
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
| 160 |
+
</style>
|
| 161 |
+
</head>
|
| 162 |
+
<body>
|
| 163 |
+
|
| 164 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">π</button>
|
| 165 |
+
|
| 166 |
+
<div class="header-container">
|
| 167 |
+
<div class="title">Easy Tools Suite</div>
|
| 168 |
+
<div class="subtitle">All your essential utilities in one place.</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
<div class="tools-grid">
|
| 172 |
+
|
| 173 |
+
<a href="/image/ui" class="tool-card">
|
| 174 |
+
<div class="icon">πΌοΈ</div>
|
| 175 |
+
<div class="card-title">Image Tools</div>
|
| 176 |
+
<div class="card-desc">Compress JPEGs to specific sizes & remove backgrounds instantly using AI.</div>
|
| 177 |
+
</a>
|
| 178 |
+
|
| 179 |
+
<a href="/pdf/ui" class="tool-card">
|
| 180 |
+
<div class="icon">π</div>
|
| 181 |
+
<div class="card-title">PDF Master</div>
|
| 182 |
+
<div class="card-desc">Merge multiple documents or rotate pages with a simple drag & drop interface.</div>
|
| 183 |
+
</a>
|
| 184 |
+
|
| 185 |
+
<a href="/drive/storage/ui" class="tool-card">
|
| 186 |
+
<div class="icon">βοΈ</div>
|
| 187 |
+
<div class="card-title">Cloud Drive</div>
|
| 188 |
+
<div class="card-desc">Upload files locally or use Aria2 for high-speed remote server downloads.</div>
|
| 189 |
+
</a>
|
| 190 |
+
|
| 191 |
+
<a href="/security/password-generator/ui" class="tool-card">
|
| 192 |
+
<div class="icon">π</div>
|
| 193 |
+
<div class="card-title">Security</div>
|
| 194 |
+
<div class="card-desc">Generate cryptographically strong passwords with advanced custom rules.</div>
|
| 195 |
+
</a>
|
| 196 |
+
|
| 197 |
+
<a href="/server-status/ui" class="tool-card">
|
| 198 |
+
<div class="icon">π</div>
|
| 199 |
+
<div class="card-title">Server Monitor</div>
|
| 200 |
+
<div class="card-desc">Real-time dashboard for CPU, RAM, Disk usage, and Network traffic.</div>
|
| 201 |
+
</a>
|
| 202 |
+
|
| 203 |
+
<a href="/docs" class="tool-card" style="border-style: dashed;">
|
| 204 |
+
<div class="icon">β‘</div>
|
| 205 |
+
<div class="card-title">API Docs</div>
|
| 206 |
+
<div class="card-desc">Explore the raw Swagger UI for testing API endpoints directly.</div>
|
| 207 |
+
</a>
|
| 208 |
+
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<footer class="dev-footer">
|
| 212 |
+
<div>Designed & Developed by</div>
|
| 213 |
+
<div class="dev-badge">
|
| 214 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 215 |
+
</div>
|
| 216 |
+
<br>
|
| 217 |
+
<div style="font-size: 0.85rem; margin-top: 5px; opacity: 0.8;">MSc in System Design, Kalyan PG College</div>
|
| 218 |
+
|
| 219 |
+
<a href="https://github.com/sameerbanchhor-git" target="_blank" class="github-link">
|
| 220 |
+
<svg height="20" width="20" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path></svg>
|
| 221 |
+
GitHub Profile
|
| 222 |
+
</a>
|
| 223 |
+
</footer>
|
| 224 |
+
|
| 225 |
+
<script>
|
| 226 |
+
// Theme Logic
|
| 227 |
+
const html = document.documentElement;
|
| 228 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 229 |
+
|
| 230 |
+
function toggleTheme() {
|
| 231 |
+
const current = html.getAttribute('data-theme');
|
| 232 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 233 |
+
html.setAttribute('data-theme', next);
|
| 234 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 235 |
+
localStorage.setItem('theme', next);
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 239 |
+
html.setAttribute('data-theme', savedTheme);
|
| 240 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 241 |
+
</script>
|
| 242 |
+
</body>
|
| 243 |
+
</html>
|
| 244 |
+
"""
|
| 245 |
+
return html_content
|
app/routers/testers/server_status.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
from fastapi import APIRouter, status
|
| 2 |
-
from fastapi.responses import JSONResponse
|
| 3 |
import platform
|
| 4 |
import psutil
|
| 5 |
|
|
@@ -10,6 +10,9 @@ router = APIRouter(
|
|
| 10 |
responses={404: {"description": "Not found"}},
|
| 11 |
)
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
@router.get(
|
| 14 |
"/full-info",
|
| 15 |
summary="Get complete server/PC status and information",
|
|
@@ -31,11 +34,14 @@ def get_server_status():
|
|
| 31 |
}
|
| 32 |
|
| 33 |
# --- CPU Information ---
|
|
|
|
|
|
|
|
|
|
| 34 |
cpu_info = {
|
| 35 |
"physical_cores": psutil.cpu_count(logical=False),
|
| 36 |
"total_cores": psutil.cpu_count(logical=True),
|
| 37 |
-
"cpu_usage_percent": psutil.cpu_percent(interval=1),
|
| 38 |
-
"cpu_frequency_mhz": f"{
|
| 39 |
}
|
| 40 |
|
| 41 |
# --- Memory Information (RAM) ---
|
|
@@ -48,18 +54,22 @@ def get_server_status():
|
|
| 48 |
}
|
| 49 |
|
| 50 |
# --- Disk Usage Information (Root partition) ---
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
| 58 |
|
| 59 |
# --- Network Information (Simple) ---
|
|
|
|
| 60 |
network_info = {
|
| 61 |
-
"bytes_sent_mb": f"{
|
| 62 |
-
"bytes_recv_mb": f"{
|
| 63 |
}
|
| 64 |
|
| 65 |
response_data = {
|
|
@@ -70,4 +80,294 @@ def get_server_status():
|
|
| 70 |
"network_io": network_info,
|
| 71 |
}
|
| 72 |
|
| 73 |
-
return JSONResponse(content=response_data, status_code=status.HTTP_200_OK)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from fastapi import APIRouter, status
|
| 2 |
+
from fastapi.responses import JSONResponse, HTMLResponse
|
| 3 |
import platform
|
| 4 |
import psutil
|
| 5 |
|
|
|
|
| 10 |
responses={404: {"description": "Not found"}},
|
| 11 |
)
|
| 12 |
|
| 13 |
+
# ==========================
|
| 14 |
+
# π API Endpoint
|
| 15 |
+
# ==========================
|
| 16 |
@router.get(
|
| 17 |
"/full-info",
|
| 18 |
summary="Get complete server/PC status and information",
|
|
|
|
| 34 |
}
|
| 35 |
|
| 36 |
# --- CPU Information ---
|
| 37 |
+
# Note: cpu_percent(interval=None) is non-blocking but requires a previous call
|
| 38 |
+
# or it returns 0.0 on first call. For an API, interval=0.1 is a good compromise.
|
| 39 |
+
freq = psutil.cpu_freq()
|
| 40 |
cpu_info = {
|
| 41 |
"physical_cores": psutil.cpu_count(logical=False),
|
| 42 |
"total_cores": psutil.cpu_count(logical=True),
|
| 43 |
+
"cpu_usage_percent": psutil.cpu_percent(interval=0.1),
|
| 44 |
+
"cpu_frequency_mhz": f"{freq.current:.2f}" if freq else "N/A",
|
| 45 |
}
|
| 46 |
|
| 47 |
# --- Memory Information (RAM) ---
|
|
|
|
| 54 |
}
|
| 55 |
|
| 56 |
# --- Disk Usage Information (Root partition) ---
|
| 57 |
+
try:
|
| 58 |
+
disk_usage = psutil.disk_usage('/')
|
| 59 |
+
disk_info = {
|
| 60 |
+
"total_gb": f"{disk_usage.total / (1024**3):.2f}",
|
| 61 |
+
"used_gb": f"{disk_usage.used / (1024**3):.2f}",
|
| 62 |
+
"free_gb": f"{disk_usage.free / (1024**3):.2f}",
|
| 63 |
+
"usage_percent": disk_usage.percent,
|
| 64 |
+
}
|
| 65 |
+
except Exception:
|
| 66 |
+
disk_info = {"error": "Could not read disk usage"}
|
| 67 |
|
| 68 |
# --- Network Information (Simple) ---
|
| 69 |
+
net_io = psutil.net_io_counters()
|
| 70 |
network_info = {
|
| 71 |
+
"bytes_sent_mb": f"{net_io.bytes_sent / (1024**2):.2f}",
|
| 72 |
+
"bytes_recv_mb": f"{net_io.bytes_recv / (1024**2):.2f}",
|
| 73 |
}
|
| 74 |
|
| 75 |
response_data = {
|
|
|
|
| 80 |
"network_io": network_info,
|
| 81 |
}
|
| 82 |
|
| 83 |
+
return JSONResponse(content=response_data, status_code=status.HTTP_200_OK)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# ==========================
|
| 87 |
+
# π₯οΈ Dashboard UI Endpoint
|
| 88 |
+
# ==========================
|
| 89 |
+
@router.get("/ui", response_class=HTMLResponse)
|
| 90 |
+
async def server_status_ui():
|
| 91 |
+
html_content = """
|
| 92 |
+
<!DOCTYPE html>
|
| 93 |
+
<html lang="en" data-theme="light">
|
| 94 |
+
<head>
|
| 95 |
+
<meta charset="UTF-8">
|
| 96 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 97 |
+
<title>Server Monitor | by Sam</title>
|
| 98 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 99 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 100 |
+
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@500&display=swap" rel="stylesheet">
|
| 101 |
+
|
| 102 |
+
<style>
|
| 103 |
+
:root {
|
| 104 |
+
--primary: #3b82f6;
|
| 105 |
+
--primary-hover: #2563eb;
|
| 106 |
+
--bg-body: #f8fafc;
|
| 107 |
+
--bg-card: #ffffff;
|
| 108 |
+
--text-main: #0f172a;
|
| 109 |
+
--text-sub: #64748b;
|
| 110 |
+
--border: #e2e8f0;
|
| 111 |
+
--shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
| 112 |
+
--success: #10b981;
|
| 113 |
+
--warning: #f59e0b;
|
| 114 |
+
--danger: #ef4444;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
[data-theme="dark"] {
|
| 118 |
+
--primary: #60a5fa;
|
| 119 |
+
--bg-body: #0f172a;
|
| 120 |
+
--bg-card: #1e293b;
|
| 121 |
+
--text-main: #f8fafc;
|
| 122 |
+
--text-sub: #94a3b8;
|
| 123 |
+
--border: #334155;
|
| 124 |
+
--shadow: 0 10px 15px -3px rgb(0 0 0 / 0.5);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
* { box-sizing: border-box; margin: 0; padding: 0; transition: all 0.3s ease; }
|
| 128 |
+
|
| 129 |
+
body {
|
| 130 |
+
font-family: 'Plus Jakarta Sans', sans-serif;
|
| 131 |
+
background-color: var(--bg-body);
|
| 132 |
+
color: var(--text-main);
|
| 133 |
+
min-height: 100vh;
|
| 134 |
+
display: flex;
|
| 135 |
+
flex-direction: column;
|
| 136 |
+
align-items: center;
|
| 137 |
+
padding: 2rem 1rem;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.navbar {
|
| 141 |
+
width: 100%; max-width: 1000px; display: flex;
|
| 142 |
+
justify-content: space-between; align-items: center; margin-bottom: 2rem;
|
| 143 |
+
}
|
| 144 |
+
.brand { font-size: 1.5rem; font-weight: 800; display: flex; align-items: center; gap: 0.5rem; }
|
| 145 |
+
.brand span { color: var(--primary); }
|
| 146 |
+
|
| 147 |
+
.theme-toggle {
|
| 148 |
+
background: var(--bg-card); border: 1px solid var(--border);
|
| 149 |
+
color: var(--text-main); padding: 8px; border-radius: 50%; cursor: pointer;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* Grid Layout */
|
| 153 |
+
.dashboard {
|
| 154 |
+
display: grid;
|
| 155 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 156 |
+
gap: 1.5rem;
|
| 157 |
+
width: 100%;
|
| 158 |
+
max-width: 1000px;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.card {
|
| 162 |
+
background: var(--bg-card);
|
| 163 |
+
border-radius: 16px;
|
| 164 |
+
padding: 1.5rem;
|
| 165 |
+
border: 1px solid var(--border);
|
| 166 |
+
box-shadow: var(--shadow);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.card-header {
|
| 170 |
+
font-size: 0.9rem; font-weight: 600; color: var(--text-sub);
|
| 171 |
+
text-transform: uppercase; letter-spacing: 0.5px;
|
| 172 |
+
margin-bottom: 1rem; display: flex; justify-content: space-between;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.stat-value {
|
| 176 |
+
font-size: 2rem; font-weight: 700; color: var(--text-main);
|
| 177 |
+
font-family: 'JetBrains Mono', monospace;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.stat-sub { font-size: 0.9rem; color: var(--text-sub); margin-top: 0.2rem; }
|
| 181 |
+
|
| 182 |
+
/* Progress Bars */
|
| 183 |
+
.progress-container {
|
| 184 |
+
height: 12px; background: var(--border); border-radius: 6px;
|
| 185 |
+
margin-top: 1rem; overflow: hidden;
|
| 186 |
+
}
|
| 187 |
+
.progress-bar {
|
| 188 |
+
height: 100%; width: 0%; background: var(--primary);
|
| 189 |
+
border-radius: 6px; transition: width 0.5s ease-in-out;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
/* System List */
|
| 193 |
+
.sys-list { list-style: none; }
|
| 194 |
+
.sys-list li {
|
| 195 |
+
display: flex; justify-content: space-between;
|
| 196 |
+
padding: 0.5rem 0; border-bottom: 1px solid var(--border);
|
| 197 |
+
font-size: 0.9rem;
|
| 198 |
+
}
|
| 199 |
+
.sys-list li:last-child { border-bottom: none; }
|
| 200 |
+
.sys-label { color: var(--text-sub); }
|
| 201 |
+
.sys-val { font-weight: 600; }
|
| 202 |
+
|
| 203 |
+
/* Footer */
|
| 204 |
+
.dev-footer {
|
| 205 |
+
margin-top: 3rem; text-align: center; font-size: 0.9rem; color: var(--text-sub);
|
| 206 |
+
padding: 1rem; border-top: 1px solid var(--border); width: 100%; max-width: 600px;
|
| 207 |
+
}
|
| 208 |
+
.dev-badge {
|
| 209 |
+
display: inline-block; background: var(--bg-card); padding: 5px 15px;
|
| 210 |
+
border-radius: 20px; border: 1px solid var(--border); font-weight: 500; margin-top: 5px;
|
| 211 |
+
}
|
| 212 |
+
.dev-name { color: var(--primary); font-weight: 700; }
|
| 213 |
+
|
| 214 |
+
.refresh-dot {
|
| 215 |
+
width: 10px; height: 10px; background-color: var(--success);
|
| 216 |
+
border-radius: 50%; display: inline-block; animation: pulse 2s infinite;
|
| 217 |
+
}
|
| 218 |
+
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } }
|
| 219 |
+
</style>
|
| 220 |
+
</head>
|
| 221 |
+
<body>
|
| 222 |
+
|
| 223 |
+
<nav class="navbar">
|
| 224 |
+
<div class="brand">π Server Monitor <span class="refresh-dot" title="Live Updates"></span></div>
|
| 225 |
+
<button class="theme-toggle" onclick="toggleTheme()" id="themeBtn">π</button>
|
| 226 |
+
</nav>
|
| 227 |
+
|
| 228 |
+
<div class="dashboard">
|
| 229 |
+
|
| 230 |
+
<div class="card">
|
| 231 |
+
<div class="card-header">
|
| 232 |
+
<span>CPU Usage</span>
|
| 233 |
+
<span id="cpuCores">-- Cores</span>
|
| 234 |
+
</div>
|
| 235 |
+
<div class="stat-value" id="cpuPercent">0%</div>
|
| 236 |
+
<div class="progress-container">
|
| 237 |
+
<div class="progress-bar" id="cpuBar"></div>
|
| 238 |
+
</div>
|
| 239 |
+
<div class="stat-sub" id="cpuFreq">Freq: -- MHz</div>
|
| 240 |
+
</div>
|
| 241 |
+
|
| 242 |
+
<div class="card">
|
| 243 |
+
<div class="card-header">Memory (RAM)</div>
|
| 244 |
+
<div class="stat-value" id="memPercent">0%</div>
|
| 245 |
+
<div class="progress-container">
|
| 246 |
+
<div class="progress-bar" id="memBar"></div>
|
| 247 |
+
</div>
|
| 248 |
+
<div class="stat-sub" id="memDetails">Used: -- / -- GB</div>
|
| 249 |
+
</div>
|
| 250 |
+
|
| 251 |
+
<div class="card">
|
| 252 |
+
<div class="card-header">Disk (Root)</div>
|
| 253 |
+
<div class="stat-value" id="diskPercent">0%</div>
|
| 254 |
+
<div class="progress-container">
|
| 255 |
+
<div class="progress-bar" id="diskBar"></div>
|
| 256 |
+
</div>
|
| 257 |
+
<div class="stat-sub" id="diskDetails">Free: -- GB</div>
|
| 258 |
+
</div>
|
| 259 |
+
|
| 260 |
+
<div class="card">
|
| 261 |
+
<div class="card-header">Network I/O</div>
|
| 262 |
+
<div class="sys-list">
|
| 263 |
+
<li>
|
| 264 |
+
<span class="sys-label">β¬οΈ Received</span>
|
| 265 |
+
<span class="sys-val" id="netRecv">-- MB</span>
|
| 266 |
+
</li>
|
| 267 |
+
<li>
|
| 268 |
+
<span class="sys-label">β¬οΈ Sent</span>
|
| 269 |
+
<span class="sys-val" id="netSent">-- MB</span>
|
| 270 |
+
</li>
|
| 271 |
+
</div>
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
<div class="card" style="grid-column: 1 / -1;">
|
| 275 |
+
<div class="card-header">System Information</div>
|
| 276 |
+
<ul class="sys-list" id="sysList">
|
| 277 |
+
</ul>
|
| 278 |
+
</div>
|
| 279 |
+
|
| 280 |
+
</div>
|
| 281 |
+
|
| 282 |
+
<footer class="dev-footer">
|
| 283 |
+
<div>Designed & Developed by</div>
|
| 284 |
+
<div class="dev-badge">
|
| 285 |
+
<span class="dev-name">Sameer Banchhor</span> | Data Scientist
|
| 286 |
+
</div>
|
| 287 |
+
</footer>
|
| 288 |
+
|
| 289 |
+
<script>
|
| 290 |
+
// Theme Logic
|
| 291 |
+
const html = document.documentElement;
|
| 292 |
+
const themeBtn = document.getElementById('themeBtn');
|
| 293 |
+
|
| 294 |
+
function toggleTheme() {
|
| 295 |
+
const current = html.getAttribute('data-theme');
|
| 296 |
+
const next = current === 'light' ? 'dark' : 'light';
|
| 297 |
+
html.setAttribute('data-theme', next);
|
| 298 |
+
themeBtn.textContent = next === 'light' ? 'π' : 'βοΈ';
|
| 299 |
+
localStorage.setItem('theme', next);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
const savedTheme = localStorage.getItem('theme') || 'light';
|
| 303 |
+
html.setAttribute('data-theme', savedTheme);
|
| 304 |
+
themeBtn.textContent = savedTheme === 'light' ? 'π' : 'βοΈ';
|
| 305 |
+
|
| 306 |
+
// Data Fetching Logic
|
| 307 |
+
async function fetchData() {
|
| 308 |
+
try {
|
| 309 |
+
const res = await fetch('/server-status/full-info');
|
| 310 |
+
const data = await res.json();
|
| 311 |
+
|
| 312 |
+
updateUI(data);
|
| 313 |
+
} catch (error) {
|
| 314 |
+
console.error("Failed to fetch status:", error);
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
function updateUI(data) {
|
| 319 |
+
// CPU
|
| 320 |
+
const cpuP = data.cpu_status.cpu_usage_percent;
|
| 321 |
+
document.getElementById('cpuPercent').textContent = cpuP + '%';
|
| 322 |
+
document.getElementById('cpuBar').style.width = cpuP + '%';
|
| 323 |
+
setColor('cpuBar', cpuP);
|
| 324 |
+
document.getElementById('cpuCores').textContent = `${data.cpu_status.physical_cores}P / ${data.cpu_status.total_cores}L Cores`;
|
| 325 |
+
document.getElementById('cpuFreq').textContent = `Freq: ${data.cpu_status.cpu_frequency_mhz} MHz`;
|
| 326 |
+
|
| 327 |
+
// RAM
|
| 328 |
+
const memP = data.memory_status.usage_percent;
|
| 329 |
+
document.getElementById('memPercent').textContent = memP + '%';
|
| 330 |
+
document.getElementById('memBar').style.width = memP + '%';
|
| 331 |
+
setColor('memBar', memP);
|
| 332 |
+
document.getElementById('memDetails').textContent = `Used: ${data.memory_status.used_gb} / ${data.memory_status.total_gb} GB`;
|
| 333 |
+
|
| 334 |
+
// Disk
|
| 335 |
+
const diskP = data.disk_status.usage_percent;
|
| 336 |
+
document.getElementById('diskPercent').textContent = diskP + '%';
|
| 337 |
+
document.getElementById('diskBar').style.width = diskP + '%';
|
| 338 |
+
setColor('diskBar', diskP);
|
| 339 |
+
document.getElementById('diskDetails').textContent = `Free: ${data.disk_status.free_gb} GB`;
|
| 340 |
+
|
| 341 |
+
// Network
|
| 342 |
+
document.getElementById('netRecv').textContent = data.network_io.bytes_recv_mb + ' MB';
|
| 343 |
+
document.getElementById('netSent').textContent = data.network_io.bytes_sent_mb + ' MB';
|
| 344 |
+
|
| 345 |
+
// System Info (Only populate once if empty)
|
| 346 |
+
const sysList = document.getElementById('sysList');
|
| 347 |
+
if(sysList.children.length === 0) {
|
| 348 |
+
const info = data.system_information;
|
| 349 |
+
for (const [key, value] of Object.entries(info)) {
|
| 350 |
+
const li = document.createElement('li');
|
| 351 |
+
li.innerHTML = `<span class="sys-label">${key}</span><span class="sys-val">${value}</span>`;
|
| 352 |
+
sysList.appendChild(li);
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
function setColor(id, percent) {
|
| 358 |
+
const el = document.getElementById(id);
|
| 359 |
+
if(percent < 60) el.style.backgroundColor = 'var(--primary)';
|
| 360 |
+
else if(percent < 85) el.style.backgroundColor = 'var(--warning)';
|
| 361 |
+
else el.style.backgroundColor = 'var(--danger)';
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Init
|
| 365 |
+
fetchData();
|
| 366 |
+
// Refresh every 2 seconds
|
| 367 |
+
setInterval(fetchData, 2000);
|
| 368 |
+
|
| 369 |
+
</script>
|
| 370 |
+
</body>
|
| 371 |
+
</html>
|
| 372 |
+
"""
|
| 373 |
+
return html_content
|