Skip to content

Commit ee38f66

Browse files
committed
allow projects to specify multiple destinations
1 parent 41730b2 commit ee38f66

File tree

3 files changed

+57
-53
lines changed

3 files changed

+57
-53
lines changed

README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ debounce = 0.500 # seconds to debounce file change events
6262
[[sync]]
6363
name = 'example-local-sync'
6464
source = '~/projects/my-local-dir/'
65-
destination = '/Users/yourname/backup-dir/'
65+
destinations = ['/Users/yourname/backup-dir/']
6666

6767
sync_on_start = true
6868

@@ -75,7 +75,7 @@ node_modules
7575
[[sync]]
7676
name = 'example-remote-sync'
7777
source = '~/projects/my-project/'
78-
destination = 'username@remote-server:~/my-project'
78+
destinations = ['username@remote-server:~/my-project']
7979

8080
sync_on_start = true
8181
options = '-az -e ssh --delete'
@@ -96,15 +96,15 @@ target
9696

9797
#### Per-Sync Job Fields
9898

99-
* `name`: Unique name for the sync job.
100-
* `source`: Source directory. `~` is expanded to home.
101-
* `destination`: Destination path. Can be local or remote via SSH.
102-
* `sync_on_start`: If `true`, will sync automatically when the tool starts.
103-
* `options`: Optional `rsync` options to override the default (`-az -e ssh`).
99+
* `name` (string): Unique name for the sync job.
100+
* `source` (string): Source directory. `~` is expanded to home.
101+
* `destinations` (array of strings): List of destination paths. Can be local or remote via SSH.
102+
* `sync_on_start` (boolean):: If `true`, will sync automatically when the tool starts.
103+
* `options` (string, optional): Optional `rsync` options to override the default (`-az -e ssh`).
104104

105105
* **Note**: If `options` is not specified, it defaults to `-az -e ssh`.
106106
* Including `--delete` in the options will cause files in the destination that are not present in the source to be removed. **Use with caution!**
107-
* `ignore`: Multiline string of patterns (one per line) to exclude from sync.
107+
* `ignore` (string, optional): Multiline string of patterns (one per line) to exclude from sync.
108108

109109
* **Note**: If `ignore` is not specified, it defaults to ignoring `.git`.
110110

src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub struct Config {
1414
pub struct SyncRule {
1515
pub name: String,
1616
pub source: String,
17-
pub destination: String,
17+
pub destinations: Vec<String>,
1818

1919
#[serde(default = "default_sync_on_start")]
2020
pub sync_on_start: bool,

src/sync.rs

Lines changed: 48 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const _SELF_CONFIG_: &str = "project-sync-self-config";
1616
pub struct Sync {
1717
pub name: String,
1818
pub source: String,
19-
pub destination: String,
19+
pub destinations: Vec<String>,
2020
pub synced: Instant,
2121
//updated: Instant,
2222
pub sync_on_start: bool,
@@ -51,46 +51,50 @@ impl Sync {
5151
}
5252

5353
fn sync(&mut self) {
54-
//std::thread::sleep(Duration::from_secs_f32(0.25));
55-
print!("Syncing {} ", self.name.bright_green());
56-
57-
let verbosity = "--itemize-changes"; // -v
58-
let extras = self.sync_extras();
59-
let command_line = format!("rsync {verbosity} -az -e ssh {extras} {} '{}'", self.source, self.destination);
60-
61-
println!("{}", command_line.white().dimmed());
62-
63-
let mut attempts = 0;
64-
let start = Instant::now();
65-
66-
self.synced = Instant::now();
67-
while !std::process::Command::new("sh")
68-
.arg("-c")
69-
.arg(&command_line)
70-
.status()
71-
.expect("could not run shell command...")
72-
.success()
73-
{
74-
println!("{}... going to sleep and retry...", "FAILED".on_red());
75-
std::thread::sleep(Duration::from_secs(4));
76-
if attempts > 7 {
77-
println!("Too many failed attempts: {}, giving up...", attempts.to_string().bright_red());
78-
break;
54+
for destination in &self.destinations {
55+
//std::thread::sleep(Duration::from_secs_f32(0.25));
56+
print!("Syncing {} ", self.name.bright_green());
57+
58+
let verbosity = "--itemize-changes"; // -v
59+
let extras = self.sync_extras();
60+
let command_line = format!("rsync {verbosity} -az -e ssh {extras} {} '{}'", self.source, destination);
61+
62+
println!("{}", command_line.white().dimmed());
63+
64+
let mut attempts = 0;
65+
let start = Instant::now();
66+
67+
self.synced = Instant::now();
68+
while !std::process::Command::new("sh")
69+
.arg("-c")
70+
.arg(&command_line)
71+
.status()
72+
.expect("could not run shell command...")
73+
.success()
74+
{
75+
println!("{}... going to sleep and retry...", "FAILED".on_red());
76+
std::thread::sleep(Duration::from_secs(4));
77+
if attempts > 7 {
78+
println!("Too many failed attempts: {}, giving up...", attempts.to_string().bright_red());
79+
break;
80+
}
81+
attempts += 1;
7982
}
80-
attempts += 1;
81-
}
8283

83-
let _duration = start.elapsed();
84-
//println!("Elapsed time: {:.2?}", _duration);
84+
let _duration = start.elapsed();
85+
//println!("Elapsed time: {:.2?}", _duration);
86+
}
8587
}
8688
fn create_project_watcher(&self, transmitter: std::sync::mpsc::Sender<SyncUpdate>) -> notify::RecommendedWatcher {
8789
println!(
88-
"Adding {:<16} {:>24} → {:<32} sync-on-start: {}",
90+
"Adding {:<16} {:>24} → [", // {:<32}
8991
self.name.bright_green(),
9092
self.source,
91-
self.destination,
92-
self.sync_on_start
9393
);
94+
for d in &self.destinations {
95+
println!(" {d}");
96+
}
97+
println!("] sync-on-start: {}", self.sync_on_start);
9498

9599
let path = PathBuf::from(shellexpand::tilde(&self.source).as_ref());
96100

@@ -115,18 +119,10 @@ impl Sync {
115119
struct ProjectConfig {
116120
projects: BTreeMap<String, Sync>,
117121
debounce: f32,
118-
filter: Option<String>,
119122
}
120123

121124
impl ProjectConfig {
122125
fn sync_projects(&mut self) {
123-
// Retain only those entries that match the filter string
124-
if let Some(filter) = &self.filter {
125-
self.projects.retain(|_name, project| {
126-
project.destination.contains(filter) || project.name == _SELF_CONFIG_ //|| project.path.contains(&filter)
127-
});
128-
}
129-
130126
let (tx, rx) = std::sync::mpsc::channel::<SyncUpdate>();
131127

132128
let mut watchers = Vec::<_>::new();
@@ -181,7 +177,7 @@ where
181177
Sync {
182178
name: _SELF_CONFIG_.into(),
183179
source: config_path.to_str().unwrap().to_string(),
184-
destination: config_path.to_str().unwrap().to_string(),
180+
destinations: vec![config_path.to_str().unwrap().to_string()],
185181
synced: Instant::now(),
186182
sync_on_start: false,
187183
ignore: "".into(),
@@ -198,6 +194,15 @@ pub fn run(config_path: &Path, filter: Option<String>) {
198194
println!("Reading sync config from {}...", config_path.display().to_string().bright_yellow());
199195
let config = config::read_config(config_path);
200196
// println!("Parsed config: {:#?}", config);
197+
198+
// Retain only those entries that match the filter string
199+
fn destination_filter(mut destinations: Vec<String>, filter: &Option<String>) -> Vec<String> {
200+
if let Some(filter) = filter {
201+
destinations.retain(|dest| dest.contains(filter));
202+
}
203+
destinations
204+
}
205+
201206
let projects = config
202207
.sync
203208
.into_iter()
@@ -207,7 +212,7 @@ pub fn run(config_path: &Path, filter: Option<String>) {
207212
Sync {
208213
name: c.name,
209214
source: c.source,
210-
destination: c.destination,
215+
destinations: destination_filter(c.destinations, &filter),
211216
synced: Instant::now(),
212217
sync_on_start: c.sync_on_start,
213218
ignore: c.ignore,
@@ -220,7 +225,6 @@ pub fn run(config_path: &Path, filter: Option<String>) {
220225
ProjectConfig {
221226
projects,
222227
debounce: config.debounce,
223-
filter: filter.clone(),
224228
}
225229
};
226230

0 commit comments

Comments
 (0)