Release SyncVito 1.0.9

This commit is contained in:
Davito committed 2026-09-24 16:46:22 +02:00
1 parent bb5bfda4fe
commit 4efc1efa18
8 files changed
+136 -9

No files matched your search

+3 -2
View File
@@ -25,12 +25,12 @@ def derive_overall_status(
if TaskRuntimeState.ERROR in states:
return "error", "Fehler"
if TaskRuntimeState.WAITING_FOR_RELEASE in states:
return "error", "Synchronisiert" if running else "Unvollständig – Dateien gesperrt"
return "error", "Synchronisierung läuft" if running else "Unvollständig – Dateien gesperrt"
if globally_paused:
return "paused", "Pausiert"
if TaskRuntimeState.ACTIVE not in states:
return "paused", "Inaktiv"
return "active", "Synchronisiert" if running else "Bereit"
return "active", "Synchronisierung läuft" if running else "Bereit"
class _WorkerSignals(QObject):
@@ -186,6 +186,7 @@ class SyncCoordinator(QObject):
self._start_next()
return
self._running_task_id = task_id
self.progress_changed.emit(0, f"{task.name}: Synchronisation wird gestartet …")
self.idle_changed.emit(False)
self._emit_overall_status()
self.log_created.emit(task.name, "Start", "Synchronisation gestartet.")
+35 -4
View File
@@ -4,11 +4,13 @@ from __future__ import annotations
import os
import shutil
import stat
from collections.abc import Callable
from contextlib import suppress
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from time import monotonic
from syncvito.file_locks import LockingProcess, find_locking_processes, is_file_locked
from syncvito.models import ConflictMode, DeleteMode, SyncDirection, SyncTask
@@ -89,9 +91,13 @@ class SyncEngine:
task.validate()
source_root = Path(task.source)
target_root = Path(task.target)
if progress:
progress(0, f"{task.name}: Ordner werden geprüft …")
self._validate_roots(source_root, target_root)
source_files, source_dirs = self._inventory(source_root, task.selected_paths)
target_files, target_dirs = self._inventory(target_root, task.selected_paths)
source_files, source_dirs = self._inventory(source_root, task.selected_paths, progress, "Quelle")
target_files, target_dirs = self._inventory(target_root, task.selected_paths, progress, "Ziel")
if progress:
progress(0, f"{task.name}: Änderungen werden verglichen …")
previous = self.state_store.get(task.id)
if task.direction is SyncDirection.ONE_WAY:
@@ -157,15 +163,31 @@ class SyncEngine:
raise UnavailablePathError(f"Ziel ist kein Ordner: {target_root}")
@staticmethod
def _inventory(root: Path, selected_paths: list[str]) -> tuple[dict[str, FileRecord], set[str]]:
def _inventory(
root: Path,
selected_paths: list[str],
progress: ProgressCallback | None = None,
label: str = "Ordner",
) -> tuple[dict[str, FileRecord], set[str]]:
files: dict[str, FileRecord] = {}
directories: set[str] = set()
selections = tuple(Path(item).parts for item in selected_paths)
last_report = monotonic()
if progress:
progress(0, f"{label} wird eingelesen: {root}")
try:
for current, dir_names, file_names in os.walk(root):
current_path = Path(current)
relative_dir = current_path.relative_to(root)
dir_names[:] = [name for name in dir_names if not (current_path / name).is_symlink()]
dir_names[:] = [
name
for name in dir_names
if _is_selected((relative_dir / name).parts, selections)
and not _is_directory_link(current_path / name)
]
if progress and monotonic() - last_report >= 0.25:
progress(0, f"{label}: {len(files)} Dateien gefunden – {current_path}")
last_report = monotonic()
for directory_name in dir_names:
relative = relative_dir / directory_name
if _is_selected(relative.parts, selections):
@@ -182,6 +204,9 @@ class SyncEngine:
except OSError:
continue
files[str(relative)] = FileRecord(path, file_stat.st_size, file_stat.st_mtime_ns)
if progress and monotonic() - last_report >= 0.25:
progress(0, f"{label}: {len(files)} Dateien gefunden – {current_path}")
last_report = monotonic()
except OSError as exc:
raise UnavailablePathError(f"Ordner kann nicht gelesen werden: {root}") from exc
return files, directories
@@ -303,6 +328,12 @@ class SyncEngine:
}
def _is_directory_link(path: Path) -> bool:
"""Erkennt auch Windows-Junctions unter Python 3.11 ohne Path.is_junction."""
metadata = path.lstat()
return stat.S_ISLNK(metadata.st_mode) or getattr(metadata, "st_reparse_tag", 0) == stat.IO_REPARSE_TAG_MOUNT_POINT
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)