Skip to main content

rush_sync_server/commands/sync/
command.rs

1use crate::commands::command::Command;
2use crate::core::prelude::*;
3use crate::sync::profiles::RemoteProfileStore;
4use crate::sync::transport::{
5    git_pull, restart_service, run_remote_command, sync_pull, sync_push, test_connection,
6};
7use std::path::PathBuf;
8
9#[derive(Debug, Default)]
10pub struct SyncCommand;
11
12impl SyncCommand {
13    pub fn new() -> Self {
14        Self
15    }
16}
17
18impl Command for SyncCommand {
19    fn name(&self) -> &'static str {
20        "sync"
21    }
22
23    fn description(&self) -> &'static str {
24        "Sync files and run remote deployment actions"
25    }
26
27    fn matches(&self, command: &str) -> bool {
28        command.trim().to_lowercase().starts_with("sync")
29    }
30
31    fn execute_sync(&self, args: &[&str]) -> Result<String> {
32        let store = RemoteProfileStore::new()?;
33
34        match args.first().copied() {
35            None | Some("-h" | "--help" | "help") => Ok(self.help_text()),
36            Some("push") => self.push(&store, args),
37            Some("pull") => self.pull(&store, args),
38            Some("test") => self.test(&store, args),
39            Some("exec") => self.exec(&store, args),
40            Some("restart") => self.restart(&store, args),
41            Some("git-pull") => self.git_pull(&store, args),
42            Some(sub) => Err(AppError::Validation(format!(
43                "Unknown sync subcommand '{}'. Use 'sync help'.",
44                sub
45            ))),
46        }
47    }
48
49    fn priority(&self) -> u8 {
50        73
51    }
52}
53
54impl SyncCommand {
55    fn push(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
56        let (positionals, delete, dry_run) = parse_flags(args);
57        let remote_name = positionals.get(1).ok_or_else(|| {
58            AppError::Validation(
59                "Usage: sync push <remote> [local_path] [--delete] [--dry-run]".to_string(),
60            )
61        })?;
62
63        let profile = store.get(remote_name)?;
64        let local_path = positionals
65            .get(2)
66            .map(PathBuf::from)
67            .unwrap_or_else(|| PathBuf::from("www"));
68
69        let result = sync_push(&profile, &local_path, delete, dry_run)?;
70        Ok(format!(
71            "{}PUSH {} -> {} [{}]\n{}",
72            if dry_run { "[DRY-RUN] " } else { "" },
73            local_path.display(),
74            profile.remote_path,
75            remote_name,
76            result
77        ))
78    }
79
80    fn pull(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
81        let (positionals, delete, dry_run) = parse_flags(args);
82        let remote_name = positionals.get(1).ok_or_else(|| {
83            AppError::Validation(
84                "Usage: sync pull <remote> [local_path] [--delete] [--dry-run]".to_string(),
85            )
86        })?;
87
88        let profile = store.get(remote_name)?;
89        let local_path = positionals
90            .get(2)
91            .map(PathBuf::from)
92            .unwrap_or_else(|| PathBuf::from(format!("download-{}", remote_name)));
93
94        let result = sync_pull(&profile, &local_path, delete, dry_run)?;
95        Ok(format!(
96            "{}PULL {} <- {} [{}]\n{}",
97            if dry_run { "[DRY-RUN] " } else { "" },
98            local_path.display(),
99            profile.remote_path,
100            remote_name,
101            result
102        ))
103    }
104
105    fn test(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
106        let remote_name = args
107            .get(1)
108            .ok_or_else(|| AppError::Validation("Usage: sync test <remote>".to_string()))?;
109
110        let profile = store.get(remote_name)?;
111        test_connection(&profile)
112    }
113
114    fn exec(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
115        if args.len() < 3 {
116            return Err(AppError::Validation(
117                "Usage: sync exec <remote> <command...>".to_string(),
118            ));
119        }
120
121        let remote_name = args[1];
122        let profile = store.get(remote_name)?;
123        let command = args[2..].join(" ");
124        run_remote_command(&profile, &command)
125    }
126
127    fn restart(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
128        let remote_name = args.get(1).ok_or_else(|| {
129            AppError::Validation("Usage: sync restart <remote> [service]".to_string())
130        })?;
131
132        let service = args.get(2).copied().unwrap_or("rush-sync");
133        let profile = store.get(remote_name)?;
134
135        let result = restart_service(&profile, service)?;
136        Ok(format!(
137            "Service '{}' restarted on '{}'\n{}",
138            service, remote_name, result
139        ))
140    }
141
142    fn git_pull(&self, store: &RemoteProfileStore, args: &[&str]) -> Result<String> {
143        let remote_name = args.get(1).ok_or_else(|| {
144            AppError::Validation("Usage: sync git-pull <remote> [branch]".to_string())
145        })?;
146
147        let branch = args.get(2).copied();
148        let profile = store.get(remote_name)?;
149        let result = git_pull(&profile, branch)?;
150        Ok(format!(
151            "Remote git pull on '{}' (branch: {})\n{}",
152            remote_name,
153            branch.unwrap_or("main"),
154            result
155        ))
156    }
157
158    fn help_text(&self) -> String {
159        "Sync and remote actions\n\n\
160         Commands:\n\
161           sync push <remote> [local_path] [--delete] [--dry-run]\n\
162           sync pull <remote> [local_path] [--delete] [--dry-run]\n\
163           sync test <remote>\n\
164           sync exec <remote> <command...>\n\
165           sync restart <remote> [service]\n\
166           sync git-pull <remote> [branch]\n\n\
167         Flags:\n\
168           --delete    Remove files on destination not present in source\n\
169           --dry-run   Show what would be transferred without making changes (rsync only)\n\n\
170         Notes:\n\
171           - Uses rsync over SSH when available.\n\
172           - Falls back to scp when rsync is not installed.\n\
173           - Configure remotes via the 'remote' command."
174            .to_string()
175    }
176}
177
178fn parse_flags(args: &[&str]) -> (Vec<String>, bool, bool) {
179    let mut delete = false;
180    let mut dry_run = false;
181    let mut positionals = Vec::new();
182
183    for arg in args {
184        match *arg {
185            "--delete" => delete = true,
186            "--dry-run" | "-n" => dry_run = true,
187            _ => positionals.push((*arg).to_string()),
188        }
189    }
190
191    (positionals, delete, dry_run)
192}