Add FTP/SFTP support and tree-view folder picker with lazy loading
FTP/SFTP processor: - New ftp_processor.py with adapter pattern for FTP (passive) and SFTP - Same design as smb_processor: read PDFs, forward via SMTP, move to processed - Eingangs-/Ausgangsbelege with separate paths, modes (forward/separator) - paramiko==3.5.0 for SFTP support - Schema v9 with new ftp_* settings - Integrated in scheduler Tree-view folder picker (SMB + FTP): - Reusable tree rendering from flat path lists - Expandable/collapsible nodes with toggle arrows - Lazy loading: only top-level folders on open, sub-folders on-demand - Auto-expand ancestors of currently selected value (with preload) - Reload button stays for manual refresh - Always fresh load when opening picker - New endpoints: /api/list-smb-subfolders, /api/list-ftp-subfolders FTP-specific fixes: - list_pdfs uses LIST instead of NLST (more reliable across servers) - Stateful CWD bug fixed in ensure_dir/stat_exists/rename (previously created /Buch/Buch/X instead of /Buch/X due to CWD drift) - All operations reset CWD via _reset_cwd() before stateful calls - _resolve() helper for SFTP to handle empty path / chroot users Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+26
-5
@@ -99,7 +99,7 @@ def _move_smb_file(source: str, dest_dir: str, filename: str):
|
||||
|
||||
|
||||
def _list_smb_folders_recursive(
|
||||
base_path: str, max_depth: int = 3, _current_depth: int = 0, _prefix: str = ""
|
||||
base_path: str, max_depth: int = 5, _current_depth: int = 0, _prefix: str = ""
|
||||
) -> list[str]:
|
||||
"""Recursively list folders on the SMB share, returning relative paths."""
|
||||
folders = []
|
||||
@@ -307,7 +307,7 @@ async def process_smb_share() -> dict:
|
||||
|
||||
|
||||
async def test_smb_connection() -> dict:
|
||||
"""Test SMB connection and return folder list."""
|
||||
"""Test SMB connection and return TOP-LEVEL folders only (lazy loading)."""
|
||||
settings = await get_settings()
|
||||
|
||||
if not settings.get("smb_server") or not settings.get("smb_share"):
|
||||
@@ -315,7 +315,7 @@ async def test_smb_connection() -> dict:
|
||||
|
||||
try:
|
||||
base_path = await asyncio.to_thread(_smb_register_session, settings)
|
||||
folders = await asyncio.to_thread(_list_smb_folders_recursive, base_path, 3)
|
||||
folders = await asyncio.to_thread(_list_smb_folders_recursive, base_path, 1)
|
||||
return {"success": True, "folders": sorted(folders)}
|
||||
|
||||
except Exception as e:
|
||||
@@ -347,13 +347,34 @@ async def create_smb_folder(folder_path: str) -> dict:
|
||||
|
||||
|
||||
async def list_smb_folders() -> dict:
|
||||
"""Return current folder list from SMB share."""
|
||||
"""Return TOP-LEVEL folder list from SMB share (lazy loading)."""
|
||||
settings = await get_settings()
|
||||
if not settings.get("smb_server") or not settings.get("smb_share"):
|
||||
return {"folders": []}
|
||||
try:
|
||||
base_path = await asyncio.to_thread(_smb_register_session, settings)
|
||||
folders = await asyncio.to_thread(_list_smb_folders_recursive, base_path, 3)
|
||||
folders = await asyncio.to_thread(_list_smb_folders_recursive, base_path, 1)
|
||||
return {"folders": sorted(folders)}
|
||||
except Exception:
|
||||
return {"folders": []}
|
||||
|
||||
|
||||
async def list_smb_subfolders(parent_path: str) -> dict:
|
||||
"""List direct subfolders of a path (one level deep, for lazy tree expansion)."""
|
||||
settings = await get_settings()
|
||||
if not settings.get("smb_server") or not settings.get("smb_share"):
|
||||
return {"success": False, "error": "SMB nicht konfiguriert", "folders": []}
|
||||
try:
|
||||
base_path = await asyncio.to_thread(_smb_register_session, settings)
|
||||
full_path = _smb_unc_path(base_path, parent_path) if parent_path else base_path
|
||||
# max_depth=1 returns only direct children
|
||||
rel_folders = await asyncio.to_thread(_list_smb_folders_recursive, full_path, 1)
|
||||
# Prefix with parent_path so the frontend has full paths
|
||||
if parent_path:
|
||||
folders = [f"{parent_path}/{f}" for f in rel_folders]
|
||||
else:
|
||||
folders = rel_folders
|
||||
return {"success": True, "folders": sorted(folders)}
|
||||
except Exception as e:
|
||||
logger.error(f"SMB-Subfolder-Liste fehlgeschlagen: {e}")
|
||||
return {"success": False, "error": str(e), "folders": []}
|
||||
|
||||
Reference in New Issue
Block a user