sos_core/
paths.rs

1//! File system paths for applications.
2use 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/// File system paths.
26///
27/// Clients and servers may be configured to run on the same machine
28/// and point to the same data directory so different prefixes are
29/// used to distinguish.
30///
31/// Clients write to a `local` directory whilst servers write to a
32/// `remote` directory.
33///
34/// Several functions require a user identifier and will panic if
35/// a user identifier has not been set, see the function documentation
36/// for details.
37#[derive(Default, Debug, Clone, Serialize, Deserialize)]
38pub struct Paths {
39    /// Whether these paths are for server storage.
40    server: bool,
41    /// User identifier.
42    account_id: Option<AccountId>,
43    /// Top-level documents folder.
44    documents_dir: PathBuf,
45    /// Directory for application logs.
46    logs_dir: PathBuf,
47
48    /*
49     * v1 paths (filesystem)
50     */
51    /// Directory for identity vaults.
52    identity_dir: PathBuf,
53    /// Directory for local storage.
54    local_dir: PathBuf,
55    /// File for local audit logs.
56    audit_file: PathBuf,
57    /// User segregated storage.
58    user_dir: PathBuf,
59    /// User file storage.
60    files_dir: PathBuf,
61    /// User vault storage.
62    vaults_dir: PathBuf,
63    /// User devices storage.
64    device_file: PathBuf,
65
66    /*
67     * v2 paths (database)
68     */
69    /// Database file.
70    database_file: PathBuf,
71    /// External file blob storage.
72    blobs_dir: PathBuf,
73}
74
75impl Paths {
76    /// Create paths for a client without an account identifier.
77    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    /// Create paths for a server without an account identifier.
82    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        // Version 2 of the account storage backend (SQLite)
106        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    /// Whether these paths are for server-side storage.
128    pub fn is_server(&self) -> bool {
129        self.server
130    }
131
132    /// Clone of paths with an account identifier.
133    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    /// Ensure the local storage directory exists.
143    ///
144    /// If a user identifier is available this will
145    /// also create some user-specific directories.
146    pub async fn ensure(&self) -> Result<()> {
147        // Version 1 needs to local/remote directory
148        vfs::create_dir_all(&self.local_dir).await?;
149
150        if !self.is_global() {
151            // Version 1 file system - needs to be removed eventually
152            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    /// Ensure the local storage directories exist
160    /// for version 2 database storage.
161    pub async fn ensure_db(&self) -> Result<()> {
162        // Version 2 just needs the blobs directory
163        vfs::create_dir_all(&self.blobs_dir).await?;
164
165        if !self.is_global() {
166            // Version 2 database backend needs a blobs folder
167            // for the account
168            vfs::create_dir_all(self.blobs_account_dir()).await?;
169        }
170        Ok(())
171    }
172
173    /// Determine if a database file exists.
174    pub fn is_using_db(&self) -> bool {
175        self.database_file().exists()
176    }
177
178    /// Try to determine if the account is ready to be used
179    /// by checking for the presence of required files on disc.
180    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    /// Expected directory for external file blobs
193    /// for an account.
194    ///
195    /// # Panics
196    ///
197    /// If this set of paths are global (no user identifier).
198    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    /// Expected location for the directory containing
208    /// all the external files for a folder.
209    ///
210    /// # Panics
211    ///
212    /// If this set of paths are global (no user identifier).
213    pub fn into_file_folder_path(&self, folder_id: &VaultId) -> PathBuf {
214        self.into_files_dir().join(folder_id.to_string())
215    }
216
217    /// Expected location for the directory containing
218    /// all the external files for a secret.
219    ///
220    /// # Panics
221    ///
222    /// If this set of paths are global (no user identifier).
223    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    /// External file path.
233    ///
234    /// # Panics
235    ///
236    /// If this set of paths are global (no user identifier).
237    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    /// External file path from parts.
246    ///
247    /// # Panics
248    ///
249    /// If this set of paths are global (no user identifier).
250    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    /// Path to the database file for an account.
261    pub fn database_file(&self) -> &PathBuf {
262        &self.database_file
263    }
264
265    /// External file blobs directory.
266    #[doc(hidden)]
267    #[cfg(debug_assertions)]
268    pub fn blobs_dir(&self) -> &PathBuf {
269        &self.blobs_dir
270    }
271
272    /// Expected location for the directory containing
273    /// all the external file blobs for an account.
274    ///
275    /// # Panics
276    ///
277    /// If this set of paths are global (no user identifier).
278    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    /// Expected location for an external file blob.
289    ///
290    /// # Panics
291    ///
292    /// If this set of paths are global (no user identifier).
293    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    /// User identifier.
306    pub fn account_id(&self) -> Option<&AccountId> {
307        self.account_id.as_ref()
308    }
309
310    /// Top-level storage directory.
311    pub fn documents_dir(&self) -> &PathBuf {
312        &self.documents_dir
313    }
314
315    /// Determine if the paths are global.
316    ///
317    /// Paths are global when a user identifier
318    /// is not available.
319    pub fn is_global(&self) -> bool {
320        self.account_id.is_none()
321    }
322
323    /// Path to the identity vault directory.
324    pub fn identity_dir(&self) -> &PathBuf {
325        &self.identity_dir
326    }
327
328    /// Path to the local storage.
329    pub fn local_dir(&self) -> &PathBuf {
330        &self.local_dir
331    }
332
333    /// Path to the logs directory.
334    pub fn logs_dir(&self) -> &PathBuf {
335        &self.logs_dir
336    }
337
338    /// Path to the audit file.
339    pub fn audit_file(&self) -> &PathBuf {
340        &self.audit_file
341    }
342
343    /// Path to the file used to store global or
344    /// account-level preferences.
345    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    /// Path to the file used to store account-level system messages.
356    ///
357    /// # Panics
358    ///
359    /// If this set of paths are global (no user identifier).
360    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    /// User specific storage directory.
368    ///
369    /// # Panics
370    ///
371    /// If this set of paths are global (no user identifier).
372    pub fn user_dir(&self) -> &PathBuf {
373        assert!(!self.is_global(), "not accessible for global paths");
374        &self.user_dir
375    }
376
377    /// User's files directory.
378    ///
379    /// # Panics
380    ///
381    /// If this set of paths are global (no user identifier).
382    pub fn files_dir(&self) -> &PathBuf {
383        assert!(!self.is_global(), "not accessible for global paths");
384        &self.files_dir
385    }
386
387    /// Expected location for a file.
388    ///
389    /// # Panics
390    ///
391    /// If this set of paths are global (no user identifier).
392    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    /// User's vaults storage directory.
405    ///
406    /// # Panics
407    ///
408    /// If this set of paths are global (no user identifier).
409    pub fn vaults_dir(&self) -> &PathBuf {
410        assert!(!self.is_global(), "not accessible for global paths");
411        &self.vaults_dir
412    }
413
414    /// User's device signing key vault file.
415    ///
416    /// # Panics
417    ///
418    /// If this set of paths are global (no user identifier).
419    pub fn device_file(&self) -> &PathBuf {
420        assert!(!self.is_global(), "not accessible for global paths");
421        &self.device_file
422    }
423
424    /// Path to the identity vault file for this user.
425    ///
426    /// # Panics
427    ///
428    /// If this set of paths are global (no user identifier).
429    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    /// Path to the identity events log for this user.
439    ///
440    /// # Panics
441    ///
442    /// If this set of paths are global (no user identifier).
443    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    /// Path to a vault file from it's identifier.
450    ///
451    /// # Panics
452    ///
453    /// If this set of paths are global (no user identifier).
454    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    /// Path to an event log file from it's identifier.
462    ///
463    /// # Panics
464    ///
465    /// If this set of paths are global (no user identifier).
466    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    /// Path to the user's account event log file.
474    ///
475    /// # Panics
476    ///
477    /// If this set of paths are global (no user identifier).
478    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    /// Path to the user's event log of device changes.
486    ///
487    /// # Panics
488    ///
489    /// If this set of paths are global (no user identifier).
490    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    /// Path to the user's event log of external file changes.
498    ///
499    /// # Panics
500    ///
501    /// If this set of paths are global (no user identifier).
502    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    /// Path to the file used to store remote origins.
510    ///
511    /// # Panics
512    ///
513    /// If this set of paths are global (no user identifier).
514    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    /// Ensure the root directories exist for storage.
522    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    /// Set an explicit data directory used to store all
541    /// application files.
542    pub fn set_data_dir(path: PathBuf) {
543        DATA_DIR.get_or_init(|| path);
544    }
545
546    /// Get the default root directory used for caching application data.
547    ///
548    /// If the `SOS_DATA_DIR` environment variable is set it is used.
549    ///
550    /// Otherwise if an explicit directory has been set
551    /// using `set_data_dir()` then that will be used instead.
552    ///
553    /// Finally if no environment variable or explicit directory has been
554    /// set then a path will be computed by platform convention.
555    ///
556    /// When running with `debug_assertions` a `debug` path is appended
557    /// (except when executing tests) so that we can use different
558    /// storage locations for debug and release builds.
559    ///
560    /// If the `SOS_TEST` environment variable is set then we use
561    /// `test` rather than `debug` as the nested directory so that
562    /// test data does not collide with debug data.
563    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            // Don't follow the convention for separating debug and
577            // release data when running the integration tests as it
578            // makes paths very hard to reason about when they are
579            // being explicitly set in test specs.
580            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        // Backwards compatible when we moved from app_dirs2
620        path.pop();
621        Ok(path)
622    }
623}
624
625#[cfg(target_arch = "wasm32")]
626fn default_storage_dir() -> Result<PathBuf> {
627    Ok(PathBuf::from("/"))
628}