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 async fn ensure_db(&self) -> Result<()> {
162 vfs::create_dir_all(&self.blobs_dir).await?;
164
165 if !self.is_global() {
166 vfs::create_dir_all(self.blobs_account_dir()).await?;
169 }
170 Ok(())
171 }
172
173 pub fn is_using_db(&self) -> bool {
175 self.database_file().exists()
176 }
177
178 pub async fn is_usable(&self) -> Result<bool> {
181 assert!(!self.is_global(), "not accessible for global paths");
182 if self.is_using_db() {
183 Ok(vfs::try_exists(self.database_file()).await?)
184 } else {
185 Ok(vfs::try_exists(self.identity_vault()).await?
186 && vfs::try_exists(self.identity_events()).await?
187 && vfs::try_exists(self.account_events()).await?
188 && vfs::try_exists(self.device_events()).await?)
189 }
190 }
191
192 pub fn into_files_dir(&self) -> PathBuf {
199 assert!(!self.is_global(), "not accessible for global paths");
200 if self.is_using_db() {
201 self.blobs_account_dir()
202 } else {
203 self.files_dir().to_owned()
204 }
205 }
206
207 pub fn into_file_folder_path(&self, folder_id: &VaultId) -> PathBuf {
214 self.into_files_dir().join(folder_id.to_string())
215 }
216
217 pub fn into_file_secret_path(
224 &self,
225 folder_id: &VaultId,
226 secret_id: &SecretId,
227 ) -> PathBuf {
228 self.into_file_folder_path(folder_id)
229 .join(secret_id.to_string())
230 }
231
232 pub fn into_file_path(&self, file: &ExternalFile) -> PathBuf {
238 self.into_file_path_parts(
239 file.vault_id(),
240 file.secret_id(),
241 file.file_name(),
242 )
243 }
244
245 pub fn into_file_path_parts(
251 &self,
252 folder_id: &VaultId,
253 secret_id: &SecretId,
254 file_name: &ExternalFileName,
255 ) -> PathBuf {
256 self.into_file_secret_path(folder_id, secret_id)
257 .join(file_name.to_string())
258 }
259
260 pub fn database_file(&self) -> &PathBuf {
262 &self.database_file
263 }
264
265 #[doc(hidden)]
267 #[cfg(debug_assertions)]
268 pub fn blobs_dir(&self) -> &PathBuf {
269 &self.blobs_dir
270 }
271
272 fn blobs_account_dir(&self) -> PathBuf {
279 if self.is_global() {
280 panic!(
281 "blobs account directory is not accessible for global paths"
282 );
283 }
284 self.blobs_dir
285 .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap())
286 }
287
288 pub fn into_blob_file_path(
294 &self,
295 vault_id: &VaultId,
296 secret_id: &SecretId,
297 file_name: impl AsRef<str>,
298 ) -> PathBuf {
299 self.blobs_account_dir()
300 .join(vault_id.to_string())
301 .join(secret_id.to_string())
302 .join(file_name.as_ref())
303 }
304
305 pub fn account_id(&self) -> Option<&AccountId> {
307 self.account_id.as_ref()
308 }
309
310 pub fn documents_dir(&self) -> &PathBuf {
312 &self.documents_dir
313 }
314
315 pub fn is_global(&self) -> bool {
320 self.account_id.is_none()
321 }
322
323 pub fn identity_dir(&self) -> &PathBuf {
325 &self.identity_dir
326 }
327
328 pub fn local_dir(&self) -> &PathBuf {
330 &self.local_dir
331 }
332
333 pub fn logs_dir(&self) -> &PathBuf {
335 &self.logs_dir
336 }
337
338 pub fn audit_file(&self) -> &PathBuf {
340 &self.audit_file
341 }
342
343 pub fn preferences_file(&self) -> PathBuf {
346 let mut path = if self.is_global() {
347 self.documents_dir().join(PREFERENCES_FILE)
348 } else {
349 self.user_dir().join(PREFERENCES_FILE)
350 };
351 path.set_extension(JSON_EXT);
352 path
353 }
354
355 pub fn system_messages_file(&self) -> PathBuf {
361 assert!(!self.is_global(), "not accessible for global paths");
362 let mut path = self.user_dir().join(SYSTEM_MESSAGES_FILE);
363 path.set_extension(JSON_EXT);
364 path
365 }
366
367 pub fn user_dir(&self) -> &PathBuf {
373 assert!(!self.is_global(), "not accessible for global paths");
374 &self.user_dir
375 }
376
377 pub fn files_dir(&self) -> &PathBuf {
383 assert!(!self.is_global(), "not accessible for global paths");
384 &self.files_dir
385 }
386
387 pub fn into_legacy_file_path(
393 &self,
394 vault_id: &VaultId,
395 secret_id: &SecretId,
396 file_name: impl AsRef<str>,
397 ) -> PathBuf {
398 self.files_dir()
399 .join(vault_id.to_string())
400 .join(secret_id.to_string())
401 .join(file_name.as_ref())
402 }
403
404 pub fn vaults_dir(&self) -> &PathBuf {
410 assert!(!self.is_global(), "not accessible for global paths");
411 &self.vaults_dir
412 }
413
414 pub fn device_file(&self) -> &PathBuf {
420 assert!(!self.is_global(), "not accessible for global paths");
421 &self.device_file
422 }
423
424 pub fn identity_vault(&self) -> PathBuf {
430 assert!(!self.is_global(), "not accessible for global paths");
431 let mut identity_vault_file = self
432 .identity_dir
433 .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap());
434 identity_vault_file.set_extension(VAULT_EXT);
435 identity_vault_file
436 }
437
438 pub fn identity_events(&self) -> PathBuf {
444 let mut events_path = self.identity_vault();
445 events_path.set_extension(EVENT_LOG_EXT);
446 events_path
447 }
448
449 pub fn vault_path(&self, id: &VaultId) -> PathBuf {
455 assert!(!self.is_global(), "not accessible for global paths");
456 let mut vault_path = self.vaults_dir.join(id.to_string());
457 vault_path.set_extension(VAULT_EXT);
458 vault_path
459 }
460
461 pub fn event_log_path(&self, id: &VaultId) -> PathBuf {
467 assert!(!self.is_global(), "not accessible for global paths");
468 let mut vault_path = self.vaults_dir.join(id.to_string());
469 vault_path.set_extension(EVENT_LOG_EXT);
470 vault_path
471 }
472
473 pub fn account_events(&self) -> PathBuf {
479 assert!(!self.is_global(), "not accessible for global paths");
480 let mut vault_path = self.user_dir.join(ACCOUNT_EVENTS);
481 vault_path.set_extension(EVENT_LOG_EXT);
482 vault_path
483 }
484
485 pub fn device_events(&self) -> PathBuf {
491 assert!(!self.is_global(), "not accessible for global paths");
492 let mut vault_path = self.user_dir.join(DEVICE_EVENTS);
493 vault_path.set_extension(EVENT_LOG_EXT);
494 vault_path
495 }
496
497 pub fn file_events(&self) -> PathBuf {
503 assert!(!self.is_global(), "not accessible for global paths");
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 assert!(!self.is_global(), "not accessible for global paths");
516 let mut vault_path = self.user_dir.join(REMOTES_FILE);
517 vault_path.set_extension(JSON_EXT);
518 vault_path
519 }
520
521 pub async fn scaffold(
523 data_dir: impl Into<Option<&PathBuf>>,
524 ) -> Result<()> {
525 let data_dir = if let Some(data_dir) = data_dir.into() {
526 data_dir.to_owned()
527 } else {
528 Paths::data_dir()?
529 };
530
531 let paths = Self::new_client(data_dir);
532 vfs::create_dir_all(paths.documents_dir()).await?;
533 if !paths.is_using_db() {
534 vfs::create_dir_all(paths.identity_dir()).await?;
535 }
536 vfs::create_dir_all(paths.logs_dir()).await?;
537 Ok(())
538 }
539
540 pub fn set_data_dir(path: PathBuf) {
543 DATA_DIR.get_or_init(|| path);
544 }
545
546 pub fn data_dir() -> Result<PathBuf> {
564 let dir = if let Ok(env_data_dir) = std::env::var("SOS_DATA_DIR") {
565 Ok(PathBuf::from(env_data_dir))
566 } else {
567 if let Some(data_dir) = DATA_DIR.get() {
568 Ok(data_dir.to_owned())
569 } else {
570 default_storage_dir()
571 }
572 };
573
574 let has_explicit_env = std::env::var("SOS_DATA_DIR").ok().is_some();
575 if cfg!(debug_assertions) && !has_explicit_env {
576 if !cfg!(test) {
581 let sub_dir = if std::env::var("SOS_TEST").is_ok() {
582 "test"
583 } else {
584 "debug"
585 };
586 dir.map(|dir| dir.join(sub_dir))
587 } else {
588 dir
589 }
590 } else {
591 dir
592 }
593 }
594}
595
596#[cfg(target_os = "android")]
597fn default_storage_dir() -> Result<PathBuf> {
598 Ok(PathBuf::from(""))
599}
600
601#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
602fn default_storage_dir() -> Result<PathBuf> {
603 let strategy = choose_native_strategy(AppStrategyArgs {
604 top_level_domain: "com".to_string(),
605 author: APP_AUTHOR.to_string(),
606 app_name: APP_NAME.to_string(),
607 })
608 .map_err(Box::from)?;
609
610 #[cfg(not(windows))]
611 {
612 let mut path = strategy.data_dir();
613 path.set_file_name(APP_AUTHOR);
614 Ok(path)
615 }
616 #[cfg(windows)]
617 {
618 let mut path = strategy.cache_dir();
619 path.pop();
621 Ok(path)
622 }
623}
624
625#[cfg(target_arch = "wasm32")]
626fn default_storage_dir() -> Result<PathBuf> {
627 Ok(PathBuf::from("/"))
628}