Skip to main content

zlayer_storage/
config.rs

1//! Configuration types for layer storage
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6// ============================================================================
7// SQLite Replicator Configuration
8// ============================================================================
9
10/// Configuration for `SQLite` WAL-based replication to S3
11///
12/// This configuration controls how the `SQLite` replicator monitors database changes
13/// and syncs them to S3 for persistence and disaster recovery.
14///
15/// # Example
16///
17/// ```rust
18/// use zlayer_storage::config::SqliteReplicatorConfig;
19/// use std::path::PathBuf;
20///
21/// let config = SqliteReplicatorConfig {
22///     db_path: PathBuf::from("/var/lib/myapp/data.db"),
23///     s3_bucket: "my-bucket".to_string(),
24///     s3_prefix: "sqlite-backups/myapp/".to_string(),
25///     cache_dir: PathBuf::from("/tmp/zlayer-replicator/cache"),
26///     max_cache_size: 100 * 1024 * 1024, // 100MB
27///     auto_restore: true,
28///     snapshot_interval_secs: 3600,
29/// };
30/// ```
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct SqliteReplicatorConfig {
33    /// Path to the `SQLite` database file to replicate
34    pub db_path: PathBuf,
35
36    /// S3 bucket for storing backups
37    pub s3_bucket: String,
38
39    /// S3 key prefix for this database's backups
40    ///
41    /// The replicator will create the following structure under this prefix:
42    /// - `{prefix}/snapshots/` - Full database snapshots
43    /// - `{prefix}/wal/` - WAL segments
44    /// - `{prefix}/metadata.json` - Replication metadata
45    pub s3_prefix: String,
46
47    /// Local directory for caching WAL segments before upload
48    ///
49    /// This provides network tolerance - if S3 is temporarily unavailable,
50    /// WAL segments are cached here and uploaded when connectivity returns.
51    pub cache_dir: PathBuf,
52
53    /// Maximum size of the local cache in bytes
54    ///
55    /// When the cache exceeds this size, the oldest entries will be evicted
56    /// (and lost if not yet uploaded). Default: 100MB
57    #[serde(default = "default_max_cache_size")]
58    pub max_cache_size: u64,
59
60    /// Whether to automatically restore from S3 on startup if local DB is missing
61    ///
62    /// When enabled, the replicator will download and restore the database
63    /// from S3 if the local file doesn't exist. Default: true
64    #[serde(default = "default_auto_restore")]
65    pub auto_restore: bool,
66
67    /// Interval between full database snapshots in seconds
68    ///
69    /// Snapshots provide a restore point and allow cleanup of old WAL segments.
70    /// Default: 3600 (1 hour)
71    #[serde(default = "default_snapshot_interval")]
72    pub snapshot_interval_secs: u64,
73}
74
75fn default_max_cache_size() -> u64 {
76    100 * 1024 * 1024 // 100MB
77}
78
79fn default_auto_restore() -> bool {
80    true
81}
82
83fn default_snapshot_interval() -> u64 {
84    3600 // 1 hour
85}
86
87impl Default for SqliteReplicatorConfig {
88    fn default() -> Self {
89        Self {
90            db_path: PathBuf::new(),
91            s3_bucket: String::new(),
92            s3_prefix: "sqlite-replication/".to_string(),
93            cache_dir: PathBuf::from("/tmp/zlayer-replicator/cache"),
94            max_cache_size: default_max_cache_size(),
95            auto_restore: default_auto_restore(),
96            snapshot_interval_secs: default_snapshot_interval(),
97        }
98    }
99}
100
101impl SqliteReplicatorConfig {
102    /// Create a new config with the required fields
103    pub fn new(
104        db_path: impl Into<PathBuf>,
105        s3_bucket: impl Into<String>,
106        s3_prefix: impl Into<String>,
107    ) -> Self {
108        Self {
109            db_path: db_path.into(),
110            s3_bucket: s3_bucket.into(),
111            s3_prefix: s3_prefix.into(),
112            ..Default::default()
113        }
114    }
115
116    /// Set the cache directory
117    #[must_use]
118    pub fn with_cache_dir(mut self, cache_dir: impl Into<PathBuf>) -> Self {
119        self.cache_dir = cache_dir.into();
120        self
121    }
122
123    /// Set the maximum cache size
124    #[must_use]
125    pub fn with_max_cache_size(mut self, size: u64) -> Self {
126        self.max_cache_size = size;
127        self
128    }
129
130    /// Set whether to auto-restore on startup
131    #[must_use]
132    pub fn with_auto_restore(mut self, auto_restore: bool) -> Self {
133        self.auto_restore = auto_restore;
134        self
135    }
136
137    /// Set the snapshot interval
138    #[must_use]
139    pub fn with_snapshot_interval(mut self, interval_secs: u64) -> Self {
140        self.snapshot_interval_secs = interval_secs;
141        self
142    }
143}
144
145// ============================================================================
146// Layer Storage Configuration
147// ============================================================================
148
149/// Configuration for S3-backed layer storage
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct LayerStorageConfig {
152    /// S3 bucket name for storing layers
153    pub bucket: String,
154
155    /// S3 key prefix for layer objects (e.g., "layers/")
156    #[serde(default = "default_prefix")]
157    pub prefix: String,
158
159    /// AWS region (if not using environment/profile defaults)
160    pub region: Option<String>,
161
162    /// Custom S3 endpoint URL (for S3-compatible storage like `MinIO`)
163    pub endpoint_url: Option<String>,
164
165    /// Local directory for staging tarballs before upload
166    pub staging_dir: PathBuf,
167
168    /// Local database path for sync state persistence
169    pub state_db_path: PathBuf,
170
171    /// Multipart upload part size in bytes (default: 64MB)
172    #[serde(default = "default_part_size")]
173    pub part_size_bytes: u64,
174
175    /// Maximum concurrent part uploads
176    #[serde(default = "default_concurrent_uploads")]
177    pub max_concurrent_uploads: usize,
178
179    /// Compression level for zstd (1-22, default: 3)
180    #[serde(default = "default_compression_level")]
181    pub compression_level: i32,
182
183    /// Sync interval in seconds (how often to check for changes)
184    #[serde(default = "default_sync_interval")]
185    pub sync_interval_secs: u64,
186}
187
188fn default_prefix() -> String {
189    "layers/".to_string()
190}
191
192fn default_part_size() -> u64 {
193    64 * 1024 * 1024 // 64MB
194}
195
196fn default_concurrent_uploads() -> usize {
197    4
198}
199
200fn default_compression_level() -> i32 {
201    3
202}
203
204fn default_sync_interval() -> u64 {
205    30
206}
207
208impl Default for LayerStorageConfig {
209    fn default() -> Self {
210        Self {
211            bucket: String::new(),
212            prefix: default_prefix(),
213            region: None,
214            endpoint_url: None,
215            staging_dir: PathBuf::from("/tmp/zlayer-storage/staging"),
216            state_db_path: zlayer_paths::ZLayerDirs::system_default()
217                .data_dir()
218                .join("layer-state.sqlite"),
219            part_size_bytes: default_part_size(),
220            max_concurrent_uploads: default_concurrent_uploads(),
221            compression_level: default_compression_level(),
222            sync_interval_secs: default_sync_interval(),
223        }
224    }
225}
226
227impl LayerStorageConfig {
228    /// Create a new config with the required bucket name
229    pub fn new(bucket: impl Into<String>) -> Self {
230        Self {
231            bucket: bucket.into(),
232            ..Default::default()
233        }
234    }
235
236    /// Set the S3 key prefix
237    #[must_use]
238    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
239        self.prefix = prefix.into();
240        self
241    }
242
243    /// Set the AWS region
244    #[must_use]
245    pub fn with_region(mut self, region: impl Into<String>) -> Self {
246        self.region = Some(region.into());
247        self
248    }
249
250    /// Set a custom S3 endpoint URL
251    #[must_use]
252    pub fn with_endpoint_url(mut self, url: impl Into<String>) -> Self {
253        self.endpoint_url = Some(url.into());
254        self
255    }
256
257    /// Set the staging directory
258    #[must_use]
259    pub fn with_staging_dir(mut self, path: impl Into<PathBuf>) -> Self {
260        self.staging_dir = path.into();
261        self
262    }
263
264    /// Set the state database path
265    #[must_use]
266    pub fn with_state_db_path(mut self, path: impl Into<PathBuf>) -> Self {
267        self.state_db_path = path.into();
268        self
269    }
270
271    /// Build the S3 object key for a given layer digest
272    #[must_use]
273    pub fn object_key(&self, digest: &str) -> String {
274        format!("{}{}.tar.zst", self.prefix, digest)
275    }
276
277    /// Build the S3 object key for layer metadata
278    #[must_use]
279    pub fn metadata_key(&self, digest: &str) -> String {
280        format!("{}{}.meta.json", self.prefix, digest)
281    }
282}