311 lines
11 KiB
Python
311 lines
11 KiB
Python
import os
|
|
import asyncio
|
|
import logging
|
|
import tempfile
|
|
|
|
import smbclient
|
|
|
|
from app.database import get_settings, add_log_entry
|
|
from app.mail_processor import _connect_smtp, _build_forward_email
|
|
from app.scanner import detect_separator_pages, split_pdf
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _smb_register_session(settings: dict) -> str:
|
|
"""Register SMB session and return the UNC base path \\\\server\\share."""
|
|
server = settings["smb_server"]
|
|
port = int(settings.get("smb_port", 445))
|
|
username = settings.get("smb_username", "")
|
|
password = settings.get("smb_password", "")
|
|
domain = settings.get("smb_domain", "")
|
|
|
|
if domain:
|
|
auth_username = f"{domain}\\{username}"
|
|
else:
|
|
auth_username = username
|
|
|
|
smbclient.register_session(
|
|
server,
|
|
port=port,
|
|
username=auth_username,
|
|
password=password,
|
|
connection_timeout=10,
|
|
)
|
|
|
|
share = settings["smb_share"]
|
|
return f"\\\\{server}\\{share}"
|
|
|
|
|
|
def _smb_unc_path(base: str, *parts: str) -> str:
|
|
"""Join UNC path segments using backslash."""
|
|
result = base
|
|
for p in parts:
|
|
if p:
|
|
result = result.rstrip("\\") + "\\" + p.replace("/", "\\").strip("\\")
|
|
return result
|
|
|
|
|
|
def _ensure_smb_folder(path: str):
|
|
"""Create SMB directory if it does not exist."""
|
|
try:
|
|
smbclient.stat(path)
|
|
except OSError:
|
|
smbclient.makedirs(path, exist_ok=True)
|
|
|
|
|
|
def _list_pdf_files(source_path: str) -> list[str]:
|
|
"""Return list of .pdf filenames in the SMB source directory."""
|
|
try:
|
|
entries = smbclient.listdir(source_path)
|
|
except OSError:
|
|
return []
|
|
return sorted(
|
|
e for e in entries if e.lower().endswith(".pdf") and not e.startswith(".")
|
|
)
|
|
|
|
|
|
def _read_smb_file(filepath: str) -> bytes:
|
|
"""Read a file from SMB share into memory."""
|
|
with smbclient.open_file(filepath, mode="rb") as f:
|
|
return f.read()
|
|
|
|
|
|
def _move_smb_file(source: str, dest_dir: str, filename: str):
|
|
"""Move a file on the SMB share to the destination directory."""
|
|
dest = _smb_unc_path(dest_dir, filename)
|
|
# Handle duplicate filenames
|
|
try:
|
|
smbclient.stat(dest)
|
|
name, ext = os.path.splitext(filename)
|
|
counter = 1
|
|
while True:
|
|
new_name = f"{name}_{counter}{ext}"
|
|
dest = _smb_unc_path(dest_dir, new_name)
|
|
try:
|
|
smbclient.stat(dest)
|
|
counter += 1
|
|
except OSError:
|
|
break
|
|
except OSError:
|
|
pass
|
|
smbclient.rename(source, dest)
|
|
|
|
|
|
def _list_smb_folders_recursive(
|
|
base_path: str, max_depth: int = 3, _current_depth: int = 0, _prefix: str = ""
|
|
) -> list[str]:
|
|
"""Recursively list folders on the SMB share, returning relative paths."""
|
|
folders = []
|
|
try:
|
|
for entry in smbclient.scandir(base_path):
|
|
if entry.is_dir() and not entry.name.startswith("."):
|
|
rel_path = f"{_prefix}/{entry.name}" if _prefix else entry.name
|
|
folders.append(rel_path)
|
|
if _current_depth < max_depth - 1:
|
|
sub_path = _smb_unc_path(base_path, entry.name)
|
|
folders.extend(
|
|
_list_smb_folders_recursive(
|
|
sub_path, max_depth, _current_depth + 1, rel_path
|
|
)
|
|
)
|
|
except OSError:
|
|
pass
|
|
return folders
|
|
|
|
|
|
async def process_smb_share() -> dict:
|
|
"""Process PDF files from SMB share - main pipeline."""
|
|
settings = await get_settings()
|
|
|
|
if settings.get("smb_enabled") != "true":
|
|
return {"processed": 0, "skipped": 0, "errors": 0}
|
|
|
|
if not settings.get("smb_server") or not settings.get("smb_share"):
|
|
return {"processed": 0, "skipped": 0, "errors": 0, "error": "SMB nicht konfiguriert"}
|
|
|
|
if not settings.get("import_email"):
|
|
return {"processed": 0, "skipped": 0, "errors": 0, "error": "Import-Email nicht konfiguriert"}
|
|
|
|
mode = settings.get("smb_mode", "forward")
|
|
smtp_from = settings.get("smtp_username", "")
|
|
import_email = settings["import_email"]
|
|
|
|
processed = 0
|
|
skipped = 0
|
|
errors = 0
|
|
smtp_conn = None
|
|
|
|
try:
|
|
base_path = await asyncio.to_thread(_smb_register_session, settings)
|
|
source_path = _smb_unc_path(base_path, settings.get("smb_source_path", ""))
|
|
processed_path = _smb_unc_path(base_path, settings.get("smb_processed_path", "Verarbeitet"))
|
|
|
|
await asyncio.to_thread(_ensure_smb_folder, processed_path)
|
|
|
|
pdf_files = await asyncio.to_thread(_list_pdf_files, source_path)
|
|
if not pdf_files:
|
|
logger.info("Keine PDF-Dateien im SMB-Ordner gefunden")
|
|
return {"processed": 0, "skipped": 0, "errors": 0}
|
|
|
|
logger.info(f"{len(pdf_files)} PDF-Datei(en) im SMB-Ordner gefunden")
|
|
|
|
smtp_conn = _connect_smtp(settings)
|
|
|
|
for filename in pdf_files:
|
|
file_path = _smb_unc_path(source_path, filename)
|
|
try:
|
|
pdf_data = await asyncio.to_thread(_read_smb_file, file_path)
|
|
|
|
if mode == "separator":
|
|
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
|
|
tmp.write(pdf_data)
|
|
tmp_path = tmp.name
|
|
try:
|
|
separator_pages = await asyncio.to_thread(
|
|
detect_separator_pages, tmp_path, None
|
|
)
|
|
documents = await asyncio.to_thread(
|
|
split_pdf, tmp_path, separator_pages
|
|
)
|
|
finally:
|
|
os.unlink(tmp_path)
|
|
|
|
if not documents:
|
|
skipped += 1
|
|
continue
|
|
|
|
for i, doc_bytes in enumerate(documents):
|
|
doc_filename = f"{os.path.splitext(filename)[0]}_Teil_{i + 1}.pdf"
|
|
subject = f"SMB-Import: {filename} (Dokument {i + 1}/{len(documents)})"
|
|
msg = _build_forward_email(
|
|
from_addr=smtp_from,
|
|
to_addr=import_email,
|
|
original_subject=subject,
|
|
original_from="SMB-Import",
|
|
attachments=[(doc_filename, doc_bytes)],
|
|
)
|
|
smtp_conn.send_message(msg)
|
|
|
|
await add_log_entry(
|
|
email_subject=f"SMB: {filename}",
|
|
email_from="SMB-Import",
|
|
attachments_count=len(documents),
|
|
status="success",
|
|
)
|
|
logger.info(
|
|
f"SMB verarbeitet: {filename} -> {len(documents)} Dokument(e) "
|
|
f"({len(separator_pages)} Trennseite(n))"
|
|
)
|
|
else:
|
|
msg = _build_forward_email(
|
|
from_addr=smtp_from,
|
|
to_addr=import_email,
|
|
original_subject=f"SMB-Import: {filename}",
|
|
original_from="SMB-Import",
|
|
attachments=[(filename, pdf_data)],
|
|
)
|
|
smtp_conn.send_message(msg)
|
|
|
|
await add_log_entry(
|
|
email_subject=f"SMB: {filename}",
|
|
email_from="SMB-Import",
|
|
attachments_count=1,
|
|
status="success",
|
|
)
|
|
logger.info(f"SMB verarbeitet: {filename}")
|
|
|
|
await asyncio.to_thread(_move_smb_file, file_path, processed_path, filename)
|
|
processed += 1
|
|
|
|
except Exception as e:
|
|
errors += 1
|
|
logger.error(f"Fehler bei SMB-Datei {filename}: {e}")
|
|
try:
|
|
await add_log_entry(
|
|
email_subject=f"SMB: {filename}",
|
|
email_from="SMB-Import",
|
|
attachments_count=0,
|
|
status="error",
|
|
error_message=str(e),
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
except Exception as e:
|
|
logger.error(f"SMB-Verbindungsfehler: {e}")
|
|
try:
|
|
await add_log_entry(
|
|
email_subject="",
|
|
email_from="SMB-Import",
|
|
attachments_count=0,
|
|
status="error",
|
|
error_message=f"SMB-Verbindungsfehler: {e}",
|
|
)
|
|
except Exception:
|
|
pass
|
|
return {"processed": processed, "skipped": skipped, "errors": errors + 1, "error": str(e)}
|
|
|
|
finally:
|
|
if smtp_conn:
|
|
try:
|
|
smtp_conn.quit()
|
|
except Exception:
|
|
pass
|
|
|
|
logger.info(f"SMB fertig: {processed} verarbeitet, {skipped} übersprungen, {errors} Fehler")
|
|
return {"processed": processed, "skipped": skipped, "errors": errors}
|
|
|
|
|
|
async def test_smb_connection() -> dict:
|
|
"""Test SMB connection and return folder list."""
|
|
settings = await get_settings()
|
|
|
|
if not settings.get("smb_server") or not settings.get("smb_share"):
|
|
return {"success": False, "error": "SMB-Server oder Freigabe nicht konfiguriert", "folders": []}
|
|
|
|
try:
|
|
base_path = await asyncio.to_thread(_smb_register_session, settings)
|
|
folders = await asyncio.to_thread(_list_smb_folders_recursive, base_path, 3)
|
|
return {"success": True, "folders": sorted(folders)}
|
|
|
|
except Exception as e:
|
|
logger.error(f"SMB-Test fehlgeschlagen: {e}")
|
|
return {"success": False, "error": str(e), "folders": []}
|
|
|
|
|
|
async def create_smb_folder(folder_path: str) -> dict:
|
|
"""Create a folder on the SMB share."""
|
|
settings = await get_settings()
|
|
|
|
if not settings.get("smb_server") or not settings.get("smb_share"):
|
|
return {"success": False, "error": "SMB nicht konfiguriert"}
|
|
|
|
if not folder_path or not folder_path.strip():
|
|
return {"success": False, "error": "Ordnername darf nicht leer sein"}
|
|
|
|
folder_path = folder_path.strip()
|
|
|
|
try:
|
|
base_path = await asyncio.to_thread(_smb_register_session, settings)
|
|
full_path = _smb_unc_path(base_path, folder_path)
|
|
await asyncio.to_thread(smbclient.makedirs, full_path, True)
|
|
return {"success": True}
|
|
|
|
except Exception as e:
|
|
logger.error(f"SMB-Ordner erstellen fehlgeschlagen: {e}")
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
async def list_smb_folders() -> dict:
|
|
"""Return current folder list from SMB share."""
|
|
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)
|
|
return {"folders": sorted(folders)}
|
|
except Exception:
|
|
return {"folders": []}
|