Skip to main content

garmin_cli/storage/
mod.rs

1//! Storage layer for Garmin data
2//!
3//! This module provides time-partitioned Parquet storage for Garmin data,
4//! enabling concurrent read access during sync operations.
5//!
6//! ## Architecture
7//!
8//! - **Parquet files**: Time-partitioned storage for activities, health, performance, etc.
9//! - **SQLite**: Sync state and task queue for operational data
10//!
11//! ## Storage Layout
12//!
13//! ```text
14//! ~/.local/share/garmin/
15//! ├── sync.db                      # SQLite for sync state + task queue
16//! ├── profiles.parquet             # Single file (small, rarely changes)
17//! ├── activities/
18//! │   ├── 2024-W48.parquet        # Weekly partitions
19//! │   └── ...
20//! ├── track_points/
21//! │   ├── 2024-12-01.parquet      # Daily partitions
22//! │   └── ...
23//! ├── daily_health/
24//! │   ├── 2024-12.parquet         # Monthly partitions
25//! │   └── ...
26//! ├── performance_metrics/
27//! │   ├── 2024-12.parquet         # Monthly partitions
28//! │   └── ...
29//! └── weight/
30//!     ├── 2024-12.parquet         # Monthly partitions
31//!     └── ...
32//! ```
33//!
34//! ## Concurrent Access
35//!
36//! Parquet files are written atomically (temp file + rename), so readers always
37//! see consistent data. External apps can query data using DuckDB:
38//!
39//! ```sql
40//! SELECT * FROM 'activities/*.parquet' WHERE start_time_local > '2024-12-01';
41//! ```
42
43mod parquet;
44mod partitions;
45mod sync_db;
46
47pub use parquet::ParquetStore;
48pub use partitions::EntityType;
49pub use sync_db::SyncDb;
50
51use std::path::PathBuf;
52
53use crate::error::Result;
54
55/// Get the default storage path
56pub fn default_storage_path() -> PathBuf {
57    dirs::data_local_dir()
58        .unwrap_or_else(|| PathBuf::from("."))
59        .join("garmin")
60}
61
62/// Get the default sync database path
63pub fn default_sync_db_path() -> PathBuf {
64    default_storage_path().join("sync.db")
65}
66
67/// Storage manager combining Parquet store and sync database
68pub struct Storage {
69    pub parquet: ParquetStore,
70    pub sync_db: SyncDb,
71}
72
73impl Storage {
74    /// Open storage at the default location
75    pub fn open_default() -> Result<Self> {
76        let base_path = default_storage_path();
77        Self::open(base_path)
78    }
79
80    /// Open storage at a custom location
81    pub fn open(base_path: PathBuf) -> Result<Self> {
82        std::fs::create_dir_all(&base_path).map_err(|e| {
83            crate::error::GarminError::Database(format!(
84                "Failed to create storage directory: {}",
85                e
86            ))
87        })?;
88
89        let parquet = ParquetStore::new(&base_path);
90        let sync_db = SyncDb::open(base_path.join("sync.db"))?;
91
92        Ok(Self { parquet, sync_db })
93    }
94
95    /// Open in-memory storage (for testing)
96    pub fn open_in_memory(temp_path: PathBuf) -> Result<Self> {
97        std::fs::create_dir_all(&temp_path).map_err(|e| {
98            crate::error::GarminError::Database(format!("Failed to create temp directory: {}", e))
99        })?;
100
101        let parquet = ParquetStore::new(&temp_path);
102        let sync_db = SyncDb::open_in_memory()?;
103
104        Ok(Self { parquet, sync_db })
105    }
106
107    /// Create a new storage handle that shares the Parquet store but opens a new SyncDb connection.
108    pub fn clone_with_new_db(&self) -> Result<Self> {
109        let parquet = self.parquet.clone();
110        let sync_db = SyncDb::open(self.base_path().join("sync.db"))?;
111        Ok(Self { parquet, sync_db })
112    }
113
114    /// Get the base path for external readers
115    pub fn base_path(&self) -> &std::path::Path {
116        self.parquet.base_path()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use tempfile::TempDir;
124
125    #[test]
126    fn test_storage_open() {
127        let temp = TempDir::new().unwrap();
128        let storage = Storage::open(temp.path().to_path_buf()).unwrap();
129        assert!(storage.base_path().exists());
130    }
131}