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 system_messages_file(&self) -> PathBuf {
347 assert!(!self.is_global(), "not accessible for global paths");
348 let mut path = self.user_dir().join(SYSTEM_MESSAGES_FILE);
349 path.set_extension(JSON_EXT);
350 path
351 }
352
353 pub fn user_dir(&self) -> &PathBuf {
359 assert!(!self.is_global(), "not accessible for global paths");
360 &self.user_dir
361 }
362
363 pub fn files_dir(&self) -> &PathBuf {
369 assert!(!self.is_global(), "not accessible for global paths");
370 &self.files_dir
371 }
372
373 pub fn into_legacy_file_path(
379 &self,
380 vault_id: &VaultId,
381 secret_id: &SecretId,
382 file_name: impl AsRef<str>,
383 ) -> PathBuf {
384 self.files_dir()
385 .join(vault_id.to_string())
386 .join(secret_id.to_string())
387 .join(file_name.as_ref())
388 }
389
390 pub fn vaults_dir(&self) -> &PathBuf {
396 assert!(!self.is_global(), "not accessible for global paths");
397 &self.vaults_dir
398 }
399
400 pub fn device_file(&self) -> &PathBuf {
406 assert!(!self.is_global(), "not accessible for global paths");
407 &self.device_file
408 }
409
410 pub fn identity_vault(&self) -> PathBuf {
416 assert!(!self.is_global(), "not accessible for global paths");
417 let mut identity_vault_file = self
418 .identity_dir
419 .join(self.account_id.as_ref().map(|id| id.to_string()).unwrap());
420 identity_vault_file.set_extension(VAULT_EXT);
421 identity_vault_file
422 }
423
424 pub fn identity_events(&self) -> PathBuf {
430 let mut events_path = self.identity_vault();
431 events_path.set_extension(EVENT_LOG_EXT);
432 events_path
433 }
434
435 pub fn vault_path(&self, id: &VaultId) -> PathBuf {
441 assert!(!self.is_global(), "not accessible for global paths");
442 let mut vault_path = self.vaults_dir.join(id.to_string());
443 vault_path.set_extension(VAULT_EXT);
444 vault_path
445 }
446
447 pub fn event_log_path(&self, id: &VaultId) -> PathBuf {
453 assert!(!self.is_global(), "not accessible for global paths");
454 let mut vault_path = self.vaults_dir.join(id.to_string());
455 vault_path.set_extension(EVENT_LOG_EXT);
456 vault_path
457 }
458
459 pub fn account_events(&self) -> PathBuf {
465 assert!(!self.is_global(), "not accessible for global paths");
466 let mut vault_path = self.user_dir.join(ACCOUNT_EVENTS);
467 vault_path.set_extension(EVENT_LOG_EXT);
468 vault_path
469 }
470
471 pub fn device_events(&self) -> PathBuf {
477 assert!(!self.is_global(), "not accessible for global paths");
478 let mut vault_path = self.user_dir.join(DEVICE_EVENTS);
479 vault_path.set_extension(EVENT_LOG_EXT);
480 vault_path
481 }
482
483 pub fn file_events(&self) -> PathBuf {
489 assert!(!self.is_global(), "not accessible for global paths");
490 let mut vault_path = self.user_dir.join(FILE_EVENTS);
491 vault_path.set_extension(EVENT_LOG_EXT);
492 vault_path
493 }
494
495 pub fn remote_origins(&self) -> PathBuf {
501 assert!(!self.is_global(), "not accessible for global paths");
502 let mut vault_path = self.user_dir.join(REMOTES_FILE);
503 vault_path.set_extension(JSON_EXT);
504 vault_path
505 }
506
507 pub async fn scaffold(
509 data_dir: impl Into<Option<&PathBuf>>,
510 ) -> Result<()> {
511 let data_dir = if let Some(data_dir) = data_dir.into() {
512 data_dir.to_owned()
513 } else {
514 Paths::data_dir()?
515 };
516
517 let paths = Self::new_client(data_dir);
518 vfs::create_dir_all(paths.documents_dir()).await?;
519 if !paths.is_using_db() {
520 vfs::create_dir_all(paths.identity_dir()).await?;
521 }
522 vfs::create_dir_all(paths.logs_dir()).await?;
523 Ok(())
524 }
525
526 pub fn set_data_dir(path: PathBuf) {
529 DATA_DIR.get_or_init(|| path);
530 }
531
532 pub fn data_dir() -> Result<PathBuf> {
550 let dir = if let Ok(env_data_dir) = std::env::var("SOS_DATA_DIR") {
551 Ok(PathBuf::from(env_data_dir))
552 } else {
553 if let Some(data_dir) = DATA_DIR.get() {
554 Ok(data_dir.to_owned())
555 } else {
556 default_storage_dir()
557 }
558 };
559
560 let has_explicit_env = std::env::var("SOS_DATA_DIR").ok().is_some();
561 if cfg!(debug_assertions) && !has_explicit_env {
562 if !cfg!(test) {
567 let sub_dir = if std::env::var("SOS_TEST").is_ok() {
568 "test"
569 } else {
570 "debug"
571 };
572 dir.map(|dir| dir.join(sub_dir))
573 } else {
574 dir
575 }
576 } else {
577 dir
578 }
579 }
580}
581
582#[cfg(target_os = "android")]
583fn default_storage_dir() -> Result<PathBuf> {
584 Ok(PathBuf::from(""))
585}
586
587#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
588fn default_storage_dir() -> Result<PathBuf> {
589 let strategy = choose_native_strategy(AppStrategyArgs {
590 top_level_domain: "com".to_string(),
591 author: APP_AUTHOR.to_string(),
592 app_name: APP_NAME.to_string(),
593 })
594 .map_err(Box::from)?;
595
596 #[cfg(not(windows))]
597 {
598 let mut path = strategy.data_dir();
599 path.set_file_name(APP_AUTHOR);
600 Ok(path)
601 }
602 #[cfg(windows)]
603 {
604 let mut path = strategy.cache_dir();
605 path.pop();
607 Ok(path)
608 }
609}
610
611#[cfg(target_arch = "wasm32")]
612fn default_storage_dir() -> Result<PathBuf> {
613 Ok(PathBuf::from("/"))
614}