1use std::fs;
8use std::io;
9use std::path::{Path, PathBuf};
10
11use serde::{Deserialize, Serialize};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "kebab-case")]
16#[derive(Default)]
17pub enum StorageLayout {
18 Minimal,
20 #[default]
22 Standard,
23 Performance,
25 Max,
27}
28
29#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(default)]
32pub struct LayoutOverrides {
33 pub dedicated_wal_dir: Option<bool>,
34 pub dedicated_index_dir: Option<bool>,
35 pub dedicated_cache_dir: Option<bool>,
36 pub dedicated_snapshot_dir: Option<bool>,
37 pub dedicated_blob_dir: Option<bool>,
38 pub dedicated_temp_dir: Option<bool>,
39 pub dedicated_metrics_dir: Option<bool>,
40 #[serde(default)]
42 pub logs: LogRoutingOverrides,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "kebab-case", tag = "kind", content = "path")]
55pub enum LogDestination {
56 Stderr,
57 File(PathBuf),
58 Syslog,
59}
60
61impl LogDestination {
62 pub fn describe(&self) -> String {
64 match self {
65 Self::Stderr => "stderr".to_string(),
66 Self::Syslog => "syslog".to_string(),
67 Self::File(path) => format!("file:{}", path.display()),
68 }
69 }
70
71 pub fn file_path(&self) -> Option<&Path> {
73 match self {
74 Self::File(path) => Some(path.as_path()),
75 _ => None,
76 }
77 }
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(default)]
83pub struct LogRoutingOverrides {
84 pub audit_log: Option<LogDestination>,
85 pub slow_log: Option<LogDestination>,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90pub struct LayoutToggles {
91 pub dedicated_wal_dir: bool,
92 pub dedicated_index_dir: bool,
93 pub dedicated_cache_dir: bool,
94 pub dedicated_snapshot_dir: bool,
95 pub dedicated_blob_dir: bool,
96 pub dedicated_temp_dir: bool,
97 pub dedicated_metrics_dir: bool,
98}
99
100impl StorageLayout {
101 pub fn default_audit_log_in(self, support_dir: &Path) -> LogDestination {
106 match self {
107 Self::Performance | Self::Max => {
108 LogDestination::File(support_dir.join("logs").join("audit.log"))
109 }
110 Self::Minimal | Self::Standard => LogDestination::Stderr,
111 }
112 }
113
114 pub fn default_slow_log_in(self, support_dir: &Path) -> LogDestination {
117 match self {
118 Self::Performance | Self::Max => {
119 LogDestination::File(support_dir.join("logs").join("slow.log"))
120 }
121 Self::Minimal | Self::Standard => LogDestination::Stderr,
122 }
123 }
124
125 pub fn expand(self, overrides: &LayoutOverrides) -> LayoutToggles {
126 let mut toggles = match self {
127 Self::Minimal => LayoutToggles {
128 dedicated_wal_dir: false,
129 dedicated_index_dir: false,
130 dedicated_cache_dir: false,
131 dedicated_snapshot_dir: false,
132 dedicated_blob_dir: false,
133 dedicated_temp_dir: false,
134 dedicated_metrics_dir: false,
135 },
136 Self::Standard => LayoutToggles {
137 dedicated_wal_dir: false,
138 dedicated_index_dir: true,
139 dedicated_cache_dir: false,
140 dedicated_snapshot_dir: true,
141 dedicated_blob_dir: false,
142 dedicated_temp_dir: false,
143 dedicated_metrics_dir: false,
144 },
145 Self::Performance => LayoutToggles {
146 dedicated_wal_dir: true,
147 dedicated_index_dir: true,
148 dedicated_cache_dir: true,
149 dedicated_snapshot_dir: true,
150 dedicated_blob_dir: true,
151 dedicated_temp_dir: false,
152 dedicated_metrics_dir: false,
153 },
154 Self::Max => LayoutToggles {
155 dedicated_wal_dir: true,
156 dedicated_index_dir: true,
157 dedicated_cache_dir: true,
158 dedicated_snapshot_dir: true,
159 dedicated_blob_dir: true,
160 dedicated_temp_dir: true,
161 dedicated_metrics_dir: true,
162 },
163 };
164
165 if let Some(value) = overrides.dedicated_wal_dir {
166 toggles.dedicated_wal_dir = value;
167 }
168 if let Some(value) = overrides.dedicated_index_dir {
169 toggles.dedicated_index_dir = value;
170 }
171 if let Some(value) = overrides.dedicated_cache_dir {
172 toggles.dedicated_cache_dir = value;
173 }
174 if let Some(value) = overrides.dedicated_snapshot_dir {
175 toggles.dedicated_snapshot_dir = value;
176 }
177 if let Some(value) = overrides.dedicated_blob_dir {
178 toggles.dedicated_blob_dir = value;
179 }
180 if let Some(value) = overrides.dedicated_temp_dir {
181 toggles.dedicated_temp_dir = value;
182 }
183 if let Some(value) = overrides.dedicated_metrics_dir {
184 toggles.dedicated_metrics_dir = value;
185 }
186
187 toggles
188 }
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
193pub struct TieredLayoutPaths {
194 pub data_file: PathBuf,
195 pub support_dir: PathBuf,
196 pub wal_file: PathBuf,
197 pub logical_wal_file: PathBuf,
198 pub temp_file: PathBuf,
199 pub snapshot_dir: Option<PathBuf>,
200 pub index_dir: Option<PathBuf>,
201 pub cache_dir: Option<PathBuf>,
202 pub blob_dir: Option<PathBuf>,
203 pub metrics_dir: Option<PathBuf>,
204 pub logs_dir: Option<PathBuf>,
208 pub audit_log_destination: LogDestination,
209 pub slow_log_destination: LogDestination,
210 pub toggles: LayoutToggles,
211}
212
213impl TieredLayoutPaths {
214 pub fn new(
215 data_path: &Path,
216 layout: StorageLayout,
217 overrides: LayoutOverrides,
218 ) -> TieredLayoutPaths {
219 let toggles = layout.expand(&overrides);
220 let data_file = data_path.to_path_buf();
221 let support_dir = sibling_path(data_path, &format!("{}.red", file_name(data_path)));
222
223 let wal_file = if toggles.dedicated_wal_dir {
224 support_dir
225 .join("wal")
226 .join(sidecar_file_name(data_path, "rdb-uwal"))
227 } else {
228 data_path.with_extension("rdb-uwal")
229 };
230 let logical_wal_file = if toggles.dedicated_wal_dir {
231 support_dir
232 .join("wal")
233 .join(format!("{}.logical.wal", file_name(data_path)))
234 } else {
235 sibling_path(data_path, &format!("{}.logical.wal", file_name(data_path)))
236 };
237 let temp_file = if toggles.dedicated_temp_dir {
238 support_dir
239 .join("tmp")
240 .join(sidecar_file_name(data_path, "rdb-tmp"))
241 } else {
242 data_path.with_extension("rdb-tmp")
243 };
244
245 let audit_log_destination = overrides
246 .logs
247 .audit_log
248 .clone()
249 .unwrap_or_else(|| layout.default_audit_log_in(&support_dir));
250 let slow_log_destination = overrides
251 .logs
252 .slow_log
253 .clone()
254 .unwrap_or_else(|| layout.default_slow_log_in(&support_dir));
255 let logs_dir = match (
256 audit_log_destination.file_path(),
257 slow_log_destination.file_path(),
258 ) {
259 (None, None) => None,
260 _ => Some(support_dir.join("logs")),
261 };
262
263 TieredLayoutPaths {
264 data_file,
265 support_dir: support_dir.clone(),
266 wal_file,
267 logical_wal_file,
268 temp_file,
269 snapshot_dir: toggles
270 .dedicated_snapshot_dir
271 .then(|| support_dir.join("snapshots")),
272 index_dir: toggles
273 .dedicated_index_dir
274 .then(|| support_dir.join("indexes")),
275 cache_dir: toggles
276 .dedicated_cache_dir
277 .then(|| support_dir.join("cache")),
278 blob_dir: toggles
279 .dedicated_blob_dir
280 .then(|| support_dir.join("blobs")),
281 metrics_dir: toggles
282 .dedicated_metrics_dir
283 .then(|| support_dir.join("metrics")),
284 logs_dir,
285 audit_log_destination,
286 slow_log_destination,
287 toggles,
288 }
289 }
290
291 pub fn dirs_to_create(&self) -> Vec<PathBuf> {
292 let mut dirs = Vec::new();
293 push_parent(&mut dirs, &self.data_file);
294 push_parent(&mut dirs, &self.wal_file);
295 push_parent(&mut dirs, &self.logical_wal_file);
296 push_parent(&mut dirs, &self.temp_file);
297 push_optional(&mut dirs, self.snapshot_dir.as_ref());
298 push_optional(&mut dirs, self.index_dir.as_ref());
299 push_optional(&mut dirs, self.cache_dir.as_ref());
300 push_optional(&mut dirs, self.blob_dir.as_ref());
301 push_optional(&mut dirs, self.metrics_dir.as_ref());
302 push_optional(&mut dirs, self.logs_dir.as_ref());
303 if let Some(path) = self.audit_log_destination.file_path() {
304 push_parent(&mut dirs, path);
305 }
306 if let Some(path) = self.slow_log_destination.file_path() {
307 push_parent(&mut dirs, path);
308 }
309 dirs.sort();
310 dirs.dedup();
311 dirs
312 }
313
314 pub fn ensure_dirs(&self) -> io::Result<()> {
315 for dir in self.dirs_to_create() {
316 fs::create_dir_all(dir)?;
317 }
318 Ok(())
319 }
320
321 pub fn turbo_snapshot_path(&self, collection: &str) -> Option<PathBuf> {
329 if !self.toggles.dedicated_snapshot_dir {
330 return None;
331 }
332 if let Some(dir) = &self.snapshot_dir {
333 return Some(dir.join(format!("{collection}.tv")));
334 }
335 let stem = file_name(&self.data_file);
336 Some(sibling_path(
337 &self.data_file,
338 &format!("{stem}.{collection}.tv"),
339 ))
340 }
341}
342
343fn file_name(path: &Path) -> String {
344 path.file_name()
345 .and_then(|name| name.to_str())
346 .unwrap_or("data.rdb")
347 .to_string()
348}
349
350fn sibling_path(path: &Path, file_name: &str) -> PathBuf {
351 match path.parent() {
352 Some(parent) if !parent.as_os_str().is_empty() => parent.join(file_name),
353 _ => PathBuf::from(file_name),
354 }
355}
356
357fn sidecar_file_name(path: &Path, extension: &str) -> String {
358 path.with_extension(extension)
359 .file_name()
360 .and_then(|name| name.to_str())
361 .unwrap_or("data.rdb")
362 .to_string()
363}
364
365fn push_parent(dirs: &mut Vec<PathBuf>, path: &Path) {
366 if let Some(parent) = path.parent() {
367 if !parent.as_os_str().is_empty() {
368 dirs.push(parent.to_path_buf());
369 }
370 }
371}
372
373fn push_optional(dirs: &mut Vec<PathBuf>, path: Option<&PathBuf>) {
374 if let Some(path) = path {
375 dirs.push(path.clone());
376 }
377}