feat: explorador de archivos local y autenticacion/conexion a unidad de red UNC en storage root

This commit is contained in:
Carlos Tello
2026-08-17 00:05:15 -03:00
parent 7badc34816
commit f3b6d8ef71
5 changed files with 457 additions and 25 deletions
+110
View File
@@ -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)}"
)
+14
View File
@@ -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
+60 -15
View File
@@ -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: