1use std::{fs, path::PathBuf};
2
3use common::prelude::SecretKey;
4use serde::{Deserialize, Serialize};
5
6pub const APP_NAME: &str = "jax";
7pub const CONFIG_FILE_NAME: &str = "config.toml";
8pub const DB_FILE_NAME: &str = "db.sqlite";
9pub const KEY_FILE_NAME: &str = "key.pem";
10pub const BLOBS_DIR_NAME: &str = "blobs";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AppConfig {
14 #[serde(default = "default_api_port")]
16 pub api_port: u16,
17 #[serde(default = "default_gateway_port")]
19 pub gateway_port: u16,
20 #[serde(default)]
22 pub peer_port: Option<u16>,
23 #[serde(default)]
25 pub blob_store: BlobStoreConfig,
26 #[serde(default = "default_max_import_size")]
28 pub max_import_size: u64,
29}
30
31fn default_api_port() -> u16 {
32 5001
33}
34
35fn default_gateway_port() -> u16 {
36 8080
37}
38
39fn default_max_import_size() -> u64 {
40 object_store::DEFAULT_MAX_IMPORT_SIZE
41}
42
43impl Default for AppConfig {
44 fn default() -> Self {
45 Self {
46 api_port: default_api_port(),
47 gateway_port: default_gateway_port(),
48 peer_port: None,
49 blob_store: BlobStoreConfig::default(),
50 max_import_size: default_max_import_size(),
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
58#[serde(tag = "type", rename_all = "snake_case")]
59pub enum BlobStoreConfig {
60 #[default]
62 Legacy,
63
64 Filesystem {
66 path: PathBuf,
68 #[serde(default)]
70 db_path: Option<PathBuf>,
71 },
72
73 S3 {
75 url: String,
78 },
79}
80
81#[derive(Debug, Clone)]
83pub struct S3Config {
84 pub endpoint: String,
85 pub access_key: String,
86 pub secret_key: String,
87 pub bucket: String,
88}
89
90impl BlobStoreConfig {
91 pub fn parse_s3_url(url: &str) -> Result<S3Config, StateError> {
94 let url = url
96 .strip_prefix("s3://")
97 .ok_or_else(|| StateError::InvalidS3Url("URL must start with s3://".to_string()))?;
98
99 let (creds, rest) = url
101 .split_once('@')
102 .ok_or_else(|| StateError::InvalidS3Url("Missing @ separator".to_string()))?;
103
104 let (access_key, secret_key) = creds
106 .split_once(':')
107 .ok_or_else(|| StateError::InvalidS3Url("Missing : in credentials".to_string()))?;
108
109 let (endpoint, bucket) = rest
111 .split_once('/')
112 .ok_or_else(|| StateError::InvalidS3Url("Missing / before bucket".to_string()))?;
113
114 if bucket.is_empty() {
115 return Err(StateError::InvalidS3Url("Bucket name is empty".to_string()));
116 }
117
118 let protocol = if endpoint.contains("localhost") || endpoint.contains("127.0.0.1") {
120 "http"
121 } else {
122 "https"
123 };
124
125 Ok(S3Config {
126 endpoint: format!("{}://{}", protocol, endpoint),
127 access_key: access_key.to_string(),
128 secret_key: secret_key.to_string(),
129 bucket: bucket.to_string(),
130 })
131 }
132}
133
134#[derive(Debug, Clone)]
135pub struct AppState {
136 pub jax_dir: PathBuf,
138 pub db_path: PathBuf,
140 pub key_path: PathBuf,
142 pub blobs_path: PathBuf,
144 pub config_path: PathBuf,
146 pub config: AppConfig,
148}
149
150impl AppState {
151 pub fn jax_dir(custom_path: Option<PathBuf>) -> Result<PathBuf, StateError> {
153 if let Some(path) = custom_path {
154 return Ok(path);
155 }
156
157 let home = dirs::home_dir().ok_or(StateError::NoHomeDirectory)?;
159 Ok(home.join(format!(".{}", APP_NAME)))
160 }
161
162 #[allow(dead_code)]
164 pub fn exists(custom_path: Option<PathBuf>) -> Result<bool, StateError> {
165 let jax_dir = Self::jax_dir(custom_path)?;
166 Ok(jax_dir.exists())
167 }
168
169 pub fn init(
171 custom_path: Option<PathBuf>,
172 config: Option<AppConfig>,
173 ) -> Result<Self, StateError> {
174 let jax_dir = Self::jax_dir(custom_path)?;
175
176 if jax_dir.exists() {
178 return Err(StateError::AlreadyInitialized);
179 }
180
181 fs::create_dir_all(&jax_dir)?;
182
183 let blobs_path = jax_dir.join(BLOBS_DIR_NAME);
185 fs::create_dir_all(&blobs_path)?;
186
187 let key = SecretKey::generate();
189 let key_path = jax_dir.join(KEY_FILE_NAME);
190 fs::write(&key_path, key.to_pem())?;
191
192 let config = config.unwrap_or_default();
194 let config_path = jax_dir.join(CONFIG_FILE_NAME);
195 let config_toml = toml::to_string_pretty(&config)?;
196 fs::write(&config_path, config_toml)?;
197
198 let db_path = jax_dir.join(DB_FILE_NAME);
200 fs::write(&db_path, "")?;
201
202 Ok(Self {
203 jax_dir,
204 db_path,
205 key_path,
206 blobs_path,
207 config_path,
208 config,
209 })
210 }
211
212 pub fn load(custom_path: Option<PathBuf>) -> Result<Self, StateError> {
214 let jax_dir = Self::jax_dir(custom_path)?;
215
216 if !jax_dir.exists() {
217 return Err(StateError::NotInitialized);
218 }
219
220 let db_path = jax_dir.join(DB_FILE_NAME);
222 let key_path = jax_dir.join(KEY_FILE_NAME);
223 let blobs_path = jax_dir.join(BLOBS_DIR_NAME);
224 let config_path = jax_dir.join(CONFIG_FILE_NAME);
225
226 if !db_path.exists() {
228 return Err(StateError::MissingFile("db.sqlite".to_string()));
229 }
230 if !key_path.exists() {
231 return Err(StateError::MissingFile("key.pem".to_string()));
232 }
233 if !blobs_path.exists() {
234 return Err(StateError::MissingFile("blobs/".to_string()));
235 }
236 if !config_path.exists() {
237 return Err(StateError::MissingFile("config.toml".to_string()));
238 }
239
240 let config_toml = fs::read_to_string(&config_path)?;
242 let config: AppConfig = toml::from_str(&config_toml)?;
243
244 Ok(Self {
245 jax_dir,
246 db_path,
247 key_path,
248 blobs_path,
249 config_path,
250 config,
251 })
252 }
253
254 pub fn load_key(&self) -> Result<SecretKey, StateError> {
256 let pem = fs::read_to_string(&self.key_path)?;
257 let key = SecretKey::from_pem(&pem).map_err(|e| StateError::InvalidKey(e.to_string()))?;
258 Ok(key)
259 }
260}
261
262#[derive(Debug, thiserror::Error)]
263pub enum StateError {
264 #[error("jax directory not initialized. Run 'cli init' first")]
265 NotInitialized,
266
267 #[error("jax directory already initialized")]
268 AlreadyInitialized,
269
270 #[error("no home directory found")]
271 NoHomeDirectory,
272
273 #[error("missing required file: {0}")]
274 MissingFile(String),
275
276 #[error("invalid key: {0}")]
277 InvalidKey(String),
278
279 #[error("invalid S3 URL: {0}")]
280 InvalidS3Url(String),
281
282 #[error("IO error: {0}")]
283 Io(#[from] std::io::Error),
284
285 #[error("TOML serialization error: {0}")]
286 TomlSer(#[from] toml::ser::Error),
287
288 #[error("TOML deserialization error: {0}")]
289 TomlDe(#[from] toml::de::Error),
290}