garmin_cli/cli/commands/
sync.rs1use chrono::NaiveDate;
4use std::path::Path;
5
6use crate::client::GarminClient;
7use crate::config::CredentialStore;
8use crate::error::Result;
9use crate::storage::{default_storage_path, Storage, SyncDb};
10use crate::sync::progress::SyncMode;
11use crate::sync::{SyncEngine, SyncOptions, TaskQueue};
12
13use super::auth::refresh_token;
14
15#[allow(clippy::too_many_arguments)]
17pub async fn run(
18 profile: Option<String>,
19 storage_path: Option<String>,
20 activities: bool,
21 health: bool,
22 performance: bool,
23 from: Option<String>,
24 to: Option<String>,
25 dry_run: bool,
26 backfill: bool,
27 force: bool,
28) -> Result<()> {
29 let store = CredentialStore::new(profile.clone())?;
30 let (oauth1, oauth2) = refresh_token(&store).await?;
31
32 let storage_path = storage_path
34 .map(std::path::PathBuf::from)
35 .unwrap_or_else(default_storage_path);
36
37 let storage = Storage::open(storage_path)?;
38
39 let mode = if backfill {
41 SyncMode::Backfill
42 } else {
43 SyncMode::Latest
44 };
45
46 let sync_all = !activities && !health && !performance;
48 let opts = SyncOptions {
49 sync_activities: activities || sync_all,
50 sync_health: health || sync_all,
51 sync_performance: performance || sync_all,
52 from_date: from.as_ref().and_then(|s| parse_date(s)),
53 to_date: to.as_ref().and_then(|s| parse_date(s)),
54 dry_run,
55 force,
56 concurrency: 4,
57 mode,
58 };
59
60 let client = GarminClient::new(&oauth1.domain);
62 let mut engine = SyncEngine::with_storage(storage, client, oauth2)?;
63 engine.run(opts).await?;
64
65 Ok(())
66}
67
68pub async fn status(profile: Option<String>, storage_path: Option<String>) -> Result<()> {
70 let storage_path = storage_path
71 .map(std::path::PathBuf::from)
72 .unwrap_or_else(default_storage_path);
73
74 if !storage_path.exists() {
75 println!("No storage found at: {}", storage_path.display());
76 println!("Run 'garmin sync run' to create one.");
77 return Ok(());
78 }
79
80 let sync_db_path = storage_path.join("sync.db");
81 if !sync_db_path.exists() {
82 println!("No sync database found at: {}", sync_db_path.display());
83 println!("Run 'garmin sync run' to create one.");
84 return Ok(());
85 }
86
87 let sync_db = SyncDb::open(&sync_db_path)?;
88
89 let requested_profile = profile.as_deref();
92 let mut profile_note: Option<String> = None;
93 let (profile_name, profile_id) = match requested_profile {
94 Some(name) => match sync_db.get_profile_id(name)? {
95 Some(id) => (name.to_string(), Some(id)),
96 None => match sync_db.get_latest_profile()? {
97 Some((id, resolved_name)) => {
98 profile_note = Some(format!(
99 "Requested profile '{}' not found in sync database; showing latest synced profile '{}'.",
100 name, resolved_name
101 ));
102 (resolved_name, Some(id))
103 }
104 None => {
105 profile_note = Some(format!(
106 "Requested profile '{}' not found in sync database.",
107 name
108 ));
109 (name.to_string(), None)
110 }
111 },
112 },
113 None => match sync_db.get_latest_profile()? {
114 Some((id, resolved_name)) => (resolved_name, Some(id)),
115 None => ("default".to_string(), None),
116 },
117 };
118
119 let activity_files = count_partition_files(&storage_path, "activities");
121 let health_files = count_partition_files(&storage_path, "daily_health");
122 let performance_files = count_partition_files(&storage_path, "performance_metrics");
123 let track_files = count_partition_files(&storage_path, "track_points");
124
125 let pending_count = if let Some(pid) = profile_id {
127 sync_db.count_pending_tasks(pid, None)?
128 } else {
129 0
130 };
131
132 println!("Storage: {}", storage_path.display());
133 println!("Profile: {}", profile_name);
134 if let Some(note) = profile_note {
135 println!("Note: {}", note);
136 }
137 println!();
138 println!("Parquet files:");
139 println!(" Activity partitions: {:>4}", activity_files);
140 println!(" Health partitions: {:>4}", health_files);
141 println!(" Performance partitions: {:>4}", performance_files);
142 println!(" Track point partitions: {:>4}", track_files);
143 println!();
144 if pending_count > 0 {
145 println!("Pending sync tasks: {}", pending_count);
146 }
147
148 Ok(())
149}
150
151fn parse_date(s: &str) -> Option<NaiveDate> {
153 NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()
154}
155
156fn count_partition_files(storage_path: &Path, dirname: &str) -> usize {
157 let partition_path = storage_path.join(dirname);
158 if !partition_path.exists() {
159 return 0;
160 }
161
162 std::fs::read_dir(&partition_path)
163 .map(|entries| entries.filter(|entry| entry.is_ok()).count())
164 .unwrap_or(0)
165}
166
167pub async fn reset(storage_path: Option<String>) -> Result<()> {
169 let storage_path = storage_path
170 .map(std::path::PathBuf::from)
171 .unwrap_or_else(default_storage_path);
172
173 let sync_db_path = storage_path.join("sync.db");
174 if !sync_db_path.exists() {
175 println!("No sync database found at: {}", sync_db_path.display());
176 return Ok(());
177 }
178
179 let sync_db = SyncDb::open(&sync_db_path)?;
180 let queue = TaskQueue::new(sync_db, 1, None); let reset_count = queue.reset_failed()?;
183 println!("Reset {} failed tasks to pending", reset_count);
184
185 Ok(())
186}
187
188pub async fn clear(storage_path: Option<String>) -> Result<()> {
190 let storage_path = storage_path
191 .map(std::path::PathBuf::from)
192 .unwrap_or_else(default_storage_path);
193
194 let sync_db_path = storage_path.join("sync.db");
195 if !sync_db_path.exists() {
196 println!("No sync database found at: {}", sync_db_path.display());
197 return Ok(());
198 }
199
200 let sync_db = SyncDb::open(&sync_db_path)?;
201 let queue = TaskQueue::new(sync_db, 1, None); let cleared = queue.clear_pending()?;
204 println!("Cleared {} pending tasks", cleared);
205
206 Ok(())
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn count_partition_files_returns_zero_for_missing_directory() {
215 let temp_dir = tempfile::tempdir().unwrap();
216 assert_eq!(count_partition_files(temp_dir.path(), "activities"), 0);
217 }
218
219 #[test]
220 fn count_partition_files_counts_existing_entries() {
221 let temp_dir = tempfile::tempdir().unwrap();
222 let activities_dir = temp_dir.path().join("activities");
223 std::fs::create_dir(&activities_dir).unwrap();
224 std::fs::write(activities_dir.join("2026-W10.parquet"), b"test").unwrap();
225 std::fs::write(activities_dir.join("2026-W11.parquet"), b"test").unwrap();
226
227 assert_eq!(count_partition_files(temp_dir.path(), "activities"), 2);
228 }
229}