commit 0d670f4fdce65f7f79478f339fe888577a948cbb
parent c63fba3363f01a6efcf2d23c82ba09df99de3712
Author: Erik Loualiche <[email protected]>
Date: Mon, 24 Feb 2025 15:02:28 -0600
quick sync with default config
Diffstat:
| M | esync/cli.py | | | 160 | ++++++++++++++++++++++++++++++++++++------------------------------------------- |
| M | esync/config.py | | | 82 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- |
2 files changed, 154 insertions(+), 88 deletions(-)
diff --git a/esync/cli.py b/esync/cli.py
@@ -14,10 +14,10 @@ from .config import (
find_config_file,
ESyncConfig,
SyncConfig,
- SSHConfig
+ SSHConfig,
+ get_default_config,
+ create_config_for_paths
)
-# --------------------------------------------------------------------------------------------------
-
app = typer.Typer(
name="esync",
@@ -37,15 +37,17 @@ verbose_help_sync = """
esync - File synchronization tool
Basic Usage:
- esync sync # Initialize a new configuration
-"""
+ esync sync # Start syncing with configuration file
+ esync sync -c esync.toml # Use specific configuration file
+ esync sync -l ./local -r ./remote # Override paths in config
-
-# --------------------------------------------------------------------------------------------------
+Quick Sync:
+ esync sync --quick -l ./local -r ./remote # Quick sync with default settings
+ esync sync -q -l ./local -r user@host:/path # Quick sync to remote SSH
+"""
console = Console()
-# --------------------------------------------------------------------------------------------------
class WatcherType(str, Enum):
WATCHDOG = "watchdog"
WATCHMAN = "watchman"
@@ -101,10 +103,7 @@ def display_config(config: ESyncConfig) -> None:
table.add_row("Rsync", key, str(value))
console.print(table)
-# --------------------------------------------------------------------------------------------------
-
-# --------------------------------------------------------------------------------------------------
@app.callback()
def main():
"""File synchronization tool with watchdog/watchman support."""
@@ -126,13 +125,13 @@ def sync(
None,
"--local",
"-l",
- help="Override local path"
+ help="Local path to sync from"
),
remote: Optional[str] = typer.Option(
None,
"--remote",
"-r",
- help="Override remote path"
+ help="Remote path to sync to"
),
watcher: Optional[WatcherType] = typer.Option(
None,
@@ -140,6 +139,12 @@ def sync(
"-w",
help="Override watcher type"
),
+ quick: bool = typer.Option(
+ False,
+ "--quick",
+ "-q",
+ help="Quick sync with default settings"
+ ),
verbose: bool = typer.Option(False, "--verbose", help="Enable verbose output"),
help_override: bool = typer.Option(False, "--help", is_eager=True, help="Show help message"),
):
@@ -148,44 +153,62 @@ def sync(
console.print(ctx.get_help(), style="bold")
if verbose:
console.print(verbose_help_sync, style="italic")
- # typer.echo(verbose_help_init)
raise typer.Exit()
try:
- # Find and load config file
- config_path = config_file or find_config_file()
- if not config_path:
- console.print("[red]No configuration file found![/]")
- raise typer.Exit(1)
-
- # Show which config file we're using
- console.print(f"[bold blue]Loading configuration from:[/] {config_path.resolve()}")
-
- try:
- config = load_config(config_path)
- except Exception as e:
- console.print(f"[red]Failed to load config: {e}[/]")
- raise typer.Exit(1)
-
+ # Handle quick sync option
+ if quick:
+ if not local or not remote:
+ console.print("[red]Both local and remote paths are required with --quick option[/]")
+ raise typer.Exit(1)
+
+ # Create quick configuration
+ config = create_config_for_paths(local, remote, watcher.value if watcher else None)
+ console.print("[bold blue]Using quick sync configuration[/]")
+
+ # Display effective configuration
+ console.print("\n[bold]Quick Sync Configuration:[/]")
+ display_config(config)
+ else:
+ # Find and load config file (original flow)
+ config_path = config_file or find_config_file()
+ if not config_path:
+ console.print("[red]No configuration file found![/]")
+ console.print("\t[green]Try running 'esync init' to create one.")
+ console.print("\tOr use 'esync sync --quick -l LOCAL -r REMOTE' for quick syncing.[/]")
+ raise typer.Exit(1)
+
+ # Show which config file we're using
+ console.print(f"[bold blue]Loading configuration from:[/] {config_path.resolve()}")
+
+ try:
+ config = load_config(config_path)
+ except Exception as e:
+ console.print(f"[red]Failed to load config: {e}[/]")
+ raise typer.Exit(1)
+
+ sync_data = config.model_dump().get('sync', {})
+
+ # Validate required sections
+ if 'local' not in sync_data or 'remote' not in sync_data:
+ console.print("[red]Invalid configuration: 'sync.local' and 'sync.remote' sections required[/]")
+ raise typer.Exit(1)
+
+ # Override config with CLI options
+ if local:
+ sync_data['local']['path'] = local
+ if remote:
+ sync_data['remote']['path'] = remote
+ if watcher:
+ config.settings.esync.watcher = watcher.value
+
+ # Display effective configuration
+ console.print("\n[bold]Effective Configuration:[/]")
+ display_config(config)
+
+ # Get sync data from config
sync_data = config.model_dump().get('sync', {})
- # Validate required sections
- if 'local' not in sync_data or 'remote' not in sync_data:
- console.print("[red]Invalid configuration: 'sync.local' and 'sync.remote' sections required[/]")
- raise typer.Exit(1)
-
- # Override config with CLI options
- if local:
- sync_data['local']['path'] = local
- if remote:
- sync_data['remote']['path'] = remote
- if watcher:
- config.settings.esync.watcher = watcher.value
-
- # Display effective configuration
- console.print("\n[bold]Effective Configuration:[/]")
- display_config(config)
-
# Prepare paths
local_path = Path(sync_data['local']['path']).expanduser().resolve()
local_path.mkdir(parents=True, exist_ok=True)
@@ -231,6 +254,7 @@ def sync(
# --------------------------------------------------------------------------------------------------
+
# --------------------------------------------------------------------------------------------------
@app.command()
def init(
@@ -246,55 +270,18 @@ def init(
console.print(ctx.get_help(), style="bold")
if verbose:
console.print(verbose_help_init, style="italic")
- # typer.echo(verbose_help_init)
raise typer.Exit()
-
if config_file.exists():
overwrite = typer.confirm(
f"Config file {config_file} already exists. Overwrite?",
abort=True
)
- # Create default config
- default_config = {
- "sync": {
- "local": {
- "path": "./local",
- "interval": 1
- },
- "remote": {
- "path": "./remote"
- }
- },
- "settings": {
- "esync": {
- "watcher": "watchdog",
- "ignore": [
- "*.log",
- "*.tmp",
- ".env"
- ]
- },
- "rsync": {
- "backup_enabled": True,
- "backup_dir": ".rsync_backup",
- "compression": True,
- "verbose": False,
- "archive": True,
- "compress": True,
- "human_readable": True,
- "progress": True,
- "ignore": [
- "*.swp",
- ".git/",
- "node_modules/",
- "**/__pycache__/",
- ]
- }
- }
- }
+ # Get default config from the central location
+ default_config = get_default_config()
+ # Write config to file
import tomli_w
with open(config_file, 'wb') as f:
tomli_w.dump(default_config, f)
@@ -303,7 +290,6 @@ def init(
# --------------------------------------------------------------------------------------------------
-
# --------------------------------------------------------------------------------------------------
if __name__ == "__main__":
app()
diff --git a/esync/config.py b/esync/config.py
@@ -26,7 +26,6 @@ class SyncConfig(BaseModel):
return Path(self.target).expanduser()
return self.target
-
class RemoteConfig(BaseModel):
path: Union[Path, str]
ssh: Optional[SSHConfig] = None
@@ -54,6 +53,87 @@ class ESyncConfig(BaseModel):
sync: Dict[str, Any] = Field(default_factory=dict)
settings: Settings = Field(default_factory=Settings)
+def get_default_config() -> Dict[str, Any]:
+ """Get the default configuration."""
+ return {
+ "sync": {
+ "local": {
+ "path": "./local",
+ "interval": 1
+ },
+ "remote": {
+ "path": "./remote"
+ }
+ },
+ "settings": {
+ "esync": {
+ "watcher": "watchdog",
+ "ignore": [
+ "*.log",
+ "*.tmp",
+ ".env"
+ ]
+ },
+ "rsync": {
+ "backup_enabled": True,
+ "backup_dir": ".rsync_backup",
+ "compression": True,
+ "verbose": False,
+ "archive": True,
+ "compress": True,
+ "human_readable": True,
+ "progress": True,
+ "ignore": [
+ "*.swp",
+ ".git/",
+ "node_modules/",
+ "**/__pycache__/",
+ ]
+ }
+ }
+ }
+
+def create_config_for_paths(local_path: str, remote_path: str, watcher_type: Optional[str] = None) -> ESyncConfig:
+ """Create a configuration with specific paths."""
+ # Start with default config
+ config_dict = get_default_config()
+
+ # Update paths
+ config_dict["sync"]["local"]["path"] = local_path
+
+ # Set watcher if provided
+ if watcher_type:
+ config_dict["settings"]["esync"]["watcher"] = watcher_type
+
+ # Handle SSH configuration if needed
+ if "@" in remote_path and ":" in remote_path:
+ # Extract user, host, and path
+ user_host, path = remote_path.split(":", 1)
+ if "@" in user_host:
+ user, host = user_host.split("@", 1)
+ config_dict["sync"]["remote"] = {
+ "path": path,
+ "ssh": {
+ "host": host,
+ "user": user,
+ "port": 22
+ }
+ }
+ else:
+ # No user specified
+ config_dict["sync"]["remote"] = {
+ "path": path,
+ "ssh": {
+ "host": user_host,
+ "port": 22
+ }
+ }
+ else:
+ # Local path
+ config_dict["sync"]["remote"]["path"] = remote_path
+
+ return ESyncConfig(**config_dict)
+
def load_config(config_path: Path) -> ESyncConfig:
"""Load and validate TOML configuration file."""
try: