1use crate::{constants::PREFERENCES_FILE, AccountId, Result};
4
5#[cfg(not(target_arch = "wasm32"))]
6use etcetera::{
7 app_strategy::choose_native_strategy, AppStrategy, AppStrategyArgs,
8};
9
10use crate::{
11 constants::{
12 ACCOUNT_EVENTS, APP_AUTHOR, APP_NAME, AUDIT_FILE_NAME, BLOBS_DIR,
13 DATABASE_FILE, DEVICE_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR,
14 FILE_EVENTS, IDENTITY_DIR, JSON_EXT, LOCAL_DIR, LOGS_DIR,
15 REMOTES_FILE, REMOTE_DIR, SYSTEM_MESSAGES_FILE, VAULTS_DIR,
16 VAULT_EXT,
17 },
18 SecretId, VaultId,
19};
20use once_cell::sync::Lazy;
21use serde::{Deserialize, Serialize};
22use std::{
23 path::{Path, PathBuf},
24 sync::RwLock,
25};
26
27use sos_vfs as vfs;
28
29static DATA_DIR: Lazy<RwLock<Option<PathBuf>>> =
30 Lazy::new(|| RwLock::new(None));
31
32#[derive(Default, Debug, Clone, Serialize, Deserialize)]
45pub struct Paths {
46 server: bool,
48
49 user_id: String,
51 documents_dir: PathBuf,
53
54 identity_dir: PathBuf,
56 local_dir: PathBuf,
58 database_file: PathBuf,
60 logs_dir: PathBuf,
62 audit_file: PathBuf,
64 user_dir: PathBuf,
66 blobs_dir: PathBuf,
68 files_dir: PathBuf,
70 vaults_dir: PathBuf,
72 device_file: PathBuf,
74}
75
76impl Paths {
77 pub fn new(
79 documents_dir: impl AsRef<Path>,
80 user_id: impl AsRef<str>,
81 ) -> Self {
82 Self::new_with_prefix(false, documents_dir, user_id, LOCAL_DIR)
83 }
84
85 pub fn new_server(
87 documents_dir: impl AsRef<Path>,
88 user_id: impl AsRef<str>,
89 ) -> Self {
90 Self::new_with_prefix(true, documents_dir, user_id, REMOTE_DIR)
91 }
92
93 pub fn new_global(documents_dir: impl AsRef<Path>) -> Self {
98 Self::new(documents_dir, "")
99 }
100
101 pub fn new_global_server(documents_dir: impl AsRef<Path>) -> Self {
106 Self::new_server(documents_dir, "")
107 }
108
109 fn new_with_prefix(
110 server: bool,
111 documents_dir: impl AsRef<Path>,
112 user_id: impl AsRef<str>,
113 prefix: impl AsRef<Path>,
114 ) -> Self {
115 let documents_dir = documents_dir.as_ref().to_path_buf();
116 let local_dir = documents_dir.join(prefix);
117 let logs_dir = documents_dir.join(LOGS_DIR);
118 let identity_dir = documents_dir.join(IDENTITY_DIR);
119 let audit_file = local_dir.join(AUDIT_FILE_NAME);
120 let user_dir = local_dir.join(user_id.as_ref());
121
122 let files_dir = user_dir.join(FILES_DIR);
123 let vaults_dir = user_dir.join(VAULTS_DIR);
124 let device_file =
125 user_dir.join(format!("{}.{}", DEVICE_FILE, VAULT_EXT));
126
127 let blobs_dir = documents_dir.join(BLOBS_DIR);
129 let database_file = documents_dir.join(DATABASE_FILE);
130
131 Self {
132 server,
133 user_id: user_id.as_ref().to_owned(),
134 documents_dir,
135 database_file,
136 blobs_dir,
137 logs_dir,
138
139 identity_dir,
140 local_dir,
141 audit_file,
142 user_dir,
143 files_dir,
144 vaults_dir,
145 device_file,
146 }
147 }
148
149 pub fn is_server(&self) -> bool {
151 self.server
152 }
153
154 pub fn with_account_id(&self, account_id: &AccountId) -> Self {
156 if self.server {
157 Self::new_server(&self.documents_dir, account_id.to_string())
158 } else {
159 Self::new(&self.documents_dir, account_id.to_string())
160 }
161 }
162
163 pub async fn ensure(&self) -> Result<()> {
168 vfs::create_dir_all(&self.local_dir).await?;
170
171 vfs::create_dir_all(&self.blobs_dir).await?;
173
174 if !self.is_global() {
175 vfs::create_dir_all(&self.user_dir).await?;
177 vfs::create_dir_all(&self.files_dir).await?;
178 vfs::create_dir_all(&self.vaults_dir).await?;
179
180 vfs::create_dir_all(self.blobs_dir.join(self.user_id())).await?;
182 }
183 Ok(())
184 }
185
186 pub async fn ensure_db(&self) -> Result<()> {
189 vfs::create_dir_all(&self.blobs_dir).await?;
191 if !self.is_global() {
192 vfs::create_dir_all(self.blobs_account_dir()).await?;
194 }
195 Ok(())
196 }
197
198 pub fn is_using_db(&self) -> bool {
200 self.database_file().exists()
201 }
202
203 pub async fn is_usable(&self) -> Result<bool> {
206 if self.is_global() {
207 panic!("is_usable is not accessible for global paths");
208 }
209
210 let identity_vault = self.identity_vault();
211 let identity_events = self.identity_events();
212 let account_events = self.account_events();
213 let device_events = self.device_events();
214
215 Ok(vfs::try_exists(identity_vault).await?
216 && vfs::try_exists(identity_events).await?
217 && vfs::try_exists(account_events).await?
218 && vfs::try_exists(device_events).await?)
219 }
220
221 pub fn database_file(&self) -> &PathBuf {
227 &self.database_file
228 }
229
230 pub fn blobs_dir(&self) -> &PathBuf {
232 &self.blobs_dir
233 }
234
235 pub fn blobs_account_dir(&self) -> PathBuf {
242 if self.is_global() {
243 panic!(
244 "blobs account directory is not accessible for global paths"
245 );
246 }
247 self.blobs_dir().join(self.user_id())
248 }
249
250 pub fn blob_folder_location(&self, vault_id: &VaultId) -> PathBuf {
257 self.blobs_account_dir().join(vault_id.to_string())
258 }
259
260 pub fn blob_location(
266 &self,
267 vault_id: &VaultId,
268 secret_id: &SecretId,
269 file_name: impl AsRef<str>,
270 ) -> PathBuf {
271 self.blob_folder_location(vault_id)
272 .join(secret_id.to_string())
273 .join(file_name.as_ref())
274 }
275
276 pub fn user_id(&self) -> &str {
278 &self.user_id
279 }
280
281 pub fn documents_dir(&self) -> &PathBuf {
283 &self.documents_dir
284 }
285
286 pub fn is_global(&self) -> bool {
291 self.user_id.is_empty()
292 }
293
294 pub fn identity_dir(&self) -> &PathBuf {
296 &self.identity_dir
297 }
298
299 pub fn local_dir(&self) -> &PathBuf {
301 &self.local_dir
302 }
303
304 pub fn logs_dir(&self) -> &PathBuf {
306 &self.logs_dir
307 }
308
309 pub fn audit_file(&self) -> &PathBuf {
311 &self.audit_file
312 }
313
314 pub fn preferences_file(&self) -> PathBuf {
317 let mut path = if self.is_global() {
318 self.documents_dir().join(PREFERENCES_FILE)
319 } else {
320 self.user_dir().join(PREFERENCES_FILE)
321 };
322 path.set_extension(JSON_EXT);
323 path
324 }
325
326 pub fn system_messages_file(&self) -> PathBuf {
332 if self.is_global() {
333 panic!("system messages are not accessible for global paths");
334 }
335 let mut path = self.user_dir().join(SYSTEM_MESSAGES_FILE);
336 path.set_extension(JSON_EXT);
337 path
338 }
339
340 pub fn user_dir(&self) -> &PathBuf {
346 if self.is_global() {
347 panic!("user directory is not accessible for global paths");
348 }
349 &self.user_dir
350 }
351
352 pub fn files_dir(&self) -> &PathBuf {
358 if self.is_global() {
359 panic!("files directory is not accessible for global paths");
360 }
361 &self.files_dir
362 }
363
364 pub fn file_folder_location(&self, vault_id: &VaultId) -> PathBuf {
371 self.files_dir().join(vault_id.to_string())
372 }
373
374 pub fn file_location(
380 &self,
381 vault_id: &VaultId,
382 secret_id: &SecretId,
383 file_name: impl AsRef<str>,
384 ) -> PathBuf {
385 self.file_folder_location(vault_id)
386 .join(secret_id.to_string())
387 .join(file_name.as_ref())
388 }
389
390 pub fn vaults_dir(&self) -> &PathBuf {
396 if self.is_global() {
397 panic!("vaults directory is not accessible for global paths");
398 }
399 &self.vaults_dir
400 }
401
402 pub fn device_file(&self) -> &PathBuf {
408 if self.is_global() {
409 panic!("devices file is not accessible for global paths");
410 }
411 &self.device_file
412 }
413
414 pub fn identity_vault(&self) -> PathBuf {
420 if self.is_global() {
421 panic!("identity vault is not accessible for global paths");
422 }
423 let mut identity_vault_file = self.identity_dir.join(&self.user_id);
424 identity_vault_file.set_extension(VAULT_EXT);
425 identity_vault_file
426 }
427
428 pub fn identity_events(&self) -> PathBuf {
434 let mut events_path = self.identity_vault();
435 events_path.set_extension(EVENT_LOG_EXT);
436 events_path
437 }
438
439 pub fn vault_path(&self, id: &VaultId) -> PathBuf {
445 if self.is_global() {
446 panic!("vault path is not accessible for global paths");
447 }
448 let mut vault_path = self.vaults_dir.join(id.to_string());
449 vault_path.set_extension(VAULT_EXT);
450 vault_path
451 }
452
453 pub fn event_log_path(&self, id: &VaultId) -> PathBuf {
459 if self.is_global() {
460 panic!("event log path is not accessible for global paths");
461 }
462 let mut vault_path = self.vaults_dir.join(id.to_string());
463 vault_path.set_extension(EVENT_LOG_EXT);
464 vault_path
465 }
466
467 pub fn account_events(&self) -> PathBuf {
473 if self.is_global() {
474 panic!("account events are not accessible for global paths");
475 }
476 let mut vault_path = self.user_dir.join(ACCOUNT_EVENTS);
477 vault_path.set_extension(EVENT_LOG_EXT);
478 vault_path
479 }
480
481 pub fn device_events(&self) -> PathBuf {
487 if self.is_global() {
488 panic!("device events are not accessible for global paths");
489 }
490 let mut vault_path = self.user_dir.join(DEVICE_EVENTS);
491 vault_path.set_extension(EVENT_LOG_EXT);
492 vault_path
493 }
494
495 pub fn file_events(&self) -> PathBuf {
501 if self.is_global() {
502 panic!("file events are not accessible for global paths");
503 }
504 let mut vault_path = self.user_dir.join(FILE_EVENTS);
505 vault_path.set_extension(EVENT_LOG_EXT);
506 vault_path
507 }
508
509 pub fn remote_origins(&self) -> PathBuf {
515 if self.is_global() {
516 panic!("remote origins are not accessible for global paths");
517 }
518 let mut vault_path = self.user_dir.join(REMOTES_FILE);
519 vault_path.set_extension(JSON_EXT);
520 vault_path
521 }
522
523 pub async fn scaffold(data_dir: Option<PathBuf>) -> Result<()> {
525 let data_dir = if let Some(data_dir) = data_dir {
526 data_dir
527 } else {
528 Paths::data_dir()?
529 };
530 let paths = Self::new_global(data_dir);
531 vfs::create_dir_all(paths.documents_dir()).await?;
532 vfs::create_dir_all(paths.identity_dir()).await?;
533 vfs::create_dir_all(paths.logs_dir()).await?;
534 Ok(())
535 }
536
537 pub async fn scaffold_db(data_dir: Option<PathBuf>) -> Result<()> {
539 let data_dir = if let Some(data_dir) = data_dir {
540 data_dir
541 } else {
542 Paths::data_dir()?
543 };
544 let paths = Self::new_global(data_dir);
545 vfs::create_dir_all(paths.documents_dir()).await?;
546 vfs::create_dir_all(paths.logs_dir()).await?;
547 Ok(())
548 }
549
550 pub fn set_data_dir(path: PathBuf) {
553 let mut writer = DATA_DIR.write().unwrap();
554 *writer = Some(path);
555 }
556
557 pub fn clear_data_dir() {
559 let mut writer = DATA_DIR.write().unwrap();
560 *writer = None;
561 }
562
563 pub fn data_dir() -> Result<PathBuf> {
581 let dir = if let Ok(env_data_dir) = std::env::var("SOS_DATA_DIR") {
582 Ok(PathBuf::from(env_data_dir))
583 } else {
584 let reader = DATA_DIR.read().unwrap();
585 if let Some(explicit) = reader.as_ref() {
586 Ok(explicit.to_path_buf())
587 } else {
588 default_storage_dir()
589 }
590 };
591
592 let has_explicit_env = std::env::var("SOS_DATA_DIR").ok().is_some();
593 if cfg!(debug_assertions) && !has_explicit_env {
594 if !cfg!(test) {
599 let sub_dir = if std::env::var("SOS_TEST").is_ok() {
600 "test"
601 } else {
602 "debug"
603 };
604 dir.map(|dir| dir.join(sub_dir))
605 } else {
606 dir
607 }
608 } else {
609 dir
610 }
611 }
612}
613
614#[cfg(target_os = "android")]
615fn default_storage_dir() -> Result<PathBuf> {
616 Ok(PathBuf::from(""))
617}
618
619#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
620fn default_storage_dir() -> Result<PathBuf> {
621 let strategy = choose_native_strategy(AppStrategyArgs {
622 top_level_domain: "com".to_string(),
623 author: APP_AUTHOR.to_string(),
624 app_name: APP_NAME.to_string(),
625 })
626 .map_err(Box::from)?;
627
628 #[cfg(not(windows))]
629 {
630 let mut path = strategy.data_dir();
631 path.set_file_name(APP_AUTHOR);
632 Ok(path)
633 }
634 #[cfg(windows)]
635 {
636 let mut path = strategy.cache_dir();
637 path.pop();
638 Ok(path)
639 }
640}
641
642#[cfg(target_arch = "wasm32")]
643fn default_storage_dir() -> Result<PathBuf> {
644 Ok(PathBuf::from("/"))
645}