feat: explorador de archivos local y autenticacion/conexion a unidad de red UNC en storage root
This commit is contained in:
@@ -1,5 +1,12 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from typing import Optional
|
||||
import os
|
||||
import string
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.api.deps import require_admin
|
||||
from app.models.models import User
|
||||
@@ -8,6 +15,11 @@ from app.services.settings_service import get_all_settings, update_settings_serv
|
||||
|
||||
router = APIRouter(prefix="/settings", tags=["System Settings"])
|
||||
|
||||
class TestShareRequest(BaseModel):
|
||||
path: str
|
||||
username: str
|
||||
password: str
|
||||
|
||||
@router.get("", response_model=SystemSettingsResponse)
|
||||
async def get_settings(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
@@ -22,6 +34,13 @@ async def get_settings(
|
||||
"default_keep_daily": int(settings_dict.get("default_keep_daily", 7)),
|
||||
"default_keep_weekly": int(settings_dict.get("default_keep_weekly", 4)),
|
||||
"default_keep_monthly": int(settings_dict.get("default_keep_monthly", 12)),
|
||||
"auth_mode": settings_dict.get("auth_mode", "local"),
|
||||
"radius_host": settings_dict.get("radius_host", ""),
|
||||
"radius_port": int(settings_dict.get("radius_port", 1812)) if settings_dict.get("radius_port") else 1812,
|
||||
"radius_secret": settings_dict.get("radius_secret", ""),
|
||||
"storage_network_enabled": settings_dict.get("storage_network_enabled", "false").lower() == "true",
|
||||
"storage_network_user": settings_dict.get("storage_network_user", ""),
|
||||
"storage_network_pass": settings_dict.get("storage_network_pass", ""),
|
||||
}
|
||||
|
||||
@router.put("", response_model=SystemSettingsResponse)
|
||||
@@ -49,4 +68,95 @@ async def update_settings(
|
||||
"default_keep_daily": int(settings_dict.get("default_keep_daily", 7)),
|
||||
"default_keep_weekly": int(settings_dict.get("default_keep_weekly", 4)),
|
||||
"default_keep_monthly": int(settings_dict.get("default_keep_monthly", 12)),
|
||||
"auth_mode": settings_dict.get("auth_mode", "local"),
|
||||
"radius_host": settings_dict.get("radius_host", ""),
|
||||
"radius_port": int(settings_dict.get("radius_port", 1812)) if settings_dict.get("radius_port") else 1812,
|
||||
"radius_secret": settings_dict.get("radius_secret", ""),
|
||||
"storage_network_enabled": settings_dict.get("storage_network_enabled", "false").lower() == "true",
|
||||
"storage_network_user": settings_dict.get("storage_network_user", ""),
|
||||
"storage_network_pass": settings_dict.get("storage_network_pass", ""),
|
||||
}
|
||||
|
||||
@router.get("/explore-dir")
|
||||
async def explore_directory(
|
||||
path: Optional[str] = None,
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
"""
|
||||
Explore directories on the host filesystem.
|
||||
"""
|
||||
import platform
|
||||
|
||||
if not path or path.strip() == "":
|
||||
if platform.system().lower() == "windows":
|
||||
drives = []
|
||||
for letter in string.ascii_uppercase:
|
||||
drive_path = f"{letter}:\\"
|
||||
if os.path.exists(drive_path):
|
||||
drives.append({"name": drive_path, "path": drive_path})
|
||||
return {"current_path": "", "folders": drives}
|
||||
else:
|
||||
return {"current_path": "/", "folders": [{"name": "/", "path": "/"}]}
|
||||
|
||||
# Resolve path
|
||||
target = Path(path).resolve()
|
||||
if not target.exists() or not target.is_dir():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"The path '{path}' does not exist or is not a directory."
|
||||
)
|
||||
|
||||
try:
|
||||
folders = []
|
||||
for entry in os.scandir(target):
|
||||
try:
|
||||
if entry.is_dir():
|
||||
folders.append({
|
||||
"name": entry.name,
|
||||
"path": str(Path(entry.path).resolve())
|
||||
})
|
||||
except Exception:
|
||||
continue # Skip inaccessible
|
||||
|
||||
folders.sort(key=lambda x: x["name"].lower())
|
||||
|
||||
return {
|
||||
"current_path": str(target),
|
||||
"parent_path": str(target.parent) if target.parent != target else None,
|
||||
"folders": folders
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Failed to read directory contents: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/test-network-share")
|
||||
async def test_network_share(
|
||||
payload: TestShareRequest,
|
||||
admin_user: User = Depends(require_admin)
|
||||
):
|
||||
"""
|
||||
Attempts to connect to a UNC network share and tests write access.
|
||||
"""
|
||||
from app.services.settings_service import connect_network_share
|
||||
|
||||
ok = connect_network_share(payload.path, payload.username, payload.password)
|
||||
if not ok:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Failed to connect to the network share. Check the server path, username, and password."
|
||||
)
|
||||
|
||||
try:
|
||||
target_path = Path(payload.path)
|
||||
os.makedirs(target_path, exist_ok=True)
|
||||
test_file = target_path / f".write_test_{uuid.uuid4().hex[:6]}"
|
||||
test_file.touch()
|
||||
test_file.unlink()
|
||||
return {"message": "Network share connected and write access verified successfully!"}
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Network share connected, but write access test failed: {str(e)}"
|
||||
)
|
||||
|
||||
@@ -256,6 +256,13 @@ class SystemSettingsResponse(BaseModel):
|
||||
default_keep_daily: int
|
||||
default_keep_weekly: int
|
||||
default_keep_monthly: int
|
||||
auth_mode: Optional[str] = "local"
|
||||
radius_host: Optional[str] = ""
|
||||
radius_port: Optional[int] = 1812
|
||||
radius_secret: Optional[str] = ""
|
||||
storage_network_enabled: Optional[bool] = False
|
||||
storage_network_user: Optional[str] = ""
|
||||
storage_network_pass: Optional[str] = ""
|
||||
|
||||
class SystemSettingsUpdate(BaseModel):
|
||||
storage_root: Optional[str] = None
|
||||
@@ -264,6 +271,13 @@ class SystemSettingsUpdate(BaseModel):
|
||||
default_keep_daily: Optional[int] = None
|
||||
default_keep_weekly: Optional[int] = None
|
||||
default_keep_monthly: Optional[int] = None
|
||||
auth_mode: Optional[str] = None
|
||||
radius_host: Optional[str] = None
|
||||
radius_port: Optional[int] = None
|
||||
radius_secret: Optional[str] = None
|
||||
storage_network_enabled: Optional[bool] = None
|
||||
storage_network_user: Optional[str] = None
|
||||
storage_network_pass: Optional[str] = None
|
||||
|
||||
class AgentJobRegister(BaseModel):
|
||||
name: str
|
||||
|
||||
@@ -16,8 +16,40 @@ DEFAULT_SETTINGS = {
|
||||
"radius_host": "",
|
||||
"radius_port": "1812",
|
||||
"radius_secret": "",
|
||||
"storage_network_enabled": "false",
|
||||
"storage_network_user": "",
|
||||
"storage_network_pass": "",
|
||||
}
|
||||
|
||||
def connect_network_share(path: str, username: str, password: str) -> bool:
|
||||
"""
|
||||
Mounts a UNC network share on Windows using 'net use'.
|
||||
"""
|
||||
if not (path.startswith(r"\\") or path.startswith("//")):
|
||||
return True
|
||||
|
||||
parts = [p for p in path.replace("/", "\\").split("\\") if p]
|
||||
if len(parts) < 2:
|
||||
return False
|
||||
unc_base = f"\\\\{parts[0]}\\{parts[1]}"
|
||||
|
||||
import subprocess
|
||||
print(f"Connecting to network share {unc_base} with user '{username}'...")
|
||||
|
||||
# 1. Remove existing connection if any (ignore errors)
|
||||
subprocess.run(["net", "use", unc_base, "/delete", "/y"], capture_output=True)
|
||||
|
||||
# 2. Add connection
|
||||
cmd = ["net", "use", unc_base]
|
||||
if password:
|
||||
cmd.append(password)
|
||||
if username:
|
||||
cmd.append(f"/user:{username}")
|
||||
cmd.append("/persistent:yes")
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
return result.returncode == 0
|
||||
|
||||
async def get_setting(db: AsyncSession, key: str) -> str:
|
||||
result = await db.execute(select(SystemSetting).where(SystemSetting.key == key))
|
||||
setting = result.scalar_one_or_none()
|
||||
@@ -43,23 +75,36 @@ async def get_all_settings(db: AsyncSession) -> dict:
|
||||
async def update_settings_service(db: AsyncSession, new_settings: dict) -> dict:
|
||||
from app.storage.local import storage_provider
|
||||
|
||||
current = await get_all_settings(db)
|
||||
merged = {**current, **new_settings}
|
||||
|
||||
# Connect network share if enabled and it's a UNC path
|
||||
net_enabled = str(merged.get("storage_network_enabled", "false")).lower() == "true"
|
||||
net_user = merged.get("storage_network_user", "")
|
||||
net_pass = merged.get("storage_network_pass", "")
|
||||
root_path = merged.get("storage_root", "")
|
||||
|
||||
if net_enabled and root_path and (root_path.startswith(r"\\") or root_path.startswith("//")):
|
||||
ok = connect_network_share(root_path, net_user, net_pass)
|
||||
if not ok:
|
||||
raise ValueError("Failed to authenticate or connect to the specified network share (UNC). Verify path and credentials.")
|
||||
|
||||
# Perform standard storage root validation
|
||||
if "storage_root" in new_settings:
|
||||
v = new_settings["storage_root"]
|
||||
path = Path(v).resolve()
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
test_file = path / ".write_test"
|
||||
test_file.touch()
|
||||
test_file.unlink()
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid or unwritable storage root path: {str(e)}")
|
||||
|
||||
storage_provider.root_dir = path
|
||||
|
||||
for k, v in new_settings.items():
|
||||
if k in DEFAULT_SETTINGS and v is not None:
|
||||
# If changing storage root, validate and apply
|
||||
if k == "storage_root":
|
||||
path = Path(v).resolve()
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
# Test write access
|
||||
test_file = path / ".write_test"
|
||||
test_file.touch()
|
||||
test_file.unlink()
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid or unwritable storage root path: {str(e)}")
|
||||
|
||||
# Apply to in-memory storage provider
|
||||
storage_provider.root_dir = path
|
||||
|
||||
result = await db.execute(select(SystemSetting).where(SystemSetting.key == k))
|
||||
setting = result.scalar_one_or_none()
|
||||
if setting:
|
||||
|
||||
Reference in New Issue
Block a user