1use std::process::Command;
2
3use serde::Deserialize;
4use tokio::sync::oneshot;
5
6#[derive(Debug, Deserialize)]
7#[serde(rename_all = "kebab-case")]
8pub struct Config {
9 pub port: u16,
10 #[serde(skip)]
11 pub port_tx: Option<oneshot::Sender<u16>>,
12 pub replication: Option<ReplicationConfig>,
13 pub storage_path: String,
14}
15
16impl Config {
17 #[must_use]
18 pub fn seal_storage_path(&self) -> String {
19 if self.using_inmemory_storage() {
20 self.storage_path.to_string()
21 } else {
22 let maybe_slash = if self.storage_path.ends_with('/') {
23 ""
24 } else {
25 "/"
26 };
27 format!("{}{maybe_slash}{}", self.storage_path, "seal.db")
28 }
29 }
30
31 #[must_use]
32 pub fn encrypted_storage_path(&self) -> String {
33 if self.using_inmemory_storage() {
34 self.storage_path.to_string()
35 } else {
36 let maybe_slash = if self.storage_path.ends_with('/') {
37 ""
38 } else {
39 "/"
40 };
41 format!("{}{maybe_slash}{}", self.storage_path, "covert.db")
42 }
43 }
44
45 #[must_use]
46 pub fn using_inmemory_storage(&self) -> bool {
47 self.storage_path.contains(":memory:")
48 }
49
50 pub fn sanitize(&self) -> anyhow::Result<()> {
51 if self.replication.is_some() {
52 if self.using_inmemory_storage() {
53 return Err(anyhow::Error::msg(
54 "Replication is not supported for inmemory storage",
55 ));
56 }
57
58 let cmd = Command::new("litestream").arg("version").status();
60 match cmd {
61 Ok(s) if s.success() => (),
62 _ => {
63 return Err(anyhow::Error::msg(
64 "Litestream command not found, can not perform replication",
65 ));
66 }
67 }
68 }
69
70 if !self.using_inmemory_storage() {
71 let storage_path = std::path::Path::new(&self.storage_path);
72 if !storage_path.exists()
73 && std::fs::DirBuilder::new()
74 .recursive(true)
75 .create(storage_path)
76 .is_err()
77 {
78 return Err(anyhow::Error::msg("Failed to create storage directory"));
79 }
80
81 if !storage_path.is_dir() {
82 return Err(anyhow::Error::msg(
83 "The storage path provided is not a directory",
84 ));
85 }
86 }
87
88 Ok(())
89 }
90}
91
92#[derive(Debug, Deserialize, Clone, PartialEq, Eq)]
93#[serde(rename_all = "kebab-case")]
94pub struct ReplicationConfig {
95 pub access_key_id: String,
96 pub secret_access_key: String,
97 pub bucket_url: String,
99}
100
101impl ReplicationConfig {
102 #[must_use]
103 pub fn seal_bucket_url(&self) -> String {
104 let maybe_slash = if self.bucket_url.ends_with('/') {
105 ""
106 } else {
107 "/"
108 };
109 format!("{}{maybe_slash}{}", self.bucket_url, "seal.db")
110 }
111
112 #[must_use]
113 pub fn encrypted_bucket_url(&self) -> String {
114 let maybe_slash = if self.bucket_url.ends_with('/') {
115 ""
116 } else {
117 "/"
118 };
119 format!("{}{maybe_slash}{}", self.bucket_url, "covert.db")
120 }
121}