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("/var/lib/zlayer/sqlite-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: zlayer_paths::ZLayerDirs::system_default()
94                .data_dir()
95                .join("sqlite-replicator/cache"),
96            max_cache_size: default_max_cache_size(),
97            auto_restore: default_auto_restore(),
98            snapshot_interval_secs: default_snapshot_interval(),
99        }
100    }
101}
102
103impl SqliteReplicatorConfig {
104    /// Create a new config with the required fields
105    pub fn new(
106        db_path: impl Into<PathBuf>,
107        s3_bucket: impl Into<String>,
108        s3_prefix: impl Into<String>,
109    ) -> Self {
110        Self {
111            db_path: db_path.into(),
112            s3_bucket: s3_bucket.into(),
113            s3_prefix: s3_prefix.into(),
114            ..Default::default()
115        }
116    }
117
118    /// Set the cache directory
119    #[must_use]
120    pub fn with_cache_dir(mut self, cache_dir: impl Into<PathBuf>) -> Self {
121        self.cache_dir = cache_dir.into();
122        self
123    }
124
125    /// Set the maximum cache size
126    #[must_use]
127    pub fn with_max_cache_size(mut self, size: u64) -> Self {
128        self.max_cache_size = size;
129        self
130    }
131
132    /// Set whether to auto-restore on startup
133    #[must_use]
134    pub fn with_auto_restore(mut self, auto_restore: bool) -> Self {
135        self.auto_restore = auto_restore;
136        self
137    }
138
139    /// Set the snapshot interval
140    #[must_use]
141    pub fn with_snapshot_interval(mut self, interval_secs: u64) -> Self {
142        self.snapshot_interval_secs = interval_secs;
143        self
144    }
145}
146
147// ============================================================================
148// Layer Storage Configuration
149// ============================================================================
150
151/// Configuration for S3-backed layer storage
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct LayerStorageConfig {
154    /// S3 bucket name for storing layers
155    pub bucket: String,
156
157    /// S3 key prefix for layer objects (e.g., "layers/")
158    #[serde(default = "default_prefix")]
159    pub prefix: String,
160
161    /// AWS region (if not using environment/profile defaults)
162    pub region: Option<String>,
163
164    /// Custom S3 endpoint URL (for S3-compatible storage like `MinIO`)
165    pub endpoint_url: Option<String>,
166
167    /// Local directory for staging tarballs before upload
168    pub staging_dir: PathBuf,
169
170    /// Local database path for sync state persistence
171    pub state_db_path: PathBuf,
172
173    /// Multipart upload part size in bytes (default: 64MB)
174    #[serde(default = "default_part_size")]
175    pub part_size_bytes: u64,
176
177    /// Maximum concurrent part uploads
178    #[serde(default = "default_concurrent_uploads")]
179    pub max_concurrent_uploads: usize,
180
181    /// Compression level for zstd (1-22, default: 3)
182    #[serde(default = "default_compression_level")]
183    pub compression_level: i32,
184
185    /// Sync interval in seconds (how often to check for changes)
186    #[serde(default = "default_sync_interval")]
187    pub sync_interval_secs: u64,
188}
189
190fn default_prefix() -> String {
191    "layers/".to_string()
192}
193
194fn default_part_size() -> u64 {
195    64 * 1024 * 1024 // 64MB
196}
197
198fn default_concurrent_uploads() -> usize {
199    4
200}
201
202fn default_compression_level() -> i32 {
203    3
204}
205
206fn default_sync_interval() -> u64 {
207    30
208}
209
210impl Default for LayerStorageConfig {
211    fn default() -> Self {
212        Self {
213            bucket: String::new(),
214            prefix: default_prefix(),
215            region: None,
216            endpoint_url: None,
217            staging_dir: zlayer_paths::ZLayerDirs::system_default()
218                .data_dir()
219                .join("layer-staging"),
220            state_db_path: zlayer_paths::ZLayerDirs::system_default()
221                .data_dir()
222                .join("layer-state.sqlite"),
223            part_size_bytes: default_part_size(),
224            max_concurrent_uploads: default_concurrent_uploads(),
225            compression_level: default_compression_level(),
226            sync_interval_secs: default_sync_interval(),
227        }
228    }
229}
230
231impl LayerStorageConfig {
232    /// Create a new config with the required bucket name
233    pub fn new(bucket: impl Into<String>) -> Self {
234        Self {
235            bucket: bucket.into(),
236            ..Default::default()
237        }
238    }
239
240    /// Set the S3 key prefix
241    #[must_use]
242    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
243        self.prefix = prefix.into();
244        self
245    }
246
247    /// Set the AWS region
248    #[must_use]
249    pub fn with_region(mut self, region: impl Into<String>) -> Self {
250        self.region = Some(region.into());
251        self
252    }
253
254    /// Set a custom S3 endpoint URL
255    #[must_use]
256    pub fn with_endpoint_url(mut self, url: impl Into<String>) -> Self {
257        self.endpoint_url = Some(url.into());
258        self
259    }
260
261    /// Set the staging directory
262    #[must_use]
263    pub fn with_staging_dir(mut self, path: impl Into<PathBuf>) -> Self {
264        self.staging_dir = path.into();
265        self
266    }
267
268    /// Set the state database path
269    #[must_use]
270    pub fn with_state_db_path(mut self, path: impl Into<PathBuf>) -> Self {
271        self.state_db_path = path.into();
272        self
273    }
274
275    /// Build the S3 object key for a given layer digest
276    #[must_use]
277    pub fn object_key(&self, digest: &str) -> String {
278        format!("{}{}.tar.zst", self.prefix, digest)
279    }
280
281    /// Build the S3 object key for layer metadata
282    #[must_use]
283    pub fn metadata_key(&self, digest: &str) -> String {
284        format!("{}{}.meta.json", self.prefix, digest)
285    }
286}