1use crate::{
3 constants::{
4 ACCOUNT_EVENTS, APP_AUTHOR, APP_NAME, AUDIT_FILE_NAME, BLOBS_DIR,
5 DATABASE_FILE, DEVICE_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR,
6 FILE_EVENTS, IDENTITY_DIR, JSON_EXT, LOCAL_DIR, LOGS_DIR,
7 PREFERENCES_FILE, REMOTES_FILE, REMOTE_DIR, SYSTEM_MESSAGES_FILE,
8 VAULTS_DIR, VAULT_EXT,
9 },
10 AccountId, ExternalFile, ExternalFileName, Result, SecretId, VaultId,
11};
12#[cfg(not(target_arch = "wasm32"))]
13use etcetera::{
14 app_strategy::choose_native_strategy, AppStrategy, AppStrategyArgs,
15};
16use serde::{Deserialize, Serialize};
17use sos_vfs as vfs;
18use std::{
19 path::{Path, PathBuf},
20 sync::{Arc, OnceLock},
21};
22
23static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
24
25#[derive(Default, Debug, Clone, Serialize, Deserialize)]
38pub struct Paths {
39 server: bool,
41 account_id: Option<AccountId>,
43 documents_dir: PathBuf,
45 logs_dir: PathBuf,
47
48 identity_dir: PathBuf,
53 local_dir: PathBuf,
55 audit_file: PathBuf,
57 user_dir: PathBuf,
59 files_dir: PathBuf,
61 vaults_dir: PathBuf,
63 device_file: PathBuf,
65
66 database_file: PathBuf,
71 blobs_dir: PathBuf,
73}
74
75impl Paths {
76 pub fn new_client(documents_dir: impl AsRef<Path>) -> Arc<Self> {
78 Arc::new(Self::new_with_prefix(false, documents_dir, None, LOCAL_DIR))
79 }
80
81 pub fn new_server(documents_dir: impl AsRef<Path>) -> Arc<Self> {
83 Arc::new(Self::new_with_prefix(true, documents_dir, None, REMOTE_DIR))
84 }
85
86 fn new_with_prefix(
87 server: bool,
88 documents_dir: impl AsRef<Path>,
89 account_id: Option<&AccountId>,
90 prefix: impl AsRef<Path>,
91 ) -> Self {
92 let documents_dir = documents_dir.as_ref().to_path_buf();
93 let local_dir = documents_dir.join(prefix);
94 let logs_dir = documents_dir.join(LOGS_DIR);
95 let identity_dir = documents_dir.join(IDENTITY_DIR);
96 let audit_file = local_dir.join(AUDIT_FILE_NAME);
97 let user_dir = local_dir
98 .join(account_id.map(|id| id.to_string()).unwrap_or_default());
99
100 let files_dir = user_dir.join(FILES_DIR);
101 let vaults_dir = user_dir.join(VAULTS_DIR);
102 let device_file =
103 user_dir.join(format!("{}.{}", DEVICE_FILE, VAULT_EXT));
104
105 let blobs_dir = documents_dir.join(BLOBS_DIR);
107 let database_file = documents_dir.join(DATABASE_FILE);
108
109 Self {
110 server,
111 account_id: account_id.cloned(),
112 documents_dir,
113 database_file,
114 blobs_dir,
115 logs_dir,
116
117 identity_dir,
118 local_dir,
119 audit_file,
120 user_dir,
121 files_dir,
122 vaults_dir,
123 device_file,
124 }
125 }
126
127 pub fn is_server(&self) -> bool {
129 self.server
130 }
131
132 pub fn with_account_id(&self, account_id: &AccountId) -> Arc<Self> {
134 Arc::new(Self::new_with_prefix(
135 self.server,
136 &self.documents_dir,
137 Some(account_id),
138 if self.server { REMOTE_DIR } else { LOCAL_DIR },
139 ))
140 }
141
142 pub async fn ensure(&self) -> Result<()> {
147 vfs::create_dir_all(&self.local_dir).await?;
149
150 if !self.is_global() {
151 vfs::create_dir_all(&self.user_dir).await?;
153 vfs::create_dir_all(&self.files_dir).await?;
154 vfs::create_dir_all(&self.vaults_dir).await?;
155 }
156 Ok(())
157 }
158
159 pub fn is_using_db(&self) -> bool {
161 self.database_file().exists()
162 }
163
164 pub async fn is_usable(&self) -> Result<bool> {
167 assert!(!self.is_global(), "not accessible for global paths");
168 if self.is_using_db() {
169 Ok(vfs::try_exists(self.database_file()).await?)
170 } else {
171 Ok(vfs::try_exists(self.identity_vault()).await?
172 && vfs::try_exists(self.identity_events()).await?
173 && vfs::try_exists(self.account_events()).await?
174 && vfs::try_exists(self.device_events()).await?)
175 }
176 }
177
178 pub fn into_files_dir(&self) -> PathBuf {
185 assert!(!self.is_global(), "not accessible for global paths");
186 if self.is_using_db() {
187 self.blobs_account_dir()
188 } else {
189 self.files_dir().to_owned()
190 }
191 }
192
193 pub fn into_file_folder_path(&self, folder_id: &VaultId) -> PathBuf {
200 self.into_files_dir().join(folder_id.to_string())
201 }
202
203 pub fn into_file_secret_path(
210 &self,
211 folder_id: &VaultId,
212 secret_id: &SecretId,
213 ) -> PathBuf {
214 self.into_file_folder_path(folder_id)
215 .join(secret_id.to_string())
216 }
217
218 pub fn into_file_path(&self, file: &ExternalFile) -> PathBuf {
224 self.into_file_path_parts(
225 file.vault_id(),
226 file.secret_id(),
227 file.file_name(),
228 )
229 }
230
231 pub fn into_file_path_parts(
237 &self,
238 folder_id: &VaultId,
239 secret_id: &SecretId,
240 file_name: &ExternalFileName,
241 ) -> PathBuf {
242 self.into_file_secret_path(folder_id, secret_id)
243 .join(file_name.to_string())
244 }
245
246 pub fn database_file(&self) -> &PathBuf {
248 &self.database_file
249 }
250
251 #[doc(hidden)]
253 #[cfg(debug_assertions)]
254 pub fn blobs_dir(&self) -> &PathBuf {
255 &self.blobs_dir
256 }
257
258 fn blobs_account_dir(&self) -> PathBuf {
265 if self.is_global() {
266 panic!(
267 "blobs account directory is not accessible for global paths"
268 );
269 }
270 self.blobs_dir
271 .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap())
272 }
273
274 pub fn into_blob_file_path(
280 &self,
281 vault_id: &VaultId,
282 secret_id: &SecretId,
283 file_name: impl AsRef<str>,
284 ) -> PathBuf {
285 self.blobs_account_dir()
286 .join(vault_id.to_string())
287 .join(secret_id.to_string())
288 .join(file_name.as_ref())
289 }
290
291 pub fn account_id(&self) -> Option<&AccountId> {
293 self.account_id.as_ref()
294 }
295
296 pub fn documents_dir(&self) -> &PathBuf {
298 &self.documents_dir
299 }
300
301 pub fn is_global(&self) -> bool {
306 self.account_id.is_none()
307 }
308
309 pub fn identity_dir(&self) -> &PathBuf {
311 &self.identity_dir
312 }
313
314 pub fn local_dir(&self) -> &PathBuf {
316 &self.local_dir
317 }
318
319 pub fn logs_dir(&self) -> &PathBuf {
321 &self.logs_dir
322 }
323
324 pub fn audit_file(&self) -> &PathBuf {
326 &self.audit_file
327 }
328
329 pub fn preferences_file(&self) -> PathBuf {
332 let mut path = if self.is_global() {
333 self.documents_dir().join(PREFERENCES_FILE)
334 } else {
335 self.user_dir().join(PREFERENCES_FILE)
336 };
337 path.set_extension(JSON_EXT);
338 path
339 }
340
341 pub fn global_preferences_file(&self) -> PathBuf {
343 let mut path = self.documents_dir().join(PREFERENCES_FILE);
344 path.set_extension(JSON_EXT);
345 path
346 }
347
348 pub fn system_messages_file(&self) -> PathBuf {
354 assert!(!self.is_global(), "not accessible for global paths");
355 let mut path = self.user_dir().join(SYSTEM_MESSAGES_FILE);
356 path.set_extension(JSON_EXT);
357 path
358 }
359
360 pub fn user_dir(&self) -> &PathBuf {
366 assert!(!self.is_global(), "not accessible for global paths");
367 &self.user_dir
368 }
369
370 pub fn files_dir(&self) -> &PathBuf {
376 assert!(!self.is_global(), "not accessible for global paths");
377 &self.files_dir
378 }
379
380 pub fn into_legacy_file_path(
386 &self,
387 vault_id: &VaultId,
388 secret_id: &SecretId,
389 file_name: impl AsRef<str>,
390 ) -> PathBuf {
391 self.files_dir()
392 .join(vault_id.to_string())
393 .join(secret_id.to_string())
394 .join(file_name.as_ref())
395 }
396
397 pub fn vaults_dir(&self) -> &PathBuf {
403 assert!(!self.is_global(), "not accessible for global paths");
404 &self.vaults_dir
405 }
406
407 pub fn device_file(&self) -> &PathBuf {
413 assert!(!self.is_global(), "not accessible for global paths");
414 &self.device_file
415 }
416
417 pub fn identity_vault(&self) -> PathBuf {
423 assert!(!self.is_global(), "not accessible for global paths");
424 let mut identity_vault_file = self
425 .identity_dir
426 .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap());
427 identity_vault_file.set_extension(VAULT_EXT);
428 identity_vault_file
429 }
430
431 pub fn identity_events(&self) -> PathBuf {
437 let mut events_path = self.identity_vault();
438 events_path.set_extension(EVENT_LOG_EXT);
439 events_path
440 }
441
442 pub fn vault_path(&self, id: &VaultId) -> PathBuf {
448 assert!(!self.is_global(), "not accessible for global paths");
449 let mut vault_path = self.vaults_dir.join(id.to_string());
450 vault_path.set_extension(VAULT_EXT);
451 vault_path
452 }
453
454 pub fn event_log_path(&self, id: &VaultId) -> PathBuf {
460 assert!(!self.is_global(), "not accessible for global paths");
461 let mut vault_path = self.vaults_dir.join(id.to_string());
462 vault_path.set_extension(EVENT_LOG_EXT);
463 vault_path
464 }
465
466 pub fn account_events(&self) -> PathBuf {
472 assert!(!self.is_global(), "not accessible for global paths");
473 let mut vault_path = self.user_dir.join(ACCOUNT_EVENTS);
474 vault_path.set_extension(EVENT_LOG_EXT);
475 vault_path
476 }
477
478 pub fn device_events(&self) -> PathBuf {
484 assert!(!self.is_global(), "not accessible for global paths");
485 let mut vault_path = self.user_dir.join(DEVICE_EVENTS);
486 vault_path.set_extension(EVENT_LOG_EXT);
487 vault_path
488 }
489
490 pub fn file_events(&self) -> PathBuf {
496 assert!(!self.is_global(), "not accessible for global paths");
497 let mut vault_path = self.user_dir.join(FILE_EVENTS);
498 vault_path.set_extension(EVENT_LOG_EXT);
499 vault_path
500 }
501
502 pub fn remote_origins(&self) -> PathBuf {
508 assert!(!self.is_global(), "not accessible for global paths");
509 let mut vault_path = self.user_dir.join(REMOTES_FILE);
510 vault_path.set_extension(JSON_EXT);
511 vault_path
512 }
513
514 pub async fn scaffold(
516 data_dir: impl Into<Option<&PathBuf>>,
517 ) -> Result<()> {
518 let data_dir = if let Some(data_dir) = data_dir.into() {
519 data_dir.to_owned()
520 } else {
521 Paths::data_dir()?
522 };
523
524 let paths = Self::new_client(data_dir);
525 vfs::create_dir_all(paths.documents_dir()).await?;
526 if !paths.is_using_db() {
527 vfs::create_dir_all(paths.identity_dir()).await?;
528 }
529 vfs::create_dir_all(paths.logs_dir()).await?;
530 Ok(())
531 }
532
533 pub fn set_data_dir(path: PathBuf) {
536 DATA_DIR.get_or_init(|| path);
537 }
538
539 pub fn data_dir() -> Result<PathBuf> {
557 let dir = if let Ok(env_data_dir) = std::env::var("SOS_DATA_DIR") {
558 Ok(PathBuf::from(env_data_dir))
559 } else {
560 if let Some(data_dir) = DATA_DIR.get() {
561 Ok(data_dir.to_owned())
562 } else {
563 default_storage_dir()
564 }
565 };
566
567 let has_explicit_env = std::env::var("SOS_DATA_DIR").ok().is_some();
568 if cfg!(debug_assertions) && !has_explicit_env {
569 if !cfg!(test) {
574 let sub_dir = if std::env::var("SOS_TEST").is_ok() {
575 "test"
576 } else {
577 "debug"
578 };
579 dir.map(|dir| dir.join(sub_dir))
580 } else {
581 dir
582 }
583 } else {
584 dir
585 }
586 }
587}
588
589#[cfg(target_os = "android")]
590fn default_storage_dir() -> Result<PathBuf> {
591 Ok(PathBuf::from(""))
592}
593
594#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
595fn default_storage_dir() -> Result<PathBuf> {
596 let strategy = choose_native_strategy(AppStrategyArgs {
597 top_level_domain: "com".to_string(),
598 author: APP_AUTHOR.to_string(),
599 app_name: APP_NAME.to_string(),
600 })
601 .map_err(Box::from)?;
602
603 #[cfg(not(windows))]
604 {
605 let mut path = strategy.data_dir();
606 path.set_file_name(APP_AUTHOR);
607 Ok(path)
608 }
609 #[cfg(windows)]
610 {
611 let mut path = strategy.cache_dir();
612 path.pop();
614 Ok(path)
615 }
616}
617
618#[cfg(target_arch = "wasm32")]
619fn default_storage_dir() -> Result<PathBuf> {
620 Ok(PathBuf::from("/"))
621}