commit d3f5512b446ee026766294ec15bcf8f7abb97a5e
parent 0fdb06e6b457e3daacc5d9acc2d5036f5b59cd41
Author: Erik Loualiche <eloualic@umn.edu>
Date: Tue, 25 Feb 2025 16:38:20 -0600
bug parsing the remote and detecting ssh or not
Diffstat:
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