Release SyncVito 1.0.7
This commit is contained in:
1 parent
3d6ab87ce3
commit
a04db4ee96
10 files changed
+278
-28
No files matched your search
+25
-16
@@ -25,7 +25,7 @@ def derive_overall_status(
|
||||
if TaskRuntimeState.ERROR in states:
|
||||
return "error", "Fehler"
|
||||
if TaskRuntimeState.WAITING_FOR_RELEASE in states:
|
||||
return "error", "Wartet auf Dateifreigabe"
|
||||
return "error", "Synchronisiert" if running else "Unvollständig – Dateien gesperrt"
|
||||
if globally_paused:
|
||||
return "paused", "Pausiert"
|
||||
if TaskRuntimeState.ACTIVE not in states:
|
||||
@@ -205,25 +205,31 @@ class SyncCoordinator(QObject):
|
||||
if result.blocked:
|
||||
paths = [blocked.path for blocked in result.blocked]
|
||||
self._retry_tasks[task_id] = paths
|
||||
descriptions: list[str] = []
|
||||
for blocked in result.blocked:
|
||||
names = ", ".join(f"{process.name} (PID {process.pid})" for process in blocked.processes)
|
||||
descriptions.append(f"{blocked.path.name}: {names or 'sperrender Prozess nicht ermittelbar'}")
|
||||
message = "Zurückgestellt – " + "; ".join(descriptions)
|
||||
message = (
|
||||
f"{result.copied} kopiert, {len(result.blocked)} Dateien gesperrt – Sicherung unvollständig. "
|
||||
"Andere Dateien wurden weiterverarbeitet. Automatischer Wiederholungsversuch nach Freigabe; "
|
||||
"ggf. betroffene Programme schließen. Details im Protokoll."
|
||||
)
|
||||
self._lock_messages[task_id] = message
|
||||
self._emit_lock_summary()
|
||||
self.log_created.emit(task.name, "Blockiert", message)
|
||||
for blocked in result.blocked:
|
||||
names = ", ".join(f"{process.name} (PID {process.pid})" for process in blocked.processes)
|
||||
self.log_created.emit(
|
||||
task.name, "Blockiert", f"{blocked.path}: {names or 'sperrender Prozess nicht ermittelbar'}"
|
||||
)
|
||||
self._set_task_state(task_id, TaskRuntimeState.WAITING_FOR_RELEASE)
|
||||
elif result.errors:
|
||||
self._retry_tasks[task_id] = []
|
||||
self._lock_messages.pop(task_id, None)
|
||||
self._emit_lock_summary()
|
||||
self.log_created.emit(task.name, "Fehler", " | ".join(result.errors[:5]))
|
||||
self._set_task_state(task_id, TaskRuntimeState.ERROR)
|
||||
else:
|
||||
self._retry_tasks.pop(task_id, None)
|
||||
self._lock_messages.pop(task_id, None)
|
||||
self._emit_lock_summary()
|
||||
if result.errors:
|
||||
# Auch zusätzliche Fehler eines teilweise blockierten Laufs sichtbar halten.
|
||||
self._retry_tasks[task_id] = []
|
||||
for error in result.errors:
|
||||
self.log_created.emit(task.name, "Fehler", error)
|
||||
self._set_task_state(task_id, TaskRuntimeState.ERROR)
|
||||
elif not result.blocked:
|
||||
self._retry_tasks.pop(task_id, None)
|
||||
summary = f"{result.copied} kopiert, {result.deleted} gelöscht, {result.conflicts} Konfliktkopien"
|
||||
self.log_created.emit(task.name, "Erfolg", summary)
|
||||
self._set_task_state(task_id, TaskRuntimeState.ACTIVE)
|
||||
@@ -273,13 +279,12 @@ class SyncCoordinator(QObject):
|
||||
if self.config.settings.paused or self.is_running:
|
||||
return
|
||||
for task_id, paths in list(self._retry_tasks.items()):
|
||||
if paths and any(is_file_locked(path) for path in paths):
|
||||
if paths and all(is_file_locked(path) for path in paths):
|
||||
continue
|
||||
self.log_created.emit(
|
||||
"Automatischer Versuch", "Hinweis", "Blockierte oder nicht erreichbare Datei wird erneut geprüft."
|
||||
)
|
||||
self.enqueue(task_id)
|
||||
break
|
||||
|
||||
def _task(self, task_id: str) -> SyncTask | None:
|
||||
return next((task for task in self.config.tasks if task.id == task_id), None)
|
||||
@@ -296,4 +301,8 @@ class SyncCoordinator(QObject):
|
||||
self.overall_status_changed.emit(status, text)
|
||||
|
||||
def _emit_lock_summary(self) -> None:
|
||||
self.lock_changed.emit("\n".join(self._lock_messages.values()))
|
||||
messages = list(self._lock_messages.values())
|
||||
summary = "\n".join(messages[:2])
|
||||
if len(messages) > 2:
|
||||
summary += f"\nWeitere {len(messages) - 2} Aufgaben mit gesperrten Dateien. Details im Protokoll."
|
||||
self.lock_changed.emit(summary)
|
||||
@@ -23,7 +23,22 @@ def is_file_locked(path: Path) -> bool:
|
||||
"""Prüft eine vorhandene Datei durch exklusives Öffnen ohne Schreibzugriff."""
|
||||
if not path.exists() or os.name != "nt":
|
||||
return False
|
||||
handle = ctypes.windll.kernel32.CreateFileW(
|
||||
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
create_file = kernel32.CreateFileW
|
||||
create_file.argtypes = [
|
||||
wintypes.LPCWSTR,
|
||||
wintypes.DWORD,
|
||||
wintypes.DWORD,
|
||||
ctypes.c_void_p,
|
||||
wintypes.DWORD,
|
||||
wintypes.DWORD,
|
||||
wintypes.HANDLE,
|
||||
]
|
||||
create_file.restype = wintypes.HANDLE
|
||||
close_handle = kernel32.CloseHandle
|
||||
close_handle.argtypes = [wintypes.HANDLE]
|
||||
close_handle.restype = wintypes.BOOL
|
||||
handle = create_file(
|
||||
str(path),
|
||||
0x80000000,
|
||||
0,
|
||||
@@ -34,8 +49,8 @@ def is_file_locked(path: Path) -> bool:
|
||||
)
|
||||
invalid_handle = ctypes.c_void_p(-1).value
|
||||
if handle == invalid_handle:
|
||||
return ctypes.get_last_error() in {5, 32, 33}
|
||||
ctypes.windll.kernel32.CloseHandle(handle)
|
||||
return ctypes.get_last_error() in {32, 33}
|
||||
close_handle(handle)
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from syncvito.file_locks import LockingProcess, find_locking_processes
|
||||
from syncvito.file_locks import LockingProcess, find_locking_processes, is_file_locked
|
||||
from syncvito.models import ConflictMode, DeleteMode, SyncDirection, SyncTask
|
||||
from syncvito.state_store import StateStore
|
||||
|
||||
@@ -117,12 +117,20 @@ class SyncEngine:
|
||||
for index, operation in enumerate(operations, start=1):
|
||||
try:
|
||||
self._execute(operation, result)
|
||||
except PermissionError as exc:
|
||||
processes = tuple(find_locking_processes(operation.target))
|
||||
result.blocked.append(BlockedFile(operation.target, processes))
|
||||
result.errors.append(f"Zugriff verweigert: {operation.target} ({exc})")
|
||||
except (OSError, shutil.Error) as exc:
|
||||
result.errors.append(f"{operation.target}: {exc}")
|
||||
failed_path = (
|
||||
Path(exc.filename2 or exc.filename)
|
||||
if isinstance(exc, OSError) and (exc.filename2 or exc.filename)
|
||||
else operation.target
|
||||
)
|
||||
blocked_path = _blocked_path(operation, exc)
|
||||
if blocked_path is not None:
|
||||
processes = tuple(find_locking_processes(blocked_path))
|
||||
result.blocked.append(BlockedFile(blocked_path, processes))
|
||||
elif isinstance(exc, PermissionError):
|
||||
result.errors.append(f"Zugriff verweigert: {failed_path} ({exc})")
|
||||
else:
|
||||
result.errors.append(f"{failed_path}: {exc}")
|
||||
if progress:
|
||||
detail = f"{operation.kind}: {operation.relative}"
|
||||
progress(round(index / total * 100), detail)
|
||||
@@ -295,6 +303,25 @@ class SyncEngine:
|
||||
}
|
||||
|
||||
|
||||
def _blocked_path(operation: _Operation, exc: OSError | shutil.Error) -> Path | None:
|
||||
"""Ordnet auch Windows-Kopierfehler ohne Dateinamen der gesperrten Seite zu."""
|
||||
winerror = getattr(exc, "winerror", None)
|
||||
if winerror not in {5, 32, 33}:
|
||||
return None
|
||||
reported = (exc.filename2 or exc.filename) if isinstance(exc, OSError) else None
|
||||
if reported and winerror in {32, 33}:
|
||||
return Path(reported)
|
||||
candidates = [Path(reported)] if reported else []
|
||||
candidates.extend(path for path in (operation.source, operation.target) if path is not None)
|
||||
for path in dict.fromkeys(candidates):
|
||||
if is_file_locked(path):
|
||||
return path
|
||||
# Eine inzwischen freigegebene Datei beim nächsten Lauf erneut versuchen.
|
||||
if winerror in {32, 33}:
|
||||
return operation.source or operation.target
|
||||
return None
|
||||
|
||||
|
||||
def _atomic_copy(source: Path, target: Path) -> None:
|
||||
temporary = target.with_name(f".{target.name}.syncvito.tmp")
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user