Ruhivig65 commited on
Commit
4597a38
·
verified ·
1 Parent(s): b3118e9

Upload 6 files

Browse files
api/routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """RUHI-CORE API Routes"""
api/routes/auth.py CHANGED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ============================================
3
+ RUHI-CORE - Auth Routes
4
+ ============================================
5
+ """
6
+
7
+ from datetime import datetime, timedelta
8
+ from fastapi import APIRouter, HTTPException, Request, Response
9
+ from fastapi.responses import JSONResponse
10
+ from pydantic import BaseModel
11
+ from loguru import logger
12
+
13
+ from core.auth import authenticate_admin, create_access_token, get_current_user, verify_token
14
+ from core.config import settings
15
+
16
+ router = APIRouter(prefix="/api/auth", tags=["Authentication"])
17
+
18
+
19
+ class LoginRequest(BaseModel):
20
+ username: str
21
+ password: str
22
+
23
+
24
+ class LoginResponse(BaseModel):
25
+ access_token: str
26
+ token_type: str = "bearer"
27
+ expires_in: int
28
+ username: str
29
+
30
+
31
+ @router.post("/login", response_model=LoginResponse)
32
+ async def login(credentials: LoginRequest, request: Request):
33
+ """Admin login endpoint"""
34
+
35
+ client_ip = request.client.host
36
+ forwarded = request.headers.get("X-Forwarded-For")
37
+ if forwarded:
38
+ client_ip = forwarded.split(",")[0].strip()
39
+
40
+ if not authenticate_admin(credentials.username, credentials.password):
41
+ logger.warning(f"🚫 Failed login attempt from {client_ip} with username: {credentials.username}")
42
+ raise HTTPException(
43
+ status_code=401,
44
+ detail="Invalid username or password"
45
+ )
46
+
47
+ # Create JWT token
48
+ token = create_access_token(
49
+ data={"sub": credentials.username, "ip": client_ip},
50
+ expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
51
+ )
52
+
53
+ logger.info(f"✅ Admin login successful from {client_ip}")
54
+
55
+ response = JSONResponse(content={
56
+ "access_token": token,
57
+ "token_type": "bearer",
58
+ "expires_in": settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
59
+ "username": credentials.username
60
+ })
61
+
62
+ # Set cookie too
63
+ response.set_cookie(
64
+ key="access_token",
65
+ value=token,
66
+ max_age=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60,
67
+ httponly=True,
68
+ samesite="lax"
69
+ )
70
+
71
+ return response
72
+
73
+
74
+ @router.post("/logout")
75
+ async def logout(response: Response):
76
+ """Logout endpoint"""
77
+ response = JSONResponse(content={"message": "Logged out successfully"})
78
+ response.delete_cookie("access_token")
79
+ return response
80
+
81
+
82
+ @router.get("/verify")
83
+ async def verify(request: Request):
84
+ """Verify if current token is valid"""
85
+ try:
86
+ user = await get_current_user(request)
87
+ return {
88
+ "valid": True,
89
+ "username": user["username"],
90
+ "expires": user.get("exp")
91
+ }
92
+ except HTTPException:
93
+ return {"valid": False}
api/routes/files.py ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ============================================
3
+ 🔥 RUHI-CORE - File Management API Routes
4
+ ============================================
5
+ Complete REST API for file operations:
6
+ - Browse directories
7
+ - Read/Write/Edit files
8
+ - Create/Delete/Rename
9
+ - Search files
10
+ - Upload/Download
11
+ - ZIP deploy
12
+ - Disk usage
13
+ - Bulk operations
14
+ """
15
+
16
+ import os
17
+ from typing import Optional, List
18
+ from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form, Query, Request
19
+ from fastapi.responses import FileResponse, StreamingResponse
20
+ from pydantic import BaseModel
21
+ from loguru import logger
22
+
23
+ from core.auth import get_current_user
24
+ from core.file_manager import file_manager
25
+ from core.zip_deployer import zip_deployer
26
+ from core.config import settings
27
+
28
+ router = APIRouter(prefix="/api/files", tags=["File Manager"])
29
+
30
+
31
+ # =========================================
32
+ # Request Models
33
+ # =========================================
34
+ class WriteFileRequest(BaseModel):
35
+ path: str
36
+ content: str
37
+
38
+
39
+ class CreateFileRequest(BaseModel):
40
+ path: str
41
+ content: str = ""
42
+ is_directory: bool = False
43
+
44
+
45
+ class RenameRequest(BaseModel):
46
+ path: str
47
+ new_name: str
48
+
49
+
50
+ class MoveRequest(BaseModel):
51
+ source: str
52
+ destination: str
53
+
54
+
55
+ class CopyRequest(BaseModel):
56
+ source: str
57
+ destination: str
58
+
59
+
60
+ class BulkDeleteRequest(BaseModel):
61
+ paths: List[str]
62
+
63
+
64
+ class ChmodRequest(BaseModel):
65
+ path: str
66
+ permissions: str
67
+
68
+
69
+ class DeployRequest(BaseModel):
70
+ deploy_id: str
71
+ extract_dir: str
72
+ service_name: str
73
+ language: str = "python"
74
+ entry_file: str = "main.py"
75
+ service_type: str = "web"
76
+ auto_start: bool = True
77
+ install_deps: bool = True
78
+ env_vars: dict = {}
79
+ command: str = ""
80
+ description: str = ""
81
+
82
+
83
+ # =========================================
84
+ # Directory Browsing
85
+ # =========================================
86
+ @router.get("/browse")
87
+ async def browse_directory(
88
+ path: str = Query(default="", description="Relative path to browse"),
89
+ show_hidden: bool = Query(default=False, description="Show hidden files"),
90
+ user=Depends(get_current_user)
91
+ ):
92
+ """Browse a directory and list its contents"""
93
+ try:
94
+ result = await file_manager.list_directory(path, show_hidden)
95
+ return result
96
+ except FileNotFoundError as e:
97
+ raise HTTPException(status_code=404, detail=str(e))
98
+ except PermissionError as e:
99
+ raise HTTPException(status_code=403, detail=str(e))
100
+ except NotADirectoryError as e:
101
+ raise HTTPException(status_code=400, detail=str(e))
102
+
103
+
104
+ # =========================================
105
+ # File Read/Write
106
+ # =========================================
107
+ @router.get("/read")
108
+ async def read_file(
109
+ path: str = Query(description="File path to read"),
110
+ user=Depends(get_current_user)
111
+ ):
112
+ """Read a file's content for the code editor"""
113
+ try:
114
+ result = await file_manager.read_file(path)
115
+ return result
116
+ except FileNotFoundError as e:
117
+ raise HTTPException(status_code=404, detail=str(e))
118
+ except IsADirectoryError as e:
119
+ raise HTTPException(status_code=400, detail=str(e))
120
+ except PermissionError as e:
121
+ raise HTTPException(status_code=403, detail=str(e))
122
+
123
+
124
+ @router.post("/write")
125
+ async def write_file(
126
+ req: WriteFileRequest,
127
+ user=Depends(get_current_user)
128
+ ):
129
+ """Write/save content to a file (Code Editor save)"""
130
+ try:
131
+ result = await file_manager.write_file(req.path, req.content)
132
+ return result
133
+ except PermissionError as e:
134
+ raise HTTPException(status_code=403, detail=str(e))
135
+ except IOError as e:
136
+ raise HTTPException(status_code=500, detail=str(e))
137
+
138
+
139
+ # =========================================
140
+ # Create File/Directory
141
+ # =========================================
142
+ @router.post("/create")
143
+ async def create_file_or_dir(
144
+ req: CreateFileRequest,
145
+ user=Depends(get_current_user)
146
+ ):
147
+ """Create a new file or directory"""
148
+ try:
149
+ if req.is_directory:
150
+ result = await file_manager.create_directory(req.path)
151
+ else:
152
+ result = await file_manager.create_file(req.path, req.content)
153
+ return result
154
+ except FileExistsError as e:
155
+ raise HTTPException(status_code=409, detail=str(e))
156
+ except PermissionError as e:
157
+ raise HTTPException(status_code=403, detail=str(e))
158
+
159
+
160
+ # =========================================
161
+ # Delete
162
+ # =========================================
163
+ @router.delete("/delete")
164
+ async def delete_file_or_dir(
165
+ path: str = Query(description="Path to delete"),
166
+ user=Depends(get_current_user)
167
+ ):
168
+ """Delete a file or directory"""
169
+ try:
170
+ result = await file_manager.delete_path(path)
171
+ return result
172
+ except FileNotFoundError as e:
173
+ raise HTTPException(status_code=404, detail=str(e))
174
+ except PermissionError as e:
175
+ raise HTTPException(status_code=403, detail=str(e))
176
+
177
+
178
+ @router.post("/bulk-delete")
179
+ async def bulk_delete(
180
+ req: BulkDeleteRequest,
181
+ user=Depends(get_current_user)
182
+ ):
183
+ """Delete multiple files/directories"""
184
+ result = await file_manager.bulk_delete(req.paths)
185
+ return result
186
+
187
+
188
+ # =========================================
189
+ # Rename & Move & Copy
190
+ # =========================================
191
+ @router.post("/rename")
192
+ async def rename_file_or_dir(
193
+ req: RenameRequest,
194
+ user=Depends(get_current_user)
195
+ ):
196
+ """Rename a file or directory"""
197
+ try:
198
+ result = await file_manager.rename_path(req.path, req.new_name)
199
+ return result
200
+ except (FileNotFoundError, FileExistsError, ValueError) as e:
201
+ raise HTTPException(status_code=400, detail=str(e))
202
+
203
+
204
+ @router.post("/move")
205
+ async def move_file_or_dir(
206
+ req: MoveRequest,
207
+ user=Depends(get_current_user)
208
+ ):
209
+ """Move a file or directory"""
210
+ try:
211
+ result = await file_manager.move_path(req.source, req.destination)
212
+ return result
213
+ except (FileNotFoundError, FileExistsError) as e:
214
+ raise HTTPException(status_code=400, detail=str(e))
215
+
216
+
217
+ @router.post("/copy")
218
+ async def copy_file_or_dir(
219
+ req: CopyRequest,
220
+ user=Depends(get_current_user)
221
+ ):
222
+ """Copy a file or directory"""
223
+ try:
224
+ result = await file_manager.copy_path(req.source, req.destination)
225
+ return result
226
+ except (FileNotFoundError, FileExistsError) as e:
227
+ raise HTTPException(status_code=400, detail=str(e))
228
+
229
+
230
+ # =========================================
231
+ # File Info & Search
232
+ # =========================================
233
+ @router.get("/info")
234
+ async def get_file_info(
235
+ path: str = Query(description="File/directory path"),
236
+ user=Depends(get_current_user)
237
+ ):
238
+ """Get detailed file/directory information"""
239
+ try:
240
+ result = await file_manager.get_file_info(path)
241
+ return result
242
+ except FileNotFoundError as e:
243
+ raise HTTPException(status_code=404, detail=str(e))
244
+
245
+
246
+ @router.get("/search")
247
+ async def search_files(
248
+ query: str = Query(description="Search query"),
249
+ path: str = Query(default="", description="Search within path"),
250
+ max_results: int = Query(default=100, ge=1, le=500),
251
+ user=Depends(get_current_user)
252
+ ):
253
+ """Search for files by name"""
254
+ try:
255
+ result = await file_manager.search_files(query, path, max_results)
256
+ return result
257
+ except NotADirectoryError as e:
258
+ raise HTTPException(status_code=400, detail=str(e))
259
+
260
+
261
+ # =========================================
262
+ # Disk Usage
263
+ # =========================================
264
+ @router.get("/disk-usage")
265
+ async def get_disk_usage(
266
+ path: str = Query(default="", description="Path to analyze"),
267
+ user=Depends(get_current_user)
268
+ ):
269
+ """Get detailed disk usage analysis"""
270
+ try:
271
+ result = await file_manager.get_disk_usage(path)
272
+ return result
273
+ except NotADirectoryError as e:
274
+ raise HTTPException(status_code=400, detail=str(e))
275
+
276
+
277
+ # =========================================
278
+ # File Download
279
+ # =========================================
280
+ @router.get("/download")
281
+ async def download_file(
282
+ path: str = Query(description="File path to download"),
283
+ user=Depends(get_current_user)
284
+ ):
285
+ """Download a file"""
286
+ try:
287
+ file_path, mime_type = await file_manager.download_file_path(path)
288
+ return FileResponse(
289
+ path=str(file_path),
290
+ filename=file_path.name,
291
+ media_type=mime_type
292
+ )
293
+ except FileNotFoundError as e:
294
+ raise HTTPException(status_code=404, detail=str(e))
295
+ except IsADirectoryError as e:
296
+ raise HTTPException(status_code=400, detail=str(e))
297
+
298
+
299
+ # =========================================
300
+ # File Upload (Simple)
301
+ # =========================================
302
+ @router.post("/upload")
303
+ async def upload_file(
304
+ file: UploadFile = File(...),
305
+ path: str = Form(default="uploads", description="Upload destination directory"),
306
+ user=Depends(get_current_user)
307
+ ):
308
+ """Upload a single file to a specified directory"""
309
+ try:
310
+ content = await file.read()
311
+
312
+ # Check size
313
+ if len(content) > settings.MAX_UPLOAD_SIZE:
314
+ raise HTTPException(
315
+ status_code=413,
316
+ detail=f"File too large. Max: {settings.MAX_UPLOAD_SIZE / (1024*1024):.0f}MB"
317
+ )
318
+
319
+ # Save file
320
+ dest_path = f"{path}/{file.filename}"
321
+ result = await file_manager.write_file(dest_path, "")
322
+
323
+ # Write binary content
324
+ import aiofiles
325
+ full_path = file_manager._resolve_path(dest_path)
326
+ full_path.parent.mkdir(parents=True, exist_ok=True)
327
+
328
+ async with aiofiles.open(full_path, 'wb') as f:
329
+ await f.write(content)
330
+
331
+ size = len(content)
332
+
333
+ return {
334
+ "filename": file.filename,
335
+ "path": dest_path,
336
+ "size": size,
337
+ "size_formatted": file_manager._format_size(size),
338
+ "content_type": file.content_type,
339
+ "message": f"File '{file.filename}' uploaded successfully"
340
+ }
341
+ except Exception as e:
342
+ raise HTTPException(status_code=500, detail=str(e))
343
+
344
+
345
+ # =========================================
346
+ # Multi-File Upload
347
+ # =========================================
348
+ @router.post("/upload-multiple")
349
+ async def upload_multiple_files(
350
+ files: List[UploadFile] = File(...),
351
+ path: str = Form(default="uploads"),
352
+ user=Depends(get_current_user)
353
+ ):
354
+ """Upload multiple files"""
355
+ results = []
356
+ total_size = 0
357
+
358
+ for file in files:
359
+ try:
360
+ content = await file.read()
361
+ dest_path = f"{path}/{file.filename}"
362
+
363
+ full_path = file_manager._resolve_path(dest_path)
364
+ full_path.parent.mkdir(parents=True, exist_ok=True)
365
+
366
+ import aiofiles
367
+ async with aiofiles.open(full_path, 'wb') as f:
368
+ await f.write(content)
369
+
370
+ size = len(content)
371
+ total_size += size
372
+
373
+ results.append({
374
+ "filename": file.filename,
375
+ "path": dest_path,
376
+ "size": size,
377
+ "success": True
378
+ })
379
+ except Exception as e:
380
+ results.append({
381
+ "filename": file.filename,
382
+ "error": str(e),
383
+ "success": False
384
+ })
385
+
386
+ return {
387
+ "total_files": len(files),
388
+ "successful": sum(1 for r in results if r['success']),
389
+ "failed": sum(1 for r in results if not r['success']),
390
+ "total_size": total_size,
391
+ "total_size_formatted": file_manager._format_size(total_size),
392
+ "results": results
393
+ }
394
+
395
+
396
+ # =========================================
397
+ # ZIP Deploy Routes
398
+ # =========================================
399
+ @router.post("/deploy/upload")
400
+ async def deploy_upload_zip(
401
+ file: UploadFile = File(...),
402
+ service_name: str = Form(default=""),
403
+ auto_start: bool = Form(default=True),
404
+ user=Depends(get_current_user)
405
+ ):
406
+ """
407
+ 🚀 Quick Deploy: Upload ZIP -> Auto-detect -> Deploy
408
+ One-click deployment!
409
+ """
410
+ try:
411
+ content = await file.read()
412
+
413
+ # Check size
414
+ if len(content) > settings.MAX_UPLOAD_SIZE:
415
+ raise HTTPException(
416
+ status_code=413,
417
+ detail=f"File too large. Max: {settings.MAX_UPLOAD_SIZE / (1024*1024):.0f}MB"
418
+ )
419
+
420
+ result = await zip_deployer.quick_deploy(
421
+ filename=file.filename,
422
+ content=content,
423
+ service_name=service_name or None,
424
+ auto_start=auto_start
425
+ )
426
+
427
+ return result
428
+
429
+ except ValueError as e:
430
+ raise HTTPException(status_code=400, detail=str(e))
431
+ except RuntimeError as e:
432
+ raise HTTPException(status_code=500, detail=str(e))
433
+
434
+
435
+ @router.post("/deploy/extract")
436
+ async def deploy_extract(
437
+ file: UploadFile = File(...),
438
+ user=Depends(get_current_user)
439
+ ):
440
+ """
441
+ Step 1 of manual deploy: Upload and extract archive.
442
+ Returns file list and auto-detection results.
443
+ """
444
+ try:
445
+ content = await file.read()
446
+
447
+ # Save
448
+ upload_info = await zip_deployer.save_upload(file.filename, content)
449
+
450
+ # Extract
451
+ extract_info = await zip_deployer.extract_archive(
452
+ upload_info["path"],
453
+ upload_info["deploy_id"]
454
+ )
455
+
456
+ # Auto-detect
457
+ detection = await zip_deployer.auto_detect(extract_info["extract_dir"])
458
+
459
+ return {
460
+ "upload": upload_info,
461
+ "extraction": extract_info,
462
+ "detection": detection,
463
+ "message": "Archive extracted and analyzed. Ready to deploy."
464
+ }
465
+
466
+ except Exception as e:
467
+ raise HTTPException(status_code=500, detail=str(e))
468
+
469
+
470
+ @router.post("/deploy/confirm")
471
+ async def deploy_confirm(
472
+ req: DeployRequest,
473
+ user=Depends(get_current_user)
474
+ ):
475
+ """
476
+ Step 2 of manual deploy: Confirm settings and deploy.
477
+ """
478
+ try:
479
+ result = await zip_deployer.deploy(
480
+ deploy_id=req.deploy_id,
481
+ extract_dir=req.extract_dir,
482
+ service_name=req.service_name,
483
+ language=req.language,
484
+ entry_file=req.entry_file,
485
+ service_type=req.service_type,
486
+ auto_start=req.auto_start,
487
+ install_deps=req.install_deps,
488
+ env_vars=req.env_vars,
489
+ command=req.command,
490
+ description=req.description
491
+ )
492
+ return result
493
+ except Exception as e:
494
+ raise HTTPException(status_code=500, detail=str(e))
495
+
496
+
497
+ @router.get("/deploy/history")
498
+ async def deployment_history(user=Depends(get_current_user)):
499
+ """Get deployment history"""
500
+ history = await zip_deployer.get_deployment_history()
501
+ return {"deployments": history}
502
+
503
+
504
+ # =========================================
505
+ # Permissions
506
+ # =========================================
507
+ @router.post("/chmod")
508
+ async def change_permissions(
509
+ req: ChmodRequest,
510
+ user=Depends(get_current_user)
511
+ ):
512
+ """Change file/directory permissions"""
513
+ try:
514
+ result = await file_manager.chmod_path(req.path, req.permissions)
515
+ return result
516
+ except (FileNotFoundError, ValueError) as e:
517
+ raise HTTPException(status_code=400, detail=str(e))
518
+
519
+
520
+ # =========================================
521
+ # Tree View
522
+ # =========================================
523
+ @router.get("/tree")
524
+ async def get_file_tree(
525
+ path: str = Query(default="", description="Root path"),
526
+ depth: int = Query(default=3, ge=1, le=5),
527
+ user=Depends(get_current_user)
528
+ ):
529
+ """Get a tree view of the directory structure"""
530
+
531
+ def build_tree(dir_path, current_depth=0, max_depth=3):
532
+ if current_depth >= max_depth:
533
+ return []
534
+
535
+ items = []
536
+ try:
537
+ entries = sorted(dir_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
538
+ for entry in entries:
539
+ if entry.name.startswith('.') and entry.name not in ('.env', '.gitignore'):
540
+ continue
541
+
542
+ item = {
543
+ "name": entry.name,
544
+ "path": str(entry.relative_to(file_manager.base_path)),
545
+ "is_dir": entry.is_dir(),
546
+ "icon": "📁" if entry.is_dir() else file_manager._get_file_icon(entry.name),
547
+ }
548
+
549
+ if entry.is_dir():
550
+ item["children"] = build_tree(entry, current_depth + 1, max_depth)
551
+ else:
552
+ try:
553
+ item["size"] = file_manager._format_size(entry.stat().st_size)
554
+ except OSError:
555
+ item["size"] = "?"
556
+
557
+ items.append(item)
558
+ except PermissionError:
559
+ pass
560
+
561
+ return items
562
+
563
+ root = file_manager._resolve_path(path)
564
+ if not root.is_dir():
565
+ raise HTTPException(status_code=400, detail="Not a directory")
566
+
567
+ tree = build_tree(root, max_depth=depth)
568
+
569
+ return {
570
+ "path": path or "/data",
571
+ "depth": depth,
572
+ "tree": tree
573
+ }
api/routes/metrics.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ============================================
3
+ RUHI-CORE - Metrics & Monitoring Routes
4
+ ============================================
5
+ """
6
+
7
+ import os
8
+ import shutil
9
+ from fastapi import APIRouter, Depends, Query
10
+ from loguru import logger
11
+
12
+ from core.auth import get_current_user
13
+ from core.metrics_collector import metrics_collector
14
+ from core.process_manager import process_manager
15
+ from core.websocket_handler import ws_manager
16
+ from core.config import settings
17
+
18
+ router = APIRouter(prefix="/api/metrics", tags=["Metrics"])
19
+
20
+
21
+ @router.get("/current")
22
+ async def get_current_metrics(user=Depends(get_current_user)):
23
+ """Get current system metrics snapshot"""
24
+ metrics = await metrics_collector.collect_all()
25
+ metrics["services"] = process_manager.get_stats()
26
+ metrics["websocket_connections"] = ws_manager.total_connections
27
+ return {"metrics": metrics}
28
+
29
+
30
+ @router.get("/history")
31
+ async def get_metrics_history(
32
+ minutes: int = Query(default=60, ge=1, le=1440),
33
+ user=Depends(get_current_user)
34
+ ):
35
+ """Get metrics history for charts"""
36
+ history = metrics_collector.get_history(minutes)
37
+ return {
38
+ "period_minutes": minutes,
39
+ "data_points": len(history),
40
+ "history": history
41
+ }
42
+
43
+
44
+ @router.get("/summary")
45
+ async def get_system_summary(user=Depends(get_current_user)):
46
+ """Get a complete system summary - perfect for dashboard overview"""
47
+ metrics = await metrics_collector.collect_all()
48
+ service_stats = process_manager.get_stats()
49
+
50
+ # Storage breakdown
51
+ data_path = str(settings.DATA_DIR)
52
+ storage_breakdown = {}
53
+ for subdir in ["apps", "logs", "uploads", "backups", "db", "config"]:
54
+ dir_path = os.path.join(data_path, subdir)
55
+ if os.path.exists(dir_path):
56
+ total_size = 0
57
+ for dirpath, dirnames, filenames in os.walk(dir_path):
58
+ for f in filenames:
59
+ fp = os.path.join(dirpath, f)
60
+ try:
61
+ total_size += os.path.getsize(fp)
62
+ except OSError:
63
+ pass
64
+ storage_breakdown[subdir] = round(total_size / (1024 * 1024), 2)
65
+
66
+ return {
67
+ "system": {
68
+ "platform": metrics.get("platform"),
69
+ "hostname": metrics.get("hostname"),
70
+ "python_version": metrics.get("python_version"),
71
+ "uptime": metrics.get("ruhi_uptime_formatted"),
72
+ },
73
+ "cpu": {
74
+ "usage_percent": metrics.get("cpu_percent"),
75
+ "cores_logical": metrics.get("cpu_cores_logical"),
76
+ "cores_physical": metrics.get("cpu_cores_physical"),
77
+ "frequency_mhz": metrics.get("cpu_freq_current"),
78
+ "load_average": [
79
+ metrics.get("load_avg_1"),
80
+ metrics.get("load_avg_5"),
81
+ metrics.get("load_avg_15")
82
+ ]
83
+ },
84
+ "memory": {
85
+ "usage_percent": metrics.get("memory_percent"),
86
+ "used_mb": metrics.get("memory_used_mb"),
87
+ "total_mb": metrics.get("memory_total_mb"),
88
+ "available_mb": metrics.get("memory_available_mb"),
89
+ },
90
+ "disk": {
91
+ "data_usage_percent": metrics.get("disk_data_percent"),
92
+ "data_used_gb": metrics.get("disk_data_used_gb"),
93
+ "data_total_gb": metrics.get("disk_data_total_gb"),
94
+ "data_free_gb": metrics.get("disk_data_free_gb"),
95
+ "breakdown_mb": storage_breakdown
96
+ },
97
+ "network": {
98
+ "sent_rate_kbps": metrics.get("net_sent_rate_kbps"),
99
+ "recv_rate_kbps": metrics.get("net_recv_rate_kbps"),
100
+ "total_sent_mb": round(metrics.get("net_bytes_sent_total", 0) / (1024*1024), 1),
101
+ "total_recv_mb": round(metrics.get("net_bytes_recv_total", 0) / (1024*1024), 1),
102
+ "active_connections": metrics.get("net_active_connections"),
103
+ },
104
+ "services": service_stats,
105
+ "processes": {
106
+ "total": metrics.get("total_processes"),
107
+ "running": metrics.get("running_processes"),
108
+ "sleeping": metrics.get("sleeping_processes"),
109
+ "zombie": metrics.get("zombie_processes"),
110
+ "top": metrics.get("top_processes", [])[:5]
111
+ },
112
+ "websockets": {
113
+ "total_connections": ws_manager.total_connections,
114
+ "dashboard_connections": len(ws_manager.dashboard_connections),
115
+ }
116
+ }
api/routes/services.py ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ============================================
3
+ RUHI-CORE - Service Management Routes
4
+ ============================================
5
+ """
6
+
7
+ from typing import Optional
8
+ from fastapi import APIRouter, HTTPException, Depends, Request
9
+ from pydantic import BaseModel
10
+ from loguru import logger
11
+
12
+ from core.auth import get_current_user
13
+ from core.process_manager import process_manager
14
+ from core.port_manager import port_manager
15
+
16
+ router = APIRouter(prefix="/api/services", tags=["Services"])
17
+
18
+
19
+ class CreateServiceRequest(BaseModel):
20
+ name: str
21
+ type: str = "web" # web, worker, bot, cron
22
+ language: str = "python" # python, node, shell
23
+ entry_file: str = "main.py"
24
+ command: Optional[str] = ""
25
+ auto_restart: bool = True
26
+ env_vars: dict = {}
27
+ description: str = ""
28
+ max_memory_mb: int = 512
29
+ port: Optional[int] = None
30
+
31
+
32
+ class UpdateServiceRequest(BaseModel):
33
+ name: Optional[str] = None
34
+ entry_file: Optional[str] = None
35
+ command: Optional[str] = None
36
+ auto_restart: Optional[bool] = None
37
+ env_vars: Optional[dict] = None
38
+ description: Optional[str] = None
39
+ max_memory_mb: Optional[int] = None
40
+
41
+
42
+ @router.get("/")
43
+ async def list_services(user=Depends(get_current_user)):
44
+ """List all services"""
45
+ return {
46
+ "services": process_manager.get_all_services(),
47
+ "stats": process_manager.get_stats()
48
+ }
49
+
50
+
51
+ @router.post("/")
52
+ async def create_service(req: CreateServiceRequest, user=Depends(get_current_user)):
53
+ """Create a new service"""
54
+ try:
55
+ service = await process_manager.create_service(
56
+ name=req.name,
57
+ type=req.type,
58
+ language=req.language,
59
+ entry_file=req.entry_file,
60
+ command=req.command,
61
+ auto_restart=req.auto_restart,
62
+ env_vars=req.env_vars,
63
+ description=req.description,
64
+ max_memory_mb=req.max_memory_mb,
65
+ port=req.port
66
+ )
67
+ return {
68
+ "message": f"Service '{req.name}' created successfully",
69
+ "service": service.get_info()
70
+ }
71
+ except ValueError as e:
72
+ raise HTTPException(status_code=400, detail=str(e))
73
+
74
+
75
+ @router.get("/{service_id}")
76
+ async def get_service(service_id: str, user=Depends(get_current_user)):
77
+ """Get service details"""
78
+ service = process_manager.get_service(service_id)
79
+ if not service:
80
+ raise HTTPException(status_code=404, detail="Service not found")
81
+ return {"service": service.get_info()}
82
+
83
+
84
+ @router.delete("/{service_id}")
85
+ async def delete_service(service_id: str, user=Depends(get_current_user)):
86
+ """Delete a service"""
87
+ try:
88
+ await process_manager.delete_service(service_id)
89
+ return {"message": "Service deleted successfully"}
90
+ except ValueError as e:
91
+ raise HTTPException(status_code=404, detail=str(e))
92
+
93
+
94
+ @router.post("/{service_id}/start")
95
+ async def start_service(service_id: str, user=Depends(get_current_user)):
96
+ """Start a service"""
97
+ try:
98
+ success = await process_manager.start_service(service_id)
99
+ service = process_manager.get_service(service_id)
100
+ return {
101
+ "message": f"Service start {'successful' if success else 'failed'}",
102
+ "success": success,
103
+ "service": service.get_info() if service else None
104
+ }
105
+ except ValueError as e:
106
+ raise HTTPException(status_code=404, detail=str(e))
107
+
108
+
109
+ @router.post("/{service_id}/stop")
110
+ async def stop_service(service_id: str, user=Depends(get_current_user)):
111
+ """Stop a service"""
112
+ try:
113
+ success = await process_manager.stop_service(service_id)
114
+ service = process_manager.get_service(service_id)
115
+ return {
116
+ "message": f"Service stop {'successful' if success else 'failed'}",
117
+ "success": success,
118
+ "service": service.get_info() if service else None
119
+ }
120
+ except ValueError as e:
121
+ raise HTTPException(status_code=404, detail=str(e))
122
+
123
+
124
+ @router.post("/{service_id}/restart")
125
+ async def restart_service(service_id: str, user=Depends(get_current_user)):
126
+ """Restart a service"""
127
+ try:
128
+ success = await process_manager.restart_service(service_id)
129
+ service = process_manager.get_service(service_id)
130
+ return {
131
+ "message": f"Service restart {'successful' if success else 'failed'}",
132
+ "success": success,
133
+ "service": service.get_info() if service else None
134
+ }
135
+ except ValueError as e:
136
+ raise HTTPException(status_code=404, detail=str(e))
137
+
138
+
139
+ @router.get("/{service_id}/logs")
140
+ async def get_service_logs(
141
+ service_id: str,
142
+ lines: int = 100,
143
+ user=Depends(get_current_user)
144
+ ):
145
+ """Get recent logs for a service"""
146
+ service = process_manager.get_service(service_id)
147
+ if not service:
148
+ raise HTTPException(status_code=404, detail="Service not found")
149
+
150
+ logs = list(service.log_buffer)[-lines:]
151
+ return {
152
+ "service_id": service_id,
153
+ "service_name": service.name,
154
+ "total_lines": len(service.log_buffer),
155
+ "returned_lines": len(logs),
156
+ "logs": logs
157
+ }
158
+
159
+
160
+ @router.put("/{service_id}")
161
+ async def update_service(
162
+ service_id: str,
163
+ req: UpdateServiceRequest,
164
+ user=Depends(get_current_user)
165
+ ):
166
+ """Update service configuration"""
167
+ service = process_manager.get_service(service_id)
168
+ if not service:
169
+ raise HTTPException(status_code=404, detail="Service not found")
170
+
171
+ if req.name is not None:
172
+ service.name = req.name
173
+ if req.entry_file is not None:
174
+ service.entry_file = req.entry_file
175
+ if req.command is not None:
176
+ service.command = req.command
177
+ if req.auto_restart is not None:
178
+ service.auto_restart = req.auto_restart
179
+ if req.env_vars is not None:
180
+ service.env_vars = req.env_vars
181
+ if req.description is not None:
182
+ service.description = req.description
183
+ if req.max_memory_mb is not None:
184
+ service.max_memory_mb = req.max_memory_mb
185
+
186
+ await service._save_to_db()
187
+
188
+ return {
189
+ "message": "Service updated successfully",
190
+ "service": service.get_info()
191
+ }
192
+
193
+
194
+ @router.post("/start-all")
195
+ async def start_all_services(user=Depends(get_current_user)):
196
+ """Start all services"""
197
+ await process_manager.start_all()
198
+ return {"message": "All services starting..."}
199
+
200
+
201
+ @router.post("/stop-all")
202
+ async def stop_all_services(user=Depends(get_current_user)):
203
+ """Stop all services"""
204
+ await process_manager.stop_all()
205
+ return {"message": "All services stopped"}
api/routes/terminal.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ============================================
3
+ RUHI-CORE - Terminal Emulator Route
4
+ ============================================
5
+ """
6
+
7
+ import os
8
+ import asyncio
9
+ import subprocess
10
+ from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, Query
11
+ from loguru import logger
12
+
13
+ from core.auth import get_current_user, verify_token
14
+ from core.config import settings
15
+
16
+ router = APIRouter(prefix="/api/terminal", tags=["Terminal"])
17
+
18
+
19
+ @router.post("/execute")
20
+ async def execute_command(
21
+ command: dict,
22
+ user=Depends(get_current_user)
23
+ ):
24
+ """Execute a shell command and return output"""
25
+ cmd = command.get("command", "").strip()
26
+ cwd = command.get("cwd", str(settings.DATA_DIR))
27
+
28
+ if not cmd:
29
+ return {"error": "No command provided"}
30
+
31
+ # Block dangerous commands
32
+ dangerous = ["rm -rf /", "mkfs", "dd if=", ":(){", "fork bomb", "shutdown", "reboot", "halt", "poweroff"]
33
+ for d in dangerous:
34
+ if d in cmd.lower():
35
+ return {"error": f"Dangerous command blocked: {cmd}"}
36
+
37
+ try:
38
+ result = await asyncio.get_event_loop().run_in_executor(
39
+ None,
40
+ lambda: subprocess.run(
41
+ cmd,
42
+ shell=True,
43
+ capture_output=True,
44
+ text=True,
45
+ timeout=30,
46
+ cwd=cwd,
47
+ env={**os.environ, "TERM": "xterm-256color"}
48
+ )
49
+ )
50
+
51
+ return {
52
+ "stdout": result.stdout,
53
+ "stderr": result.stderr,
54
+ "return_code": result.returncode,
55
+ "command": cmd,
56
+ "cwd": cwd
57
+ }
58
+ except subprocess.TimeoutExpired:
59
+ return {"error": "Command timed out (30s limit)", "command": cmd}
60
+ except Exception as e:
61
+ return {"error": str(e), "command": cmd}
62
+
63
+
64
+ @router.websocket("/ws")
65
+ async def terminal_websocket(websocket: WebSocket, token: str = Query(default="")):
66
+ """Interactive terminal via WebSocket"""
67
+
68
+ # Verify authentication
69
+ try:
70
+ verify_token(token)
71
+ except Exception:
72
+ await websocket.close(code=4001, reason="Unauthorized")
73
+ return
74
+
75
+ await websocket.accept()
76
+ logger.info("🖥️ Terminal WebSocket connected")
77
+
78
+ try:
79
+ while True:
80
+ data = await websocket.receive_text()
81
+
82
+ try:
83
+ import json
84
+ cmd_data = json.loads(data)
85
+ cmd = cmd_data.get("command", "")
86
+ cwd = cmd_data.get("cwd", str(settings.DATA_DIR))
87
+ except json.JSONDecodeError:
88
+ cmd = data
89
+ cwd = str(settings.DATA_DIR)
90
+
91
+ if not cmd.strip():
92
+ continue
93
+
94
+ # Block dangerous commands
95
+ dangerous = ["rm -rf /", "mkfs", "dd if=", ":(){", "shutdown", "reboot"]
96
+ blocked = False
97
+ for d in dangerous:
98
+ if d in cmd.lower():
99
+ await websocket.send_text(json.dumps({
100
+ "type": "error",
101
+ "message": f"⛔ Blocked: {cmd}"
102
+ }))
103
+ blocked = True
104
+ break
105
+
106
+ if blocked:
107
+ continue
108
+
109
+ try:
110
+ result = await asyncio.get_event_loop().run_in_executor(
111
+ None,
112
+ lambda: subprocess.run(
113
+ cmd, shell=True, capture_output=True,
114
+ text=True, timeout=30, cwd=cwd
115
+ )
116
+ )
117
+
118
+ import json as json_mod
119
+ await websocket.send_text(json_mod.dumps({
120
+ "type": "output",
121
+ "stdout": result.stdout,
122
+ "stderr": result.stderr,
123
+ "return_code": result.returncode,
124
+ "cwd": cwd
125
+ }))
126
+ except subprocess.TimeoutExpired:
127
+ await websocket.send_text(json.dumps({
128
+ "type": "error",
129
+ "message": "Command timed out (30s)"
130
+ }))
131
+ except Exception as e:
132
+ await websocket.send_text(json.dumps({
133
+ "type": "error",
134
+ "message": str(e)
135
+ }))
136
+
137
+ except WebSocketDisconnect:
138
+ logger.info("🖥️ Terminal WebSocket disconnected")
139
+ except Exception as e:
140
+ logger.error(f"Terminal WebSocket error: {str(e)}")