Files
zmb-webui/backend/routers/shares.py
T
Claude Code 92bed208e0 ZMB Webui: Complete Project – Rebrand & Initial Clean Commit
ARCHITECTURE
============
Backend: FastAPI + uvicorn (port 8000)
  - JWT authentication with PAM system users
  - ZFS CLI wrapper with caching (30-60s TTL)
  - WebSocket pool status broadcaster (30s interval)
  - Services: auth, zfs_runner, file_manager, shares, identities, system_info
  - Routers: pools, datasets, snapshots, shares, identities, navigator, system

Frontend: Next.js 15 + TypeScript (static export)
  - Incremental Static Regeneration (ISR) for weak hardware
  - Type-safe API client (lib/api.ts)
  - Dark mode + custom Tailwind theme
  - Pages: Dashboard, Login, Snapshots, Datasets, Shares, etc.

DEPLOYMENT
==========
Test Target: 192.168.1.179:8090 (Debian LXC)
Production: 10.66.120.3:9090 (Raspberry Pi 4GB ARM64)
Updater: Automated Gitea-based deployment (update-test.sh, update-pi.sh)

FEATURES COMPLETED
==================
Phase 3a: Dashboard Quick Stats (System, CPU, Memory, Storage)
  - Real-time stats with color-coded progress bars
  - Responsive grid layout (mobile: 1, tablet: 2, desktop: 4 columns)
  - ISR-optimized for fast loads on weak hardware

REBRANDING
==========
Renamed throughout:
  - Project: 'ZFS Manager' → 'ZMB Webui'
  - Services: 'zfs-manager' → 'zmb-webui'
  - Systemd units: zfs-manager-backend → zmb-webui-backend
  - Configuration files and documentation

Co-Authored-By: Patrick <patrick@perlbach24.de>
2026-04-22 00:43:05 +02:00

210 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
File Sharing endpoints (Samba/NFS) like cockpit-file-sharing
"""
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import Optional
from services.shares import share_manager
from services.auth import auth_service
router = APIRouter(prefix="/api/shares", tags=["shares"])
security = HTTPBearer()
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Verify JWT token and return username"""
username = auth_service.verify_token(credentials.credentials)
if not username:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
return username
class CreateSambaShareRequest(BaseModel):
name: str
path: str
comment: Optional[str] = None
class CreateNFSShareRequest(BaseModel):
path: str
clients: str
options: Optional[str] = None
class SambaConfigRequest(BaseModel):
config: str
class SambaImportRequest(BaseModel):
config_file: str
# ============== SAMBA ==============
@router.get("/samba")
async def list_samba_shares(current_user: str = Depends(get_current_user)):
"""List all Samba shares"""
try:
shares = share_manager.list_samba_shares()
return {"shares": shares}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/samba")
async def create_samba_share(
request: CreateSambaShareRequest,
current_user: str = Depends(get_current_user)
):
"""Create new Samba share"""
if not request.name.strip() or not request.path.strip():
raise HTTPException(status_code=400, detail="Name and path are required")
try:
success = share_manager.create_samba_share(
request.name,
request.path,
request.comment
)
if not success:
raise HTTPException(status_code=400, detail="Failed to create Samba share")
return {"status": "created", "name": request.name}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/samba/{name}")
async def delete_samba_share(
name: str,
current_user: str = Depends(get_current_user)
):
"""Delete Samba share"""
try:
success = share_manager.delete_samba_share(name)
if not success:
raise HTTPException(status_code=404, detail=f"Samba share '{name}' not found")
return {"status": "deleted", "name": name}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/samba/config")
async def get_samba_config(current_user: str = Depends(get_current_user)):
"""Get Samba global configuration"""
try:
config = share_manager.get_samba_global_config()
return config
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/samba/config")
async def set_samba_config(
request: SambaConfigRequest,
current_user: str = Depends(get_current_user)
):
"""Update Samba global configuration"""
try:
success = share_manager.set_samba_global_config(request.config)
if not success:
raise HTTPException(status_code=400, detail="Failed to update Samba configuration")
return {"status": "updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/samba/config/import")
async def import_samba_config(
request: SambaImportRequest,
current_user: str = Depends(get_current_user)
):
"""Import Samba configuration using net conf import"""
try:
success = share_manager.import_samba_config(request.config_file)
if not success:
raise HTTPException(status_code=400, detail="Failed to import Samba configuration")
return {"status": "imported", "config_file": request.config_file}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
# ============== NFS ==============
@router.get("/nfs")
async def list_nfs_shares(current_user: str = Depends(get_current_user)):
"""List all NFS shares"""
try:
shares = share_manager.list_nfs_shares()
return {"shares": shares}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/nfs")
async def create_nfs_share(
request: CreateNFSShareRequest,
current_user: str = Depends(get_current_user)
):
"""Create new NFS share"""
if not request.path.strip() or not request.clients.strip():
raise HTTPException(status_code=400, detail="Path and clients are required")
try:
success = share_manager.create_nfs_share(
request.path,
request.clients,
request.options
)
if not success:
raise HTTPException(status_code=400, detail="Failed to create NFS share")
return {"status": "created", "path": request.path}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/nfs")
async def delete_nfs_share(
path: str = None,
current_user: str = Depends(get_current_user)
):
"""Delete NFS share"""
try:
if not path:
raise HTTPException(status_code=400, detail="path parameter required")
success = share_manager.delete_nfs_share(path)
if not success:
raise HTTPException(status_code=404, detail=f"NFS share '{path}' not found")
return {"status": "deleted", "path": path}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/nfs/config")
async def get_nfs_config(current_user: str = Depends(get_current_user)):
"""Get NFS global configuration (/etc/exports)"""
try:
config = share_manager.get_nfs_config()
return config
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.put("/nfs/config")
async def set_nfs_config(
request: SambaConfigRequest,
current_user: str = Depends(get_current_user)
):
"""Update NFS global configuration (/etc/exports)"""
try:
success = share_manager.set_nfs_config(request.config)
if not success:
raise HTTPException(status_code=400, detail="Failed to update NFS configuration")
return {"status": "updated"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))