1use crate::{
4 constants::{
5 ACCOUNT_EVENTS, APP_AUTHOR, APP_NAME, AUDIT_FILE_NAME, BLOBS_DIR,
6 DATABASE_FILE, DEVICE_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR,
7 FILE_EVENTS, IDENTITY_DIR, JSON_EXT, LOCAL_DIR, LOGS_DIR,
8 PREFERENCES_FILE, REMOTES_FILE, REMOTE_DIR, SYSTEM_MESSAGES_FILE,
9 VAULTS_DIR, VAULT_EXT,
10 },
11 AccountId, ExternalFile, ExternalFileName, Result, SecretId, VaultId,
12};
13#[cfg(not(target_arch = "wasm32"))]
14use etcetera::{
15 app_strategy::choose_native_strategy, AppStrategy, AppStrategyArgs,
16};
17use once_cell::sync::Lazy;
18use serde::{Deserialize, Serialize};
19use sos_vfs as vfs;
20use std::{
21 path::{Path, PathBuf},
22 sync::{Arc, RwLock},
23};
24
25static DATA_DIR: Lazy<RwLock<Option<PathBuf>>> =
26 Lazy::new(|| RwLock::new(None));
27
28#[derive(Default, Debug, Clone, Serialize, Deserialize)]
41pub struct Paths {
42 server: bool,
44 account_id: Option<AccountId>,
46 documents_dir: PathBuf,
48 logs_dir: PathBuf,
50
51 identity_dir: PathBuf,
56 local_dir: PathBuf,
58 audit_file: PathBuf,
60 user_dir: PathBuf,
62 files_dir: PathBuf,
64 vaults_dir: PathBuf,
66 device_file: PathBuf,
68
69 database_file: PathBuf,
74 blobs_dir: PathBuf,
76}
77
78impl Paths {
79 pub fn new_client(documents_dir: impl AsRef<Path>) -> Arc<Self> {
81 Arc::new(Self::new_with_prefix(false, documents_dir, None, LOCAL_DIR))
82 }
83
84 pub fn new_server(documents_dir: impl AsRef<Path>) -> Arc<Self> {
86 Arc::new(Self::new_with_prefix(true, documents_dir, None, REMOTE_DIR))
87 }
88
89 fn new_with_prefix(
90 server: bool,
91 documents_dir: impl AsRef<Path>,
92 account_id: Option<&AccountId>,
93 prefix: impl AsRef<Path>,
94 ) -> Self {
95 let documents_dir = documents_dir.as_ref().to_path_buf();
96 let local_dir = documents_dir.join(prefix);
97 let logs_dir = documents_dir.join(LOGS_DIR);
98 let identity_dir = documents_dir.join(IDENTITY_DIR);
99 let audit_file = local_dir.join(AUDIT_FILE_NAME);
100 let user_dir = local_dir
101 .join(account_id.map(|id| id.to_string()).unwrap_or_default());
102
103 let files_dir = user_dir.join(FILES_DIR);
104 let vaults_dir = user_dir.join(VAULTS_DIR);
105 let device_file =
106 user_dir.join(format!("{}.{}", DEVICE_FILE, VAULT_EXT));
107
108 let blobs_dir = documents_dir.join(BLOBS_DIR);
110 let database_file = documents_dir.join(DATABASE_FILE);
111
112 Self {
113 server,
114 account_id: account_id.cloned(),
115 documents_dir,
116 database_file,
117 blobs_dir,
118 logs_dir,
119
120 identity_dir,
121 local_dir,
122 audit_file,
123 user_dir,
124 files_dir,
125 vaults_dir,
126 device_file,
127 }
128 }
129
130 pub fn is_server(&self) -> bool {
132 self.server
133 }
134
135 pub fn with_account_id(&self, account_id: &AccountId) -> Arc<Self> {
137 Arc::new(Self::new_with_prefix(
138 self.server,
139 &self.documents_dir,
140 Some(account_id),
141 if self.server { REMOTE_DIR } else { LOCAL_DIR },
142 ))
143 }
144
145 pub async fn ensure(&self) -> Result<()> {
150 vfs::create_dir_all(&self.local_dir).await?;
152
153 if !self.is_global() {
154 vfs::create_dir_all(&self.user_dir).await?;
156 vfs::create_dir_all(&self.files_dir).await?;
157 vfs::create_dir_all(&self.vaults_dir).await?;
158 }
159 Ok(())
160 }
161
162 pub async fn ensure_db(&self) -> Result<()> {
165 vfs::create_dir_all(&self.blobs_dir).await?;
167
168 if !self.is_global() {
169 vfs::create_dir_all(self.blobs_account_dir()).await?;
172 }
173 Ok(())
174 }
175
176 pub fn is_using_db(&self) -> bool {
178 self.database_file().exists()
179 }
180
181 pub async fn is_usable(&self) -> Result<bool> {
184 assert!(!self.is_global(), "not accessible for global paths");
185 if self.is_using_db() {
186 Ok(vfs::try_exists(self.database_file()).await?)
187 } else {
188 Ok(vfs::try_exists(self.identity_vault()).await?
189 && vfs::try_exists(self.identity_events()).await?
190 && vfs::try_exists(self.account_events()).await?
191 && vfs::try_exists(self.device_events()).await?)
192 }
193 }
194
195 pub fn into_files_dir(&self) -> PathBuf {
202 assert!(!self.is_global(), "not accessible for global paths");
203 if self.is_using_db() {
204 self.blobs_account_dir()
205 } else {
206 self.files_dir().to_owned()
207 }
208 }
209
210 pub fn into_file_folder_path(&self, folder_id: &VaultId) -> PathBuf {
217 self.into_files_dir().join(folder_id.to_string())
218 }
219
220 pub fn into_file_secret_path(
227 &self,
228 folder_id: &VaultId,
229 secret_id: &SecretId,
230 ) -> PathBuf {
231 self.into_file_folder_path(folder_id)
232 .join(secret_id.to_string())
233 }
234
235 pub fn into_file_path(&self, file: &ExternalFile) -> PathBuf {
241 self.into_file_path_parts(
242 file.vault_id(),
243 file.secret_id(),
244 file.file_name(),
245 )
246 }
247
248 pub fn into_file_path_parts(
254 &self,
255 folder_id: &VaultId,
256 secret_id: &SecretId,
257 file_name: &ExternalFileName,
258 ) -> PathBuf {
259 self.into_file_secret_path(folder_id, secret_id)
260 .join(file_name.to_string())
261 }
262
263 pub fn database_file(&self) -> &PathBuf {
265 &self.database_file
266 }
267
268 #[doc(hidden)]
270 #[cfg(debug_assertions)]
271 pub fn blobs_dir(&self) -> &PathBuf {
272 &self.blobs_dir
273 }
274
275 fn blobs_account_dir(&self) -> PathBuf {
282 if self.is_global() {
283 panic!(
284 "blobs account directory is not accessible for global paths"
285 );
286 }
287 self.blobs_dir
288 .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap())
289 }
290
291 pub fn into_blob_file_path(
297 &self,
298 vault_id: &VaultId,
299 secret_id: &SecretId,
300 file_name: impl AsRef<str>,
301 ) -> PathBuf {
302 self.blobs_account_dir()
303 .join(vault_id.to_string())
304 .join(secret_id.to_string())
305 .join(file_name.as_ref())
306 }
307
308 pub fn account_id(&self) -> Option<&AccountId> {
310 self.account_id.as_ref()
311 }
312
313 pub fn documents_dir(&self) -> &PathBuf {
315 &self.documents_dir
316 }
317
318 pub fn is_global(&self) -> bool {
323 self.account_id.is_none()
324 }
325
326 pub fn identity_dir(&self) -> &PathBuf {
328 &self.identity_dir
329 }
330
331 pub fn local_dir(&self) -> &PathBuf {
333 &self.local_dir
334 }
335
336 pub fn logs_dir(&self) -> &PathBuf {
338 &self.logs_dir
339 }
340
341 pub fn audit_file(&self) -> &PathBuf {
343 &self.audit_file
344 }
345
346 pub fn preferences_file(&self) -> PathBuf {
349 let mut path = if self.is_global() {
350 self.documents_dir().join(PREFERENCES_FILE)
351 } else {
352 self.user_dir().join(PREFERENCES_FILE)
353 };
354 path.set_extension(JSON_EXT);
355 path
356 }
357
358 pub fn system_messages_file(&self) -> PathBuf {
364 assert!(!self.is_global(), "not accessible for global paths");
365 let mut path = self.user_dir().join(SYSTEM_MESSAGES_FILE);
366 path.set_extension(JSON_EXT);
367 path
368 }
369
370 pub fn user_dir(&self) -> &PathBuf {
376 assert!(!self.is_global(), "not accessible for global paths");
377 &self.user_dir
378 }
379
380 pub fn files_dir(&self) -> &PathBuf {
386 assert!(!self.is_global(), "not accessible for global paths");
387 &self.files_dir
388 }
389
390 pub fn into_legacy_file_path(
396 &self,
397 vault_id: &VaultId,
398 secret_id: &SecretId,
399 file_name: impl AsRef<str>,
400 ) -> PathBuf {
401 self.files_dir()
402 .join(vault_id.to_string())
403 .join(secret_id.to_string())
404 .join(file_name.as_ref())
405 }
406
407 pub fn vaults_dir(&self) -> &PathBuf {
413 assert!(!self.is_global(), "not accessible for global paths");
414 &self.vaults_dir
415 }
416
417 pub fn device_file(&self) -> &PathBuf {
423 assert!(!self.is_global(), "not accessible for global paths");
424 &self.device_file
425 }
426
427 pub fn identity_vault(&self) -> PathBuf {
433 assert!(!self.is_global(), "not accessible for global paths");
434 let mut identity_vault_file = self
435 .identity_dir
436 .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap());
437 identity_vault_file.set_extension(VAULT_EXT);
438 identity_vault_file
439 }
440
441 pub fn identity_events(&self) -> PathBuf {
447 let mut events_path = self.identity_vault();
448 events_path.set_extension(EVENT_LOG_EXT);
449 events_path
450 }
451
452 pub fn vault_path(&self, id: &VaultId) -> PathBuf {
458 assert!(!self.is_global(), "not accessible for global paths");
459 let mut vault_path = self.vaults_dir.join(id.to_string());
460 vault_path.set_extension(VAULT_EXT);
461 vault_path
462 }
463
464 pub fn event_log_path(&self, id: &VaultId) -> PathBuf {
470 assert!(!self.is_global(), "not accessible for global paths");
471 let mut vault_path = self.vaults_dir.join(id.to_string());
472 vault_path.set_extension(EVENT_LOG_EXT);
473 vault_path
474 }
475
476 pub fn account_events(&self) -> PathBuf {
482 assert!(!self.is_global(), "not accessible for global paths");
483 let mut vault_path = self.user_dir.join(ACCOUNT_EVENTS);
484 vault_path.set_extension(EVENT_LOG_EXT);
485 vault_path
486 }
487
488 pub fn device_events(&self) -> PathBuf {
494 assert!(!self.is_global(), "not accessible for global paths");
495 let mut vault_path = self.user_dir.join(DEVICE_EVENTS);
496 vault_path.set_extension(EVENT_LOG_EXT);
497 vault_path
498 }
499
500 pub fn file_events(&self) -> PathBuf {
506 assert!(!self.is_global(), "not accessible for global paths");
507 let mut vault_path = self.user_dir.join(FILE_EVENTS);
508 vault_path.set_extension(EVENT_LOG_EXT);
509 vault_path
510 }
511
512 pub fn remote_origins(&self) -> PathBuf {
518 assert!(!self.is_global(), "not accessible for global paths");
519 let mut vault_path = self.user_dir.join(REMOTES_FILE);
520 vault_path.set_extension(JSON_EXT);
521 vault_path
522 }
523
524 pub async fn scaffold(
526 data_dir: impl Into<Option<&PathBuf>>,
527 ) -> Result<()> {
528 let data_dir = if let Some(data_dir) = data_dir.into() {
529 data_dir.to_owned()
530 } else {
531 Paths::data_dir()?
532 };
533
534 let paths = Self::new_client(data_dir);
535 vfs::create_dir_all(paths.documents_dir()).await?;
536 if !paths.is_using_db() {
537 vfs::create_dir_all(paths.identity_dir()).await?;
538 }
539 vfs::create_dir_all(paths.logs_dir()).await?;
540 Ok(())
541 }
542
543 pub fn set_data_dir(path: PathBuf) {
546 let mut writer = DATA_DIR.write().unwrap();
547 *writer = Some(path);
548 }
549
550 pub fn clear_data_dir() {
552 let mut writer = DATA_DIR.write().unwrap();
553 *writer = None;
554 }
555
556 pub fn data_dir() -> Result<PathBuf> {
574 let dir = if let Ok(env_data_dir) = std::env::var("SOS_DATA_DIR") {
575 Ok(PathBuf::from(env_data_dir))
576 } else {
577 let reader = DATA_DIR.read().unwrap();
578 if let Some(explicit) = reader.as_ref() {
579 Ok(explicit.to_path_buf())
580 } else {
581 default_storage_dir()
582 }
583 };
584
585 let has_explicit_env = std::env::var("SOS_DATA_DIR").ok().is_some();
586 if cfg!(debug_assertions) && !has_explicit_env {
587 if !cfg!(test) {
592 let sub_dir = if std::env::var("SOS_TEST").is_ok() {
593 "test"
594 } else {
595 "debug"
596 };
597 dir.map(|dir| dir.join(sub_dir))
598 } else {
599 dir
600 }
601 } else {
602 dir
603 }
604 }
605}
606
607#[cfg(target_os = "android")]
608fn default_storage_dir() -> Result<PathBuf> {
609 Ok(PathBuf::from(""))
610}
611
612#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
613fn default_storage_dir() -> Result<PathBuf> {
614 let strategy = choose_native_strategy(AppStrategyArgs {
615 top_level_domain: "com".to_string(),
616 author: APP_AUTHOR.to_string(),
617 app_name: APP_NAME.to_string(),
618 })
619 .map_err(Box::from)?;
620
621 #[cfg(not(windows))]
622 {
623 let mut path = strategy.data_dir();
624 path.set_file_name(APP_AUTHOR);
625 Ok(path)
626 }
627 #[cfg(windows)]
628 {
629 let mut path = strategy.cache_dir();
630 path.pop();
632 Ok(path)
633 }
634}
635
636#[cfg(target_arch = "wasm32")]
637fn default_storage_dir() -> Result<PathBuf> {
638 Ok(PathBuf::from("/"))
639}