esync

Directory watching and remote syncing
Log | Files | Refs | README | LICENSE

commit d3f5512b446ee026766294ec15bcf8f7abb97a5e
parent 0fdb06e6b457e3daacc5d9acc2d5036f5b59cd41
Author: Erik Loualiche <[email protected]>
Date:   Tue, 25 Feb 2025 16:38:20 -0600

bug parsing the remote and detecting ssh or not

Diffstat:
Mesync/config.py | 14++++++++++++--
Mesync/sync_manager.py | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
2 files changed, 83 insertions(+), 14 deletions(-)

diff --git a/esync/config.py b/esync/config.py @@ -1,6 +1,7 @@ from pathlib import Path from typing import Optional, List, Dict, Any, Union from pydantic import BaseModel, Field +import re import tomli from rich.console import Console @@ -19,6 +20,8 @@ class SyncConfig(BaseModel): backup_dir: str = ".rsync_backup" compress: bool = True human_readable: bool = True + verbose: bool = False + def is_remote(self) -> bool: """Check if this is a remote sync configuration.""" @@ -112,8 +115,15 @@ def create_config_for_paths(local_path: str, remote_path: str, watcher_type: Opt if watcher_type: config_dict["settings"]["esync"]["watcher"] = watcher_type - # Handle SSH configuration if needed - if "@" in remote_path and ":" in remote_path: + # Handle SSH configuration if needed -> use the function ... that is defined above like is remote path + # check if config is remote + is_remote_ssh = False # check if we have to deal with ssh or not + if ":" in remote_path: + if not ( len(remote_path) >= 2 and remote_path[1] == ':' and remote_path[0].isalpha() ): + is_remote_ssh = True + + # now we split the remote path between ssh case and non ssh case + if is_remote_ssh: # Extract user, host, and path user_host, path = remote_path.split(":", 1) if "@" in user_host: diff --git a/esync/sync_manager.py b/esync/sync_manager.py @@ -6,13 +6,15 @@ import os import logging import re from pathlib import Path -from typing import Optional, List +from typing import Optional, List, Union from rich.console import Console from rich.panel import Panel from rich.text import Text from .config import SyncConfig console = Console() +# console = Console(stderr=True, log_time=True, log_path=False) # for debugging + # Customize logger to use shorter log level names class CustomAdapter(logging.LoggerAdapter): @@ -22,11 +24,11 @@ class CustomAdapter(logging.LoggerAdapter): class ShortLevelNameFormatter(logging.Formatter): """Custom formatter with shorter level names""" short_levels = { - 'DEBUG': 'debug', - 'INFO': 'info', - 'WARNING': 'warn', - 'ERROR': 'error', - 'CRITICAL': 'critic' + 'DEBUG': 'DEBUG', + 'INFO': 'INFO', + 'WARNING': 'WARN', + 'ERROR': 'ERROR', + 'CRITICAL': 'CRITIC' } def format(self, record): @@ -63,6 +65,11 @@ class SyncManager: if log_file: self._setup_logging(log_file) + # Set verbose/quiet mode based on config + if hasattr(config, 'verbose'): + self._verbose = config.verbose + self._quiet = not config.verbose + self._sync_thread.start() # Single status panel that we'll update @@ -362,6 +369,29 @@ class SyncManager: finally: self._current_sync = None + def _parse_remote_string(self, remote_str: str) -> tuple: + """ + Parse a remote string into username, host, and path components. + Format: [user@]host:path + Returns: (username, host, path) + """ + match = re.match(r'^(?:([^@]+)@)?([^:]+):(.+)$', remote_str) + if match: + return match.groups() + return None, None, remote_str + + def _is_remote_path(self, path_str: str) -> bool: + """ + Determine if a string represents a remote path. + A remote path is in the format [user@]host:path. + """ + # Avoid treating Windows paths (C:) as remote + if len(path_str) >= 2 and path_str[1] == ':' and path_str[0].isalpha(): + return False + # Simple regex to match remote path format + return bool(re.match(r'^(?:[^@]+@)?[^/:]+:.+$', path_str)) + + def _build_rsync_command(self, source_path: Path) -> list[str]: """Build rsync command for local or remote sync.""" cmd = [ @@ -369,7 +399,10 @@ class SyncManager: "--recursive", # recursive "--times", # preserve times "--progress", # progress for parsing - "--verbose", # verbose for parsing + # "--verbose", # verbose for parsing + # "--links", # copy symlinks as symlinks + # "--copy-links", # transform symlink into referent file/dir + "--copy-unsafe-links", # only "unsafe" symlinks are transformed ] # Add backup if enabled @@ -383,6 +416,10 @@ class SyncManager: cmd.append("--compress") if hasattr(self._config, 'human_readable') and self._config.human_readable: cmd.append("--human-readable") + if hasattr(self._config, 'verbose') and self._config.verbose: + cmd.append("--verbose") + # Todo this is where we add standard rsync commands + # Add ignore patterns for pattern in self._config.ignores: @@ -393,11 +430,17 @@ class SyncManager: clean_pattern = clean_pattern[3:] # Remove **/ prefix cmd.extend(["--exclude", clean_pattern]) - # Ensure we have absolute paths + # Ensure we have absolute paths for the source source = f"{source_path.absolute()}/" + # Get target as string + target_str = str(self._config.target) + # Determine if target is a remote path + is_remote = self._is_remote_path(target_str) + + if self._config.is_remote(): - # For remote sync + # For remote sync via SSH config object ssh = self._config.ssh if ssh.user: remote = f"{ssh.user}@{ssh.host}:{self._config.target}" @@ -405,13 +448,29 @@ class SyncManager: remote = f"{ssh.host}:{self._config.target}" cmd.append(source) cmd.append(remote) + + elif is_remote: + # For direct remote specification (host:path) + # Use the target string directly without any modification + cmd.append(source) + cmd.append(target_str) + + # Log for debugging + self._log("debug", f"Remote path detected: '{target_str}'") + else: # For local sync - target = self._config.target - if isinstance(target, Path): - target = target.absolute() + try: + target = Path(target_str).expanduser() target.mkdir(parents=True, exist_ok=True) + except Exception as e: + self._log("error", f"Error creating target directory: {e}") + raise + cmd.append(source) cmd.append(str(target) + '/') + # Log the final command for debugging + self._log("debug", f"Final rsync command: {' '.join(cmd)}") + return cmd