garmin_cli/cli/commands/
sync.rs1use chrono::NaiveDate;
4
5use crate::client::GarminClient;
6use crate::config::CredentialStore;
7use crate::db::default_db_path;
8use crate::error::Result;
9use crate::sync::{SyncEngine, SyncOptions, TaskQueue};
10use crate::Database;
11
12use super::auth::refresh_token;
13
14#[allow(clippy::too_many_arguments)]
16pub async fn run(
17 profile: Option<String>,
18 db_path: Option<String>,
19 activities: bool,
20 health: bool,
21 performance: bool,
22 from: Option<String>,
23 to: Option<String>,
24 dry_run: bool,
25 simple: bool,
26) -> Result<()> {
27 let store = CredentialStore::new(profile.clone())?;
28 let (oauth1, oauth2) = refresh_token(&store).await?;
29
30 let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
32 println!("Using database: {}", db_path);
33
34 let db = Database::open(&db_path)?;
35
36 let profile_id = get_or_create_profile(&db, profile.as_deref())?;
38
39 let sync_all = !activities && !health && !performance;
41 let opts = SyncOptions {
42 sync_activities: activities || sync_all,
43 sync_health: health || sync_all,
44 sync_performance: performance || sync_all,
45 from_date: from.as_ref().and_then(|s| parse_date(s)),
46 to_date: to.as_ref().and_then(|s| parse_date(s)),
47 dry_run,
48 force: false,
49 fancy_ui: !simple, concurrency: 3,
51 };
52
53 if dry_run {
54 println!("Dry run mode - no changes will be made");
55 }
56
57 let client = GarminClient::new(&oauth1.domain);
59 let mut engine = SyncEngine::new(db, client, oauth2, profile_id);
60
61 if !simple {
62 let stats = engine.run(opts).await?;
64 let _ = stats; } else {
67 println!("Starting sync...");
69 let stats = engine.run(opts).await?;
70 println!("\nSync complete: {}", stats);
71 }
72
73 Ok(())
74}
75
76pub async fn status(profile: Option<String>, db_path: Option<String>) -> Result<()> {
78 let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
79
80 if !std::path::Path::new(&db_path).exists() {
81 println!("No database found at: {}", db_path);
82 println!("Run 'garmin sync' to create one.");
83 return Ok(());
84 }
85
86 let db = Database::open(&db_path)?;
87 let conn = db.connection();
88 let conn = conn.lock().unwrap();
89
90 let profile_name = profile.as_deref().unwrap_or("default");
92
93 let activity_count: i64 = conn
95 .query_row("SELECT COUNT(*) FROM activities", [], |row| row.get(0))
96 .unwrap_or(0);
97
98 let health_count: i64 = conn
100 .query_row("SELECT COUNT(*) FROM daily_health", [], |row| row.get(0))
101 .unwrap_or(0);
102
103 let trackpoint_count: i64 = conn
105 .query_row("SELECT COUNT(*) FROM track_points", [], |row| row.get(0))
106 .unwrap_or(0);
107
108 let pending_count: i64 = conn
110 .query_row(
111 "SELECT COUNT(*) FROM sync_tasks WHERE status IN ('pending', 'failed')",
112 [],
113 |row| row.get(0),
114 )
115 .unwrap_or(0);
116
117 println!("Database: {}", db_path);
118 println!("Profile: {}", profile_name);
119 println!();
120 println!("Data stored:");
121 println!(" Activities: {:>8}", activity_count);
122 println!(" Health days: {:>8}", health_count);
123 println!(" Track points: {:>8}", trackpoint_count);
124 println!();
125 if pending_count > 0 {
126 println!("Pending sync tasks: {}", pending_count);
127 }
128
129 Ok(())
130}
131
132fn get_or_create_profile(db: &Database, profile_name: Option<&str>) -> Result<i32> {
134 let conn = db.connection();
135 let conn = conn.lock().unwrap();
136
137 let name = profile_name.unwrap_or("default");
138
139 let existing = conn.query_row(
141 "SELECT profile_id FROM profiles WHERE display_name = ?",
142 duckdb::params![name],
143 |row| row.get::<_, i32>(0),
144 );
145
146 match existing {
147 Ok(id) => Ok(id),
148 Err(duckdb::Error::QueryReturnedNoRows) => {
149 let id: i32 = conn
151 .query_row(
152 "INSERT INTO profiles (display_name) VALUES (?) RETURNING profile_id",
153 duckdb::params![name],
154 |row| row.get(0),
155 )
156 .map_err(|e| crate::GarminError::Database(e.to_string()))?;
157
158 Ok(id)
159 }
160 Err(e) => Err(crate::GarminError::Database(e.to_string())),
161 }
162}
163
164fn parse_date(s: &str) -> Option<NaiveDate> {
166 NaiveDate::parse_from_str(s, "%Y-%m-%d").ok()
167}
168
169pub async fn reset(db_path: Option<String>) -> Result<()> {
171 let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
172
173 if !std::path::Path::new(&db_path).exists() {
174 println!("No database found at: {}", db_path);
175 return Ok(());
176 }
177
178 let db = Database::open(&db_path)?;
179 let queue = TaskQueue::new(db);
180
181 let reset_count = queue.reset_failed()?;
182 println!("Reset {} failed tasks to pending", reset_count);
183
184 Ok(())
185}
186
187pub async fn clear(db_path: Option<String>) -> Result<()> {
189 let db_path = db_path.unwrap_or_else(|| default_db_path().unwrap());
190
191 if !std::path::Path::new(&db_path).exists() {
192 println!("No database found at: {}", db_path);
193 return Ok(());
194 }
195
196 let db = Database::open(&db_path)?;
197 let queue = TaskQueue::new(db);
198
199 let cleared = queue.clear_pending()?;
200 println!("Cleared {} pending tasks", cleared);
201
202 Ok(())
203}